Remove pre-1.0 bridge governance scaffolding
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@ PLATFORM_STORAGE_BACKEND=file
|
|||||||
|
|
||||||
# MySQL metadata example. Change PLATFORM_STORAGE_BACKEND above from file to mysql,
|
# MySQL metadata example. Change PLATFORM_STORAGE_BACKEND above from file to mysql,
|
||||||
# then uncomment and adjust PLATFORM_MYSQL_DSN.
|
# then uncomment and adjust PLATFORM_MYSQL_DSN.
|
||||||
# MySQL is for users/plugins/servers/jobs/audit/log stream metadata, not row-per-log-line bodies.
|
# MySQL is for users/plugins/servers/jobs/log stream metadata, not row-per-log-line bodies.
|
||||||
# PLATFORM_MYSQL_DSN=platform:platform@tcp(127.0.0.1:3306)/platform?parseTime=true
|
# PLATFORM_MYSQL_DSN=platform:platform@tcp(127.0.0.1:3306)/platform?parseTime=true
|
||||||
|
|
||||||
PLATFORM_DATA_DIR=.platform-data
|
PLATFORM_DATA_DIR=.platform-data
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ The platform must not use one overloaded channel for everything. Run communicati
|
|||||||
|
|
||||||
Logs are historical data, not a UI-only stream. Browser realtime tail may use platform SSE/WebSocket later, but run-to-platform logs must use durable ingest semantics.
|
Logs are historical data, not a UI-only stream. Browser realtime tail may use platform SSE/WebSocket later, but run-to-platform logs must use durable ingest semantics.
|
||||||
|
|
||||||
## Governance
|
## Repository Rules
|
||||||
|
|
||||||
Read `AGENTS.md` before changing code. Each subproject also has a local `AGENTS.md` with stricter rules for that area.
|
Read `AGENTS.md` before changing code. Each subproject also has a local `AGENTS.md` with stricter rules for that area.
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,5 @@
|
|||||||
"runEndpoints": [],
|
"runEndpoints": [],
|
||||||
"jobs": [],
|
"jobs": [],
|
||||||
"artifacts": [],
|
"artifacts": [],
|
||||||
"logStreams": [],
|
"logStreams": []
|
||||||
"auditEvents": []
|
|
||||||
}
|
}
|
||||||
@@ -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.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.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.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 {
|
if _, err := coreService.CreateGamePlugin(plugin); err != nil {
|
||||||
t.Fatalf("create bridge plugin: %v", err)
|
t.Fatalf("create bridge plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1519,7 +1519,7 @@ func TestPluginLifecycleAndAIConfigRoutesAreDurableAndRedacted(t *testing.T) {
|
|||||||
serverID := createRuntimeAPIFixtures(t, router, adminSession)
|
serverID := createRuntimeAPIFixtures(t, router, adminSession)
|
||||||
createAIProviderFixture(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)
|
assertStatus(t, lifecycleRecorder, http.StatusAccepted)
|
||||||
lifecycle := decodeBody[dto.PluginLifecycleActionResponse](t, lifecycleRecorder)
|
lifecycle := decodeBody[dto.PluginLifecycleActionResponse](t, lifecycleRecorder)
|
||||||
if lifecycle.Status != "queued" || lifecycle.Job.ID == "" || lifecycle.Installation.ID == "" {
|
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"},
|
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"}}}},
|
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"
|
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 GameClientBridgeCommandDeclaration struct {
|
||||||
Type string
|
Type string
|
||||||
Title string
|
Title string
|
||||||
Permission string
|
Permission string
|
||||||
ApprovalLevel GameClientBridgeApprovalLevel
|
|
||||||
PayloadSchemaRef string
|
PayloadSchemaRef string
|
||||||
ResultSchemaRef string
|
ResultSchemaRef string
|
||||||
TimeoutSeconds int
|
TimeoutSeconds int
|
||||||
@@ -113,57 +95,11 @@ type GameClientBridgeDataPackDeclaration struct {
|
|||||||
DataRefs []string
|
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 {
|
type GameClientBridgePageContract struct {
|
||||||
PageKey string
|
PageKey string
|
||||||
CommandTypes []string
|
CommandTypes []string
|
||||||
SnapshotTypes []string
|
SnapshotTypes []string
|
||||||
QueryTemplateKeys []string
|
QueryTemplateKeys []string
|
||||||
OperationKeys []string
|
|
||||||
FeatureKeys []string
|
FeatureKeys []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +133,6 @@ type GameClientBridgeManifest struct {
|
|||||||
QueryTemplates []GameClientBridgeQueryTemplateDeclaration
|
QueryTemplates []GameClientBridgeQueryTemplateDeclaration
|
||||||
LogProjections []GameClientBridgeLogProjectionDeclaration
|
LogProjections []GameClientBridgeLogProjectionDeclaration
|
||||||
DataPacks []GameClientBridgeDataPackDeclaration
|
DataPacks []GameClientBridgeDataPackDeclaration
|
||||||
OperationTemplates []GameClientBridgeOperationTemplateDeclaration
|
|
||||||
Retention GameClientBridgeRetention
|
Retention GameClientBridgeRetention
|
||||||
Pages []GameClientBridgePageContract
|
Pages []GameClientBridgePageContract
|
||||||
Features []GameClientBridgeFeatureDeclaration
|
Features []GameClientBridgeFeatureDeclaration
|
||||||
@@ -224,7 +159,6 @@ type GameClientBridgeCommand struct {
|
|||||||
IdempotencyKey string
|
IdempotencyKey string
|
||||||
Priority int
|
Priority int
|
||||||
State GameClientBridgeCommandState
|
State GameClientBridgeCommandState
|
||||||
ApprovalState GameClientBridgeApprovalState
|
|
||||||
RequesterID string
|
RequesterID string
|
||||||
Claim GameClientBridgeClaim
|
Claim GameClientBridgeClaim
|
||||||
Cancellation GameClientBridgeCancellation
|
Cancellation GameClientBridgeCancellation
|
||||||
@@ -489,14 +423,12 @@ func CopyGameClientBridgeManifest(value GameClientBridgeManifest) GameClientBrid
|
|||||||
value.DataPacks[index].ConfigMapRefs = CopyStringSlice(value.DataPacks[index].ConfigMapRefs)
|
value.DataPacks[index].ConfigMapRefs = CopyStringSlice(value.DataPacks[index].ConfigMapRefs)
|
||||||
value.DataPacks[index].DataRefs = CopyStringSlice(value.DataPacks[index].DataRefs)
|
value.DataPacks[index].DataRefs = CopyStringSlice(value.DataPacks[index].DataRefs)
|
||||||
}
|
}
|
||||||
value.OperationTemplates = append([]GameClientBridgeOperationTemplateDeclaration(nil), value.OperationTemplates...)
|
|
||||||
value.Pages = append([]GameClientBridgePageContract(nil), value.Pages...)
|
value.Pages = append([]GameClientBridgePageContract(nil), value.Pages...)
|
||||||
value.Features = append([]GameClientBridgeFeatureDeclaration(nil), value.Features...)
|
value.Features = append([]GameClientBridgeFeatureDeclaration(nil), value.Features...)
|
||||||
for index := range value.Pages {
|
for index := range value.Pages {
|
||||||
value.Pages[index].CommandTypes = CopyStringSlice(value.Pages[index].CommandTypes)
|
value.Pages[index].CommandTypes = CopyStringSlice(value.Pages[index].CommandTypes)
|
||||||
value.Pages[index].SnapshotTypes = CopyStringSlice(value.Pages[index].SnapshotTypes)
|
value.Pages[index].SnapshotTypes = CopyStringSlice(value.Pages[index].SnapshotTypes)
|
||||||
value.Pages[index].QueryTemplateKeys = CopyStringSlice(value.Pages[index].QueryTemplateKeys)
|
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)
|
value.Pages[index].FeatureKeys = CopyStringSlice(value.Pages[index].FeatureKeys)
|
||||||
}
|
}
|
||||||
for index := range value.Features {
|
for index := range value.Features {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ func TestCopyGameClientBridgeDeclarationsCopiesQueryTemplateSlices(t *testing.T)
|
|||||||
Presence: &GameClientBridgeLogProjectionPresenceDeclaration{TimestampField: "lastLoginAt", ActiveWindowSeconds: 600, ActivityTarget: &GameClientBridgeLogProjectionTargetDeclaration{Collection: "activity", UpsertKeys: []string{"steamId"}, CaptureMappings: map[string]string{"steamId": "steamId"}}},
|
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"}}},
|
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"}}},
|
||||||
Pages: []GameClientBridgePageContract{{PageKey: "players", QueryTemplateKeys: []string{"player.lookup"}, OperationKeys: []string{"player.fame.set"}}},
|
|
||||||
}
|
}
|
||||||
manifestCopy := CopyGameClientBridgeManifest(manifest)
|
manifestCopy := CopyGameClientBridgeManifest(manifest)
|
||||||
manifestCopy.QueryTemplates[0].Key = "mutated"
|
manifestCopy.QueryTemplates[0].Key = "mutated"
|
||||||
@@ -22,10 +21,8 @@ func TestCopyGameClientBridgeDeclarationsCopiesQueryTemplateSlices(t *testing.T)
|
|||||||
manifestCopy.LogProjections[0].Presence.ActivityTarget.CaptureMappings["steamId"] = "mutated"
|
manifestCopy.LogProjections[0].Presence.ActivityTarget.CaptureMappings["steamId"] = "mutated"
|
||||||
manifestCopy.DataPacks[0].LogParserRefs[0] = "mutated"
|
manifestCopy.DataPacks[0].LogParserRefs[0] = "mutated"
|
||||||
manifestCopy.DataPacks[0].DataRefs[0] = "mutated"
|
manifestCopy.DataPacks[0].DataRefs[0] = "mutated"
|
||||||
manifestCopy.OperationTemplates[0].Key = "mutated"
|
|
||||||
manifestCopy.Pages[0].QueryTemplateKeys[0] = "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.Pages[0].QueryTemplateKeys[0] != "player.lookup" {
|
||||||
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" {
|
|
||||||
t.Fatalf("manifest copy aliases query template declarations: source=%#v copy=%#v", manifest, manifestCopy)
|
t.Fatalf("manifest copy aliases query template declarations: source=%#v copy=%#v", manifest, manifestCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ type PluginLifecycleRequest struct {
|
|||||||
Operation PluginLifecycleOperation
|
Operation PluginLifecycleOperation
|
||||||
TargetVersion string
|
TargetVersion string
|
||||||
IdempotencyKey string
|
IdempotencyKey string
|
||||||
Confirmed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginLifecycleResult struct {
|
type PluginLifecycleResult struct {
|
||||||
|
|||||||
@@ -372,7 +372,6 @@ type GamePluginManifestAI struct {
|
|||||||
type GamePluginProductionLifecycle struct {
|
type GamePluginProductionLifecycle struct {
|
||||||
Operations []string
|
Operations []string
|
||||||
DependencyPolicy string
|
DependencyPolicy string
|
||||||
ApprovalRequired []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GamePluginRemoteAccess struct {
|
type GamePluginRemoteAccess struct {
|
||||||
@@ -1068,7 +1067,6 @@ const (
|
|||||||
JobCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
|
JobCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
|
||||||
JobCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
|
JobCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
|
||||||
JobCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
|
JobCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
|
||||||
JobCapabilityRemoteRunProtectedSQL = "remote.run.protected.sql"
|
|
||||||
JobCapabilityRemoteRunProgram = "remote.run.program.command"
|
JobCapabilityRemoteRunProgram = "remote.run.program.command"
|
||||||
JobCapabilityRunSelfUpdate = "run.self-update"
|
JobCapabilityRunSelfUpdate = "run.self-update"
|
||||||
JobCapabilityDistributionBuild = "distribution.build"
|
JobCapabilityDistributionBuild = "distribution.build"
|
||||||
@@ -1749,7 +1747,6 @@ func CopyPluginCreateFields(fields []PluginCreateField) []PluginCreateField {
|
|||||||
|
|
||||||
func CopyGamePluginProductionLifecycle(lifecycle GamePluginProductionLifecycle) GamePluginProductionLifecycle {
|
func CopyGamePluginProductionLifecycle(lifecycle GamePluginProductionLifecycle) GamePluginProductionLifecycle {
|
||||||
lifecycle.Operations = CopyStringSlice(lifecycle.Operations)
|
lifecycle.Operations = CopyStringSlice(lifecycle.Operations)
|
||||||
lifecycle.ApprovalRequired = CopyStringSlice(lifecycle.ApprovalRequired)
|
|
||||||
return lifecycle
|
return lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ type GameClientBridgeCommandResponse struct {
|
|||||||
RunJobID string `json:"runJobId,omitempty"`
|
RunJobID string `json:"runJobId,omitempty"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
ApprovalState string `json:"approvalState"`
|
|
||||||
RequesterID string `json:"requesterId,omitempty"`
|
RequesterID string `json:"requesterId,omitempty"`
|
||||||
ResultSummary string `json:"resultSummary,omitempty"`
|
ResultSummary string `json:"resultSummary,omitempty"`
|
||||||
Result *GameClientBridgeCommandResultResponse `json:"result,omitempty"`
|
Result *GameClientBridgeCommandResultResponse `json:"result,omitempty"`
|
||||||
@@ -244,7 +243,6 @@ func GameClientBridgeCommandFromDomain(value domain.GameClientBridgeCommand) Gam
|
|||||||
RunJobID: value.RunJobID,
|
RunJobID: value.RunJobID,
|
||||||
Priority: value.Priority,
|
Priority: value.Priority,
|
||||||
State: string(value.State),
|
State: string(value.State),
|
||||||
ApprovalState: string(value.ApprovalState),
|
|
||||||
RequesterID: value.RequesterID,
|
RequesterID: value.RequesterID,
|
||||||
ResultSummary: value.Result.Summary,
|
ResultSummary: value.Result.Summary,
|
||||||
ExpiresAt: value.ExpiresAt,
|
ExpiresAt: value.ExpiresAt,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestGameClientBridgeBrowserProjectionsAreCompleteAndOmitInternalData(t *tes
|
|||||||
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
||||||
command := domain.GameClientBridgeCommand{
|
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,
|
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)},
|
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)},
|
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)},
|
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"`
|
Operation string `json:"operation"`
|
||||||
TargetVersion string `json:"targetVersion,omitempty"`
|
TargetVersion string `json:"targetVersion,omitempty"`
|
||||||
IdempotencyKey string `json:"idempotencyKey"`
|
IdempotencyKey string `json:"idempotencyKey"`
|
||||||
Confirmed bool `json:"confirmed"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginLifecycleInstallationResponse struct {
|
type PluginLifecycleInstallationResponse struct {
|
||||||
@@ -82,7 +81,7 @@ type AIConfigDiffApprovalResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (request PluginLifecycleActionRequest) ToDomain(pluginID string) domain.PluginLifecycleRequest {
|
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 {
|
func PluginLifecycleFromDomain(installation domain.PluginLifecycleInstallation) PluginLifecycleInstallationResponse {
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ type GamePluginManifestAIBody struct {
|
|||||||
type GamePluginProductionLifecycleBody struct {
|
type GamePluginProductionLifecycleBody struct {
|
||||||
Operations []string `json:"operations"`
|
Operations []string `json:"operations"`
|
||||||
DependencyPolicy string `json:"dependencyPolicy"`
|
DependencyPolicy string `json:"dependencyPolicy"`
|
||||||
ApprovalRequired []string `json:"approvalRequired"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GamePluginRemoteAccessBody struct {
|
type GamePluginRemoteAccessBody struct {
|
||||||
@@ -265,7 +264,6 @@ type GameClientBridgeCommandDeclarationBody struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
ApprovalLevel string `json:"approvalLevel"`
|
|
||||||
PayloadSchemaRef string `json:"payloadSchemaRef"`
|
PayloadSchemaRef string `json:"payloadSchemaRef"`
|
||||||
ResultSchemaRef string `json:"resultSchemaRef,omitempty"`
|
ResultSchemaRef string `json:"resultSchemaRef,omitempty"`
|
||||||
TimeoutSeconds int `json:"timeoutSeconds"`
|
TimeoutSeconds int `json:"timeoutSeconds"`
|
||||||
@@ -339,50 +337,11 @@ type GameClientBridgeDataPackDeclarationBody struct {
|
|||||||
DataRefs []string `json:"dataRefs,omitempty"`
|
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 {
|
type GameClientBridgePageContractBody struct {
|
||||||
PageKey string `json:"pageKey"`
|
PageKey string `json:"pageKey"`
|
||||||
CommandTypes []string `json:"commandTypes,omitempty"`
|
CommandTypes []string `json:"commandTypes,omitempty"`
|
||||||
SnapshotTypes []string `json:"snapshotTypes,omitempty"`
|
SnapshotTypes []string `json:"snapshotTypes,omitempty"`
|
||||||
QueryTemplateKeys []string `json:"queryTemplateKeys,omitempty"`
|
QueryTemplateKeys []string `json:"queryTemplateKeys,omitempty"`
|
||||||
OperationKeys []string `json:"operationKeys,omitempty"`
|
|
||||||
FeatureKeys []string `json:"featureKeys,omitempty"`
|
FeatureKeys []string `json:"featureKeys,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +375,6 @@ type GameClientBridgeManifestBody struct {
|
|||||||
QueryTemplates []GameClientBridgeQueryTemplateDeclarationBody `json:"queryTemplates,omitempty"`
|
QueryTemplates []GameClientBridgeQueryTemplateDeclarationBody `json:"queryTemplates,omitempty"`
|
||||||
LogProjections []GameClientBridgeLogProjectionDeclarationBody `json:"logProjections,omitempty"`
|
LogProjections []GameClientBridgeLogProjectionDeclarationBody `json:"logProjections,omitempty"`
|
||||||
DataPacks []GameClientBridgeDataPackDeclarationBody `json:"dataPacks,omitempty"`
|
DataPacks []GameClientBridgeDataPackDeclarationBody `json:"dataPacks,omitempty"`
|
||||||
OperationTemplates []GameClientBridgeOperationTemplateDeclarationBody `json:"operationTemplates,omitempty"`
|
|
||||||
CommandRetentionSeconds int `json:"commandRetentionSeconds"`
|
CommandRetentionSeconds int `json:"commandRetentionSeconds"`
|
||||||
MaxCommands int `json:"maxCommands"`
|
MaxCommands int `json:"maxCommands"`
|
||||||
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
|
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
|
||||||
@@ -1163,7 +1121,7 @@ func (ai GamePluginManifestAIBody) ToDomain() domain.GamePluginManifestAI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (lifecycle GamePluginProductionLifecycleBody) ToDomain() domain.GamePluginProductionLifecycle {
|
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 {
|
func (remote GamePluginRemoteAccessBody) ToDomain() domain.GamePluginRemoteAccess {
|
||||||
@@ -1179,7 +1137,7 @@ func (remote GamePluginRemoteAccessBody) ToDomain() domain.GamePluginRemoteAcces
|
|||||||
func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManifest {
|
func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManifest {
|
||||||
commands := make([]domain.GameClientBridgeCommandDeclaration, len(body.Commands))
|
commands := make([]domain.GameClientBridgeCommandDeclaration, len(body.Commands))
|
||||||
for index, command := range 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))
|
snapshots := make([]domain.GameClientBridgeSnapshotDeclaration, len(body.Snapshots))
|
||||||
for index, snapshot := range body.Snapshots {
|
for index, snapshot := range body.Snapshots {
|
||||||
@@ -1202,13 +1160,9 @@ func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManif
|
|||||||
for index, dataPack := range body.DataPacks {
|
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)}
|
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))
|
pages := make([]domain.GameClientBridgePageContract, len(body.Pages))
|
||||||
for index, page := range 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))
|
features := make([]domain.GameClientBridgeFeatureDeclaration, len(body.Features))
|
||||||
for index, feature := range body.Features {
|
for index, feature := range body.Features {
|
||||||
@@ -1218,7 +1172,7 @@ func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManif
|
|||||||
if body.Companion != nil {
|
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}
|
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 {
|
func gameClientBridgeLogProjectionToDomain(value GameClientBridgeLogProjectionDeclarationBody) domain.GameClientBridgeLogProjectionDeclaration {
|
||||||
@@ -1633,14 +1587,14 @@ func MarketplacePluginFromDomain(plugin domain.PluginMarketplacePlugin) Marketpl
|
|||||||
|
|
||||||
func productionLifecycleFromDomain(lifecycle domain.GamePluginProductionLifecycle) GamePluginProductionLifecycleBody {
|
func productionLifecycleFromDomain(lifecycle domain.GamePluginProductionLifecycle) GamePluginProductionLifecycleBody {
|
||||||
lifecycle = domain.CopyGamePluginProductionLifecycle(lifecycle)
|
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 {
|
func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) GameClientBridgeManifestBody {
|
||||||
value = domain.CopyGameClientBridgeManifest(value)
|
value = domain.CopyGameClientBridgeManifest(value)
|
||||||
commands := make([]GameClientBridgeCommandDeclarationBody, len(value.Commands))
|
commands := make([]GameClientBridgeCommandDeclarationBody, len(value.Commands))
|
||||||
for index, command := range 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))
|
snapshots := make([]GameClientBridgeSnapshotDeclarationBody, len(value.Snapshots))
|
||||||
for index, snapshot := range value.Snapshots {
|
for index, snapshot := range value.Snapshots {
|
||||||
@@ -1662,13 +1616,9 @@ func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) G
|
|||||||
for index, dataPack := range value.DataPacks {
|
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)}
|
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))
|
pages := make([]GameClientBridgePageContractBody, len(value.Pages))
|
||||||
for index, page := range 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))
|
features := make([]GameClientBridgeFeatureDeclarationBody, len(value.Features))
|
||||||
for index, feature := range value.Features {
|
for index, feature := range value.Features {
|
||||||
@@ -1678,7 +1628,7 @@ func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) G
|
|||||||
if value.Companion.ProfileKey != "" {
|
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}
|
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 {
|
func gameClientBridgeLogProjectionFromDomain(value domain.GameClientBridgeLogProjectionDeclaration) GameClientBridgeLogProjectionDeclarationBody {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package dto
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"browser.local/platform/domain"
|
"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{
|
ProductionLifecycle: domain.GamePluginProductionLifecycle{
|
||||||
Operations: []string{"install", "upgrade", "rollback"},
|
Operations: []string{"install", "upgrade", "rollback"},
|
||||||
DependencyPolicy: "required",
|
DependencyPolicy: "required",
|
||||||
ApprovalRequired: []string{"rollback"},
|
|
||||||
},
|
},
|
||||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}}},
|
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}}},
|
||||||
Permissions: domain.PluginPermissions{
|
Permissions: domain.PluginPermissions{
|
||||||
@@ -69,7 +68,6 @@ func TestGamePluginModelRoundTripCopiesSlices(t *testing.T) {
|
|||||||
roundTrip.Tags[0] = "mutated"
|
roundTrip.Tags[0] = "mutated"
|
||||||
roundTrip.AIPurposes[0] = "config.suggest"
|
roundTrip.AIPurposes[0] = "config.suggest"
|
||||||
roundTrip.ProductionLifecycle.Operations[0] = "retire"
|
roundTrip.ProductionLifecycle.Operations[0] = "retire"
|
||||||
roundTrip.ProductionLifecycle.ApprovalRequired[0] = "disable"
|
|
||||||
roundTrip.RuntimeProfiles.LifecycleProfiles[0].Capabilities[0] = "process.stop"
|
roundTrip.RuntimeProfiles.LifecycleProfiles[0].Capabilities[0] = "process.stop"
|
||||||
|
|
||||||
if source.RequiredRunCapabilities[0] != "process.start" {
|
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" {
|
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)
|
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)
|
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" {
|
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) {
|
if !request.ExpiresAt.After(stamp) {
|
||||||
return domain.GameClientBridgeCommand{}, validationError("bridge command expiresAt must be in the future")
|
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++
|
svc.bridgeSeq++
|
||||||
command := domain.GameClientBridgeCommand{
|
command := domain.GameClientBridgeCommand{
|
||||||
ID: fmt.Sprintf("bridge-command-%d-%d", stamp.UnixNano(), svc.bridgeSeq),
|
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,
|
IdempotencyKey: request.IdempotencyKey,
|
||||||
Priority: request.Priority,
|
Priority: request.Priority,
|
||||||
State: domain.GameClientBridgeCommandPending,
|
State: domain.GameClientBridgeCommandPending,
|
||||||
ApprovalState: approvalState,
|
|
||||||
RequesterID: requesterID,
|
RequesterID: requesterID,
|
||||||
ExpiresAt: request.ExpiresAt,
|
ExpiresAt: request.ExpiresAt,
|
||||||
CreatedAt: stamp,
|
CreatedAt: stamp,
|
||||||
@@ -351,9 +340,6 @@ func (svc *CoreService) claimGameClientBridgeCommands(component gameClientBridge
|
|||||||
if command.RunJobID != "" {
|
if command.RunJobID != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if command.ApprovalState != domain.GameClientBridgeApprovalNotRequired && command.ApprovalState != domain.GameClientBridgeApprovalApproved {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fencingToken := command.Claim.FencingToken + 1
|
fencingToken := command.Claim.FencingToken + 1
|
||||||
command.State = domain.GameClientBridgeCommandClaimed
|
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}
|
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()
|
t.Helper()
|
||||||
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
||||||
store := repo.NewMemoryStore()
|
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 {
|
if err := store.GamePlugins().Create(plugin); err != nil {
|
||||||
t.Fatalf("seed bridge plugin: %v", err)
|
t.Fatalf("seed bridge plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestPluginLifecycleBridgeDispatchesBoundedJob(t *testing.T) {
|
|||||||
response, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
|
response, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
|
||||||
RequestID: "bridge-plugin-install", PluginID: plugin.ID, RouteKey: "operations",
|
RequestID: "bridge-plugin-install", PluginID: plugin.ID, RouteKey: "operations",
|
||||||
ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionPluginLifecycle,
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("execute lifecycle bridge: %v", err)
|
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 {
|
func (svc *CoreService) executeBridgePluginLifecycle(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
|
||||||
confirmed, err := strconv.ParseBool(defaultBridgeValue(request.Payload["confirmed"], "false"))
|
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 {
|
|
||||||
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})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return bridgeExecutionError(base, err)
|
return bridgeExecutionError(base, err)
|
||||||
}
|
}
|
||||||
@@ -1579,9 +1573,6 @@ func normalizedProductionLifecycle(lifecycle domain.GamePluginProductionLifecycl
|
|||||||
if lifecycle.DependencyPolicy == "" {
|
if lifecycle.DependencyPolicy == "" {
|
||||||
lifecycle.DependencyPolicy = "optional"
|
lifecycle.DependencyPolicy = "optional"
|
||||||
}
|
}
|
||||||
if len(lifecycle.ApprovalRequired) == 0 {
|
|
||||||
lifecycle.ApprovalRequired = []string{"disable", "rollback", "retire"}
|
|
||||||
}
|
|
||||||
return lifecycle
|
return lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1957,7 +1957,7 @@ func validPluginManifestRegistration() domain.GamePluginManifestRegistration {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
|
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"}}}},
|
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) {
|
if !validPluginLifecycleOperation(request.Operation) {
|
||||||
violations = append(violations, "operation is invalid")
|
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}} {
|
for _, value := range []fieldString{{field: "targetVersion", value: request.TargetVersion}, {field: "idempotencyKey", value: request.IdempotencyKey}} {
|
||||||
if unsafeProductionText(value.value) {
|
if unsafeProductionText(value.value) {
|
||||||
violations = append(violations, value.field+" is unsafe")
|
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 {
|
func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []string, pages []domain.GamePluginPage, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
|
||||||
companionPresent := bridge.Companion != (domain.GameClientBridgeCompanionDeclaration{})
|
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
|
return nil
|
||||||
}
|
}
|
||||||
var violations []string
|
var violations []string
|
||||||
@@ -513,9 +513,6 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
|||||||
if !containsString(permissions, command.Permission) {
|
if !containsString(permissions, command.Permission) {
|
||||||
violations = append(violations, prefix+".permission must be declared by the plugin")
|
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) {
|
if !safeRelativeJSONRef(command.PayloadSchemaRef) || command.ResultSchemaRef != "" && !safeRelativeJSONRef(command.ResultSchemaRef) {
|
||||||
violations = append(violations, prefix+" schema references must be safe relative JSON references")
|
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{}
|
pageDeclarations := map[string]domain.GamePluginPage{}
|
||||||
for _, page := range pages {
|
for _, page := range pages {
|
||||||
pageDeclarations[page.Key] = page
|
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")
|
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 {
|
for _, featureKey := range page.FeatureKeys {
|
||||||
feature, exists := features[featureKey]
|
feature, exists := features[featureKey]
|
||||||
if !exists {
|
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")
|
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 {
|
func ValidatePluginBridgeAuthorizeRequest(request domain.PluginBridgeAuthorizeRequest) error {
|
||||||
var violations []string
|
var violations []string
|
||||||
violations = appendRequired(violations, "pluginId", request.PluginID)
|
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, "tags", plugin.Tags)
|
||||||
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
|
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
|
||||||
values = appendStringSliceFields(values, "productionLifecycle.operations", plugin.ProductionLifecycle.Operations)
|
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 = append(values, fieldString{field: "productionLifecycle.dependencyPolicy", value: plugin.ProductionLifecycle.DependencyPolicy})
|
||||||
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
|
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
|
||||||
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
|
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 = 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 = 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.operations", manifest.ProductionLifecycle.Operations)
|
||||||
values = appendStringSliceFields(values, "productionLifecycle.approvalRequired", manifest.ProductionLifecycle.ApprovalRequired)
|
|
||||||
values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods)
|
values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods)
|
||||||
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities)
|
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities)
|
||||||
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines)
|
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines)
|
||||||
@@ -2339,7 +2233,7 @@ func validPluginRunCapability(capability string) bool {
|
|||||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||||
domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProgram,
|
domain.JobCapabilityRemoteRunProgram,
|
||||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||||
domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
|
domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
|
||||||
domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate,
|
domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate,
|
||||||
@@ -2372,7 +2266,7 @@ func remoteCapabilityRequiresInputRef(capability string) bool {
|
|||||||
domain.JobCapabilityRemoteRunFilesWrite,
|
domain.JobCapabilityRemoteRunFilesWrite,
|
||||||
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
||||||
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||||
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProgram:
|
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProgram:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -2516,16 +2410,6 @@ func validateProductionLifecycle(field string, lifecycle domain.GamePluginProduc
|
|||||||
if lifecycle.DependencyPolicy != "required" && lifecycle.DependencyPolicy != "optional" {
|
if lifecycle.DependencyPolicy != "required" && lifecycle.DependencyPolicy != "optional" {
|
||||||
violations = append(violations, field+".dependencyPolicy must be required or 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
|
return violations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,24 +130,19 @@ func TestValidateGamePluginManifestRegistrationValidatesRuntimeProfiles(t *testi
|
|||||||
func TestValidateGamePluginManifestRegistrationValidatesGameClientBridgeCatalog(t *testing.T) {
|
func TestValidateGamePluginManifestRegistrationValidatesGameClientBridgeCatalog(t *testing.T) {
|
||||||
registration := validGamePluginManifestRegistration()
|
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.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].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.Pages[0].BridgeActions = append(registration.Manifest.Pages[0].BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest))
|
||||||
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{
|
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{
|
||||||
{Key: "sqlite-db", Kind: "sqlite", TargetKey: "db/sqlite", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}},
|
{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-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{
|
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}}},
|
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}},
|
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},
|
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"}}},
|
Pages: []domain.GameClientBridgePageContract{{PageKey: "logs", CommandTypes: []string{"diagnostic.ping"}, SnapshotTypes: []string{"players"}, QueryTemplateKeys: []string{"player.lookup"}}},
|
||||||
}
|
}
|
||||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||||
t.Fatalf("expected bridge catalog to validate, got %v", err)
|
t.Fatalf("expected bridge catalog to validate, got %v", err)
|
||||||
@@ -168,14 +163,13 @@ func TestValidateGamePluginManifestRegistrationValidatesGameClientBridgeCatalog(
|
|||||||
unsafe := registration
|
unsafe := registration
|
||||||
unsafe.Manifest.GameClientBridge = domain.CopyGameClientBridgeManifest(registration.Manifest.GameClientBridge)
|
unsafe.Manifest.GameClientBridge = domain.CopyGameClientBridgeManifest(registration.Manifest.GameClientBridge)
|
||||||
unsafe.Manifest.GameClientBridge.Commands[0].Type = "shell.execute"
|
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.Commands[0].PayloadSchemaRef = "/etc/command.json"
|
||||||
unsafe.Manifest.GameClientBridge.Pages[0].CommandTypes = []string{"undeclared.command"}
|
unsafe.Manifest.GameClientBridge.Pages[0].CommandTypes = []string{"undeclared.command"}
|
||||||
err := ValidateGamePluginManifestRegistration(unsafe)
|
err := ValidateGamePluginManifestRegistration(unsafe)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected unsafe bridge catalog rejection")
|
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) {
|
if !strings.Contains(err.Error(), expected) {
|
||||||
t.Fatalf("expected %q in validation error: %v", expected, err)
|
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) {
|
func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermissions(t *testing.T) {
|
||||||
@@ -452,7 +398,7 @@ func validGamePluginManifestRegistration() domain.GamePluginManifestRegistration
|
|||||||
{Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}},
|
{Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}},
|
||||||
},
|
},
|
||||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
|
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"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -565,8 +565,7 @@ async function preparePluginOperations(headers, server, plugin) {
|
|||||||
serverInstanceId: server.id,
|
serverInstanceId: server.id,
|
||||||
operation: "install",
|
operation: "install",
|
||||||
targetVersion: plugin.version,
|
targetVersion: plugin.version,
|
||||||
idempotencyKey: `browser-acceptance-plugin-install-${stamp}`,
|
idempotencyKey: `browser-acceptance-plugin-install-${stamp}`
|
||||||
confirmed: false
|
|
||||||
},
|
},
|
||||||
headers
|
headers
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const plugin: GamePluginResponse = {
|
|||||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||||
tags: ["example"],
|
tags: ["example"],
|
||||||
aiPurposes: ["logs.diagnose"],
|
aiPurposes: ["logs.diagnose"],
|
||||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||||
runtimeProfiles: { lifecycleProfiles: [{ key: "local", mode: "local-process", capabilities: ["process.install", "process.start", "process.stop"] }] },
|
runtimeProfiles: { lifecycleProfiles: [{ key: "local", mode: "local-process", capabilities: ["process.install", "process.start", "process.stop"] }] },
|
||||||
status: "installed"
|
status: "installed"
|
||||||
};
|
};
|
||||||
@@ -58,7 +58,7 @@ const marketplacePlugin: MarketplacePluginResponse = {
|
|||||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||||
tags: ["example"],
|
tags: ["example"],
|
||||||
aiPurposes: ["logs.diagnose"],
|
aiPurposes: ["logs.diagnose"],
|
||||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||||
status: "installed",
|
status: "installed",
|
||||||
source: "platform-registry"
|
source: "platform-registry"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const pendingCommand = {
|
|||||||
commandType: "scum.diagnostic.ping",
|
commandType: "scum.diagnostic.ping",
|
||||||
priority: 20,
|
priority: 20,
|
||||||
state: "pending",
|
state: "pending",
|
||||||
approvalState: "pending",
|
|
||||||
requesterId: "user-1",
|
requesterId: "user-1",
|
||||||
expiresAt: later,
|
expiresAt: later,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
@@ -40,7 +39,6 @@ const pendingCommand = {
|
|||||||
const completedCommand = {
|
const completedCommand = {
|
||||||
...pendingCommand,
|
...pendingCommand,
|
||||||
state: "succeeded",
|
state: "succeeded",
|
||||||
approvalState: "approved",
|
|
||||||
resultSummary: "diagnostic completed",
|
resultSummary: "diagnostic completed",
|
||||||
result: {
|
result: {
|
||||||
status: "succeeded",
|
status: "succeeded",
|
||||||
@@ -80,7 +78,6 @@ const manifestDeclaration: GameClientBridgeManifestResponse = {
|
|||||||
type: "scum.diagnostic.ping",
|
type: "scum.diagnostic.ping",
|
||||||
title: "Diagnostic ping",
|
title: "Diagnostic ping",
|
||||||
permission: "server.game-client.command",
|
permission: "server.game-client.command",
|
||||||
approvalLevel: "operator",
|
|
||||||
payloadSchemaRef: "schemas/bridge/commands/diagnostic-ping.request.json",
|
payloadSchemaRef: "schemas/bridge/commands/diagnostic-ping.request.json",
|
||||||
resultSchemaRef: "schemas/bridge/commands/diagnostic-ping.result.json",
|
resultSchemaRef: "schemas/bridge/commands/diagnostic-ping.result.json",
|
||||||
timeoutSeconds: 30,
|
timeoutSeconds: 30,
|
||||||
@@ -126,8 +123,8 @@ const pluginBridgeProjection: Pick<GamePluginResponse, "gameClientBridge"> & Pic
|
|||||||
describe("PlatformApiClient Game Client Bridge operator API", () => {
|
describe("PlatformApiClient Game Client Bridge operator API", () => {
|
||||||
afterEach(() => vi.unstubAllGlobals());
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
it("types plugin and marketplace manifest declarations with approval metadata", () => {
|
it("types plugin and marketplace manifest declarations", () => {
|
||||||
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ approvalLevel: "operator" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } });
|
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ type: "scum.diagnostic.ping" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } });
|
||||||
expect(JSON.stringify(pluginBridgeProjection)).not.toMatch(/authKey|componentKey|sessionToken|credential|secretRef/i);
|
expect(JSON.stringify(pluginBridgeProjection)).not.toMatch(/authKey|componentKey|sessionToken|credential|secretRef/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +154,7 @@ describe("PlatformApiClient Game Client Bridge operator API", () => {
|
|||||||
|
|
||||||
await expect(client.getGameClientBridgeStatus("server-1")).resolves.toMatchObject({ available: false, profiles: [{ profileKey: "scum-client" }] });
|
await expect(client.getGameClientBridgeStatus("server-1")).resolves.toMatchObject({ available: false, profiles: [{ profileKey: "scum-client" }] });
|
||||||
await expect(client.listGameClientBridgeCommands("server-1", { profileKey: "scum-client", state: "pending", commandType: "scum.diagnostic.ping" })).resolves.toMatchObject({ count: 1 });
|
await expect(client.listGameClientBridgeCommands("server-1", { profileKey: "scum-client", state: "pending", commandType: "scum.diagnostic.ping" })).resolves.toMatchObject({ count: 1 });
|
||||||
await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending", approvalState: "pending" });
|
await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending" });
|
||||||
await expect(client.getGameClientBridgeCommand("server-1", pendingCommand.id)).resolves.toMatchObject({ result: { status: "succeeded", payload: { delivered: true } } });
|
await expect(client.getGameClientBridgeCommand("server-1", pendingCommand.id)).resolves.toMatchObject({ result: { status: "succeeded", payload: { delivered: true } } });
|
||||||
await expect(client.cancelGameClientBridgeCommand("server-1", pendingCommand.id, { reason: "maintenance window changed" })).resolves.toMatchObject({ state: "cancelled" });
|
await expect(client.cancelGameClientBridgeCommand("server-1", pendingCommand.id, { reason: "maintenance window changed" })).resolves.toMatchObject({ state: "cancelled" });
|
||||||
await expect(client.listGameClientBridgeSnapshots("server-1", { profileKey: "scum-client", type: "scum.players", streamKey: "current", observedAfter: now, limit: 20 })).resolves.toMatchObject({ count: 1, items: [{ sequence: 7 }] });
|
await expect(client.listGameClientBridgeSnapshots("server-1", { profileKey: "scum-client", type: "scum.players", streamKey: "current", observedAfter: now, limit: 20 })).resolves.toMatchObject({ count: 1, items: [{ sequence: 7 }] });
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe("PlatformApiClient plugin operations", () => {
|
|||||||
const client = new PlatformApiClient("/api/v1", () => "session-token");
|
const client = new PlatformApiClient("/api/v1", () => "session-token");
|
||||||
|
|
||||||
await client.listPluginLifecycles({ pluginId: "game.scum" });
|
await client.listPluginLifecycles({ pluginId: "game.scum" });
|
||||||
await client.runPluginLifecycle("game.scum", { serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1", confirmed: false });
|
await client.runPluginLifecycle("game.scum", { serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1" });
|
||||||
await client.listAIConfigDiffs({ state: "pending" });
|
await client.listAIConfigDiffs({ state: "pending" });
|
||||||
await client.approveAIConfigDiff("diff-1", "approve-1");
|
await client.approveAIConfigDiff("diff-1", "approve-1");
|
||||||
|
|
||||||
@@ -26,6 +26,6 @@ describe("PlatformApiClient plugin operations", () => {
|
|||||||
]);
|
]);
|
||||||
const serialized = JSON.stringify(calls);
|
const serialized = JSON.stringify(calls);
|
||||||
expect(serialized).not.toMatch(/apiKey|token|secret|providerBaseUrl|runSocket|runEndpointUrl|hostPath|credential|dsn|rcon/i);
|
expect(serialized).not.toMatch(/apiKey|token|secret|providerBaseUrl|runSocket|runEndpointUrl|hostPath|credential|dsn|rcon/i);
|
||||||
expect(calls[1]?.body).toEqual({ serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1", confirmed: false });
|
expect(calls[1]?.body).toEqual({ serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ export type RunUpdatePhase = "queued" | "downloading" | "staged" | "restart-requ
|
|||||||
export type ServerLifecycleAction = "create" | "start" | "stop" | "status";
|
export type ServerLifecycleAction = "create" | "start" | "stop" | "status";
|
||||||
|
|
||||||
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "cancelled" | "expired" | "unknown";
|
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "cancelled" | "expired" | "unknown";
|
||||||
export type GameClientBridgeApprovalState = "not_required" | "pending" | "approved" | "rejected";
|
|
||||||
export type GameClientBridgeApprovalLevel = "none" | "operator" | "platform-admin";
|
|
||||||
export type GameClientBridgeResultStatus = "succeeded" | "failed" | "cancelled" | "unknown";
|
export type GameClientBridgeResultStatus = "succeeded" | "failed" | "cancelled" | "unknown";
|
||||||
export type GameClientBridgeJsonValue = string | number | boolean | null | GameClientBridgeJsonValue[] | GameClientBridgeJsonObject;
|
export type GameClientBridgeJsonValue = string | number | boolean | null | GameClientBridgeJsonValue[] | GameClientBridgeJsonObject;
|
||||||
|
|
||||||
@@ -27,7 +25,6 @@ export interface GameClientBridgeCommandDeclarationResponse {
|
|||||||
type: string;
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
approvalLevel: GameClientBridgeApprovalLevel;
|
|
||||||
payloadSchemaRef: string;
|
payloadSchemaRef: string;
|
||||||
resultSchemaRef?: string;
|
resultSchemaRef?: string;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
@@ -141,7 +138,6 @@ export interface GameClientBridgeCommandResponse {
|
|||||||
commandType: string;
|
commandType: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
state: GameClientBridgeCommandState;
|
state: GameClientBridgeCommandState;
|
||||||
approvalState: GameClientBridgeApprovalState;
|
|
||||||
requesterId?: string;
|
requesterId?: string;
|
||||||
resultSummary?: string;
|
resultSummary?: string;
|
||||||
result?: GameClientBridgeCommandResultResponse;
|
result?: GameClientBridgeCommandResultResponse;
|
||||||
@@ -1589,7 +1585,6 @@ export interface AIConfigRecommendationResponse {
|
|||||||
export interface PluginProductionLifecycleDeclaration {
|
export interface PluginProductionLifecycleDeclaration {
|
||||||
operations: PluginLifecycleOperation[];
|
operations: PluginLifecycleOperation[];
|
||||||
dependencyPolicy: "required" | "optional";
|
dependencyPolicy: "required" | "optional";
|
||||||
approvalRequired: Array<"disable" | "rollback" | "retire">;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginLifecycleOperation = "install" | "enable" | "disable" | "upgrade" | "rollback" | "retire" | "dependency-check";
|
export type PluginLifecycleOperation = "install" | "enable" | "disable" | "upgrade" | "rollback" | "retire" | "dependency-check";
|
||||||
@@ -1611,7 +1606,7 @@ export interface PluginLifecycleInstallationResponse {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
export interface PluginLifecycleListResponse { items: PluginLifecycleInstallationResponse[]; count: number; }
|
export interface PluginLifecycleListResponse { items: PluginLifecycleInstallationResponse[]; count: number; }
|
||||||
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; confirmed: boolean; }
|
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; }
|
||||||
export interface PluginLifecycleActionResponse { status: string; installation: PluginLifecycleInstallationResponse; job: JobResponse; }
|
export interface PluginLifecycleActionResponse { status: string; installation: PluginLifecycleInstallationResponse; job: JobResponse; }
|
||||||
|
|
||||||
export interface AIConfigDiffPreviewResponse {
|
export interface AIConfigDiffPreviewResponse {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
|
|
||||||
import { platformApiClient } from "../api/client";
|
import { platformApiClient } from "../api/client";
|
||||||
import type { PluginLifecycleInstallationResponse, PluginLifecycleOperation, ServerInstanceResponse } from "../api/types";
|
import type { PluginLifecycleInstallationResponse, PluginLifecycleOperation, ServerInstanceResponse } from "../api/types";
|
||||||
import { ConfirmDialog } from "./OperationControls";
|
|
||||||
import { ErrorState, LoadingState, ResultBadge } from "./StateViews";
|
import { ErrorState, LoadingState, ResultBadge } from "./StateViews";
|
||||||
|
|
||||||
interface PluginLifecycleWorkbenchProps {
|
interface PluginLifecycleWorkbenchProps {
|
||||||
@@ -22,7 +21,6 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
|||||||
const [targetVersion, setTargetVersion] = useState("");
|
const [targetVersion, setTargetVersion] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [confirming, setConfirming] = useState(false);
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null);
|
const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null);
|
||||||
|
|
||||||
@@ -60,16 +58,13 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
|||||||
serverInstanceId: selectedServerId,
|
serverInstanceId: selectedServerId,
|
||||||
operation,
|
operation,
|
||||||
targetVersion: targetVersion.trim() || undefined,
|
targetVersion: targetVersion.trim() || undefined,
|
||||||
idempotencyKey: `web:plugin.lifecycle:${pluginId}:${selectedServerId}:${operation}:${Date.now()}`,
|
idempotencyKey: `web:plugin.lifecycle:${pluginId}:${selectedServerId}:${operation}:${Date.now()}`
|
||||||
confirmed: disruptiveOperations.includes(operation)
|
|
||||||
});
|
});
|
||||||
const evidence = [response.job?.id && `任务 ${response.job.id}`].filter(Boolean).join(" · ");
|
const evidence = [response.job?.id && `任务 ${response.job.id}`].filter(Boolean).join(" · ");
|
||||||
setResult({ status: response.status === "queued" || response.status === "accepted" ? "succeeded" : response.status === "deferred" ? "pending" : "failed", label: `${lifecycleOperationLabel(operation)}:${response.status}${evidence ? ` · ${evidence}` : ""}` });
|
setResult({ status: response.status === "queued" || response.status === "accepted" ? "succeeded" : response.status === "deferred" ? "pending" : "failed", label: `${lifecycleOperationLabel(operation)}:${response.status}${evidence ? ` · ${evidence}` : ""}` });
|
||||||
setConfirming(false);
|
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
setResult({ status: "failed", label: caught instanceof Error ? caught.message : "插件生命周期操作失败" });
|
setResult({ status: "failed", label: caught instanceof Error ? caught.message : "插件生命周期操作失败" });
|
||||||
setConfirming(false);
|
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -95,7 +90,7 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
|||||||
{operations.map((item) => <option key={item} value={item}>{lifecycleOperationLabel(item)}</option>)}
|
{operations.map((item) => <option key={item} value={item}>{lifecycleOperationLabel(item)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
{(operation === "install" || operation === "upgrade") && <input aria-label="目标版本" placeholder="目标版本" value={targetVersion} disabled={busy} onChange={(event) => setTargetVersion(event.target.value)} />}
|
{(operation === "install" || operation === "upgrade") && <input aria-label="目标版本" placeholder="目标版本" value={targetVersion} disabled={busy} onChange={(event) => setTargetVersion(event.target.value)} />}
|
||||||
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => setConfirming(true)}>{busy ? "提交中…" : "执行"}</button>
|
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => void submit()}>{busy ? "提交中…" : "执行"}</button>
|
||||||
</div>
|
</div>
|
||||||
{installation ? (
|
{installation ? (
|
||||||
<div className="console-record plugin-lifecycle-state">
|
<div className="console-record plugin-lifecycle-state">
|
||||||
@@ -109,14 +104,11 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
|||||||
) : <p className="console-empty-note">该服务器尚无插件生命周期记录。</p>}
|
) : <p className="console-empty-note">该服务器尚无插件生命周期记录。</p>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ConfirmDialog open={confirming} title={`确认${lifecycleOperationLabel(operation)}`} description={`插件 ${pluginName},服务器 ${selectedServerId || "--"}${targetVersion ? `,目标版本 ${targetVersion}` : ""}。`} confirmLabel={lifecycleOperationLabel(operation)} danger={disruptiveOperations.includes(operation)} busy={busy} onCancel={() => { if (!busy) setConfirming(false); }} onConfirm={() => void submit()} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lifecycleOperations: PluginLifecycleOperation[] = ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"];
|
const lifecycleOperations: PluginLifecycleOperation[] = ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"];
|
||||||
const disruptiveOperations: PluginLifecycleOperation[] = ["disable", "rollback", "retire"];
|
|
||||||
|
|
||||||
function lifecycleOperationLabel(operation: PluginLifecycleOperation) {
|
function lifecycleOperationLabel(operation: PluginLifecycleOperation) {
|
||||||
return ({ install: "安装", enable: "启用", disable: "停用", upgrade: "升级", rollback: "回滚", retire: "退役", "dependency-check": "依赖检查" } as Record<PluginLifecycleOperation, string>)[operation];
|
return ({ install: "安装", enable: "启用", disable: "停用", upgrade: "升级", rollback: "回滚", retire: "退役", "dependency-check": "依赖检查" } as Record<PluginLifecycleOperation, string>)[operation];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const plugin: GamePluginResponse = {
|
|||||||
pages: [],
|
pages: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
aiPurposes: [],
|
aiPurposes: [],
|
||||||
productionLifecycle: { operations: ["install"], dependencyPolicy: "optional", approvalRequired: [] },
|
productionLifecycle: { operations: ["install"], dependencyPolicy: "optional" },
|
||||||
status: "installed",
|
status: "installed",
|
||||||
runtimeProfiles: {
|
runtimeProfiles: {
|
||||||
transportProfiles: [{ key: "rcon", kind: "rcon", targetKey: "rcon.password", capabilities: ["remote.run.rcon.command"] }],
|
transportProfiles: [{ key: "rcon", kind: "rcon", targetKey: "rcon.password", capabilities: ["remote.run.rcon.command"] }],
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ const plugin: GamePluginResponse = {
|
|||||||
}],
|
}],
|
||||||
tags: ["scum"],
|
tags: ["scum"],
|
||||||
aiPurposes: [],
|
aiPurposes: [],
|
||||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] },
|
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required" },
|
||||||
gameClientBridge: {
|
gameClientBridge: {
|
||||||
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
|
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
|
||||||
snapshots: [{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }],
|
snapshots: [{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }],
|
||||||
commandRetentionSeconds: 86400,
|
commandRetentionSeconds: 86400,
|
||||||
maxCommands: 1000,
|
maxCommands: 1000,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const marketplacePlugin: MarketplacePluginResponse = {
|
|||||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||||
tags: ["example"],
|
tags: ["example"],
|
||||||
aiPurposes: ["logs.diagnose"],
|
aiPurposes: ["logs.diagnose"],
|
||||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||||
runtimeProfiles: {
|
runtimeProfiles: {
|
||||||
dllExtensions: [{
|
dllExtensions: [{
|
||||||
key: "scum-simple-rcon-ue4ss",
|
key: "scum-simple-rcon-ue4ss",
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const safeCommand = {
|
|||||||
commandType: "scum.player.lookup",
|
commandType: "scum.player.lookup",
|
||||||
priority: 10,
|
priority: 10,
|
||||||
state: "succeeded",
|
state: "succeeded",
|
||||||
approvalState: "not_required",
|
|
||||||
requesterId: "user-1",
|
requesterId: "user-1",
|
||||||
result: { status: "succeeded", summary: "player found", payload: { found: true }, completedAt: now },
|
result: { status: "succeeded", summary: "player found", payload: { found: true }, completedAt: now },
|
||||||
expiresAt: now,
|
expiresAt: now,
|
||||||
@@ -45,14 +44,14 @@ const safeSnapshotList = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("Game Client Bridge safe projection schema", () => {
|
describe("Game Client Bridge safe projection schema", () => {
|
||||||
it("preserves declarations, approval, result, retention and typed snapshot payloads", () => {
|
it("preserves declarations, result, retention and typed snapshot payloads", () => {
|
||||||
expect(parseSafeGameClientBridgeStatus({
|
expect(parseSafeGameClientBridgeStatus({
|
||||||
serverInstanceId: "server-1",
|
serverInstanceId: "server-1",
|
||||||
pluginId: "game.scum",
|
pluginId: "game.scum",
|
||||||
available: true,
|
available: true,
|
||||||
profiles: [{ pluginId: "game.scum", profileKey: "scum-client", available: true, commandTypes: ["scum.player.lookup"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }]
|
profiles: [{ pluginId: "game.scum", profileKey: "scum-client", available: true, commandTypes: ["scum.player.lookup"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }]
|
||||||
})).toMatchObject({ available: true, profiles: [{ queryTemplateKeys: ["scum.player.search"] }] });
|
})).toMatchObject({ available: true, profiles: [{ queryTemplateKeys: ["scum.player.search"] }] });
|
||||||
expect(parseSafeGameClientBridgeCommand(safeCommand)).toMatchObject({ approvalState: "not_required", result: { payload: { found: true } } });
|
expect(parseSafeGameClientBridgeCommand(safeCommand)).toMatchObject({ result: { payload: { found: true } } });
|
||||||
expect(parseSafeGameClientBridgeSnapshotList(safeSnapshotList)).toMatchObject({ count: 1, items: [{ retention: { maxRecords: 24 } }] });
|
expect(parseSafeGameClientBridgeSnapshotList(safeSnapshotList)).toMatchObject({ count: 1, items: [{ retention: { maxRecords: 24 } }] });
|
||||||
expect(parseSafeGameClientBridgeSnapshotList({
|
expect(parseSafeGameClientBridgeSnapshotList({
|
||||||
...safeSnapshotList,
|
...safeSnapshotList,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type {
|
import type {
|
||||||
GameClientBridgeApprovalState,
|
|
||||||
GameClientBridgeCancelResponse,
|
GameClientBridgeCancelResponse,
|
||||||
GameClientBridgeCommandCancellationResponse,
|
GameClientBridgeCommandCancellationResponse,
|
||||||
GameClientBridgeCommandListResponse,
|
GameClientBridgeCommandListResponse,
|
||||||
@@ -16,7 +15,6 @@ import type {
|
|||||||
} from "../api/types";
|
} from "../api/types";
|
||||||
|
|
||||||
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired", "unknown"]);
|
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired", "unknown"]);
|
||||||
const approvalStates = new Set<GameClientBridgeApprovalState>(["not_required", "pending", "approved", "rejected"]);
|
|
||||||
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled", "unknown"]);
|
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled", "unknown"]);
|
||||||
const forbiddenKeys = new Set([
|
const forbiddenKeys = new Set([
|
||||||
"apikey",
|
"apikey",
|
||||||
@@ -84,7 +82,6 @@ export function parseSafeGameClientBridgeCommand(value: unknown): GameClientBrid
|
|||||||
commandType: string(record.commandType, "commandType"),
|
commandType: string(record.commandType, "commandType"),
|
||||||
priority: number(record.priority, "priority"),
|
priority: number(record.priority, "priority"),
|
||||||
state: commandState(record.state),
|
state: commandState(record.state),
|
||||||
approvalState: approvalState(record.approvalState),
|
|
||||||
expiresAt: string(record.expiresAt, "expiresAt"),
|
expiresAt: string(record.expiresAt, "expiresAt"),
|
||||||
createdAt: string(record.createdAt, "createdAt"),
|
createdAt: string(record.createdAt, "createdAt"),
|
||||||
updatedAt: string(record.updatedAt, "updatedAt")
|
updatedAt: string(record.updatedAt, "updatedAt")
|
||||||
@@ -284,12 +281,6 @@ function commandState(value: unknown): GameClientBridgeCommandState {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function approvalState(value: unknown): GameClientBridgeApprovalState {
|
|
||||||
const parsed = string(value, "approvalState") as GameClientBridgeApprovalState;
|
|
||||||
if (!approvalStates.has(parsed)) throw new Error("Game Client Bridge approval state is invalid");
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resultStatus(value: unknown): GameClientBridgeResultStatus {
|
function resultStatus(value: unknown): GameClientBridgeResultStatus {
|
||||||
const parsed = string(value, "result.status") as GameClientBridgeResultStatus;
|
const parsed = string(value, "result.status") as GameClientBridgeResultStatus;
|
||||||
if (!resultStatuses.has(parsed)) throw new Error("Game Client Bridge result status is invalid");
|
if (!resultStatuses.has(parsed)) throw new Error("Game Client Bridge result status is invalid");
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const plugin: GamePluginResponse = {
|
|||||||
pages: [],
|
pages: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
aiPurposes: [],
|
aiPurposes: [],
|
||||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||||
status: "installed",
|
status: "installed",
|
||||||
runtimeProfiles: {
|
runtimeProfiles: {
|
||||||
discovery: [{ key: "root-check", kind: "file.exists", targetKey: "server-root", required: true }],
|
discovery: [{ key: "root-check", kind: "file.exists", targetKey: "server-root", required: true }],
|
||||||
|
|||||||
@@ -5,11 +5,18 @@
|
|||||||
"description": "Development game management plugin for local registry and lifecycle testing.",
|
"description": "Development game management plugin for local registry and lifecycle testing.",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"kind": "game-plugin",
|
"kind": "game-plugin",
|
||||||
"tags": ["example", "development"],
|
"tags": [
|
||||||
|
"example",
|
||||||
|
"development"
|
||||||
|
],
|
||||||
"server": {
|
"server": {
|
||||||
"type": "example",
|
"type": "example",
|
||||||
"displayName": "Example Server",
|
"displayName": "Example Server",
|
||||||
"supportedOS": ["windows", "linux", "darwin"],
|
"supportedOS": [
|
||||||
|
"windows",
|
||||||
|
"linux",
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
"createFormSchema": "schemas/create-form.schema.json"
|
"createFormSchema": "schemas/create-form.schema.json"
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
@@ -57,18 +64,46 @@
|
|||||||
"status": "actions/status.json"
|
"status": "actions/status.json"
|
||||||
},
|
},
|
||||||
"assetFiles": [
|
"assetFiles": [
|
||||||
{ "path": "actions/install.json", "mode": 384 },
|
{
|
||||||
{ "path": "actions/start.json", "mode": 384 },
|
"path": "actions/install.json",
|
||||||
{ "path": "actions/stop.json", "mode": 384 },
|
"mode": 384
|
||||||
{ "path": "actions/restart.json", "mode": 384 },
|
},
|
||||||
{ "path": "actions/status.json", "mode": 384 },
|
{
|
||||||
{ "path": "bin/install-server", "mode": 448 },
|
"path": "actions/start.json",
|
||||||
{ "path": "bin/game-server", "mode": 448 }
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "actions/stop.json",
|
||||||
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "actions/restart.json",
|
||||||
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "actions/status.json",
|
||||||
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/install-server",
|
||||||
|
"mode": 448
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/game-server",
|
||||||
|
"mode": 448
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"productionLifecycle": {
|
"productionLifecycle": {
|
||||||
"operations": ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"],
|
"operations": [
|
||||||
"dependencyPolicy": "optional",
|
"install",
|
||||||
"approvalRequired": ["disable", "rollback", "retire"]
|
"enable",
|
||||||
|
"disable",
|
||||||
|
"upgrade",
|
||||||
|
"rollback",
|
||||||
|
"retire",
|
||||||
|
"dependency-check"
|
||||||
|
],
|
||||||
|
"dependencyPolicy": "optional"
|
||||||
},
|
},
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
@@ -78,8 +113,12 @@
|
|||||||
"bundleKey": "dev-game-plugin",
|
"bundleKey": "dev-game-plugin",
|
||||||
"bundleVersion": "1.0.0",
|
"bundleVersion": "1.0.0",
|
||||||
"bundleIntegritySha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
"bundleIntegritySha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
"permissions": ["server.read"],
|
"permissions": [
|
||||||
"bridgeActions": ["server.instances.read"]
|
"server.read"
|
||||||
|
],
|
||||||
|
"bridgeActions": [
|
||||||
|
"server.instances.read"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "config",
|
"key": "config",
|
||||||
@@ -88,8 +127,15 @@
|
|||||||
"bundleKey": "dev-game-plugin",
|
"bundleKey": "dev-game-plugin",
|
||||||
"bundleVersion": "1.0.0",
|
"bundleVersion": "1.0.0",
|
||||||
"bundleIntegritySha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
"bundleIntegritySha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
"permissions": ["server.files.read", "server.files.write", "ai.invoke"],
|
"permissions": [
|
||||||
"bridgeActions": ["files.request", "ai.invoke"]
|
"server.files.read",
|
||||||
|
"server.files.write",
|
||||||
|
"ai.invoke"
|
||||||
|
],
|
||||||
|
"bridgeActions": [
|
||||||
|
"files.request",
|
||||||
|
"ai.invoke"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "logs",
|
"key": "logs",
|
||||||
@@ -98,12 +144,21 @@
|
|||||||
"bundleKey": "dev-game-plugin",
|
"bundleKey": "dev-game-plugin",
|
||||||
"bundleVersion": "1.0.0",
|
"bundleVersion": "1.0.0",
|
||||||
"bundleIntegritySha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
"bundleIntegritySha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
"permissions": ["server.logs.read", "ai.invoke"],
|
"permissions": [
|
||||||
"bridgeActions": ["logs.query", "ai.invoke"]
|
"server.logs.read",
|
||||||
|
"ai.invoke"
|
||||||
|
],
|
||||||
|
"bridgeActions": [
|
||||||
|
"logs.query",
|
||||||
|
"ai.invoke"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ai": {
|
"ai": {
|
||||||
"purposes": ["config.suggest", "logs.diagnose"],
|
"purposes": [
|
||||||
|
"config.suggest",
|
||||||
|
"logs.diagnose"
|
||||||
|
],
|
||||||
"mediation": "platform",
|
"mediation": "platform",
|
||||||
"configWritePolicy": "review-required"
|
"configWritePolicy": "review-required"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,29 @@
|
|||||||
],
|
],
|
||||||
"createFormSchema": "schemas/create-form.schema.json",
|
"createFormSchema": "schemas/create-form.schema.json",
|
||||||
"createFields": [
|
"createFields": [
|
||||||
{ "key": "serverName", "label": "服务器名称", "type": "text", "required": true, "configKey": "motd" },
|
{
|
||||||
{ "key": "gamePort", "label": "游戏端口", "type": "port", "required": true, "defaultValue": "25565", "configKey": "serverPort" },
|
"key": "serverName",
|
||||||
{ "key": "rconPort", "label": "RCON 端口", "type": "port", "required": true, "defaultValue": "25575", "configKey": "rconPort" }
|
"label": "服务器名称",
|
||||||
|
"type": "text",
|
||||||
|
"required": true,
|
||||||
|
"configKey": "motd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "gamePort",
|
||||||
|
"label": "游戏端口",
|
||||||
|
"type": "port",
|
||||||
|
"required": true,
|
||||||
|
"defaultValue": "25565",
|
||||||
|
"configKey": "serverPort"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "rconPort",
|
||||||
|
"label": "RCON 端口",
|
||||||
|
"type": "port",
|
||||||
|
"required": true,
|
||||||
|
"defaultValue": "25575",
|
||||||
|
"configKey": "rconPort"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
@@ -99,18 +119,46 @@
|
|||||||
"status": "actions/status.json"
|
"status": "actions/status.json"
|
||||||
},
|
},
|
||||||
"assetFiles": [
|
"assetFiles": [
|
||||||
{ "path": "actions/install.json", "mode": 384 },
|
{
|
||||||
{ "path": "actions/start.json", "mode": 384 },
|
"path": "actions/install.json",
|
||||||
{ "path": "actions/stop.json", "mode": 384 },
|
"mode": 384
|
||||||
{ "path": "actions/restart.json", "mode": 384 },
|
},
|
||||||
{ "path": "actions/status.json", "mode": 384 },
|
{
|
||||||
{ "path": "bin/install-server", "mode": 448 },
|
"path": "actions/start.json",
|
||||||
{ "path": "bin/game-server", "mode": 448 }
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "actions/stop.json",
|
||||||
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "actions/restart.json",
|
||||||
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "actions/status.json",
|
||||||
|
"mode": 384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/install-server",
|
||||||
|
"mode": 448
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/game-server",
|
||||||
|
"mode": 448
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"productionLifecycle": {
|
"productionLifecycle": {
|
||||||
"operations": ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"],
|
"operations": [
|
||||||
"dependencyPolicy": "required",
|
"install",
|
||||||
"approvalRequired": ["disable", "rollback", "retire"]
|
"enable",
|
||||||
|
"disable",
|
||||||
|
"upgrade",
|
||||||
|
"rollback",
|
||||||
|
"retire",
|
||||||
|
"dependency-check"
|
||||||
|
],
|
||||||
|
"dependencyPolicy": "required"
|
||||||
},
|
},
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
@@ -153,20 +201,92 @@
|
|||||||
"fileWorkspace": {
|
"fileWorkspace": {
|
||||||
"defaultDirectoryKey": "minecraft-config",
|
"defaultDirectoryKey": "minecraft-config",
|
||||||
"directories": [
|
"directories": [
|
||||||
{ "key": "minecraft-config", "label": "服务器配置", "scope": "config" },
|
{
|
||||||
{ "key": "minecraft-logs", "label": "日志文件", "scope": "logs" }
|
"key": "minecraft-config",
|
||||||
|
"label": "服务器配置",
|
||||||
|
"scope": "config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "minecraft-logs",
|
||||||
|
"label": "日志文件",
|
||||||
|
"scope": "logs"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
{ "key": "server-properties", "directoryKey": "minecraft-config", "label": "server.properties", "kind": "config", "editable": true },
|
{
|
||||||
{ "key": "ops-json", "directoryKey": "minecraft-config", "label": "ops.json", "kind": "config", "editable": true },
|
"key": "server-properties",
|
||||||
{ "key": "whitelist-json", "directoryKey": "minecraft-config", "label": "whitelist.json", "kind": "config", "editable": true },
|
"directoryKey": "minecraft-config",
|
||||||
{ "key": "minecraft-latest-log", "directoryKey": "minecraft-logs", "label": "latest.log", "kind": "log", "streamKey": "minecraft.latest" }
|
"label": "server.properties",
|
||||||
|
"kind": "config",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ops-json",
|
||||||
|
"directoryKey": "minecraft-config",
|
||||||
|
"label": "ops.json",
|
||||||
|
"kind": "config",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "whitelist-json",
|
||||||
|
"directoryKey": "minecraft-config",
|
||||||
|
"label": "whitelist.json",
|
||||||
|
"kind": "config",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "minecraft-latest-log",
|
||||||
|
"directoryKey": "minecraft-logs",
|
||||||
|
"label": "latest.log",
|
||||||
|
"kind": "log",
|
||||||
|
"streamKey": "minecraft.latest"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"configFields": [
|
"configFields": [
|
||||||
{ "key": "motd", "fileKey": "server-properties", "configKey": "motd", "label": "服务器名称", "description": "显示在 Minecraft 服务器列表中的 MOTD。", "control": "text", "defaultValue": "Minecraft Server", "restartImpact": "restart-required" },
|
{
|
||||||
{ "key": "server-port", "fileKey": "server-properties", "configKey": "server-port", "label": "游戏端口", "description": "Minecraft Java 客户端连接端口。", "control": "port", "minimum": 1, "maximum": 65535, "defaultValue": "25565", "restartImpact": "restart-required" },
|
"key": "motd",
|
||||||
{ "key": "max-players", "fileKey": "server-properties", "configKey": "max-players", "label": "最大玩家数", "description": "允许同时进入服务器的玩家上限。", "control": "number", "minimum": 1, "maximum": 200, "defaultValue": "20", "restartImpact": "restart-required" },
|
"fileKey": "server-properties",
|
||||||
{ "key": "online-mode", "fileKey": "server-properties", "configKey": "online-mode", "label": "正版验证", "description": "是否启用 Mojang 在线身份验证。", "control": "boolean", "defaultValue": "true", "restartImpact": "restart-required" }
|
"configKey": "motd",
|
||||||
|
"label": "服务器名称",
|
||||||
|
"description": "显示在 Minecraft 服务器列表中的 MOTD。",
|
||||||
|
"control": "text",
|
||||||
|
"defaultValue": "Minecraft Server",
|
||||||
|
"restartImpact": "restart-required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "server-port",
|
||||||
|
"fileKey": "server-properties",
|
||||||
|
"configKey": "server-port",
|
||||||
|
"label": "游戏端口",
|
||||||
|
"description": "Minecraft Java 客户端连接端口。",
|
||||||
|
"control": "port",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535,
|
||||||
|
"defaultValue": "25565",
|
||||||
|
"restartImpact": "restart-required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "max-players",
|
||||||
|
"fileKey": "server-properties",
|
||||||
|
"configKey": "max-players",
|
||||||
|
"label": "最大玩家数",
|
||||||
|
"description": "允许同时进入服务器的玩家上限。",
|
||||||
|
"control": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 200,
|
||||||
|
"defaultValue": "20",
|
||||||
|
"restartImpact": "restart-required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "online-mode",
|
||||||
|
"fileKey": "server-properties",
|
||||||
|
"configKey": "online-mode",
|
||||||
|
"label": "正版验证",
|
||||||
|
"description": "是否启用 Mojang 在线身份验证。",
|
||||||
|
"control": "boolean",
|
||||||
|
"defaultValue": "true",
|
||||||
|
"restartImpact": "restart-required"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -74,7 +74,6 @@
|
|||||||
"remote.run.process.start",
|
"remote.run.process.start",
|
||||||
"remote.run.process.stop",
|
"remote.run.process.stop",
|
||||||
"remote.run.logs.transfer",
|
"remote.run.logs.transfer",
|
||||||
"remote.run.protected.sql",
|
|
||||||
"remote.run.rcon.command",
|
"remote.run.rcon.command",
|
||||||
"remote.run.program.command",
|
"remote.run.program.command",
|
||||||
"client-manager.deploy",
|
"client-manager.deploy",
|
||||||
@@ -99,7 +98,6 @@
|
|||||||
"remote.run.process.start",
|
"remote.run.process.start",
|
||||||
"remote.run.process.stop",
|
"remote.run.process.stop",
|
||||||
"remote.run.logs.transfer",
|
"remote.run.logs.transfer",
|
||||||
"remote.run.protected.sql",
|
|
||||||
"remote.run.rcon.command",
|
"remote.run.rcon.command",
|
||||||
"remote.run.program.command"
|
"remote.run.program.command"
|
||||||
],
|
],
|
||||||
@@ -127,7 +125,6 @@
|
|||||||
"type": "companion.diagnostics",
|
"type": "companion.diagnostics",
|
||||||
"title": "Collect companion diagnostics",
|
"title": "Collect companion diagnostics",
|
||||||
"permission": "server.game-client.read",
|
"permission": "server.game-client.read",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/diagnostics.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/diagnostics.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/diagnostics.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/diagnostics.result.schema.json",
|
||||||
"timeoutSeconds": 30,
|
"timeoutSeconds": 30,
|
||||||
@@ -137,7 +134,6 @@
|
|||||||
"type": "player.lookup",
|
"type": "player.lookup",
|
||||||
"title": "Look up SCUM player",
|
"title": "Look up SCUM player",
|
||||||
"permission": "server.game-client.read",
|
"permission": "server.game-client.read",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/player-lookup.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/player-lookup.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/player-lookup.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/player-lookup.result.schema.json",
|
||||||
"timeoutSeconds": 30,
|
"timeoutSeconds": 30,
|
||||||
@@ -147,7 +143,6 @@
|
|||||||
"type": "reward.deliver",
|
"type": "reward.deliver",
|
||||||
"title": "Deliver SCUM reward",
|
"title": "Deliver SCUM reward",
|
||||||
"permission": "server.game-client.command",
|
"permission": "server.game-client.command",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/reward-deliver.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/reward-deliver.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/reward-deliver.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/reward-deliver.result.schema.json",
|
||||||
"timeoutSeconds": 60,
|
"timeoutSeconds": 60,
|
||||||
@@ -157,7 +152,6 @@
|
|||||||
"type": "player.notify",
|
"type": "player.notify",
|
||||||
"title": "Notify SCUM player about approved gift",
|
"title": "Notify SCUM player about approved gift",
|
||||||
"permission": "server.game-client.command",
|
"permission": "server.game-client.command",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/player-notify.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/player-notify.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/player-notify.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/player-notify.result.schema.json",
|
||||||
"timeoutSeconds": 60,
|
"timeoutSeconds": 60,
|
||||||
@@ -167,7 +161,6 @@
|
|||||||
"type": "vehicle.spawn",
|
"type": "vehicle.spawn",
|
||||||
"title": "Spawn catalogued SCUM vehicle",
|
"title": "Spawn catalogued SCUM vehicle",
|
||||||
"permission": "server.game-client.command",
|
"permission": "server.game-client.command",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/vehicle-spawn.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/vehicle-spawn.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/vehicle-spawn.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/vehicle-spawn.result.schema.json",
|
||||||
"timeoutSeconds": 60,
|
"timeoutSeconds": 60,
|
||||||
@@ -177,7 +170,6 @@
|
|||||||
"type": "event.start",
|
"type": "event.start",
|
||||||
"title": "Start SCUM event",
|
"title": "Start SCUM event",
|
||||||
"permission": "server.game-client.command",
|
"permission": "server.game-client.command",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/event-start.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/event-start.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/event-start.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/event-start.result.schema.json",
|
||||||
"timeoutSeconds": 60,
|
"timeoutSeconds": 60,
|
||||||
@@ -187,7 +179,6 @@
|
|||||||
"type": "restart.prepare",
|
"type": "restart.prepare",
|
||||||
"title": "Prepare SCUM restart",
|
"title": "Prepare SCUM restart",
|
||||||
"permission": "server.game-client.maintenance",
|
"permission": "server.game-client.maintenance",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/restart-prepare.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/restart-prepare.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/restart-prepare.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/restart-prepare.result.schema.json",
|
||||||
"timeoutSeconds": 120,
|
"timeoutSeconds": 120,
|
||||||
@@ -197,7 +188,6 @@
|
|||||||
"type": "maintenance.prepare",
|
"type": "maintenance.prepare",
|
||||||
"title": "Prepare SCUM maintenance",
|
"title": "Prepare SCUM maintenance",
|
||||||
"permission": "server.game-client.maintenance",
|
"permission": "server.game-client.maintenance",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/maintenance-prepare.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/maintenance-prepare.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/maintenance-prepare.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/maintenance-prepare.result.schema.json",
|
||||||
"timeoutSeconds": 120,
|
"timeoutSeconds": 120,
|
||||||
@@ -207,7 +197,6 @@
|
|||||||
"type": "game-state.patch",
|
"type": "game-state.patch",
|
||||||
"title": "Patch SCUM player state",
|
"title": "Patch SCUM player state",
|
||||||
"permission": "server.game-client.maintenance",
|
"permission": "server.game-client.maintenance",
|
||||||
"approvalLevel": "none",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/game-state-patch.payload.schema.json",
|
"payloadSchemaRef": "schemas/bridge/game-state-patch.payload.schema.json",
|
||||||
"resultSchemaRef": "schemas/bridge/game-state-patch.result.schema.json",
|
"resultSchemaRef": "schemas/bridge/game-state-patch.result.schema.json",
|
||||||
"timeoutSeconds": 120,
|
"timeoutSeconds": 120,
|
||||||
@@ -286,9 +275,26 @@
|
|||||||
"pollIntervalSeconds": 3,
|
"pollIntervalSeconds": 3,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_users",
|
"collection": "scum_users",
|
||||||
"upsertKeys": ["steamId"],
|
"upsertKeys": [
|
||||||
|
"steamId"
|
||||||
|
],
|
||||||
"writeMode": "merge",
|
"writeMode": "merge",
|
||||||
"columnMappings": { "userProfileId": "userProfileId", "steamId": "steamId", "gamePlayerId": "gamePlayerId", "displayName": "displayName", "squadId": "squadId", "squadName": "squadName", "famePoints": "famePoints", "normalBalance": "normalBalance", "goldBalance": "goldBalance", "x": "x", "y": "y", "z": "z", "lastLoginTime": "lastLoginTime", "lastSaveTime": "lastSaveTime" }
|
"columnMappings": {
|
||||||
|
"userProfileId": "userProfileId",
|
||||||
|
"steamId": "steamId",
|
||||||
|
"gamePlayerId": "gamePlayerId",
|
||||||
|
"displayName": "displayName",
|
||||||
|
"squadId": "squadId",
|
||||||
|
"squadName": "squadName",
|
||||||
|
"famePoints": "famePoints",
|
||||||
|
"normalBalance": "normalBalance",
|
||||||
|
"goldBalance": "goldBalance",
|
||||||
|
"x": "x",
|
||||||
|
"y": "y",
|
||||||
|
"z": "z",
|
||||||
|
"lastLoginTime": "lastLoginTime",
|
||||||
|
"lastSaveTime": "lastSaveTime"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -306,9 +312,22 @@
|
|||||||
"pollIntervalSeconds": 1800,
|
"pollIntervalSeconds": 1800,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_squads",
|
"collection": "scum_squads",
|
||||||
"upsertKeys": ["squadId"],
|
"upsertKeys": [
|
||||||
|
"squadId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "squadId": "squadId", "name": "name", "leaderProfileId": "leaderProfileId", "leaderPlayerId": "leaderPlayerId", "memberCount": "memberCount", "score": "score", "memberLimit": "memberLimit", "message": "message", "info": "info", "lastMemberLoginTime": "lastMemberLoginTime" }
|
"columnMappings": {
|
||||||
|
"squadId": "squadId",
|
||||||
|
"name": "name",
|
||||||
|
"leaderProfileId": "leaderProfileId",
|
||||||
|
"leaderPlayerId": "leaderPlayerId",
|
||||||
|
"memberCount": "memberCount",
|
||||||
|
"score": "score",
|
||||||
|
"memberLimit": "memberLimit",
|
||||||
|
"message": "message",
|
||||||
|
"info": "info",
|
||||||
|
"lastMemberLoginTime": "lastMemberLoginTime"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -326,9 +345,20 @@
|
|||||||
"pollIntervalSeconds": 1800,
|
"pollIntervalSeconds": 1800,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_squad_members",
|
"collection": "scum_squad_members",
|
||||||
"upsertKeys": ["squadId", "steamId"],
|
"upsertKeys": [
|
||||||
|
"squadId",
|
||||||
|
"steamId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "squadId": "squadId", "userProfileId": "userProfileId", "gamePlayerId": "gamePlayerId", "steamId": "steamId", "displayName": "displayName", "rank": "rank", "isLeader": "isLeader" }
|
"columnMappings": {
|
||||||
|
"squadId": "squadId",
|
||||||
|
"userProfileId": "userProfileId",
|
||||||
|
"gamePlayerId": "gamePlayerId",
|
||||||
|
"steamId": "steamId",
|
||||||
|
"displayName": "displayName",
|
||||||
|
"rank": "rank",
|
||||||
|
"isLeader": "isLeader"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -346,9 +376,21 @@
|
|||||||
"pollIntervalSeconds": 3,
|
"pollIntervalSeconds": 3,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_vehicles",
|
"collection": "scum_vehicles",
|
||||||
"upsertKeys": ["vehicleId"],
|
"upsertKeys": [
|
||||||
|
"vehicleId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "vehicleId": "vehicleId", "entityId": "entityId", "className": "className", "label": "label", "x": "x", "y": "y", "z": "z", "lastAccessTime": "lastAccessTime", "isFunctional": "isFunctional" }
|
"columnMappings": {
|
||||||
|
"vehicleId": "vehicleId",
|
||||||
|
"entityId": "entityId",
|
||||||
|
"className": "className",
|
||||||
|
"label": "label",
|
||||||
|
"x": "x",
|
||||||
|
"y": "y",
|
||||||
|
"z": "z",
|
||||||
|
"lastAccessTime": "lastAccessTime",
|
||||||
|
"isFunctional": "isFunctional"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -366,9 +408,25 @@
|
|||||||
"pollIntervalSeconds": 1800,
|
"pollIntervalSeconds": 1800,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_flags",
|
"collection": "scum_flags",
|
||||||
"upsertKeys": ["flagId"],
|
"upsertKeys": [
|
||||||
|
"flagId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "flagId": "flagId", "entityId": "entityId", "baseId": "baseId", "ownerProfileId": "ownerProfileId", "ownerPlayerId": "ownerPlayerId", "ownerSquadId": "ownerSquadId", "ownerSquadName": "ownerSquadName", "overtakerProfileId": "overtakerProfileId", "overtakeEndTime": "overtakeEndTime", "ownershipConfidence": "ownershipConfidence", "x": "x", "y": "y", "z": "z" }
|
"columnMappings": {
|
||||||
|
"flagId": "flagId",
|
||||||
|
"entityId": "entityId",
|
||||||
|
"baseId": "baseId",
|
||||||
|
"ownerProfileId": "ownerProfileId",
|
||||||
|
"ownerPlayerId": "ownerPlayerId",
|
||||||
|
"ownerSquadId": "ownerSquadId",
|
||||||
|
"ownerSquadName": "ownerSquadName",
|
||||||
|
"overtakerProfileId": "overtakerProfileId",
|
||||||
|
"overtakeEndTime": "overtakeEndTime",
|
||||||
|
"ownershipConfidence": "ownershipConfidence",
|
||||||
|
"x": "x",
|
||||||
|
"y": "y",
|
||||||
|
"z": "z"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -386,9 +444,24 @@
|
|||||||
"pollIntervalSeconds": 3,
|
"pollIntervalSeconds": 3,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_map_points",
|
"collection": "scum_map_points",
|
||||||
"upsertKeys": ["subjectType", "subjectId"],
|
"upsertKeys": [
|
||||||
|
"subjectType",
|
||||||
|
"subjectId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "subjectType": "subjectType", "subjectId": "subjectId", "userProfileId": "userProfileId", "gamePlayerId": "gamePlayerId", "vehicleId": "vehicleId", "entityId": "entityId", "baseId": "baseId", "x": "x", "y": "y", "z": "z", "observedAt": "observedAt" }
|
"columnMappings": {
|
||||||
|
"subjectType": "subjectType",
|
||||||
|
"subjectId": "subjectId",
|
||||||
|
"userProfileId": "userProfileId",
|
||||||
|
"gamePlayerId": "gamePlayerId",
|
||||||
|
"vehicleId": "vehicleId",
|
||||||
|
"entityId": "entityId",
|
||||||
|
"baseId": "baseId",
|
||||||
|
"x": "x",
|
||||||
|
"y": "y",
|
||||||
|
"z": "z",
|
||||||
|
"observedAt": "observedAt"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -406,9 +479,22 @@
|
|||||||
"pollIntervalSeconds": 1800,
|
"pollIntervalSeconds": 1800,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_tasks",
|
"collection": "scum_tasks",
|
||||||
"upsertKeys": ["taskRecordId"],
|
"upsertKeys": [
|
||||||
|
"taskRecordId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "taskRecordId": "taskRecordId", "taskKind": "taskKind", "userProfileId": "userProfileId", "mapId": "mapId", "trackingDataSetId": "trackingDataSetId", "dataAssetPath": "dataAssetPath", "sequenceIndex": "sequenceIndex", "isTracked": "isTracked", "state": "state", "completionDeadline": "completionDeadline" }
|
"columnMappings": {
|
||||||
|
"taskRecordId": "taskRecordId",
|
||||||
|
"taskKind": "taskKind",
|
||||||
|
"userProfileId": "userProfileId",
|
||||||
|
"mapId": "mapId",
|
||||||
|
"trackingDataSetId": "trackingDataSetId",
|
||||||
|
"dataAssetPath": "dataAssetPath",
|
||||||
|
"sequenceIndex": "sequenceIndex",
|
||||||
|
"isTracked": "isTracked",
|
||||||
|
"state": "state",
|
||||||
|
"completionDeadline": "completionDeadline"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -426,9 +512,25 @@
|
|||||||
"pollIntervalSeconds": 1800,
|
"pollIntervalSeconds": 1800,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_native_event_rounds",
|
"collection": "scum_native_event_rounds",
|
||||||
"upsertKeys": ["eventRecordId"],
|
"upsertKeys": [
|
||||||
|
"eventRecordId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "eventRecordId": "eventRecordId", "eventId": "eventId", "roundId": "roundId", "userProfileId": "userProfileId", "startTime": "startTime", "endTime": "endTime", "state": "state", "score": "score", "enemyKills": "enemyKills", "teamKills": "teamKills", "deaths": "deaths", "assists": "assists", "headshots": "headshots" }
|
"columnMappings": {
|
||||||
|
"eventRecordId": "eventRecordId",
|
||||||
|
"eventId": "eventId",
|
||||||
|
"roundId": "roundId",
|
||||||
|
"userProfileId": "userProfileId",
|
||||||
|
"startTime": "startTime",
|
||||||
|
"endTime": "endTime",
|
||||||
|
"state": "state",
|
||||||
|
"score": "score",
|
||||||
|
"enemyKills": "enemyKills",
|
||||||
|
"teamKills": "teamKills",
|
||||||
|
"deaths": "deaths",
|
||||||
|
"assists": "assists",
|
||||||
|
"headshots": "headshots"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -446,9 +548,17 @@
|
|||||||
"pollIntervalSeconds": 1800,
|
"pollIntervalSeconds": 1800,
|
||||||
"rowTarget": {
|
"rowTarget": {
|
||||||
"collection": "scum_timed_gift_events",
|
"collection": "scum_timed_gift_events",
|
||||||
"upsertKeys": ["timedGiftId"],
|
"upsertKeys": [
|
||||||
|
"timedGiftId"
|
||||||
|
],
|
||||||
"writeMode": "replace",
|
"writeMode": "replace",
|
||||||
"columnMappings": { "timedGiftId": "timedGiftId", "userProfileId": "userProfileId", "mapId": "mapId", "spawnTime": "spawnTime", "spawnAt": "spawnAt" }
|
"columnMappings": {
|
||||||
|
"timedGiftId": "timedGiftId",
|
||||||
|
"userProfileId": "userProfileId",
|
||||||
|
"mapId": "mapId",
|
||||||
|
"spawnTime": "spawnTime",
|
||||||
|
"spawnAt": "spawnAt"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
@@ -457,18 +567,35 @@
|
|||||||
"logProjections": [
|
"logProjections": [
|
||||||
{
|
{
|
||||||
"key": "scum.battleye.login",
|
"key": "scum.battleye.login",
|
||||||
"streamKeys": ["scum.console.stdout"],
|
"streamKeys": [
|
||||||
|
"scum.console.stdout"
|
||||||
|
],
|
||||||
"steps": [
|
"steps": [
|
||||||
{ "pattern": "Player \"(?P<displayName>[^\"]+)\" reported as player (?P<slot>\\d+)" },
|
{
|
||||||
{ "pattern": "Player (?P<slot>\\d+) SteamID \\(assumed\\): (?P<steamId>\\d+)" }
|
"pattern": "Player \"(?P<displayName>[^\"]+)\" reported as player (?P<slot>\\d+)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "Player (?P<slot>\\d+) SteamID \\(assumed\\): (?P<steamId>\\d+)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"correlationFields": [
|
||||||
|
"slot"
|
||||||
],
|
],
|
||||||
"correlationFields": ["slot"],
|
|
||||||
"maxInterveningLines": 8,
|
"maxInterveningLines": 8,
|
||||||
"target": {
|
"target": {
|
||||||
"collection": "scum_users",
|
"collection": "scum_users",
|
||||||
"upsertKeys": ["steamId"],
|
"upsertKeys": [
|
||||||
"captureMappings": { "steamId": "steamId", "displayName": "displayName", "slot": "slot" },
|
"steamId"
|
||||||
"fixedValues": { "online": "true", "source": "process.stdout" },
|
],
|
||||||
|
"captureMappings": {
|
||||||
|
"steamId": "steamId",
|
||||||
|
"displayName": "displayName",
|
||||||
|
"slot": "slot"
|
||||||
|
},
|
||||||
|
"fixedValues": {
|
||||||
|
"online": "true",
|
||||||
|
"source": "process.stdout"
|
||||||
|
},
|
||||||
"observedAtField": "lastLoginObservedAt"
|
"observedAtField": "lastLoginObservedAt"
|
||||||
},
|
},
|
||||||
"presence": {
|
"presence": {
|
||||||
@@ -476,9 +603,18 @@
|
|||||||
"activeWindowSeconds": 600,
|
"activeWindowSeconds": 600,
|
||||||
"activityTarget": {
|
"activityTarget": {
|
||||||
"collection": "scum_activity_events",
|
"collection": "scum_activity_events",
|
||||||
"upsertKeys": ["steamId", "observedAt"],
|
"upsertKeys": [
|
||||||
"captureMappings": { "steamId": "steamId", "displayName": "displayName" },
|
"steamId",
|
||||||
"fixedValues": { "eventType": "login", "source": "process.stdout" },
|
"observedAt"
|
||||||
|
],
|
||||||
|
"captureMappings": {
|
||||||
|
"steamId": "steamId",
|
||||||
|
"displayName": "displayName"
|
||||||
|
},
|
||||||
|
"fixedValues": {
|
||||||
|
"eventType": "login",
|
||||||
|
"source": "process.stdout"
|
||||||
|
},
|
||||||
"observedAtField": "observedAt"
|
"observedAtField": "observedAt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,109 +624,16 @@
|
|||||||
{
|
{
|
||||||
"key": "scum-db-v57",
|
"key": "scum-db-v57",
|
||||||
"databaseUserVersion": 57,
|
"databaseUserVersion": 57,
|
||||||
"logParserRefs": ["data-packs/scum-db-v57/log-parsers.json"],
|
"logParserRefs": [
|
||||||
"configMapRefs": ["data-packs/scum-db-v57/config-maps.json"],
|
"data-packs/scum-db-v57/log-parsers.json"
|
||||||
"dataRefs": ["data-packs/scum-db-v57/gift-items.json", "data-packs/scum-db-v57/map-geometry.json"]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"operationTemplates": [
|
"configMapRefs": [
|
||||||
{
|
"data-packs/scum-db-v57/config-maps.json"
|
||||||
"key": "player.fame.set",
|
],
|
||||||
"title": "Set SCUM player fame through RCON",
|
"dataRefs": [
|
||||||
"permission": "server.game-client.command",
|
"data-packs/scum-db-v57/gift-items.json",
|
||||||
"approvalLevel": "none",
|
"data-packs/scum-db-v57/map-geometry.json"
|
||||||
"kind": "rcon",
|
]
|
||||||
"transportKey": "scum-management",
|
|
||||||
"targetKey": "scum-management",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/player-fame-set.payload.schema.json",
|
|
||||||
"resultSchemaRef": "schemas/bridge/player-rcon-set.result.schema.json",
|
|
||||||
"timeoutSeconds": 60,
|
|
||||||
"maxPayloadBytes": 2048
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "player.currency.normal.set",
|
|
||||||
"title": "Set SCUM normal currency through RCON",
|
|
||||||
"permission": "server.game-client.command",
|
|
||||||
"approvalLevel": "none",
|
|
||||||
"kind": "rcon",
|
|
||||||
"transportKey": "scum-management",
|
|
||||||
"targetKey": "scum-management",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/player-currency-set.payload.schema.json",
|
|
||||||
"resultSchemaRef": "schemas/bridge/player-rcon-set.result.schema.json",
|
|
||||||
"timeoutSeconds": 60,
|
|
||||||
"maxPayloadBytes": 2048
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "player.currency.gold.set",
|
|
||||||
"title": "Set SCUM gold currency through RCON",
|
|
||||||
"permission": "server.game-client.command",
|
|
||||||
"approvalLevel": "none",
|
|
||||||
"kind": "rcon",
|
|
||||||
"transportKey": "scum-management",
|
|
||||||
"targetKey": "scum-management",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/player-currency-set.payload.schema.json",
|
|
||||||
"resultSchemaRef": "schemas/bridge/player-rcon-set.result.schema.json",
|
|
||||||
"timeoutSeconds": 60,
|
|
||||||
"maxPayloadBytes": 2048
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "player.notify",
|
|
||||||
"title": "Notify SCUM player through RCON chat",
|
|
||||||
"permission": "server.game-client.command",
|
|
||||||
"approvalLevel": "none",
|
|
||||||
"kind": "rcon",
|
|
||||||
"transportKey": "scum-management",
|
|
||||||
"targetKey": "scum-management",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/player-notify.payload.schema.json",
|
|
||||||
"resultSchemaRef": "schemas/bridge/player-notify.result.schema.json",
|
|
||||||
"timeoutSeconds": 60,
|
|
||||||
"maxPayloadBytes": 2048
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "reward.deliver",
|
|
||||||
"title": "Deliver SCUM reward through typed command workflow",
|
|
||||||
"permission": "server.game-client.command",
|
|
||||||
"approvalLevel": "none",
|
|
||||||
"kind": "rcon",
|
|
||||||
"transportKey": "scum-management",
|
|
||||||
"targetKey": "scum-management",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/reward-deliver.payload.schema.json",
|
|
||||||
"resultSchemaRef": "schemas/bridge/reward-deliver.result.schema.json",
|
|
||||||
"timeoutSeconds": 60,
|
|
||||||
"maxPayloadBytes": 4096
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "player.attribute.855.set",
|
|
||||||
"title": "Set SCUM DB-only player attribute 855",
|
|
||||||
"permission": "server.game-client.maintenance",
|
|
||||||
"approvalLevel": "platform-admin",
|
|
||||||
"kind": "sqlite-mutation",
|
|
||||||
"transportKey": "scum-database",
|
|
||||||
"targetKey": "scum-database",
|
|
||||||
"payloadSchemaRef": "schemas/bridge/player-attribute-855-set.payload.schema.json",
|
|
||||||
"resultSchemaRef": "schemas/bridge/player-attribute-855-set.result.schema.json",
|
|
||||||
"confirmationSchemaRef": "schemas/bridge/player-attribute-855-set.confirmation.schema.json",
|
|
||||||
"timeoutSeconds": 120,
|
|
||||||
"maxPayloadBytes": 4096,
|
|
||||||
"maxRowsAffected": 1,
|
|
||||||
"mutation": {
|
|
||||||
"fieldKey": "855",
|
|
||||||
"tableKey": "prisoner",
|
|
||||||
"identityKey": "user_profile_id",
|
|
||||||
"valueKey": "value",
|
|
||||||
"confirmationQueryKey": "scum.player.profile",
|
|
||||||
"allowedValueType": "integer",
|
|
||||||
"minValue": 0,
|
|
||||||
"maxValue": 100000
|
|
||||||
},
|
|
||||||
"safety": {
|
|
||||||
"requiresApproval": true,
|
|
||||||
"requiresOfflinePlayer": true,
|
|
||||||
"requiresMaintenanceWindow": true,
|
|
||||||
"requiresBeforeValue": true,
|
|
||||||
"requiresConfirmation": true,
|
|
||||||
"backupRequired": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commandRetentionSeconds": 604800,
|
"commandRetentionSeconds": 604800,
|
||||||
@@ -652,13 +695,6 @@
|
|||||||
"scum.player.profile",
|
"scum.player.profile",
|
||||||
"scum.positions"
|
"scum.positions"
|
||||||
],
|
],
|
||||||
"operationKeys": [
|
|
||||||
"player.fame.set",
|
|
||||||
"player.currency.normal.set",
|
|
||||||
"player.currency.gold.set",
|
|
||||||
"player.notify",
|
|
||||||
"player.attribute.855.set"
|
|
||||||
],
|
|
||||||
"featureKeys": [
|
"featureKeys": [
|
||||||
"player.intelligence",
|
"player.intelligence",
|
||||||
"state.patch"
|
"state.patch"
|
||||||
@@ -707,10 +743,6 @@
|
|||||||
"commandTypes": [
|
"commandTypes": [
|
||||||
"reward.deliver"
|
"reward.deliver"
|
||||||
],
|
],
|
||||||
"operationKeys": [
|
|
||||||
"reward.deliver",
|
|
||||||
"player.notify"
|
|
||||||
],
|
|
||||||
"featureKeys": [
|
"featureKeys": [
|
||||||
"reward.delivery"
|
"reward.delivery"
|
||||||
]
|
]
|
||||||
@@ -873,12 +905,7 @@
|
|||||||
"retire",
|
"retire",
|
||||||
"dependency-check"
|
"dependency-check"
|
||||||
],
|
],
|
||||||
"dependencyPolicy": "required",
|
"dependencyPolicy": "required"
|
||||||
"approvalRequired": [
|
|
||||||
"disable",
|
|
||||||
"rollback",
|
|
||||||
"retire"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
@@ -1359,8 +1386,7 @@
|
|||||||
"kind": "sqlite",
|
"kind": "sqlite",
|
||||||
"targetKey": "scum-database",
|
"targetKey": "scum-database",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"remote.run.db.sqlite.query",
|
"remote.run.db.sqlite.query"
|
||||||
"remote.run.protected.sql"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -164,7 +164,7 @@
|
|||||||
},
|
},
|
||||||
"productionLifecycle": {
|
"productionLifecycle": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["operations", "dependencyPolicy", "approvalRequired"],
|
"required": ["operations", "dependencyPolicy"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"operations": {
|
"operations": {
|
||||||
@@ -173,12 +173,7 @@
|
|||||||
"uniqueItems": true,
|
"uniqueItems": true,
|
||||||
"minItems": 1
|
"minItems": 1
|
||||||
},
|
},
|
||||||
"dependencyPolicy": { "enum": ["required", "optional"] },
|
"dependencyPolicy": { "enum": ["required", "optional"] }
|
||||||
"approvalRequired": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "enum": ["disable", "rollback", "retire"] },
|
|
||||||
"uniqueItems": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
@@ -269,11 +264,6 @@
|
|||||||
"items": { "$ref": "#/$defs/gameClientBridgeDataPack" },
|
"items": { "$ref": "#/$defs/gameClientBridgeDataPack" },
|
||||||
"maxItems": 64
|
"maxItems": 64
|
||||||
},
|
},
|
||||||
"operationTemplates": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "$ref": "#/$defs/gameClientBridgeOperationTemplate" },
|
|
||||||
"maxItems": 128
|
|
||||||
},
|
|
||||||
"commandRetentionSeconds": { "type": "integer", "minimum": 1, "maximum": 31536000 },
|
"commandRetentionSeconds": { "type": "integer", "minimum": 1, "maximum": 31536000 },
|
||||||
"maxCommands": { "type": "integer", "minimum": 1, "maximum": 100000 },
|
"maxCommands": { "type": "integer", "minimum": 1, "maximum": 100000 },
|
||||||
"pages": {
|
"pages": {
|
||||||
@@ -307,13 +297,12 @@
|
|||||||
},
|
},
|
||||||
"gameClientBridgeCommand": {
|
"gameClientBridgeCommand": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["type", "title", "permission", "approvalLevel", "payloadSchemaRef", "timeoutSeconds", "maxPayloadBytes"],
|
"required": ["type", "title", "permission", "payloadSchemaRef", "timeoutSeconds", "maxPayloadBytes"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$" },
|
"type": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$" },
|
||||||
"title": { "type": "string", "minLength": 1, "maxLength": 80 },
|
"title": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||||
"permission": { "$ref": "#/$defs/pluginPermission" },
|
"permission": { "$ref": "#/$defs/pluginPermission" },
|
||||||
"approvalLevel": { "enum": ["none", "operator", "platform-admin"] },
|
|
||||||
"payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
"payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
||||||
"resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
"resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
||||||
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 3600 },
|
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 3600 },
|
||||||
@@ -419,55 +408,6 @@
|
|||||||
"dataRefs": { "type": "array", "items": { "$ref": "#/$defs/relativeJsonRef" }, "uniqueItems": true }
|
"dataRefs": { "type": "array", "items": { "$ref": "#/$defs/relativeJsonRef" }, "uniqueItems": true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gameClientBridgeOperationSafety": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"requiresApproval": { "type": "boolean" },
|
|
||||||
"requiresOfflinePlayer": { "type": "boolean" },
|
|
||||||
"requiresMaintenanceWindow": { "type": "boolean" },
|
|
||||||
"requiresBeforeValue": { "type": "boolean" },
|
|
||||||
"requiresConfirmation": { "type": "boolean" },
|
|
||||||
"backupRequired": { "type": "boolean" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gameClientBridgeOperationMutation": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["fieldKey", "tableKey", "identityKey", "valueKey", "confirmationQueryKey", "allowedValueType"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"fieldKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"tableKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"identityKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"valueKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"confirmationQueryKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"allowedValueType": { "enum": ["integer", "number", "string", "boolean"] },
|
|
||||||
"minValue": { "type": "number" },
|
|
||||||
"maxValue": { "type": "number" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gameClientBridgeOperationTemplate": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["key", "title", "permission", "approvalLevel", "kind", "transportKey", "targetKey", "payloadSchemaRef", "timeoutSeconds", "maxPayloadBytes"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"key": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$" },
|
|
||||||
"title": { "type": "string", "minLength": 1, "maxLength": 80 },
|
|
||||||
"permission": { "$ref": "#/$defs/pluginPermission" },
|
|
||||||
"approvalLevel": { "enum": ["none", "operator", "platform-admin"] },
|
|
||||||
"kind": { "enum": ["rcon", "sqlite-mutation"] },
|
|
||||||
"transportKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
|
||||||
"resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
|
||||||
"confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
|
||||||
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 3600 },
|
|
||||||
"maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 },
|
|
||||||
"maxRowsAffected": { "type": "integer", "minimum": 1, "maximum": 10 },
|
|
||||||
"mutation": { "$ref": "#/$defs/gameClientBridgeOperationMutation" },
|
|
||||||
"safety": { "$ref": "#/$defs/gameClientBridgeOperationSafety" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gameClientBridgePageContract": {
|
"gameClientBridgePageContract": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["pageKey"],
|
"required": ["pageKey"],
|
||||||
@@ -477,7 +417,6 @@
|
|||||||
"commandTypes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
"commandTypes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||||
"snapshotTypes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
"snapshotTypes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||||
"queryTemplateKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
"queryTemplateKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||||
"operationKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
|
||||||
"featureKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
"featureKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -525,7 +464,6 @@
|
|||||||
"remote.run.db.sqlite.query",
|
"remote.run.db.sqlite.query",
|
||||||
"remote.run.logs.transfer",
|
"remote.run.logs.transfer",
|
||||||
"remote.run.rcon.command",
|
"remote.run.rcon.command",
|
||||||
"remote.run.protected.sql",
|
|
||||||
"remote.run.program.command",
|
"remote.run.program.command",
|
||||||
"client-manager.deploy",
|
"client-manager.deploy",
|
||||||
"client-manager.control",
|
"client-manager.control",
|
||||||
|
|||||||
@@ -675,7 +675,7 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
|||||||
if (typeof manifest !== "object" || manifest === null) {
|
if (typeof manifest !== "object" || manifest === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
type BridgeCommand = { type?: string; approvalLevel?: string; payloadSchemaRef?: string; resultSchemaRef?: string };
|
type BridgeCommand = { type?: string; payloadSchemaRef?: string; resultSchemaRef?: string };
|
||||||
type BridgeQueryTemplate = {
|
type BridgeQueryTemplate = {
|
||||||
key?: string;
|
key?: string;
|
||||||
permission?: string;
|
permission?: string;
|
||||||
@@ -704,25 +704,7 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
|||||||
activityTarget?: BridgeLogProjectionTarget;
|
activityTarget?: BridgeLogProjectionTarget;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
type BridgeOperationSafety = { requiresApproval?: boolean; requiresOfflinePlayer?: boolean; requiresMaintenanceWindow?: boolean; requiresBeforeValue?: boolean; requiresConfirmation?: boolean; backupRequired?: boolean };
|
type BridgePage = { pageKey?: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] };
|
||||||
type BridgeOperationMutation = { fieldKey?: string; tableKey?: string; identityKey?: string; valueKey?: string; confirmationQueryKey?: string; allowedValueType?: string; minValue?: number; maxValue?: number };
|
|
||||||
type BridgeOperationTemplate = {
|
|
||||||
key?: string;
|
|
||||||
permission?: string;
|
|
||||||
approvalLevel?: string;
|
|
||||||
kind?: string;
|
|
||||||
transportKey?: string;
|
|
||||||
targetKey?: string;
|
|
||||||
payloadSchemaRef?: string;
|
|
||||||
resultSchemaRef?: string;
|
|
||||||
confirmationSchemaRef?: string;
|
|
||||||
timeoutSeconds?: number;
|
|
||||||
maxPayloadBytes?: number;
|
|
||||||
maxRowsAffected?: number;
|
|
||||||
mutation?: BridgeOperationMutation;
|
|
||||||
safety?: BridgeOperationSafety;
|
|
||||||
};
|
|
||||||
type BridgePage = { pageKey?: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[]; operationKeys?: string[] };
|
|
||||||
type BridgeCompanion = {
|
type BridgeCompanion = {
|
||||||
profileKey?: string;
|
profileKey?: string;
|
||||||
configTemplateKey?: string;
|
configTemplateKey?: string;
|
||||||
@@ -745,7 +727,7 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
|||||||
remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] };
|
remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] };
|
||||||
pages?: PluginPage[];
|
pages?: PluginPage[];
|
||||||
runtimeProfiles?: { transportProfiles?: RuntimeTransportProfile[]; clientManagers?: RuntimeClientManager[] };
|
runtimeProfiles?: { transportProfiles?: RuntimeTransportProfile[]; clientManagers?: RuntimeClientManager[] };
|
||||||
gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: Array<{ type?: string }>; queryTemplates?: BridgeQueryTemplate[]; logProjections?: BridgeLogProjection[]; operationTemplates?: BridgeOperationTemplate[]; pages?: BridgePage[]; companion?: BridgeCompanion };
|
gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: Array<{ type?: string }>; queryTemplates?: BridgeQueryTemplate[]; logProjections?: BridgeLogProjection[]; pages?: BridgePage[]; companion?: BridgeCompanion };
|
||||||
};
|
};
|
||||||
const bridge = declaration.gameClientBridge;
|
const bridge = declaration.gameClientBridge;
|
||||||
if (!bridge) {
|
if (!bridge) {
|
||||||
@@ -756,7 +738,6 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
|||||||
const snapshots = new Set((bridge.snapshots ?? []).map((snapshot) => snapshot.type ?? ""));
|
const snapshots = new Set((bridge.snapshots ?? []).map((snapshot) => snapshot.type ?? ""));
|
||||||
const queryTemplates = new Map<string, BridgeQueryTemplate>();
|
const queryTemplates = new Map<string, BridgeQueryTemplate>();
|
||||||
const logProjections = new Set<string>();
|
const logProjections = new Set<string>();
|
||||||
const operationTemplates = new Map<string, BridgeOperationTemplate>();
|
|
||||||
const declaredPermissions = new Set(declaration.permissions ?? []);
|
const declaredPermissions = new Set(declaration.permissions ?? []);
|
||||||
const declaredCapabilities = new Set(declaration.capabilities ?? []);
|
const declaredCapabilities = new Set(declaration.capabilities ?? []);
|
||||||
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
|
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
|
||||||
@@ -931,92 +912,6 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
|||||||
if (!Number.isInteger(presence.activeWindowSeconds) || (presence.activeWindowSeconds ?? 0) < 1 || (presence.activeWindowSeconds ?? 0) > 31536000) errors.push(`${location}.presence.activeWindowSeconds: must be between 1 and 31536000`);
|
if (!Number.isInteger(presence.activeWindowSeconds) || (presence.activeWindowSeconds ?? 0) < 1 || (presence.activeWindowSeconds ?? 0) > 31536000) errors.push(`${location}.presence.activeWindowSeconds: must be between 1 and 31536000`);
|
||||||
if (presence.activityTarget) errors.push(...validateProjectionTarget(`${location}.presence.activityTarget`, presence.activityTarget, captures));
|
if (presence.activityTarget) errors.push(...validateProjectionTarget(`${location}.presence.activityTarget`, presence.activityTarget, captures));
|
||||||
}
|
}
|
||||||
for (const [index, operationTemplate] of (bridge.operationTemplates ?? []).entries()) {
|
|
||||||
const location = `manifest.gameClientBridge.operationTemplates[${index}]`;
|
|
||||||
const key = operationTemplate.key ?? "";
|
|
||||||
const unsafeReason = unsafeGameClientBridgeCommandTypeReason(key);
|
|
||||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(key) || unsafeReason) {
|
|
||||||
errors.push(`${location}.key: ${unsafeReason ?? "operation template key is unsafe"}`);
|
|
||||||
}
|
|
||||||
if (operationTemplates.has(key)) {
|
|
||||||
errors.push(`${location}.key: duplicate operation template ${key}`);
|
|
||||||
}
|
|
||||||
operationTemplates.set(key, operationTemplate);
|
|
||||||
if (!operationTemplate.permission || !declaredPermissions.has(operationTemplate.permission)) {
|
|
||||||
errors.push(`${location}.permission: permission must be declared by the plugin manifest`);
|
|
||||||
}
|
|
||||||
if (!new Set(["none", "operator", "platform-admin"]).has(operationTemplate.approvalLevel ?? "")) {
|
|
||||||
errors.push(`${location}.approvalLevel: must be none, operator, or platform-admin`);
|
|
||||||
}
|
|
||||||
if (!new Set(["rcon", "sqlite-mutation"]).has(operationTemplate.kind ?? "")) {
|
|
||||||
errors.push(`${location}.kind: must be rcon or sqlite-mutation`);
|
|
||||||
}
|
|
||||||
for (const [field, ref] of [["payloadSchemaRef", operationTemplate.payloadSchemaRef], ["resultSchemaRef", operationTemplate.resultSchemaRef], ["confirmationSchemaRef", operationTemplate.confirmationSchemaRef]] as const) {
|
|
||||||
if ((field === "payloadSchemaRef" && !ref) || (ref && !isSafeRelativeJsonRef(ref))) {
|
|
||||||
errors.push(`${location}.${field}: raw host paths and unsafe schema references are not allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(operationTemplate.timeoutSeconds) || (operationTemplate.timeoutSeconds ?? 0) < 1 || (operationTemplate.timeoutSeconds ?? 0) > 3600) {
|
|
||||||
errors.push(`${location}.timeoutSeconds: must be an integer between 1 and 3600`);
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(operationTemplate.maxPayloadBytes) || (operationTemplate.maxPayloadBytes ?? 0) < 1 || (operationTemplate.maxPayloadBytes ?? 0) > 65536) {
|
|
||||||
errors.push(`${location}.maxPayloadBytes: must be an integer between 1 and 65536`);
|
|
||||||
}
|
|
||||||
const transport = transportProfiles.find((profile) => profile.key === operationTemplate.transportKey);
|
|
||||||
if (!transport) {
|
|
||||||
errors.push(`${location}.transportKey: undeclared transport profile ${operationTemplate.transportKey ?? ""}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!operationTemplate.targetKey || transport.targetKey !== operationTemplate.targetKey) {
|
|
||||||
errors.push(`${location}.targetKey: must match the declared runtime transport target`);
|
|
||||||
}
|
|
||||||
if (operationTemplate.kind === "rcon") {
|
|
||||||
if (transport.kind !== "rcon" || !transport.capabilities?.includes("remote.run.rcon.command")) {
|
|
||||||
errors.push(`${location}.transportKey: rcon operations require remote.run.rcon.command transport`);
|
|
||||||
}
|
|
||||||
if (operationTemplate.maxRowsAffected !== undefined) {
|
|
||||||
errors.push(`${location}.maxRowsAffected: only sqlite-mutation operations may declare affected row bounds`);
|
|
||||||
}
|
|
||||||
if (operationTemplate.mutation !== undefined) {
|
|
||||||
errors.push(`${location}.mutation: only sqlite-mutation operations may declare mutation metadata`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (operationTemplate.kind === "sqlite-mutation") {
|
|
||||||
if (transport.kind !== "sqlite" || !transport.capabilities?.includes("remote.run.protected.sql")) {
|
|
||||||
errors.push(`${location}.transportKey: sqlite-mutation operations require sqlite remote.run.protected.sql transport`);
|
|
||||||
}
|
|
||||||
if (operationTemplate.approvalLevel !== "platform-admin") {
|
|
||||||
errors.push(`${location}.approvalLevel: sqlite-mutation operations require platform-admin approval`);
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(operationTemplate.maxRowsAffected) || (operationTemplate.maxRowsAffected ?? 0) < 1 || (operationTemplate.maxRowsAffected ?? 0) > 10) {
|
|
||||||
errors.push(`${location}.maxRowsAffected: must be an integer between 1 and 10`);
|
|
||||||
}
|
|
||||||
const safety = operationTemplate.safety;
|
|
||||||
if (!safety?.requiresBeforeValue || !safety.requiresConfirmation || (!safety.requiresOfflinePlayer && !safety.requiresMaintenanceWindow)) {
|
|
||||||
errors.push(`${location}.safety: sqlite-mutation operations require before value, confirmation, and offline or maintenance protection`);
|
|
||||||
}
|
|
||||||
const mutation = operationTemplate.mutation;
|
|
||||||
if (!mutation) {
|
|
||||||
errors.push(`${location}.mutation: sqlite-mutation operations require field/table/identity metadata`);
|
|
||||||
} else {
|
|
||||||
for (const field of ["fieldKey", "tableKey", "identityKey", "valueKey", "confirmationQueryKey"] as const) {
|
|
||||||
const value = mutation[field] ?? "";
|
|
||||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/.test(value) || unsafeGameClientBridgePayloadKey(value)) {
|
|
||||||
errors.push(`${location}.mutation.${field}: must be a safe logical key`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!new Set(["integer", "number", "string", "boolean"]).has(mutation.allowedValueType ?? "")) {
|
|
||||||
errors.push(`${location}.mutation.allowedValueType: must be integer, number, string, or boolean`);
|
|
||||||
}
|
|
||||||
if (mutation.minValue !== undefined && mutation.maxValue !== undefined && mutation.minValue > mutation.maxValue) {
|
|
||||||
errors.push(`${location}.mutation: minValue must not exceed maxValue`);
|
|
||||||
}
|
|
||||||
if (mutation.confirmationQueryKey && !queryTemplates.has(mutation.confirmationQueryKey)) {
|
|
||||||
errors.push(`${location}.mutation.confirmationQueryKey: must reference a declared query template`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const [index, page] of (bridge.pages ?? []).entries()) {
|
for (const [index, page] of (bridge.pages ?? []).entries()) {
|
||||||
for (const commandType of page.commandTypes ?? []) {
|
for (const commandType of page.commandTypes ?? []) {
|
||||||
if (!commands.has(commandType)) {
|
if (!commands.has(commandType)) {
|
||||||
@@ -1042,17 +937,6 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
|||||||
errors.push(`manifest.gameClientBridge.pages[${index}].queryTemplateKeys: page must declare remote.access.request`);
|
errors.push(`manifest.gameClientBridge.pages[${index}].queryTemplateKeys: page must declare remote.access.request`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const operationKey of page.operationKeys ?? []) {
|
|
||||||
const operationTemplate = operationTemplates.get(operationKey);
|
|
||||||
if (!operationTemplate) {
|
|
||||||
errors.push(`manifest.gameClientBridge.pages[${index}].operationKeys: undeclared operation template ${operationKey}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const pluginPage = declaration.pages?.find((candidate) => candidate.key === page.pageKey);
|
|
||||||
if (!pluginPage?.permissions?.includes(operationTemplate.permission ?? "")) {
|
|
||||||
errors.push(`manifest.gameClientBridge.pages[${index}].operationKeys: page must declare operation template permission ${operationTemplate.permission ?? ""}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
@@ -1183,8 +1067,7 @@ function referencedGameClientBridgeSchemas(manifest: unknown): GameClientBridgeS
|
|||||||
type BridgeCommand = { payloadSchemaRef?: string; resultSchemaRef?: string };
|
type BridgeCommand = { payloadSchemaRef?: string; resultSchemaRef?: string };
|
||||||
type BridgeSnapshot = { schemaRef?: string };
|
type BridgeSnapshot = { schemaRef?: string };
|
||||||
type BridgeQueryTemplate = { parameterSchemaRef?: string; resultSchemaRef?: string };
|
type BridgeQueryTemplate = { parameterSchemaRef?: string; resultSchemaRef?: string };
|
||||||
type BridgeOperationTemplate = { payloadSchemaRef?: string; resultSchemaRef?: string; confirmationSchemaRef?: string };
|
const bridge = (manifest as { gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: BridgeSnapshot[]; queryTemplates?: BridgeQueryTemplate[] } }).gameClientBridge;
|
||||||
const bridge = (manifest as { gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: BridgeSnapshot[]; queryTemplates?: BridgeQueryTemplate[]; operationTemplates?: BridgeOperationTemplate[] } }).gameClientBridge;
|
|
||||||
if (!bridge) {
|
if (!bridge) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1210,17 +1093,6 @@ function referencedGameClientBridgeSchemas(manifest: unknown): GameClientBridgeS
|
|||||||
refs.push({ location: `manifest.gameClientBridge.queryTemplates[${index}].resultSchemaRef`, ref: queryTemplate.resultSchemaRef });
|
refs.push({ location: `manifest.gameClientBridge.queryTemplates[${index}].resultSchemaRef`, ref: queryTemplate.resultSchemaRef });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [index, operationTemplate] of (bridge.operationTemplates ?? []).entries()) {
|
|
||||||
if (operationTemplate.payloadSchemaRef) {
|
|
||||||
refs.push({ location: `manifest.gameClientBridge.operationTemplates[${index}].payloadSchemaRef`, ref: operationTemplate.payloadSchemaRef });
|
|
||||||
}
|
|
||||||
if (operationTemplate.resultSchemaRef) {
|
|
||||||
refs.push({ location: `manifest.gameClientBridge.operationTemplates[${index}].resultSchemaRef`, ref: operationTemplate.resultSchemaRef });
|
|
||||||
}
|
|
||||||
if (operationTemplate.confirmationSchemaRef) {
|
|
||||||
refs.push({ location: `manifest.gameClientBridge.operationTemplates[${index}].confirmationSchemaRef`, ref: operationTemplate.confirmationSchemaRef });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return refs;
|
return refs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-55
@@ -46,7 +46,6 @@ export type RunCapability =
|
|||||||
| "remote.run.db.sqlite.query"
|
| "remote.run.db.sqlite.query"
|
||||||
| "remote.run.logs.transfer"
|
| "remote.run.logs.transfer"
|
||||||
| "remote.run.rcon.command"
|
| "remote.run.rcon.command"
|
||||||
| "remote.run.protected.sql"
|
|
||||||
| "remote.run.program.command"
|
| "remote.run.program.command"
|
||||||
| "client-manager.deploy"
|
| "client-manager.deploy"
|
||||||
| "client-manager.control"
|
| "client-manager.control"
|
||||||
@@ -150,7 +149,6 @@ export type PluginProductionLifecyclePayload = Record<string, string> & {
|
|||||||
operation: ProductionPluginLifecycleOperation;
|
operation: ProductionPluginLifecycleOperation;
|
||||||
targetVersion: string;
|
targetVersion: string;
|
||||||
idempotencyKey: string;
|
idempotencyKey: string;
|
||||||
confirmed: "true" | "false";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginRemoteAccessPayload = Record<string, string> & {
|
export type PluginRemoteAccessPayload = Record<string, string> & {
|
||||||
@@ -218,15 +216,12 @@ export interface GamePluginRemoteAccess {
|
|||||||
logTransfer?: boolean;
|
logTransfer?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameClientBridgeApprovalLevel = "none" | "operator" | "platform-admin";
|
|
||||||
export type GameClientBridgeApprovalState = "not_required" | "pending" | "approved" | "rejected";
|
|
||||||
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "unknown" | "cancelled" | "expired";
|
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "unknown" | "cancelled" | "expired";
|
||||||
|
|
||||||
export interface GameClientBridgeCommandDeclaration {
|
export interface GameClientBridgeCommandDeclaration {
|
||||||
type: string;
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
permission: PluginPermission;
|
permission: PluginPermission;
|
||||||
approvalLevel: GameClientBridgeApprovalLevel;
|
|
||||||
payloadSchemaRef: string;
|
payloadSchemaRef: string;
|
||||||
resultSchemaRef?: string;
|
resultSchemaRef?: string;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
@@ -299,52 +294,11 @@ export interface GameClientBridgeDataPackDeclaration {
|
|||||||
configMapRefs: string[];
|
configMapRefs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameClientBridgeOperationKind = "rcon" | "sqlite-mutation";
|
|
||||||
|
|
||||||
export interface GameClientBridgeOperationSafety {
|
|
||||||
requiresApproval?: boolean;
|
|
||||||
requiresOfflinePlayer?: boolean;
|
|
||||||
requiresMaintenanceWindow?: boolean;
|
|
||||||
requiresBeforeValue?: boolean;
|
|
||||||
requiresConfirmation?: boolean;
|
|
||||||
backupRequired?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameClientBridgeOperationMutationDeclaration {
|
|
||||||
fieldKey: string;
|
|
||||||
tableKey: string;
|
|
||||||
identityKey: string;
|
|
||||||
valueKey: string;
|
|
||||||
confirmationQueryKey: string;
|
|
||||||
allowedValueType: "integer" | "number" | "string" | "boolean";
|
|
||||||
minValue?: number;
|
|
||||||
maxValue?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameClientBridgeOperationTemplateDeclaration {
|
|
||||||
key: string;
|
|
||||||
title: string;
|
|
||||||
permission: PluginPermission;
|
|
||||||
approvalLevel: Exclude<GameClientBridgeApprovalLevel, "none">;
|
|
||||||
kind: GameClientBridgeOperationKind;
|
|
||||||
transportKey: string;
|
|
||||||
targetKey: string;
|
|
||||||
payloadSchemaRef: string;
|
|
||||||
resultSchemaRef?: string;
|
|
||||||
confirmationSchemaRef?: string;
|
|
||||||
timeoutSeconds: number;
|
|
||||||
maxPayloadBytes: number;
|
|
||||||
maxRowsAffected?: number;
|
|
||||||
mutation?: GameClientBridgeOperationMutationDeclaration;
|
|
||||||
safety?: GameClientBridgeOperationSafety;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameClientBridgePageContract {
|
export interface GameClientBridgePageContract {
|
||||||
pageKey: string;
|
pageKey: string;
|
||||||
commandTypes?: string[];
|
commandTypes?: string[];
|
||||||
snapshotTypes?: string[];
|
snapshotTypes?: string[];
|
||||||
queryTemplateKeys?: string[];
|
queryTemplateKeys?: string[];
|
||||||
operationKeys?: string[];
|
|
||||||
featureKeys?: string[];
|
featureKeys?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +326,6 @@ export interface GameClientBridgeManifest {
|
|||||||
queryTemplates?: GameClientBridgeQueryTemplateDeclaration[];
|
queryTemplates?: GameClientBridgeQueryTemplateDeclaration[];
|
||||||
logProjections?: GameClientBridgeLogProjectionDeclaration[];
|
logProjections?: GameClientBridgeLogProjectionDeclaration[];
|
||||||
dataPacks?: GameClientBridgeDataPackDeclaration[];
|
dataPacks?: GameClientBridgeDataPackDeclaration[];
|
||||||
operationTemplates?: GameClientBridgeOperationTemplateDeclaration[];
|
|
||||||
commandRetentionSeconds: number;
|
commandRetentionSeconds: number;
|
||||||
maxCommands: number;
|
maxCommands: number;
|
||||||
pages?: GameClientBridgePageContract[];
|
pages?: GameClientBridgePageContract[];
|
||||||
@@ -418,7 +371,6 @@ export interface GameClientBridgeCommand {
|
|||||||
commandType: string;
|
commandType: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
state: GameClientBridgeCommandState;
|
state: GameClientBridgeCommandState;
|
||||||
approvalState: GameClientBridgeApprovalState;
|
|
||||||
result?: GameClientBridgeCommandResult;
|
result?: GameClientBridgeCommandResult;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -760,7 +712,6 @@ export interface GamePluginManifest {
|
|||||||
productionLifecycle: {
|
productionLifecycle: {
|
||||||
operations: ProductionPluginLifecycleOperation[];
|
operations: ProductionPluginLifecycleOperation[];
|
||||||
dependencyPolicy: "required" | "optional";
|
dependencyPolicy: "required" | "optional";
|
||||||
approvalRequired: Array<"disable" | "rollback" | "retire">;
|
|
||||||
};
|
};
|
||||||
pages?: GamePluginPage[];
|
pages?: GamePluginPage[];
|
||||||
ai?: {
|
ai?: {
|
||||||
@@ -870,14 +821,10 @@ export function createProductionPluginLifecycleRequest(input: {
|
|||||||
operation: ProductionPluginLifecycleOperation;
|
operation: ProductionPluginLifecycleOperation;
|
||||||
targetVersion?: string;
|
targetVersion?: string;
|
||||||
idempotencyKey: string;
|
idempotencyKey: string;
|
||||||
confirmed?: boolean;
|
|
||||||
}): PluginBridgeExecutionRequest<PluginProductionLifecyclePayload> {
|
}): PluginBridgeExecutionRequest<PluginProductionLifecyclePayload> {
|
||||||
if (!input.context.serverInstanceId) {
|
if (!input.context.serverInstanceId) {
|
||||||
throw new Error("serverInstanceId is required for plugin lifecycle requests");
|
throw new Error("serverInstanceId is required for plugin lifecycle requests");
|
||||||
}
|
}
|
||||||
if (["disable", "rollback", "retire"].includes(input.operation) && !input.confirmed) {
|
|
||||||
throw new Error("disruptive plugin lifecycle requests require confirmation");
|
|
||||||
}
|
|
||||||
return createBridgeExecutionRequest({
|
return createBridgeExecutionRequest({
|
||||||
requestId: input.requestId,
|
requestId: input.requestId,
|
||||||
context: input.context,
|
context: input.context,
|
||||||
@@ -885,8 +832,7 @@ export function createProductionPluginLifecycleRequest(input: {
|
|||||||
payload: {
|
payload: {
|
||||||
operation: input.operation,
|
operation: input.operation,
|
||||||
targetVersion: input.targetVersion ?? "",
|
targetVersion: input.targetVersion ?? "",
|
||||||
idempotencyKey: input.idempotencyKey,
|
idempotencyKey: input.idempotencyKey
|
||||||
confirmed: input.confirmed ? "true" : "false"
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
parseAIInvocationResponse,
|
parseAIInvocationResponse,
|
||||||
type GameClientBridgeQueryTemplateDeclaration,
|
type GameClientBridgeQueryTemplateDeclaration,
|
||||||
type GameClientBridgeLogProjectionDeclaration,
|
type GameClientBridgeLogProjectionDeclaration,
|
||||||
type GameClientBridgeOperationTemplateDeclaration,
|
|
||||||
type GameClientBridgeCompanionDeclaration,
|
type GameClientBridgeCompanionDeclaration,
|
||||||
type GamePluginManifest,
|
type GamePluginManifest,
|
||||||
type RuntimeLogEventDeclaration,
|
type RuntimeLogEventDeclaration,
|
||||||
@@ -55,7 +54,6 @@ type MutableBridgeManifest = {
|
|||||||
commands: Array<Record<string, unknown>>;
|
commands: Array<Record<string, unknown>>;
|
||||||
snapshots: Array<Record<string, unknown>>;
|
snapshots: Array<Record<string, unknown>>;
|
||||||
queryTemplates?: Array<Record<string, unknown>>;
|
queryTemplates?: Array<Record<string, unknown>>;
|
||||||
operationTemplates?: Array<Record<string, unknown>>;
|
|
||||||
commandRetentionSeconds: number;
|
commandRetentionSeconds: number;
|
||||||
maxCommands: number;
|
maxCommands: number;
|
||||||
pages: Array<Record<string, unknown>>;
|
pages: Array<Record<string, unknown>>;
|
||||||
@@ -94,14 +92,13 @@ function validateTemporaryBridgeManifest(mutate?: (manifest: MutableBridgeManife
|
|||||||
fs.cpSync(path.join(pluginsRoot, "examples/dev-game-plugin"), fixtureDir, { recursive: true });
|
fs.cpSync(path.join(pluginsRoot, "examples/dev-game-plugin"), fixtureDir, { recursive: true });
|
||||||
const manifestPath = path.join(fixtureDir, "manifest.json");
|
const manifestPath = path.join(fixtureDir, "manifest.json");
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MutableBridgeManifest;
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MutableBridgeManifest;
|
||||||
manifest.capabilities = [...manifest.capabilities, "remote.run.db.sqlite.query", "remote.run.rcon.command", "remote.run.protected.sql"];
|
manifest.capabilities = [...manifest.capabilities, "remote.run.db.sqlite.query", "remote.run.rcon.command"];
|
||||||
manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read", "server.game-client.maintenance"];
|
manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read", "server.game-client.maintenance"];
|
||||||
manifest.remoteAccess = { methods: ["run"], runCapabilities: ["remote.run.db.sqlite.query", "remote.run.rcon.command", "remote.run.protected.sql"], databaseEngines: ["sqlite"] };
|
manifest.remoteAccess = { methods: ["run"], runCapabilities: ["remote.run.db.sqlite.query", "remote.run.rcon.command"], databaseEngines: ["sqlite"] };
|
||||||
manifest.runtimeProfiles = {
|
manifest.runtimeProfiles = {
|
||||||
transportProfiles: [
|
transportProfiles: [
|
||||||
{ key: "sqlite-db", kind: "sqlite", targetKey: "db/sqlite", capabilities: ["remote.run.db.sqlite.query"] },
|
{ key: "sqlite-db", kind: "sqlite", targetKey: "db/sqlite", capabilities: ["remote.run.db.sqlite.query"] },
|
||||||
{ key: "scum-rcon", kind: "rcon", targetKey: "scum-rcon", capabilities: ["remote.run.rcon.command"] },
|
{ key: "scum-rcon", kind: "rcon", targetKey: "scum-rcon", capabilities: ["remote.run.rcon.command"] }
|
||||||
{ key: "scum-mutation-db", kind: "sqlite", targetKey: "scum-mutation-db", capabilities: ["remote.run.protected.sql"] }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
const overviewPage = manifest.pages?.find((page) => page.key === "overview");
|
const overviewPage = manifest.pages?.find((page) => page.key === "overview");
|
||||||
@@ -110,24 +107,18 @@ function validateTemporaryBridgeManifest(mutate?: (manifest: MutableBridgeManife
|
|||||||
overviewPage.bridgeActions = [...(overviewPage.bridgeActions ?? []), "remote.access.request"];
|
overviewPage.bridgeActions = [...(overviewPage.bridgeActions ?? []), "remote.access.request"];
|
||||||
}
|
}
|
||||||
manifest.gameClientBridge = {
|
manifest.gameClientBridge = {
|
||||||
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", approvalLevel: "none", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", resultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }],
|
commands: [{ 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: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }],
|
snapshots: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }],
|
||||||
queryTemplates: [{ key: "player.by-id", title: "Find player by ID", permission: "server.game-client.read", engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", parameterSchemaRef: "schemas/bridge/player-by-id.parameters.schema.json", resultSchemaRef: "schemas/bridge/player-by-id.result.schema.json", maxRows: 1, timeoutSeconds: 10 }],
|
queryTemplates: [{ key: "player.by-id", title: "Find player by ID", permission: "server.game-client.read", engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", parameterSchemaRef: "schemas/bridge/player-by-id.parameters.schema.json", resultSchemaRef: "schemas/bridge/player-by-id.result.schema.json", maxRows: 1, timeoutSeconds: 10 }],
|
||||||
operationTemplates: [
|
|
||||||
{ key: "player.fame.set", title: "Set player fame", permission: "server.game-client.command", approvalLevel: "none", kind: "rcon", transportKey: "scum-rcon", targetKey: "scum-rcon", payloadSchemaRef: "schemas/bridge/player-fame-set.payload.schema.json", resultSchemaRef: "schemas/bridge/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: "platform-admin", kind: "sqlite-mutation", transportKey: "scum-mutation-db", targetKey: "scum-mutation-db", payloadSchemaRef: "schemas/bridge/player-attribute-855-set.payload.schema.json", resultSchemaRef: "schemas/bridge/player-attribute-855-set.result.schema.json", confirmationSchemaRef: "schemas/bridge/player-attribute-855-set.confirmation.schema.json", timeoutSeconds: 120, maxPayloadBytes: 4096, maxRowsAffected: 1, mutation: { fieldKey: "855", tableKey: "prisoner", identityKey: "user_profile_id", valueKey: "value", confirmationQueryKey: "player.by-id", allowedValueType: "integer", minValue: 0, maxValue: 100000 }, safety: { requiresApproval: true, requiresOfflinePlayer: true, requiresBeforeValue: true, requiresConfirmation: true, backupRequired: true } }
|
|
||||||
],
|
|
||||||
commandRetentionSeconds: 86400,
|
commandRetentionSeconds: 86400,
|
||||||
maxCommands: 1000,
|
maxCommands: 1000,
|
||||||
pages: [{ pageKey: "overview", commandTypes: ["diagnostic.ping"], snapshotTypes: ["players"], queryTemplateKeys: ["player.by-id"], operationKeys: ["player.fame.set", "player.attribute.855.set"] }]
|
pages: [{ pageKey: "overview", commandTypes: ["diagnostic.ping"], snapshotTypes: ["players"], queryTemplateKeys: ["player.by-id"] }]
|
||||||
};
|
};
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping.schema.json", bridgeObjectSchema({ message: { type: "string", minLength: 1, maxLength: 200 } }, ["message"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping.schema.json", bridgeObjectSchema({ message: { type: "string", minLength: 1, maxLength: 200 } }, ["message"]));
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping-result.schema.json", bridgeObjectSchema({ accepted: { type: "boolean" } }, ["accepted"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping-result.schema.json", bridgeObjectSchema({ accepted: { type: "boolean" } }, ["accepted"]));
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/players.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 100, items: bridgeObjectSchema({ id: { type: "string", minLength: 1, maxLength: 80 } }, ["id"]) } }, ["players"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/players.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 100, items: bridgeObjectSchema({ id: { type: "string", minLength: 1, maxLength: 80 } }, ["id"]) } }, ["players"]));
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]));
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.result.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 1, items: bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]) } }, ["players"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.result.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 1, items: bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]) } }, ["players"]));
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 }, fame: { type: "integer", minimum: 0, maximum: 2147483647 } }, ["playerId", "fame"]));
|
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["queued", "succeeded", "failed", "unknown"] } }, ["outcome"]));
|
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.confirmation.schema.json", bridgeObjectSchema({ playerId: { type: "string" }, fame: { type: "integer" } }, ["playerId", "fame"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.confirmation.schema.json", bridgeObjectSchema({ playerId: { type: "string" }, fame: { type: "integer" } }, ["playerId", "fame"]));
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 }, before: { type: "number" }, after: { type: "number" }, safetyWindow: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId", "before", "after", "safetyWindow"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 }, before: { type: "number" }, after: { type: "number" }, safetyWindow: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId", "before", "after", "safetyWindow"]));
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["succeeded", "failed", "unknown"] }, rowsAffected: { type: "integer", minimum: 0, maximum: 1 } }, ["outcome", "rowsAffected"]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["succeeded", "failed", "unknown"] }, rowsAffected: { type: "integer", minimum: 0, maximum: 1 } }, ["outcome", "rowsAffected"]));
|
||||||
@@ -192,12 +183,10 @@ describe("plugin manifest validation", () => {
|
|||||||
|
|
||||||
it("removes raw SQL command surfaces", () => {
|
it("removes raw SQL command surfaces", () => {
|
||||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string }>; queryTemplates: Array<{ key: string }>; operationTemplates: Array<{ key: string; kind: string }> } };
|
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string }>; queryTemplates: Array<{ key: string }> } };
|
||||||
expect(manifest.gameClientBridge.commands.some((command) => command.type === "diagnostic.ping")).toBe(false);
|
expect(manifest.gameClientBridge.commands.some((command) => command.type === "diagnostic.ping")).toBe(false);
|
||||||
expect(manifest.gameClientBridge.commands.map((command) => command.type)).not.toEqual(expect.arrayContaining(["config.read", "config.patch", "database.request", "management.rcon.request", "management.program.request"]));
|
expect(manifest.gameClientBridge.commands.map((command) => command.type)).not.toEqual(expect.arrayContaining(["config.read", "config.patch", "database.request", "management.rcon.request", "management.program.request"]));
|
||||||
expect(manifest.gameClientBridge.queryTemplates.map((query) => query.key)).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions"]));
|
expect(manifest.gameClientBridge.queryTemplates.map((query) => query.key)).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions"]));
|
||||||
expect(manifest.gameClientBridge.operationTemplates.map((operation) => operation.key)).toEqual(expect.arrayContaining(["player.fame.set", "player.currency.normal.set", "player.currency.gold.set", "player.notify", "reward.deliver", "player.attribute.855.set"]));
|
|
||||||
expect(manifest.gameClientBridge.operationTemplates.find((operation) => operation.key === "player.attribute.855.set")?.kind).toBe("sqlite-mutation");
|
|
||||||
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
|
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,7 +275,7 @@ describe("plugin manifest validation", () => {
|
|||||||
expect(local?.capabilities).toContain("remote.run.rcon.command");
|
expect(local?.capabilities).toContain("remote.run.rcon.command");
|
||||||
expect(local?.transportKeys).toContain("scum-management");
|
expect(local?.transportKeys).toContain("scum-management");
|
||||||
expect(manifest.runtimeProfiles?.transportProfiles).toEqual(expect.arrayContaining([
|
expect(manifest.runtimeProfiles?.transportProfiles).toEqual(expect.arrayContaining([
|
||||||
expect.objectContaining({ key: "scum-database", kind: "sqlite", capabilities: expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.protected.sql"]) }),
|
expect.objectContaining({ key: "scum-database", kind: "sqlite", capabilities: expect.arrayContaining(["remote.run.db.sqlite.query"]) }),
|
||||||
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.rcon.command"] }),
|
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.rcon.command"] }),
|
||||||
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
|
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
|
||||||
]));
|
]));
|
||||||
@@ -461,14 +450,13 @@ describe("plugin manifest validation", () => {
|
|||||||
commands: Array<{
|
commands: Array<{
|
||||||
type: string;
|
type: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
approvalLevel: string;
|
|
||||||
payloadSchemaRef: string;
|
payloadSchemaRef: string;
|
||||||
resultSchemaRef?: string;
|
resultSchemaRef?: string;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
maxPayloadBytes: number;
|
maxPayloadBytes: number;
|
||||||
}>;
|
}>;
|
||||||
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
|
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
|
||||||
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[]; operationKeys?: string[] }>;
|
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
|
||||||
};
|
};
|
||||||
pages: Array<{ key: string; permissions?: string[] }>;
|
pages: Array<{ key: string; permissions?: string[] }>;
|
||||||
fileWorkspace?: {
|
fileWorkspace?: {
|
||||||
@@ -511,13 +499,6 @@ describe("plugin manifest validation", () => {
|
|||||||
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"]));
|
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"]));
|
||||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
||||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
|
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
|
||||||
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "players")?.operationKeys).toEqual(expect.arrayContaining([
|
|
||||||
"player.fame.set",
|
|
||||||
"player.currency.normal.set",
|
|
||||||
"player.currency.gold.set",
|
|
||||||
"player.notify",
|
|
||||||
"player.attribute.855.set"
|
|
||||||
]));
|
|
||||||
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
|
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
|
||||||
expect(manifest.pages.map((page) => page.key)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
expect(manifest.pages.map((page) => page.key)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
||||||
expect(manifest.pages.map((page) => page.key)).not.toContain("files-config");
|
expect(manifest.pages.map((page) => page.key)).not.toContain("files-config");
|
||||||
@@ -534,7 +515,6 @@ describe("plugin manifest validation", () => {
|
|||||||
commands: Array<{
|
commands: Array<{
|
||||||
type: string;
|
type: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
approvalLevel: string;
|
|
||||||
payloadSchemaRef: string;
|
payloadSchemaRef: string;
|
||||||
resultSchemaRef?: string;
|
resultSchemaRef?: string;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
@@ -543,15 +523,15 @@ describe("plugin manifest validation", () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
const expected = {
|
const expected = {
|
||||||
"companion.diagnostics": { permission: "server.game-client.read", approvalLevel: "none" },
|
"companion.diagnostics": { permission: "server.game-client.read" },
|
||||||
"player.lookup": { permission: "server.game-client.read", approvalLevel: "none" },
|
"player.lookup": { permission: "server.game-client.read" },
|
||||||
"reward.deliver": { permission: "server.game-client.command", approvalLevel: "none" },
|
"reward.deliver": { permission: "server.game-client.command" },
|
||||||
"player.notify": { permission: "server.game-client.command", approvalLevel: "none" },
|
"player.notify": { permission: "server.game-client.command" },
|
||||||
"vehicle.spawn": { permission: "server.game-client.command", approvalLevel: "none" },
|
"vehicle.spawn": { permission: "server.game-client.command" },
|
||||||
"event.start": { permission: "server.game-client.command", approvalLevel: "none" },
|
"event.start": { permission: "server.game-client.command" },
|
||||||
"restart.prepare": { permission: "server.game-client.maintenance", approvalLevel: "none" },
|
"restart.prepare": { permission: "server.game-client.maintenance" },
|
||||||
"maintenance.prepare": { permission: "server.game-client.maintenance", approvalLevel: "none" },
|
"maintenance.prepare": { permission: "server.game-client.maintenance" },
|
||||||
"game-state.patch": { permission: "server.game-client.maintenance", approvalLevel: "none" }
|
"game-state.patch": { permission: "server.game-client.maintenance" }
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining(Object.keys(expected)));
|
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining(Object.keys(expected)));
|
||||||
@@ -561,7 +541,6 @@ describe("plugin manifest validation", () => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
expect(command.permission).toBe(policy.permission);
|
expect(command.permission).toBe(policy.permission);
|
||||||
expect(command.approvalLevel).toBe(policy.approvalLevel);
|
|
||||||
expect(command.timeoutSeconds).toBeGreaterThan(0);
|
expect(command.timeoutSeconds).toBeGreaterThan(0);
|
||||||
expect(command.timeoutSeconds).toBeLessThanOrEqual(3600);
|
expect(command.timeoutSeconds).toBeLessThanOrEqual(3600);
|
||||||
expect(command.maxPayloadBytes).toBeGreaterThan(0);
|
expect(command.maxPayloadBytes).toBeGreaterThan(0);
|
||||||
@@ -742,37 +721,6 @@ describe("plugin manifest validation", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("declares typed SCUM RCON operations without arbitrary command inputs", () => {
|
|
||||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
|
||||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
|
||||||
gameClientBridge: {
|
|
||||||
operationTemplates: Array<{ key: string; kind: string; permission: string; approvalLevel: string; payloadSchemaRef: string; resultSchemaRef?: string; confirmationSchemaRef?: string; safety?: Record<string, boolean> }>;
|
|
||||||
pages: Array<{ pageKey: string; operationKeys?: string[] }>;
|
|
||||||
};
|
|
||||||
pages: Array<{ key: string; permissions?: string[] }>;
|
|
||||||
};
|
|
||||||
const expectedKeys = ["player.fame.set", "player.currency.normal.set", "player.currency.gold.set", "player.notify", "reward.deliver"];
|
|
||||||
const operationsByKey = new Map(manifest.gameClientBridge.operationTemplates.map((operation) => [operation.key, operation]));
|
|
||||||
expect([...operationsByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
|
|
||||||
for (const key of expectedKeys) {
|
|
||||||
const operation = operationsByKey.get(key)!;
|
|
||||||
expect(operation.kind).toBe("rcon");
|
|
||||||
expect(operation.permission).toBe("server.game-client.command");
|
|
||||||
expect(operation.approvalLevel).toBe("none");
|
|
||||||
const payload = JSON.parse(fs.readFileSync(path.join(pluginDir, operation.payloadSchemaRef), "utf8"));
|
|
||||||
const result = JSON.parse(fs.readFileSync(path.join(pluginDir, operation.resultSchemaRef!), "utf8"));
|
|
||||||
expect(payload).toMatchObject({ type: "object", additionalProperties: false });
|
|
||||||
expect(result).toMatchObject({ type: "object", additionalProperties: false });
|
|
||||||
expect(JSON.stringify(payload).toLowerCase()).not.toMatch(/rcon|commandtext|requesttext|sql|dsn|hostpath/);
|
|
||||||
}
|
|
||||||
const playersPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "players");
|
|
||||||
const giftsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "gifts");
|
|
||||||
expect(playersPage?.operationKeys).toEqual(expect.arrayContaining(["player.fame.set", "player.currency.normal.set", "player.currency.gold.set", "player.notify"]));
|
|
||||||
expect(giftsPage?.operationKeys).toEqual(expect.arrayContaining(["reward.deliver", "player.notify"]));
|
|
||||||
expect(manifest.pages.find((page) => page.key === "players")?.permissions).toContain("server.game-client.command");
|
|
||||||
expect(manifest.pages.find((page) => page.key === "gifts")?.permissions).toContain("server.game-client.command");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("packages SCUM v57 config, UTF-16LE logs, and gift metadata inside the plugin", () => {
|
it("packages SCUM v57 config, UTF-16LE logs, and gift metadata inside the plugin", () => {
|
||||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||||
@@ -912,7 +860,7 @@ describe("plugin manifest validation", () => {
|
|||||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/dev-game-plugin/manifest.json"), "utf8"));
|
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/dev-game-plugin/manifest.json"), "utf8"));
|
||||||
manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read"];
|
manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read"];
|
||||||
manifest.gameClientBridge = {
|
manifest.gameClientBridge = {
|
||||||
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", resultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }],
|
commands: [{ 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: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }],
|
snapshots: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }],
|
||||||
logProjections: [{ key: "player.login", streamKeys: ["process.stdout"], steps: [{ pattern: "Player \\\"(?<name>[^\\\"]+)\\\" reported as player (?<slot>\\\\d+)" }, { pattern: "Player (?<slot>\\\\d+) SteamID: (?<steamId>\\\\d+)" }], correlationFields: ["slot"], maxInterveningLines: 16, target: { collection: "users", upsertKeys: ["steamId"], captureMappings: { steamId: "steamId", name: "name" }, observedAtField: "lastLoginAt" }, presence: { timestampField: "lastLoginAt", activeWindowSeconds: 600 } }],
|
logProjections: [{ key: "player.login", streamKeys: ["process.stdout"], steps: [{ pattern: "Player \\\"(?<name>[^\\\"]+)\\\" reported as player (?<slot>\\\\d+)" }, { pattern: "Player (?<slot>\\\\d+) SteamID: (?<steamId>\\\\d+)" }], correlationFields: ["slot"], maxInterveningLines: 16, target: { collection: "users", upsertKeys: ["steamId"], captureMappings: { steamId: "steamId", name: "name" }, observedAtField: "lastLoginAt" }, presence: { timestampField: "lastLoginAt", activeWindowSeconds: 600 } }],
|
||||||
commandRetentionSeconds: 86400,
|
commandRetentionSeconds: 86400,
|
||||||
@@ -921,7 +869,7 @@ describe("plugin manifest validation", () => {
|
|||||||
};
|
};
|
||||||
const validate = new Ajv2020({ allErrors: true }).compile(schema);
|
const validate = new Ajv2020({ allErrors: true }).compile(schema);
|
||||||
expect(validate(manifest), JSON.stringify(validate.errors)).toBe(true);
|
expect(validate(manifest), JSON.stringify(validate.errors)).toBe(true);
|
||||||
manifest.gameClientBridge.commands[0].approvalLevel = "automatic";
|
manifest.gameClientBridge.commands[0].unexpectedControlGate = "automatic";
|
||||||
expect(validate(manifest)).toBe(false);
|
expect(validate(manifest)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -946,7 +894,7 @@ describe("plugin manifest validation", () => {
|
|||||||
permissions: ["server.game-client.command"],
|
permissions: ["server.game-client.command"],
|
||||||
runtimeProfiles: { clientManagers: [{ key: "scum-client", health: { requiredCapabilities: ["game-client.bridge"] } }] },
|
runtimeProfiles: { clientManagers: [{ key: "scum-client", health: { requiredCapabilities: ["game-client.bridge"] } }] },
|
||||||
gameClientBridge: {
|
gameClientBridge: {
|
||||||
commands: [{ type: "diagnostic.ping", approvalLevel: "none", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json" }],
|
commands: [{ type: "diagnostic.ping", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json" }],
|
||||||
snapshots: [],
|
snapshots: [],
|
||||||
logProjections: [projection]
|
logProjections: [projection]
|
||||||
}
|
}
|
||||||
@@ -1047,35 +995,6 @@ describe("plugin manifest validation", () => {
|
|||||||
expect(actionErrors.some((error) => error.includes("page must declare remote.access.request"))).toBe(true);
|
expect(actionErrors.some((error) => error.includes("page must declare remote.access.request"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("validates typed operation templates and page operation bindings", () => {
|
|
||||||
expect(validateTemporaryBridgeManifest()).toEqual([]);
|
|
||||||
|
|
||||||
const unsafeKeyErrors = validateTemporaryBridgeManifest((manifest) => {
|
|
||||||
manifest.gameClientBridge.operationTemplates![0].key = "raw.sql.execute";
|
|
||||||
});
|
|
||||||
expect(unsafeKeyErrors.some((error) => error.includes("operationTemplates") && error.includes("arbitrary SQL"))).toBe(true);
|
|
||||||
|
|
||||||
const approvalErrors = validateTemporaryBridgeManifest((manifest) => {
|
|
||||||
manifest.gameClientBridge.operationTemplates![0].approvalLevel = "automatic";
|
|
||||||
});
|
|
||||||
expect(approvalErrors.some((error) => error.includes("approvalLevel") && error.includes("none, operator, or platform-admin"))).toBe(true);
|
|
||||||
|
|
||||||
const rconTransportErrors = validateTemporaryBridgeManifest((manifest) => {
|
|
||||||
Object.assign(manifest.gameClientBridge.operationTemplates![0], { transportKey: "sqlite-db", targetKey: "db/sqlite" });
|
|
||||||
});
|
|
||||||
expect(rconTransportErrors.some((error) => error.includes("rcon operations require"))).toBe(true);
|
|
||||||
|
|
||||||
const mutationSafetyErrors = validateTemporaryBridgeManifest((manifest) => {
|
|
||||||
manifest.gameClientBridge.operationTemplates![1].safety = { requiresConfirmation: true };
|
|
||||||
});
|
|
||||||
expect(mutationSafetyErrors.some((error) => error.includes("sqlite-mutation operations require before value"))).toBe(true);
|
|
||||||
|
|
||||||
const pageErrors = validateTemporaryBridgeManifest((manifest) => {
|
|
||||||
manifest.gameClientBridge.pages[0].operationKeys = ["missing.operation"];
|
|
||||||
});
|
|
||||||
expect(pageErrors.some((error) => error.includes("undeclared operation template missing.operation"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(["sqlText", "dsn", "hostPath", "shellCommand", "socketAddress", "accessToken", "credential"])("rejects unsafe query parameter schema field %s", (fieldName) => {
|
it.each(["sqlText", "dsn", "hostPath", "shellCommand", "socketAddress", "accessToken", "credential"])("rejects unsafe query parameter schema field %s", (fieldName) => {
|
||||||
const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => {
|
const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => {
|
||||||
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ [fieldName]: { type: "string", minLength: 1, maxLength: 120 } }, [fieldName]));
|
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ [fieldName]: { type: "string", minLength: 1, maxLength: 120 } }, [fieldName]));
|
||||||
@@ -1097,13 +1016,6 @@ describe("plugin manifest validation", () => {
|
|||||||
expect(errors.some((error) => error.includes("resultSchemaRef") && error.includes("missing bridge schema file"))).toBe(true);
|
expect(errors.some((error) => error.includes("resultSchemaRef") && error.includes("missing bridge schema file"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing bridge command approval metadata end to end", () => {
|
|
||||||
const errors = validateTemporaryBridgeManifest((manifest) => {
|
|
||||||
delete manifest.gameClientBridge.commands[0].approvalLevel;
|
|
||||||
});
|
|
||||||
expect(errors.some((error) => error.includes("approvalLevel") && (error.includes("required") || error.includes("approval metadata")))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects unsafe executor capabilities end to end", () => {
|
it("rejects unsafe executor capabilities end to end", () => {
|
||||||
const errors = validateTemporaryBridgeManifest((manifest) => {
|
const errors = validateTemporaryBridgeManifest((manifest) => {
|
||||||
manifest.capabilities = [...manifest.capabilities, "shell.exec"];
|
manifest.capabilities = [...manifest.capabilities, "shell.exec"];
|
||||||
@@ -1223,28 +1135,6 @@ describe("plugin SDK", () => {
|
|||||||
expect(declaration).toMatchObject({ key: "scum.player.login", correlationFields: ["slot"] });
|
expect(declaration).toMatchObject({ key: "scum.player.login", correlationFields: ["slot"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("types plugin operation template declarations", () => {
|
|
||||||
const declaration: GameClientBridgeOperationTemplateDeclaration = {
|
|
||||||
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,
|
|
||||||
mutation: { fieldKey: "855", tableKey: "prisoner", identityKey: "user_profile_id", valueKey: "value", confirmationQueryKey: "player.lookup", allowedValueType: "integer", minValue: 0, maxValue: 100000 },
|
|
||||||
safety: { requiresApproval: true, requiresOfflinePlayer: true, requiresBeforeValue: true, requiresConfirmation: true, backupRequired: true }
|
|
||||||
};
|
|
||||||
expect(declaration).toMatchObject({ kind: "sqlite-mutation", approvalLevel: "platform-admin", maxRowsAffected: 1 });
|
|
||||||
expect(JSON.stringify(declaration).toLowerCase()).not.toMatch(/sqltext|dsn|hostpath|socket|credential|password/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds safe game-client bridge requests without component transport material", () => {
|
it("builds safe game-client bridge requests without component transport material", () => {
|
||||||
const request = createGameClientBridgeQueueRequest({
|
const request = createGameClientBridgeQueueRequest({
|
||||||
profileKey: "scum-client",
|
profileKey: "scum-client",
|
||||||
@@ -1555,7 +1445,7 @@ describe("plugin SDK", () => {
|
|||||||
server: { type: "runtime", displayName: "Runtime Fixture", createFormSchema: "schemas/create-form.schema.json" },
|
server: { type: "runtime", displayName: "Runtime Fixture", createFormSchema: "schemas/create-form.schema.json" },
|
||||||
capabilities: ["process.start", "process.stop", "logs.read"],
|
capabilities: ["process.start", "process.stop", "logs.read"],
|
||||||
permissions: ["server.read", "server.lifecycle", "server.logs.read"],
|
permissions: ["server.read", "server.lifecycle", "server.logs.read"],
|
||||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] },
|
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required" },
|
||||||
runtimeProfiles: {
|
runtimeProfiles: {
|
||||||
discovery: [{ key: "java", kind: "command.version", targetKey: "java", required: true }],
|
discovery: [{ key: "java", kind: "command.version", targetKey: "java", required: true }],
|
||||||
dependencyProbes: [{ key: "java-21", kind: "java.version", targetKey: "java", minimumVersion: "21" }],
|
dependencyProbes: [{ key: "java-21", kind: "java.version", targetKey: "java", minimumVersion: "21" }],
|
||||||
@@ -1636,9 +1526,8 @@ describe("plugin SDK", () => {
|
|||||||
permissions: ["server.lifecycle"]
|
permissions: ["server.lifecycle"]
|
||||||
};
|
};
|
||||||
const request = createProductionPluginLifecycleRequest({ requestId: "plugin-upgrade-1", context, operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "plugin-upgrade-v1" });
|
const request = createProductionPluginLifecycleRequest({ requestId: "plugin-upgrade-1", context, operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "plugin-upgrade-v1" });
|
||||||
expect(request).toMatchObject({ action: "plugin-lifecycle.request", payload: { operation: "upgrade", targetVersion: "1.2.0", confirmed: "false" } });
|
expect(request).toMatchObject({ action: "plugin-lifecycle.request", payload: { operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "plugin-upgrade-v1" } });
|
||||||
expect(JSON.stringify(request)).not.toMatch(/apiKey|providerBaseUrl|runSocket|runEndpoint|hostPath|credential/i);
|
expect(JSON.stringify(request)).not.toMatch(/apiKey|providerBaseUrl|runSocket|runEndpoint|hostPath|credential/i);
|
||||||
expect(() => createProductionPluginLifecycleRequest({ requestId: "plugin-retire-1", context, operation: "retire", idempotencyKey: "plugin-retire-v1" })).toThrow(/confirmation/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user