Files
browser/platform/validator/resources.go
T

2706 lines
114 KiB
Go

package validator
import (
"bytes"
"encoding/base64"
"fmt"
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"browser.local/platform/domain"
)
const (
maxSummaryLength = 512
maxContactNoteLength = 160
maxMarketplaceKeywordSize = 80
maxMarketplaceListSize = 500
maxPluginDescriptionLength = 240
maxPluginPageTitleLength = 40
maxPluginBridgePayloadKeys = 16
maxPluginBridgePayloadSize = 16 * 1024
maxProgressMessageLength = 256
maxServerConfigContentSize = 64 * 1024
maxJobExecutionContentSize = 64 * 1024
maxLogicalFileKeyLength = 1024
maxProductionMessageLength = 320
)
var (
gameClientBridgeCollectionPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,119}$`)
gameClientBridgeFieldPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,79}$`)
gameClientBridgeCaptureNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{0,79}$`)
)
type ValidationError struct {
Violations []string
}
func (err ValidationError) Error() string {
return "validation failed: " + strings.Join(err.Violations, "; ")
}
func (err ValidationError) IsEmpty() bool {
return len(err.Violations) == 0
}
func appendRequired(violations []string, field string, value string) []string {
if strings.TrimSpace(value) == "" {
return append(violations, field+" is required")
}
return violations
}
func finish(violations []string) error {
if len(violations) == 0 {
return nil
}
return ValidationError{Violations: violations}
}
func ValidateUser(user domain.User) error {
var violations []string
violations = appendRequired(violations, "id", user.ID)
violations = appendRequired(violations, "displayName", user.DisplayName)
if !validUserStatus(user.Status) {
violations = append(violations, "status is invalid")
}
if strings.TrimSpace(user.Email) != "" && !strings.Contains(user.Email, "@") {
violations = append(violations, "email is invalid")
}
if len(user.Profile.ContactNote) > maxContactNoteLength {
violations = append(violations, "profile.contactNote is too long")
}
for i, role := range user.Roles {
if strings.TrimSpace(role) == "" {
violations = append(violations, fmt.Sprintf("roles[%d] is required", i))
}
if !validUserRole(role) {
violations = append(violations, fmt.Sprintf("roles[%d] is invalid", i))
}
}
return finish(violations)
}
func ValidateAIProvider(provider domain.AIProvider) error {
var violations []string
violations = appendRequired(violations, "id", provider.ID)
violations = appendRequired(violations, "name", provider.Name)
violations = appendRequired(violations, "baseUrl", provider.BaseURL)
if !validAIProviderKind(provider.Kind) {
violations = append(violations, "kind is invalid")
}
if !validAIRelayMode(provider.RelayMode) {
violations = append(violations, "relayMode is invalid")
}
if !validAIProviderStatus(provider.Status) {
violations = append(violations, "status is invalid")
}
if provider.RelayMode == domain.AIRelayModeDirect || provider.RelayMode == domain.AIRelayModeRelay {
violations = appendRequired(violations, "apiKeyRef", provider.APIKeyRef)
}
if looksLikeRawSecret(provider.APIKeyRef) {
violations = append(violations, "apiKeyRef must reference secret storage, not raw key material")
}
violations = appendRequired(violations, "redactionPolicy", provider.RedactionPolicy)
if provider.TimeoutMS <= 0 {
violations = append(violations, "timeoutMs must be positive")
}
if len(provider.Models) == 0 {
violations = append(violations, "models must not be empty")
}
modelSet := map[string]struct{}{}
for i, model := range provider.Models {
model = strings.TrimSpace(model)
if model == "" {
violations = append(violations, fmt.Sprintf("models[%d] is required", i))
continue
}
if _, exists := modelSet[model]; exists {
violations = append(violations, fmt.Sprintf("models[%d] duplicates %q", i, model))
}
modelSet[model] = struct{}{}
}
if provider.DefaultModel != "" {
if _, exists := modelSet[provider.DefaultModel]; !exists {
violations = append(violations, "defaultModel must be included in models")
}
}
return finish(violations)
}
func ValidateGamePlugin(plugin domain.GamePlugin) error {
var violations []string
violations = appendRequired(violations, "id", plugin.ID)
violations = appendRequired(violations, "name", plugin.Name)
if len(plugin.Description) > maxPluginDescriptionLength {
violations = append(violations, "description is too long")
}
violations = appendRequired(violations, "version", plugin.Version)
violations = appendRequired(violations, "serverType", plugin.ServerType)
violations = appendRequired(violations, "manifestRef", plugin.ManifestRef)
violations = appendRequired(violations, "createFormSchemaRef", plugin.CreateFormSchemaRef)
if !validGamePluginStatus(plugin.Status) {
violations = append(violations, "status is invalid")
}
for i, capability := range plugin.RequiredRunCapabilities {
if strings.TrimSpace(capability) == "" {
violations = append(violations, fmt.Sprintf("requiredRunCapabilities[%d] is required", i))
} else if !validPluginRunCapability(capability) {
violations = append(violations, fmt.Sprintf("requiredRunCapabilities[%d] is not allowed", i))
}
}
violations = append(violations, duplicateViolations("requiredRunCapabilities", plugin.RequiredRunCapabilities)...)
violations = append(violations, validateDeclaredPluginPermissions(plugin.DeclaredPermissions)...)
violations = append(violations, validateBridgeActions("bridgeActions", plugin.BridgeActions)...)
violations = append(violations, validatePluginPages(plugin.Pages)...)
violations = append(violations, validatePluginFileWorkspace("fileWorkspace", plugin.FileWorkspace)...)
violations = append(violations, duplicateViolations("tags", plugin.Tags)...)
violations = append(violations, validateAIPurposes(plugin.AIPurposes)...)
violations = append(violations, validateProductionLifecycle("productionLifecycle", plugin.ProductionLifecycle, true)...)
violations = append(violations, validateRemoteAccess("remoteAccess", plugin.RemoteAccess, plugin.RequiredRunCapabilities)...)
if err := ValidateGamePluginRuntimeProfiles(plugin.RuntimeProfiles); err != nil {
violations = append(violations, err.Error())
}
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...)
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...)
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...)
violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
return finish(violations)
}
func ValidateGamePluginManifestRegistration(registration domain.GamePluginManifestRegistration) error {
registration = domain.CopyGamePluginManifestRegistration(registration)
manifest := registration.Manifest
var violations []string
violations = appendRequired(violations, "manifestRef", registration.ManifestRef)
if !safeRef(registration.ManifestRef) {
violations = append(violations, "manifestRef is unsafe")
}
violations = appendRequired(violations, "manifest.id", manifest.ID)
violations = appendRequired(violations, "manifest.name", manifest.Name)
if len(manifest.Description) > maxPluginDescriptionLength {
violations = append(violations, "manifest.description is too long")
}
violations = appendRequired(violations, "manifest.version", manifest.Version)
if manifest.Kind != "game-plugin" {
violations = append(violations, "manifest.kind must be game-plugin")
}
violations = appendRequired(violations, "manifest.server.type", manifest.Server.Type)
violations = appendRequired(violations, "manifest.server.displayName", manifest.Server.DisplayName)
violations = appendRequired(violations, "manifest.server.createFormSchema", manifest.Server.CreateFormSchema)
if !safeRelativeJSONRef(manifest.Server.CreateFormSchema) {
violations = append(violations, "manifest.server.createFormSchema must be a safe relative JSON reference")
}
violations = append(violations, validatePluginCreateFields("manifest.server.createFields", manifest.Server.CreateFields)...)
for i, osName := range manifest.Server.SupportedOS {
if !validPluginSupportedOS(osName) {
violations = append(violations, fmt.Sprintf("manifest.server.supportedOs[%d] is not allowed", i))
}
}
violations = append(violations, duplicateViolations("manifest.server.supportedOs", manifest.Server.SupportedOS)...)
for i, capability := range manifest.Capabilities {
if !validPluginRunCapability(capability) {
violations = append(violations, fmt.Sprintf("manifest.capabilities[%d] is not allowed", i))
}
}
if len(manifest.Capabilities) == 0 {
violations = append(violations, "manifest.capabilities must not be empty")
}
violations = append(violations, duplicateViolations("manifest.capabilities", manifest.Capabilities)...)
if len(manifest.Permissions) == 0 {
violations = append(violations, "manifest.permissions must not be empty")
}
violations = append(violations, validateDeclaredPluginPermissions(manifest.Permissions)...)
violations = append(violations, validateBridgeActions("manifest.bridge.actions", manifest.Bridge.Actions)...)
violations = append(violations, validateLifecycleActions(manifest.Actions)...)
violations = append(violations, validatePluginPages(manifest.Pages)...)
violations = append(violations, validatePluginFileWorkspace("manifest.fileWorkspace", manifest.FileWorkspace)...)
violations = append(violations, duplicateViolations("manifest.tags", manifest.Tags)...)
violations = append(violations, validateAIPurposes(manifest.AI.Purposes)...)
violations = append(violations, validateProductionLifecycle("manifest.productionLifecycle", manifest.ProductionLifecycle, true)...)
if containsString(manifest.Permissions, "ai.invoke") || len(manifest.AI.Purposes) > 0 {
if manifest.AI.Mediation != "platform" {
violations = append(violations, "manifest.ai.mediation must be platform")
}
if manifest.AI.ConfigWritePolicy != "review-required" {
violations = append(violations, "manifest.ai.configWritePolicy must be review-required")
}
}
violations = append(violations, validateRemoteAccess("manifest.remoteAccess", manifest.RemoteAccess, manifest.Capabilities)...)
if err := ValidateGamePluginRuntimeProfiles(manifest.RuntimeProfiles); err != nil {
violations = append(violations, err.Error())
}
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...)
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...)
violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...)
violations = append(violations, validateRegistrationAssetCoverage(registration.Manifest.AssetFiles, registration.AssetFiles)...)
violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...)
return finish(violations)
}
func validatePluginAssetFileDeclarations(prefix string, files []domain.PluginAssetFile) []string {
if len(files) > 64 {
return []string{prefix + " has too many files"}
}
var violations []string
seen := map[string]struct{}{}
for i, file := range files {
field := fmt.Sprintf("%s[%d]", prefix, i)
if !validLogicalFileKey(file.Path) {
violations = append(violations, field+".path is unsafe")
}
if _, exists := seen[file.Path]; exists {
violations = append(violations, field+".path is duplicated")
}
seen[file.Path] = struct{}{}
if file.Content != "" {
violations = append(violations, field+".content must be supplied only in registration assetFiles")
}
if file.Encoding != "" {
violations = append(violations, field+".encoding must be supplied only in registration assetFiles")
}
if file.Mode != 0 && file.Mode != 0o600 && file.Mode != 0o700 {
violations = append(violations, field+".mode is unsafe")
}
}
return violations
}
func validatePluginAssetFiles(prefix string, files []domain.PluginAssetFile) []string {
if len(files) > 64 {
return []string{prefix + " has too many files"}
}
var violations []string
seen := map[string]struct{}{}
for i, file := range files {
field := fmt.Sprintf("%s[%d]", prefix, i)
if !validLogicalFileKey(file.Path) {
violations = append(violations, field+".path is unsafe")
}
if _, exists := seen[file.Path]; exists {
violations = append(violations, field+".path is duplicated")
}
seen[file.Path] = struct{}{}
content, err := pluginAssetContentBytes(file)
if err != nil {
violations = append(violations, field+err.Error())
} else if len(content) > 64*1024 || pluginAssetContentUnsafe(file, content) {
violations = append(violations, field+".content is unsafe")
}
if file.Mode != 0 && (file.Mode < 0o400 || file.Mode > 0o700 || file.Mode&0o022 != 0) {
violations = append(violations, field+".mode is unsafe")
}
}
return violations
}
func pluginAssetContentBytes(file domain.PluginAssetFile) ([]byte, error) {
switch strings.TrimSpace(file.Encoding) {
case "":
return []byte(file.Content), nil
case "base64":
content, err := base64.StdEncoding.DecodeString(strings.TrimSpace(file.Content))
if err != nil {
return nil, fmt.Errorf(".content is not valid base64")
}
return content, nil
default:
return nil, fmt.Errorf(".encoding is unsupported")
}
}
func pluginAssetContentUnsafe(file domain.PluginAssetFile, content []byte) bool {
if strings.TrimSpace(file.Encoding) == "base64" {
if bytes.ContainsRune(content, '\x00') || !utf8.Valid(content) {
return false
}
return containsUnsafeRuntimeSecret(string(content))
}
return bytes.ContainsRune(content, '\x00') || containsUnsafeRuntimeSecret(string(content))
}
func validateRegistrationAssetCoverage(declared []domain.PluginAssetFile, payload []domain.PluginAssetFile) []string {
if len(declared) == 0 {
return nil
}
allowed := map[string]struct{}{}
for _, file := range declared {
allowed[file.Path] = struct{}{}
}
provided := map[string]struct{}{}
for _, file := range payload {
provided[file.Path] = struct{}{}
if _, ok := allowed[file.Path]; !ok {
return []string{"assetFiles contains undeclared plugin asset " + file.Path}
}
}
var violations []string
for _, file := range declared {
if _, ok := provided[file.Path]; !ok {
violations = append(violations, "assetFiles is missing declared plugin asset "+file.Path)
}
}
return violations
}
func validatePluginCreateFields(prefix string, fields []domain.PluginCreateField) []string {
if len(fields) > 32 {
return []string{prefix + " must contain at most 32 fields"}
}
var violations []string
keys := map[string]struct{}{}
for i, field := range fields {
item := fmt.Sprintf("%s[%d]", prefix, i)
if !validDistributionLogicalKey(field.Key) {
violations = append(violations, item+".key is invalid")
}
if _, found := keys[field.Key]; found {
violations = append(violations, item+".key duplicates another field")
}
keys[field.Key] = struct{}{}
if strings.TrimSpace(field.Label) == "" || len(field.Label) > 60 || strings.TrimSpace(field.Label) != field.Label {
violations = append(violations, item+".label is invalid")
}
if !validPluginCreateFieldType(field.Type) {
violations = append(violations, item+".type is invalid")
}
if len(field.DefaultValue) > 256 || strings.TrimSpace(field.DefaultValue) != field.DefaultValue || containsUnsafeRuntimeSecret(field.DefaultValue) || looksLikeRawHostPath(field.DefaultValue) {
violations = append(violations, item+".defaultValue is unsafe")
}
if field.ConfigKey != "" && !validDistributionLogicalKey(field.ConfigKey) {
violations = append(violations, item+".configKey is invalid")
}
if len(field.Options) > 32 {
violations = append(violations, item+".options has too many values")
}
options := map[string]struct{}{}
for _, option := range field.Options {
if strings.TrimSpace(option) == "" || len(option) > 120 || strings.TrimSpace(option) != option || containsUnsafeRuntimeSecret(option) || looksLikeRawHostPath(option) {
violations = append(violations, item+".options contains an unsafe value")
break
}
if _, found := options[option]; found {
violations = append(violations, item+".options contains duplicates")
break
}
options[option] = struct{}{}
}
if field.Type == "select" && len(field.Options) == 0 {
violations = append(violations, item+".options is required for select")
}
if field.Type != "select" && len(field.Options) > 0 {
violations = append(violations, item+".options is only valid for select")
}
}
return violations
}
func validPluginCreateFieldType(value string) bool {
switch value {
case "text", "number", "boolean", "select", "port":
return true
default:
return false
}
}
// ValidatePluginCreateInputs keeps game settings distinct from runtime
// bindings while enforcing the plugin's published, non-executable field set.
func ValidatePluginCreateInputs(fields []domain.PluginCreateField, inputs map[string]string) error {
if len(fields) == 0 {
return nil
}
declared := make(map[string]domain.PluginCreateField, len(fields))
for _, field := range fields {
declared[field.Key] = field
}
var violations []string
for key, value := range inputs {
field, found := declared[key]
if !found {
violations = append(violations, "createInputs."+key+" is not declared by plugin")
continue
}
if len(value) > 1024 || strings.TrimSpace(value) != value || containsUnsafeRuntimeSecret(value) {
violations = append(violations, "createInputs."+key+" is invalid")
continue
}
if value == "" {
if field.Required && field.DefaultValue == "" {
violations = append(violations, "createInputs."+key+" is required")
}
continue
}
switch field.Type {
case "port":
port, err := strconv.Atoi(value)
if err != nil || port < 1 || port > 65535 {
violations = append(violations, "createInputs."+key+" must be a port between 1 and 65535")
}
case "number":
if _, err := strconv.ParseFloat(value, 64); err != nil {
violations = append(violations, "createInputs."+key+" must be numeric")
}
case "boolean":
if value != "true" && value != "false" {
violations = append(violations, "createInputs."+key+" must be true or false")
}
case "select":
if !containsString(field.Options, value) {
violations = append(violations, "createInputs."+key+" is not an allowed option")
}
}
}
for _, field := range fields {
if field.Required && field.DefaultValue == "" && strings.TrimSpace(inputs[field.Key]) == "" {
violations = append(violations, "createInputs."+field.Key+" is required")
}
}
return finish(violations)
}
func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []string, pages []domain.GamePluginPage, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
companionPresent := bridge.Companion != (domain.GameClientBridgeCompanionDeclaration{})
if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.LogProjections) == 0 && len(bridge.DataPacks) == 0 && len(bridge.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
return nil
}
var violations []string
if bridge.Retention.KeepForSeconds <= 0 || bridge.Retention.KeepForSeconds > 365*24*60*60 {
violations = append(violations, field+".commandRetentionSeconds is invalid")
}
if bridge.Retention.MaxRecords <= 0 || bridge.Retention.MaxRecords > 100000 {
violations = append(violations, field+".maxCommands is invalid")
}
if companionPresent {
prefix := field + ".companion"
companion := bridge.Companion
if !clientManagerIdentifierPattern.MatchString(companion.ProfileKey) || !clientManagerIdentifierPattern.MatchString(companion.ConfigTemplateKey) {
violations = append(violations, prefix+" profile or config template key is invalid")
}
if !safeRelativeJSONRef(companion.ConfigSchemaRef) {
violations = append(violations, prefix+".configSchemaRef must be a safe relative JSON reference")
}
if companion.ConfigFormat != "yaml" || companion.PlatformBaseURLSource != "run-control" || companion.RegistrationProof != "hmac-sha256" || companion.ProofMaterialSource != "component-package" || companion.SessionMode != "component-session" || companion.TLSPolicy != "verify-system-roots" {
violations = append(violations, prefix+" bootstrap security policy is invalid")
}
if !validCompanionProofEnvironment(companion.ProofMaterialEnv) {
violations = append(violations, prefix+".proofMaterialEnv is invalid")
}
if companion.HeartbeatIntervalSeconds < 5 || companion.HeartbeatIntervalSeconds > 300 || companion.CommandPollIntervalSeconds < 1 || companion.CommandPollIntervalSeconds > 60 || companion.RequestTimeoutSeconds < 1 || companion.RequestTimeoutSeconds > 60 {
violations = append(violations, prefix+" timing policy is invalid")
}
managerFound := false
for _, manager := range runtimeProfiles.ClientManagers {
if manager.Key != companion.ProfileKey {
continue
}
managerFound = true
if manager.Health.IntervalSeconds != companion.HeartbeatIntervalSeconds {
violations = append(violations, prefix+".heartbeatIntervalSeconds must match the Client Manager health interval")
}
for _, capability := range []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"} {
if !containsString(manager.Health.RequiredCapabilities, capability) {
violations = append(violations, prefix+" requires Client Manager capability "+capability)
}
}
templateFound := false
for _, template := range manager.ConfigTemplates {
if template.Key == companion.ConfigTemplateKey {
templateFound = true
if template.OutputRef != "config.yaml" {
violations = append(violations, prefix+" config template must materialize config.yaml")
}
}
}
if !templateFound {
violations = append(violations, prefix+".configTemplateKey must reference the Client Manager profile")
}
}
if !managerFound {
violations = append(violations, prefix+".profileKey must reference a declared Client Manager profile")
}
}
transports := map[string]domain.RuntimeTransportProfile{}
for _, transport := range runtimeProfiles.TransportProfiles {
transports[transport.Key] = transport
}
commandTypes := map[string]struct{}{}
for index, command := range bridge.Commands {
prefix := fmt.Sprintf("%s.commands[%d]", field, index)
if !clientManagerIdentifierPattern.MatchString(command.Type) || unsafeGameClientBridgeCommandType(command.Type) {
violations = append(violations, prefix+".type is invalid or unsafe")
}
if _, exists := commandTypes[command.Type]; exists {
violations = append(violations, prefix+".type is duplicated")
}
commandTypes[command.Type] = struct{}{}
if strings.TrimSpace(command.Title) == "" || len([]rune(command.Title)) > 80 {
violations = append(violations, prefix+".title is invalid")
}
if !containsString(permissions, command.Permission) {
violations = append(violations, prefix+".permission must be declared by the plugin")
}
if !safeRelativeJSONRef(command.PayloadSchemaRef) || command.ResultSchemaRef != "" && !safeRelativeJSONRef(command.ResultSchemaRef) {
violations = append(violations, prefix+" schema references must be safe relative JSON references")
}
if command.TimeoutSeconds <= 0 || command.TimeoutSeconds > 3600 {
violations = append(violations, prefix+".timeoutSeconds is invalid")
}
if command.MaxPayloadBytes <= 0 || command.MaxPayloadBytes > maxGameClientBridgePayloadSize {
violations = append(violations, prefix+".maxPayloadBytes is invalid")
}
}
snapshotTypes := map[string]struct{}{}
for index, snapshot := range bridge.Snapshots {
prefix := fmt.Sprintf("%s.snapshots[%d]", field, index)
key := snapshot.Type + "\x00" + snapshot.SchemaVersion
if !clientManagerIdentifierPattern.MatchString(snapshot.Type) || !clientManagerIdentifierPattern.MatchString(snapshot.SchemaVersion) {
violations = append(violations, prefix+" type or schemaVersion is invalid")
}
if _, exists := snapshotTypes[key]; exists {
violations = append(violations, prefix+" type and schemaVersion are duplicated")
}
snapshotTypes[key] = struct{}{}
if !safeRelativeJSONRef(snapshot.SchemaRef) {
violations = append(violations, prefix+".schemaRef must be a safe relative JSON reference")
}
if snapshot.Retention.KeepForSeconds <= 0 || snapshot.Retention.KeepForSeconds > 31*24*60*60 || snapshot.Retention.MaxRecords <= 0 || snapshot.Retention.MaxRecords > 10000 {
violations = append(violations, prefix+" retention is invalid")
}
}
queryTemplates := map[string]domain.GameClientBridgeQueryTemplateDeclaration{}
for index, template := range bridge.QueryTemplates {
prefix := fmt.Sprintf("%s.queryTemplates[%d]", field, index)
if !clientManagerIdentifierPattern.MatchString(template.Key) {
violations = append(violations, prefix+".key is invalid")
}
if _, exists := queryTemplates[template.Key]; exists {
violations = append(violations, prefix+".key is duplicated")
}
queryTemplates[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.Engine != "sqlite" {
violations = append(violations, prefix+".engine must be sqlite")
}
if !safeRelativeJSONRef(template.ParameterSchemaRef) || !safeRelativeJSONRef(template.ResultSchemaRef) {
violations = append(violations, prefix+" schema references must be safe relative JSON references")
}
if template.MaxRows < 1 || template.MaxRows > 500 {
violations = append(violations, prefix+".maxRows is invalid")
}
if template.TimeoutSeconds < 1 || template.TimeoutSeconds > 60 {
violations = append(violations, prefix+".timeoutSeconds is invalid")
}
if template.PollIntervalSeconds < 0 || template.PollIntervalSeconds > 86400 {
violations = append(violations, prefix+".pollIntervalSeconds is invalid")
}
if template.SQLRef != "" && !safeRelativeSQLRef(template.SQLRef) {
violations = append(violations, prefix+".sqlRef must reference a package-relative SQL asset")
}
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")
}
if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery) {
violations = append(violations, prefix+" transport must be sqlite with remote.run.db.sqlite.query capability")
}
}
logProjectionKeys := map[string]struct{}{}
for index, projection := range bridge.LogProjections {
prefix := fmt.Sprintf("%s.logProjections[%d]", field, index)
if !clientManagerIdentifierPattern.MatchString(projection.Key) {
violations = append(violations, prefix+".key is invalid")
}
if _, exists := logProjectionKeys[projection.Key]; exists {
violations = append(violations, prefix+".key is duplicated")
}
logProjectionKeys[projection.Key] = struct{}{}
violations = append(violations, validateGameClientBridgeLogProjection(prefix, projection)...)
}
dataPackKeys := map[string]struct{}{}
for index, dataPack := range bridge.DataPacks {
prefix := fmt.Sprintf("%s.dataPacks[%d]", field, index)
if !validDistributionLogicalKey(dataPack.Key) {
violations = append(violations, prefix+".key is invalid")
}
if _, exists := dataPackKeys[dataPack.Key]; exists {
violations = append(violations, prefix+".key is duplicated")
}
dataPackKeys[dataPack.Key] = struct{}{}
if dataPack.DatabaseUserVersion < 1 || len(dataPack.LogParserRefs) == 0 || len(dataPack.ConfigMapRefs) == 0 {
violations = append(violations, prefix+" must declare a database version and parser/config assets")
}
refs := append(domain.CopyStringSlice(dataPack.LogParserRefs), dataPack.ConfigMapRefs...)
refs = append(refs, dataPack.DataRefs...)
for _, ref := range refs {
if !safeRelativeJSONRef(ref) {
violations = append(violations, prefix+" asset reference is invalid")
}
}
}
pageDeclarations := map[string]domain.GamePluginPage{}
for _, page := range pages {
pageDeclarations[page.Key] = page
}
features := map[string]domain.GameClientBridgeFeatureDeclaration{}
for index, feature := range bridge.Features {
prefix := fmt.Sprintf("%s.features[%d]", field, index)
if !clientManagerIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) {
violations = append(violations, prefix+".key is invalid or unsafe")
}
if _, exists := features[feature.Key]; exists {
violations = append(violations, prefix+".key is duplicated")
}
features[feature.Key] = feature
if strings.TrimSpace(feature.Title) == "" || len([]rune(feature.Title)) > 80 {
violations = append(violations, prefix+".title is invalid")
}
if !containsString(permissions, feature.Permission) {
violations = append(violations, prefix+".permission must be declared by the plugin")
}
if len(feature.RequiredHandlers) == 0 && len(feature.RequiredEventProducers) == 0 {
violations = append(violations, prefix+" must require a handler or event producer")
}
for _, handler := range feature.RequiredHandlers {
if !clientManagerIdentifierPattern.MatchString(handler) {
violations = append(violations, prefix+".requiredHandlers contains an invalid handler")
}
}
for _, producer := range feature.RequiredEventProducers {
if !clientManagerIdentifierPattern.MatchString(producer) {
violations = append(violations, prefix+".requiredEventProducers contains an invalid producer")
}
}
violations = append(violations, duplicateViolations(prefix+".requiredHandlers", feature.RequiredHandlers)...)
violations = append(violations, duplicateViolations(prefix+".requiredEventProducers", feature.RequiredEventProducers)...)
}
for _, page := range pages {
for _, featureKey := range page.FeatureKeys {
feature, exists := features[featureKey]
if !exists {
violations = append(violations, field+" page "+page.Key+" references undeclared feature "+featureKey)
continue
}
if !containsString(page.Permissions, feature.Permission) {
violations = append(violations, field+" page "+page.Key+" must declare feature permission "+feature.Permission)
}
}
}
for index, page := range bridge.Pages {
prefix := fmt.Sprintf("%s.pages[%d]", field, index)
pageDeclaration, pageExists := pageDeclarations[page.PageKey]
if !pageExists {
violations = append(violations, prefix+".pageKey must reference a declared plugin page")
}
for _, commandType := range page.CommandTypes {
if _, exists := commandTypes[commandType]; !exists {
violations = append(violations, prefix+" references undeclared command "+commandType)
}
}
for _, snapshotType := range page.SnapshotTypes {
found := false
for key := range snapshotTypes {
if strings.HasPrefix(key, snapshotType+"\x00") {
found = true
break
}
}
if !found {
violations = append(violations, prefix+" references undeclared snapshot "+snapshotType)
}
}
for _, templateKey := range page.QueryTemplateKeys {
template, exists := queryTemplates[templateKey]
if !exists {
violations = append(violations, prefix+" references undeclared query template "+templateKey)
continue
}
if !containsString(pageDeclaration.Permissions, template.Permission) {
violations = append(violations, prefix+" must declare query template permission "+template.Permission)
}
if !containsString(pageDeclaration.BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest)) {
violations = append(violations, prefix+" must declare remote.access.request for query templates")
}
}
for _, featureKey := range page.FeatureKeys {
feature, exists := features[featureKey]
if !exists {
violations = append(violations, prefix+" references undeclared feature "+featureKey)
continue
}
if !containsString(pageDeclaration.Permissions, feature.Permission) {
violations = append(violations, prefix+" must declare feature permission "+feature.Permission)
}
}
}
return violations
}
func validateGameClientBridgeLogProjection(prefix string, projection domain.GameClientBridgeLogProjectionDeclaration) []string {
var violations []string
if len(projection.StreamKeys) < 1 || len(projection.StreamKeys) > 64 {
violations = append(violations, prefix+".streamKeys must contain between 1 and 64 streams")
}
for _, streamKey := range projection.StreamKeys {
if !clientManagerIdentifierPattern.MatchString(streamKey) {
violations = append(violations, prefix+".streamKeys contains an invalid stream key")
}
}
violations = append(violations, duplicateViolations(prefix+".streamKeys", projection.StreamKeys)...)
captures := map[string]struct{}{}
if len(projection.Steps) < 1 || len(projection.Steps) > 64 {
violations = append(violations, prefix+".steps must contain between 1 and 64 patterns")
}
for index, step := range projection.Steps {
stepPrefix := fmt.Sprintf("%s.steps[%d].pattern", prefix, index)
if strings.TrimSpace(step.Pattern) == "" || len([]rune(step.Pattern)) > 16384 {
violations = append(violations, stepPrefix+" is empty or too large")
continue
}
compiled, err := regexp.Compile(step.Pattern)
if err != nil {
violations = append(violations, stepPrefix+" must be a valid regular expression")
continue
}
for _, capture := range compiled.SubexpNames() {
if capture != "" {
captures[capture] = struct{}{}
}
}
}
if len(projection.CorrelationFields) < 1 || len(projection.CorrelationFields) > 64 {
violations = append(violations, prefix+".correlationFields must contain between 1 and 64 captures")
}
for _, field := range projection.CorrelationFields {
if !gameClientBridgeCaptureNamePattern.MatchString(field) {
violations = append(violations, prefix+".correlationFields contains an invalid capture name")
continue
}
if _, exists := captures[field]; !exists {
violations = append(violations, prefix+".correlationFields references undeclared capture "+field)
}
}
violations = append(violations, duplicateViolations(prefix+".correlationFields", projection.CorrelationFields)...)
if projection.MaxInterveningLines < 0 || projection.MaxInterveningLines > 100000 {
violations = append(violations, prefix+".maxInterveningLines is invalid")
}
violations = append(violations, validateGameClientBridgeLogProjectionTarget(prefix+".target", projection.Target, captures)...)
if projection.Presence == nil {
return violations
}
presence := projection.Presence
if !gameClientBridgeFieldPattern.MatchString(presence.TimestampField) || !gameClientBridgeLogProjectionTargetDeclaresField(projection.Target, presence.TimestampField) {
violations = append(violations, prefix+".presence.timestampField must reference a projected target field")
}
if presence.ActiveWindowSeconds < 1 || presence.ActiveWindowSeconds > 31536000 {
violations = append(violations, prefix+".presence.activeWindowSeconds is invalid")
}
if presence.ActivityTarget != nil {
violations = append(violations, validateGameClientBridgeLogProjectionTarget(prefix+".presence.activityTarget", *presence.ActivityTarget, captures)...)
}
return violations
}
func validateGameClientBridgeLogProjectionTarget(prefix string, target domain.GameClientBridgeLogProjectionTargetDeclaration, captures map[string]struct{}) []string {
var violations []string
if !gameClientBridgeCollectionPattern.MatchString(target.Collection) {
violations = append(violations, prefix+".collection is invalid")
}
if len(target.UpsertKeys) < 1 || len(target.UpsertKeys) > 8 {
violations = append(violations, prefix+".upsertKeys must contain between 1 and 8 fields")
}
for _, key := range target.UpsertKeys {
if !gameClientBridgeFieldPattern.MatchString(key) {
violations = append(violations, prefix+".upsertKeys contains an invalid field")
}
if !gameClientBridgeLogProjectionTargetDeclaresField(target, key) {
violations = append(violations, prefix+".upsertKeys field "+key+" is not projected")
}
}
violations = append(violations, duplicateViolations(prefix+".upsertKeys", target.UpsertKeys)...)
if len(target.CaptureMappings) < 1 || len(target.CaptureMappings) > 64 {
violations = append(violations, prefix+".captureMappings must contain between 1 and 64 mappings")
}
projectedFields := map[string]struct{}{}
for destination, capture := range target.CaptureMappings {
if !gameClientBridgeFieldPattern.MatchString(destination) || !gameClientBridgeCaptureNamePattern.MatchString(capture) {
violations = append(violations, prefix+".captureMappings contains an invalid field or capture")
}
if _, exists := captures[capture]; !exists {
violations = append(violations, prefix+".captureMappings references undeclared capture "+capture)
}
projectedFields[destination] = struct{}{}
}
if len(target.FixedValues) > 64 {
violations = append(violations, prefix+".fixedValues contains too many fields")
}
for destination, value := range target.FixedValues {
if !gameClientBridgeFieldPattern.MatchString(destination) || len([]rune(value)) > 4096 {
violations = append(violations, prefix+".fixedValues contains an invalid field or oversized value")
}
if _, exists := projectedFields[destination]; exists {
violations = append(violations, prefix+" declares field "+destination+" more than once")
}
projectedFields[destination] = struct{}{}
}
if target.ObservedAtField != "" {
if !gameClientBridgeFieldPattern.MatchString(target.ObservedAtField) {
violations = append(violations, prefix+".observedAtField is invalid")
}
if _, exists := projectedFields[target.ObservedAtField]; exists {
violations = append(violations, prefix+" declares field "+target.ObservedAtField+" more than once")
}
}
return violations
}
func gameClientBridgeLogProjectionTargetDeclaresField(target domain.GameClientBridgeLogProjectionTargetDeclaration, field string) bool {
if target.ObservedAtField == field {
return true
}
if _, exists := target.CaptureMappings[field]; exists {
return true
}
_, exists := target.FixedValues[field]
return exists
}
func validCompanionProofEnvironment(value string) bool {
if len(value) < 3 || len(value) > 64 || value[0] < 'A' || value[0] > 'Z' {
return false
}
for _, character := range value[1:] {
if character != '_' && (character < 'A' || character > 'Z') && (character < '0' || character > '9') {
return false
}
}
reserved := map[string]struct{}{
"COMSPEC": {}, "DYLD_INSERT_LIBRARIES": {}, "DYLD_LIBRARY_PATH": {}, "HOME": {}, "LD_LIBRARY_PATH": {}, "LD_PRELOAD": {},
"PATH": {}, "PATHEXT": {}, "SHELL": {}, "SYSTEMROOT": {}, "TEMP": {}, "TMP": {}, "USERPROFILE": {}, "WINDIR": {},
}
if _, exists := reserved[value]; exists {
return false
}
return true
}
func unsafeGameClientBridgeCommandType(value string) bool {
tokens := gameClientBridgePayloadKeyTokens(value)
tokenSet := make(map[string]struct{}, len(tokens))
for _, token := range tokens {
tokenSet[token] = struct{}{}
}
has := func(values ...string) bool {
for _, candidate := range values {
if _, ok := tokenSet[candidate]; ok {
return true
}
}
return false
}
if has("sql") || has("database", "db") && has("execute", "exec", "eval", "run", "query", "statement") || has("query") && has("execute", "exec", "eval", "raw", "statement") {
return true
}
return has("shell", "powershell", "script", "terminal", "execute", "exec", "eval") || has("command", "cmd", "process", "system", "os", "executor") && has("run")
}
func ValidatePluginBridgeAuthorizeRequest(request domain.PluginBridgeAuthorizeRequest) error {
var violations []string
violations = appendRequired(violations, "pluginId", request.PluginID)
violations = appendRequired(violations, "routeKey", request.RouteKey)
violations = appendRequired(violations, "action", string(request.Action))
if !validPluginBridgeAction(request.Action) {
violations = append(violations, "action is not supported")
}
if request.Action == domain.PluginBridgeActionAIInvoke {
violations = appendRequired(violations, "aiPurpose", request.AIPurpose)
if strings.TrimSpace(request.AIPurpose) != "" && !validAIPurpose(request.AIPurpose) {
violations = append(violations, "aiPurpose is not allowed")
}
}
return finish(violations)
}
func ValidatePluginBridgeExecuteRequest(request domain.PluginBridgeExecuteRequest) error {
request = domain.CopyPluginBridgeExecuteRequest(request)
var violations []string
violations = appendRequired(violations, "requestId", request.RequestID)
violations = appendRequired(violations, "pluginId", request.PluginID)
violations = appendRequired(violations, "routeKey", request.RouteKey)
violations = appendRequired(violations, "action", string(request.Action))
if !validPluginBridgeAction(request.Action) {
violations = append(violations, "action is not supported")
}
if request.Action != domain.PluginBridgeActionAIInvoke {
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
}
if request.Action == domain.PluginBridgeActionAIInvoke {
violations = appendRequired(violations, "aiPurpose", request.AIPurpose)
if strings.TrimSpace(request.AIPurpose) != "" && !validAIPurpose(request.AIPurpose) {
violations = append(violations, "aiPurpose is not allowed")
}
}
if len(request.Payload) > maxPluginBridgePayloadKeys {
violations = append(violations, "payload has too many keys")
}
payloadSize := 0
for key, value := range request.Payload {
payloadSize += len(key) + len(value)
if strings.TrimSpace(key) == "" || strings.TrimSpace(key) != key || len([]rune(key)) > 80 {
violations = append(violations, "payload key is invalid")
}
valueLimit := 1024
if remoteAdapterSQLInputKey(strings.TrimPrefix(key, "input.")) {
valueLimit = 16 * 1024
}
if len([]rune(value)) > valueLimit {
violations = append(violations, "payload value is too long")
}
for _, reason := range unsafePluginStringReasons(key) {
violations = append(violations, "payload key: "+reason)
}
for _, reason := range unsafePluginStringReasons(value) {
violations = append(violations, "payload."+key+": "+reason)
}
if containsUnsafeRuntimeSecret(value) || strings.Contains(strings.ToLower(value), "unix://") || strings.Contains(strings.ToLower(value), "tcp://") {
violations = append(violations, "payload contains unsafe content")
}
}
if payloadSize > maxPluginBridgePayloadSize {
violations = append(violations, "payload is too large")
}
return finish(violations)
}
func ValidatePluginMarketplaceFilter(filter domain.PluginMarketplaceFilter) error {
var violations []string
if filter.Status != "" && !validGamePluginStatus(filter.Status) {
violations = append(violations, "status is invalid")
}
if strings.TrimSpace(filter.ServerType) != filter.ServerType {
violations = append(violations, "serverType must not have surrounding whitespace")
}
if strings.TrimSpace(filter.Capability) != filter.Capability {
violations = append(violations, "capability must not have surrounding whitespace")
}
if len([]rune(filter.Keyword)) > maxMarketplaceKeywordSize {
violations = append(violations, "keyword is too long")
}
for _, value := range []fieldString{
{field: "serverType", value: filter.ServerType},
{field: "capability", value: filter.Capability},
{field: "keyword", value: filter.Keyword},
} {
for _, reason := range unsafePluginStringReasons(value.value) {
violations = append(violations, "filter."+value.field+": "+reason)
}
}
return finish(violations)
}
func ValidatePluginMarketplaceStateAction(action domain.PluginMarketplaceStateAction) error {
if validPluginMarketplaceStateAction(action) {
return nil
}
return ValidationError{Violations: []string{"action is not supported"}}
}
func ValidatePluginMarketplacePlugins(plugins []domain.PluginMarketplacePlugin) error {
var violations []string
if len(plugins) > maxMarketplaceListSize {
violations = append(violations, "items is too long")
}
for i, plugin := range plugins {
prefix := fmt.Sprintf("items[%d]", i)
violations = append(violations, validatePluginMarketplacePlugin(prefix, plugin)...)
}
return finish(violations)
}
func ValidatePluginMarketplacePlugin(plugin domain.PluginMarketplacePlugin) error {
return finish(validatePluginMarketplacePlugin("plugin", plugin))
}
func validatePluginMarketplacePlugin(prefix string, plugin domain.PluginMarketplacePlugin) []string {
var violations []string
violations = appendRequired(violations, prefix+".id", plugin.ID)
violations = appendRequired(violations, prefix+".name", plugin.Name)
violations = appendRequired(violations, prefix+".version", plugin.Version)
violations = appendRequired(violations, prefix+".serverType", plugin.ServerType)
violations = appendRequired(violations, prefix+".manifestRef", plugin.ManifestRef)
violations = appendRequired(violations, prefix+".createFormSchemaRef", plugin.CreateFormSchemaRef)
violations = appendRequired(violations, prefix+".source", plugin.Source)
if !validGamePluginStatus(plugin.Status) {
violations = append(violations, prefix+".status is invalid")
}
if len(plugin.Description) > maxPluginDescriptionLength {
violations = append(violations, prefix+".description is too long")
}
if len(plugin.Capabilities) == 0 {
violations = append(violations, prefix+".capabilities must not be empty")
}
for i, capability := range plugin.Capabilities {
if !validPluginRunCapability(capability) {
violations = append(violations, fmt.Sprintf("%s.capabilities[%d] is not allowed", prefix, i))
}
}
for i, osName := range plugin.SupportedOS {
if !validPluginSupportedOS(osName) {
violations = append(violations, fmt.Sprintf("%s.supportedOs[%d] is not allowed", prefix, i))
}
}
violations = append(violations, duplicateViolations(prefix+".supportedOs", plugin.SupportedOS)...)
violations = append(violations, duplicateViolations(prefix+".capabilities", plugin.Capabilities)...)
violations = append(violations, validateDeclaredPluginPermissions(plugin.DeclaredPermissions)...)
violations = append(violations, validateBridgeActions(prefix+".bridgeActions", plugin.BridgeActions)...)
violations = append(violations, validatePluginPages(plugin.Pages)...)
violations = append(violations, duplicateViolations(prefix+".tags", plugin.Tags)...)
violations = append(violations, validateAIPurposes(plugin.AIPurposes)...)
violations = append(violations, validateRemoteAccess(prefix+".remoteAccess", plugin.RemoteAccess, plugin.Capabilities)...)
violations = append(violations, validateSafePluginStrings(prefix, marketplacePluginSafeStrings(plugin))...)
return violations
}
func AuthorizePluginBridgeAction(plugin domain.GamePlugin, request domain.PluginBridgeAuthorizeRequest) (domain.PluginBridgeAuthorization, error) {
result := domain.PluginBridgeAuthorization{
PluginID: request.PluginID,
RouteKey: request.RouteKey,
ServerInstanceID: request.ServerInstanceID,
Action: request.Action,
RequiredPermissions: requiredBridgePermissions(request.Action),
EffectivePermissions: effectivePagePermissions(plugin, request.RouteKey),
}
if err := ValidateGamePlugin(plugin); err != nil {
return result, err
}
if err := ValidatePluginBridgeAuthorizeRequest(request); err != nil {
return result, err
}
if plugin.ID != request.PluginID {
result.Reason = "pluginId must match installed plugin"
return result, nil
}
if plugin.Status != domain.GamePluginStatusInstalled {
result.Reason = "plugin must be installed"
return result, nil
}
if !containsString(plugin.BridgeActions, string(request.Action)) {
result.Reason = "bridge action is not declared by plugin"
return result, nil
}
page, pageFound := findPluginPage(plugin.Pages, request.RouteKey)
if !pageFound {
result.Reason = "routeKey is not declared by plugin"
return result, nil
}
if len(page.BridgeActions) > 0 && !containsString(page.BridgeActions, string(request.Action)) {
result.Reason = "bridge action is not declared by page"
return result, nil
}
if !containsAll(result.EffectivePermissions, result.RequiredPermissions) {
result.Reason = "required permission is missing"
return result, nil
}
if request.Action == domain.PluginBridgeActionAIInvoke && !containsString(plugin.AIPurposes, request.AIPurpose) {
result.Reason = "ai purpose is not declared by plugin"
return result, nil
}
result.Allowed = true
return result, nil
}
func ValidateServerInstance(instance domain.ServerInstance) error {
return validateServerInstance(instance, false)
}
func ValidateStoredServerInstance(instance domain.ServerInstance) error {
return validateServerInstance(instance, true)
}
func validateServerInstance(instance domain.ServerInstance, allowDeleted bool) error {
var violations []string
violations = appendRequired(violations, "id", instance.ID)
violations = appendRequired(violations, "pluginId", instance.PluginID)
violations = appendRequired(violations, "pluginVersion", instance.PluginVersion)
if instance.State != domain.ServerInstanceStateDraft && instance.State != domain.ServerInstanceStateDeleted {
violations = appendRequired(violations, "runEndpointId", instance.RunEndpointID)
}
violations = appendRequired(violations, "name", instance.Name)
if strings.TrimSpace(instance.OwnerUserID) != instance.OwnerUserID {
violations = append(violations, "ownerUserId must not have surrounding whitespace")
}
for i, userID := range instance.AdminUserIDs {
if strings.TrimSpace(userID) == "" {
violations = append(violations, fmt.Sprintf("adminUserIds[%d] is required", i))
}
if strings.TrimSpace(userID) != userID {
violations = append(violations, fmt.Sprintf("adminUserIds[%d] must not have surrounding whitespace", i))
}
if userID == instance.OwnerUserID {
violations = append(violations, fmt.Sprintf("adminUserIds[%d] must not duplicate ownerUserId", i))
}
}
violations = append(violations, duplicateViolations("adminUserIds", instance.AdminUserIDs)...)
if !validServerInstanceState(instance.State) {
violations = append(violations, "state is invalid")
}
if instance.State == domain.ServerInstanceStateDeleted && !allowDeleted {
violations = append(violations, "state must not be deleted on create")
}
if instance.ConfigVersion < 0 {
violations = append(violations, "configVersion must not be negative")
}
if err := ValidateServerDeploymentDefinition(instance.Deployment); err != nil {
violations = append(violations, err.Error())
}
return finish(violations)
}
func ValidateServerDeploymentDefinition(definition domain.ServerDeploymentDefinition) error {
if definition.Mode == "" {
return nil
}
var violations []string
if definition.Mode != domain.ServerDeploymentModeGuided && definition.Mode != domain.ServerDeploymentModeExisting && definition.Mode != domain.ServerDeploymentModeCustom {
violations = append(violations, "deployment mode is invalid")
}
if definition.Shell != domain.ServerCommandShellNone && definition.Shell != domain.ServerCommandShellPosix && definition.Shell != domain.ServerCommandShellPowerShell && definition.Shell != domain.ServerCommandShellCmd {
violations = append(violations, "deployment shell is invalid")
}
if definition.Revision < 0 {
violations = append(violations, "deployment revision must not be negative")
}
for key, value := range definition.CreateInputs {
if !validDistributionLogicalKey(key) || len(value) > 1024 || strings.TrimSpace(value) != value || containsUnsafeRuntimeSecret(value) {
violations = append(violations, "deployment createInputs are invalid")
break
}
}
for key, value := range definition.RuntimeBindings {
if !validDistributionLogicalKey(key) || strings.TrimSpace(value) != value || looksLikeRawHostPath(value) || containsUnsafeRuntimeSecret(value) {
violations = append(violations, "deployment runtimeBindings are invalid")
break
}
}
for _, value := range []string{definition.ServerRoot, definition.WorkingDirectory} {
if value != "" && (len(value) > 1024 || strings.TrimSpace(value) != value || !looksLikeAbsoluteHostPath(value)) {
violations = append(violations, "deployment path must be an absolute host path")
break
}
}
for _, value := range []string{definition.InstallCommand, definition.StartCommand, definition.StopCommand, definition.StatusCommand} {
if value != "" && (len(value) > 4096 || strings.TrimSpace(value) != value || strings.ContainsAny(value, "\x00\r\n") || containsUnsafeRuntimeSecret(value)) {
violations = append(violations, "deployment command is invalid or contains a secret")
break
}
}
if definition.Mode == domain.ServerDeploymentModeCustom && strings.TrimSpace(definition.StartCommand) == "" {
violations = append(violations, "custom deployment requires a start command")
}
return finish(violations)
}
func looksLikeAbsoluteHostPath(value string) bool {
trimmed := strings.TrimSpace(value)
return strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, `\\`) || (len(trimmed) >= 3 && trimmed[1] == ':' && (trimmed[2] == '\\' || trimmed[2] == '/'))
}
func ValidateServerInstanceUpdate(update domain.ServerInstanceUpdate) error {
var violations []string
if update.Name != nil {
name := strings.TrimSpace(*update.Name)
if name == "" {
violations = append(violations, "name is required")
}
if name != *update.Name {
violations = append(violations, "name must not have surrounding whitespace")
}
}
return finish(violations)
}
func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) error {
return ValidateServerInstanceDependenciesForCapabilities(instance, plugin, endpoint, plugin.RequiredRunCapabilities)
}
func ValidateServerInstanceDependenciesForCapabilities(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint, requiredRunCapabilities []string) error {
var violations []string
if plugin.ID == "" {
violations = append(violations, "plugin is required")
} else {
if plugin.ID != instance.PluginID {
violations = append(violations, "pluginId must match plugin")
}
if plugin.Status != domain.GamePluginStatusInstalled {
violations = append(violations, "plugin must be installed")
}
if plugin.Version != instance.PluginVersion {
violations = append(violations, "pluginVersion must match plugin")
}
}
if endpoint.ID == "" {
violations = append(violations, "run endpoint is required")
} else {
if endpoint.ID != instance.RunEndpointID {
violations = append(violations, "runEndpointId must match run endpoint")
}
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
violations = append(violations, "run endpoint must be online or degraded")
}
missing := MissingCapabilities(endpoint.Capabilities, requiredRunCapabilities)
if len(missing) > 0 {
violations = append(violations, "run endpoint missing required capabilities: "+strings.Join(missing, ", "))
}
}
return finish(violations)
}
func ValidatePlatformResourceUsage(usage domain.PlatformResourceUsage) error {
var violations []string
violations = appendPercentViolation(violations, "cpuPercent", usage.CPUPercent)
violations = appendPercentViolation(violations, "memoryPercent", usage.MemoryPercent)
violations = appendPercentViolation(violations, "diskPercent", usage.DiskPercent)
violations = appendRequired(violations, "source", usage.Source)
if usage.CollectedAt.IsZero() {
violations = append(violations, "collectedAt is required")
}
return finish(violations)
}
func ValidateServerMetricsList(items []domain.ServerMetrics) error {
var violations []string
if len(items) > 1000 {
violations = append(violations, "items is too long")
}
for i, item := range items {
field := fmt.Sprintf("items[%d]", i)
if strings.TrimSpace(item.ServerInstanceID) == "" {
violations = append(violations, field+".serverInstanceId is required")
}
if item.CollectedAt.IsZero() {
violations = append(violations, field+".collectedAt is required")
}
if strings.TrimSpace(item.Source) == "" {
violations = append(violations, field+".source is required")
}
violations = appendOptionalPercentViolation(violations, field+".cpuPercent", item.CPUPercent)
violations = appendOptionalPercentViolation(violations, field+".memoryPercent", item.MemoryPercent)
violations = appendOptionalPercentViolation(violations, field+".diskPercent", item.DiskPercent)
if item.PlayerCount != nil && *item.PlayerCount < 0 {
violations = append(violations, field+".playerCount must not be negative")
}
if item.MaxPlayers != nil && *item.MaxPlayers < 0 {
violations = append(violations, field+".maxPlayers must not be negative")
}
if item.TPS != nil && (*item.TPS < 0 || *item.TPS > 100) {
violations = append(violations, field+".tps must be between 0 and 100")
}
if item.LatencyMS != nil && *item.LatencyMS < 0 {
violations = append(violations, field+".latencyMs must not be negative")
}
}
return finish(violations)
}
func ValidateServerConfig(config domain.ServerConfig) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", config.ServerInstanceID)
violations = appendRequired(violations, "format", config.Format)
violations = appendRequired(violations, "key", config.Key)
if config.ConfigVersion <= 0 {
violations = append(violations, "configVersion must be positive")
}
if len([]byte(config.Content)) > maxServerConfigContentSize {
violations = append(violations, "content is too large")
}
if config.Checksum != "" && !validSHA256Checksum(config.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if config.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
if containsUnsafeRuntimeSecret(config.Content) {
violations = append(violations, "content must not expose raw secrets, host paths, or direct sockets")
}
return finish(violations)
}
func ValidateServerConfigDiffRequest(request domain.ServerConfigDiffRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "key", request.Key)
if request.ExpectedConfigVersion <= 0 {
violations = append(violations, "expectedConfigVersion must be positive")
}
if !validLogicalFileKey(request.Key) || !validConfigFileKey(request.Key) {
violations = append(violations, "key is not allowed")
}
if len([]byte(request.ProposedContent)) > maxServerConfigContentSize {
violations = append(violations, "proposedContent is too large")
}
if containsUnsafeRuntimeSecret(request.ProposedContent) {
violations = append(violations, "proposedContent must not expose raw secrets, host paths, or direct sockets")
}
if request.ProposedContentInputRef != "" && !validScopedInputRef(request.ProposedContentInputRef) {
violations = append(violations, "proposedContentInputRef is not allowed")
}
return finish(violations)
}
func ValidateServerConfigWriteApproval(approval domain.ServerConfigWriteApproval) error {
request := domain.ServerConfigDiffRequest{
ServerInstanceID: approval.ServerInstanceID,
ExpectedConfigVersion: approval.ExpectedConfigVersion,
Key: approval.Key,
ProposedContent: approval.ProposedContent,
ProposedContentInputRef: approval.ProposedContentInputRef,
}
var violations []string
if err := ValidateServerConfigDiffRequest(request); err != nil {
if validationErr, ok := err.(ValidationError); ok {
violations = append(violations, validationErr.Violations...)
} else {
violations = append(violations, err.Error())
}
}
violations = appendRequired(violations, "idempotencyKey", approval.IdempotencyKey)
if containsUnsafeRuntimeSecret(approval.IdempotencyKey) || looksLikeRawHostPath(approval.IdempotencyKey) {
violations = append(violations, "idempotencyKey is not allowed")
}
return finish(violations)
}
func ValidateFileOperationDispatchRequest(request domain.FileOperationDispatchRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "key", request.Key)
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
if !validFileOperationKind(request.Operation) {
violations = append(violations, "operation is invalid")
}
if !validLogicalFileKey(request.Key) {
violations = append(violations, "key is not allowed")
}
if request.Operation == domain.FileOperationWrite && request.InputRef == "" {
violations = append(violations, "inputRef is required for writes")
}
if request.InputRef != "" && !validScopedInputRef(request.InputRef) {
violations = append(violations, "inputRef is not allowed")
}
if len([]byte(request.Content)) > maxJobExecutionContentSize {
violations = append(violations, "content is too large")
}
if containsUnsafeRuntimeSecret(request.Content) {
violations = append(violations, "content must not expose raw secrets, host paths, or direct sockets")
}
if request.ExpectedConfigVersion < 0 {
violations = append(violations, "expectedConfigVersion must not be negative")
}
for _, value := range []string{request.PluginID, request.IdempotencyKey} {
if containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "unix://") {
violations = append(violations, "request contains unsafe content")
}
}
return finish(violations)
}
func ValidateServerFileListRequest(request domain.ServerFileListRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "directoryKey", request.DirectoryKey)
if !validLogicalFileKey(request.DirectoryKey) {
violations = append(violations, "directoryKey is not allowed")
}
if request.Path != "" && !validLogicalFileKey(request.Path) {
violations = append(violations, "path is not allowed")
}
if len([]rune(request.Query)) > 80 || containsUnsafeRuntimeSecret(request.Query) || looksLikeRawHostPath(request.Query) {
violations = append(violations, "query is not allowed")
}
if request.IdempotencyKey != "" && (containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey)) {
violations = append(violations, "idempotencyKey is not allowed")
}
return finish(violations)
}
func ValidateServerFileReadRequest(request domain.ServerFileReadRequest) error {
return ValidateFileOperationDispatchRequest(domain.FileOperationDispatchRequest{ServerInstanceID: request.ServerInstanceID, PluginID: request.PluginID, Operation: domain.FileOperationRead, Key: request.Key, IdempotencyKey: request.IdempotencyKey})
}
func ValidateServerFileReadSnapshotRequest(serverInstanceID string, key string) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", serverInstanceID)
violations = appendRequired(violations, "key", key)
if !validLogicalFileKey(key) {
violations = append(violations, "key is not allowed")
}
return finish(violations)
}
func ValidateServerFileWriteRequest(request domain.ServerFileWriteRequest) error {
return ValidateFileOperationDispatchRequest(domain.FileOperationDispatchRequest{ServerInstanceID: request.ServerInstanceID, PluginID: request.PluginID, Operation: domain.FileOperationWrite, Key: request.Key, InputRef: request.InputRef, Content: request.Content, ExpectedConfigVersion: request.ExpectedVersion, ExpectedChecksum: request.ExpectedChecksum, IdempotencyKey: request.IdempotencyKey})
}
func ValidateServerFileUploadRequest(request domain.ServerFileUploadRequest) error {
request = domain.CopyServerFileUploadRequest(request)
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "directoryKey", request.DirectoryKey)
violations = appendRequired(violations, "filename", request.Filename)
violations = appendRequired(violations, "checksum", request.Checksum)
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
if !validLogicalFileKey(request.DirectoryKey) {
violations = append(violations, "directoryKey is not allowed")
}
if request.RelativePath != "" && !validLogicalFileKey(request.RelativePath) {
violations = append(violations, "relativePath is not allowed")
}
if !validUploadFilename(request.Filename) {
violations = append(violations, "filename is not allowed")
}
if len(request.Payload) == 0 {
violations = append(violations, "payload is required")
}
if int64(len(request.Payload)) > MaxArtifactBytes {
violations = append(violations, fmt.Sprintf("payload must not exceed %d", MaxArtifactBytes))
}
if request.Checksum != "" {
if !validSHA256Checksum(request.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
} else if request.Checksum != BytesChecksum(request.Payload) {
violations = append(violations, "checksum does not match payload")
}
}
for _, value := range []string{request.ServerInstanceID, request.DirectoryKey, request.RelativePath, request.Filename, request.IdempotencyKey} {
if containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "unix://") {
violations = append(violations, "request contains unsafe content")
break
}
}
return finish(violations)
}
func ValidateServerFileDownloadRequest(request domain.ServerFileDownloadRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "key", request.Key)
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
if !validLogicalFileKey(request.Key) {
violations = append(violations, "key is not allowed")
}
if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) {
violations = append(violations, "idempotencyKey is not allowed")
}
return finish(violations)
}
func ValidateRunFileInputChunkRequest(request domain.RunFileInputChunkRequest) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", request.RunEndpointID)
violations = appendRequired(violations, "sessionToken", request.SessionToken)
violations = appendRequired(violations, "jobId", request.JobID)
violations = appendRequired(violations, "leaseToken", request.LeaseToken)
if request.Attempt <= 0 {
violations = append(violations, "attempt must be positive")
}
if request.Offset < 0 {
violations = append(violations, "offset must not be negative")
}
if request.Length <= 0 || request.Length > MaxArtifactDownloadBytes {
violations = append(violations, fmt.Sprintf("length must be between 1 and %d", MaxArtifactDownloadBytes))
}
return finish(violations)
}
func appendPercentViolation(violations []string, field string, value float64) []string {
if value < 0 || value > 100 {
return append(violations, field+" must be between 0 and 100")
}
return violations
}
func appendOptionalPercentViolation(violations []string, field string, value *float64) []string {
if value == nil {
return violations
}
return appendPercentViolation(violations, field, *value)
}
func containsUnsafeRuntimeSecret(value string) bool {
lower := strings.ToLower(value)
unsafeFragments := []string{
"/users/",
"/var/run/",
"unix://",
"tcp://",
"bearer ",
"api_key=",
"apikey=",
"password=",
"secret=",
"sk-",
}
for _, fragment := range unsafeFragments {
if strings.Contains(lower, fragment) {
return true
}
}
return false
}
func ValidateRunEndpoint(endpoint domain.RunEndpoint) error {
var violations []string
violations = appendRequired(violations, "id", endpoint.ID)
violations = appendRequired(violations, "displayName", endpoint.DisplayName)
violations = appendRequired(violations, "version", endpoint.Version)
if endpoint.Architecture != "" {
if !validDistributionTargetOS(endpoint.Platform) {
violations = append(violations, "platform is invalid")
}
if !validDistributionTargetArch(endpoint.Architecture) {
violations = append(violations, "architecture is invalid")
}
}
if !validRunEndpointStatus(endpoint.Status) {
violations = append(violations, "status is invalid")
}
violations = appendCapacityViolations(violations, endpoint.Capacity)
for i, capability := range endpoint.Capabilities {
if strings.TrimSpace(capability) == "" {
violations = append(violations, fmt.Sprintf("capabilities[%d] is required", i))
}
}
return finish(violations)
}
func ValidateJob(job domain.Job) error {
var violations []string
violations = appendRequired(violations, "id", job.ID)
violations = appendRequired(violations, "runEndpointId", job.RunEndpointID)
violations = appendRequired(violations, "capability", job.Capability)
violations = appendRequired(violations, "idempotencyKey", job.IdempotencyKey)
if !validJobState(job.State) {
violations = append(violations, "state is invalid")
}
if job.Progress.Percent < 0 || job.Progress.Percent > 100 {
violations = append(violations, "progress.percent must be between 0 and 100")
}
if job.Progress.Phase != "" && !validDeploymentProgressPhase(job.Progress.Phase) {
violations = append(violations, "progress.phase is invalid")
}
if len(job.Progress.Message) > maxProgressMessageLength {
violations = append(violations, "progress.message is too long")
}
if job.Attempt < 0 {
violations = append(violations, "attempt must not be negative")
}
if job.RetryPolicy.MaxAttempts <= 0 {
violations = append(violations, "retryPolicy.maxAttempts must be positive")
}
if job.RetryPolicy.InitialBackoffSeconds <= 0 || job.RetryPolicy.MaxBackoffSeconds < job.RetryPolicy.InitialBackoffSeconds {
violations = append(violations, "retryPolicy backoff must be positive and bounded")
}
if job.Attempt > job.RetryPolicy.MaxAttempts {
violations = append(violations, "attempt must not exceed retryPolicy.maxAttempts")
}
if job.LeaseTokenHash != "" && len(job.LeaseTokenHash) != 64 {
violations = append(violations, "leaseTokenHash must be a SHA-256 hash")
}
if len(job.CancelReason) > maxProgressMessageLength {
violations = append(violations, "cancelReason is too long")
}
if len(job.ReconcileOutcome) > maxProgressMessageLength {
violations = append(violations, "reconcileOutcome is too long")
}
if job.TargetKey != "" && !validLogicalFileKey(job.TargetKey) {
violations = append(violations, "targetKey is not allowed")
}
if job.InputRef != "" && !validScopedInputRef(job.InputRef) {
violations = append(violations, "inputRef is not allowed")
}
if len([]byte(job.ExecutionInput.Content)) > maxJobExecutionContentSize {
violations = append(violations, "executionInput.content is too large")
}
if job.ExecutionInput.MaxReadBytes < 0 || job.ExecutionInput.MaxReadBytes > maxJobExecutionContentSize {
violations = append(violations, "executionInput.maxReadBytes is out of bounds")
}
if job.ExecutionInput.ExpectedChecksum != "" && !validSHA256Checksum(job.ExecutionInput.ExpectedChecksum) {
violations = append(violations, "executionInput.expectedChecksum must be sha256:<hex>")
}
if len(job.ExecutionInput.DLLExtensions) > 16 {
violations = append(violations, "executionInput.dllExtensions must not exceed 16")
}
if len(job.ExecutionInput.DLLExtensions) > 0 && (job.Capability != domain.LifecycleCapabilityStart || job.ExecutionInput.LifecycleOperation != "start" || job.ServerInstanceID == "") {
violations = append(violations, "executionInput.dllExtensions are allowed only for scoped process.start jobs")
}
for i, plan := range job.ExecutionInput.DLLExtensions {
violations = append(violations, validateRuntimeDLLExtensionPlan(fmt.Sprintf("executionInput.dllExtensions[%d]", i), plan)...)
}
if job.ExecutionInput.LogSource != nil {
violations = append(violations, validateRuntimeLogSourcePlanForJob("executionInput.logSource", job.ExecutionInput.LogSource)...)
if job.Capability != domain.JobCapabilityLogsBackfill || job.ServerInstanceID == "" {
violations = append(violations, "executionInput.logSource is allowed only for logs.backfill jobs")
}
}
if len(job.ExecutionInput.LogSources) > 0 {
if job.Capability != domain.LifecycleCapabilityStart || job.ExecutionInput.LifecycleOperation != "start" || job.ServerInstanceID == "" {
violations = append(violations, "executionInput.logSources are allowed only for scoped process.start jobs")
}
seenKinds := map[string]struct{}{}
for i, source := range job.ExecutionInput.LogSources {
prefix := fmt.Sprintf("executionInput.logSources[%d]", i)
violations = append(violations, validateRuntimeProcessLogSourcePlanForJob(prefix, source)...)
if _, exists := seenKinds[source.Kind]; exists {
violations = append(violations, "executionInput.logSources kind is duplicated")
}
seenKinds[source.Kind] = struct{}{}
}
}
if job.ExecutionInput.SourceRCON != nil {
violations = append(violations, validateRuntimeSourceRCONPlan("executionInput.sourceRcon", job.ExecutionInput.SourceRCON)...)
isSourceCommand := job.Capability == domain.JobCapabilityRemoteRunRCONCommand
if !isSourceCommand || job.ExecutionInput.RemoteAdapterKind != "rcon" {
violations = append(violations, "executionInput.sourceRcon is allowed only for rcon jobs")
}
if job.RetryPolicy.MaxAttempts != 1 {
violations = append(violations, "executionInput.sourceRcon jobs must have one attempt")
}
if len(job.ExecutionInput.Inputs) != 0 {
violations = append(violations, "executionInput.sourceRcon must not persist adapter inputs")
}
}
violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...)
if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) {
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
}
if len([]byte(job.ExecutionResult.Content)) > maxJobExecutionContentSize {
violations = append(violations, "executionResult.content is too large")
}
if len(job.ExecutionResult.Summary) > maxSummaryLength {
violations = append(violations, "executionResult.summary is too long")
}
if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesList || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite {
if job.ServerInstanceID == "" {
violations = append(violations, "serverInstanceId is required for scoped file jobs")
}
if job.TargetKey == "" {
violations = append(violations, "targetKey is required for scoped file jobs")
}
}
if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesWrite {
if job.InputRef == "" {
violations = append(violations, "inputRef is required for scoped write jobs")
}
}
if isRemoteRunCapability(job.Capability) {
if job.ServerInstanceID == "" {
violations = append(violations, "serverInstanceId is required for remote access jobs")
}
if remoteCapabilityRequiresTargetKey(job.Capability) && job.TargetKey == "" {
violations = append(violations, "targetKey is required for remote access jobs")
}
if remoteCapabilityRequiresInputRef(job.Capability) && job.InputRef == "" {
violations = append(violations, "inputRef is required for remote access jobs")
}
}
return finish(violations)
}
func validateRuntimeLogSourcePlanForJob(prefix string, source *domain.RuntimeLogSource) []string {
if source == nil {
return nil
}
var violations []string
violations = append(violations, validateProfileKey(prefix+".key", source.Key)...)
violations = append(violations, validateProfileKey(prefix+".targetKey", source.TargetKey)...)
violations = append(violations, validateProfileKey(prefix+".streamKey", source.StreamKey)...)
if source.Kind != "file.tail" {
violations = append(violations, prefix+".kind must be file.tail")
}
if source.CursorKind != "" && !oneOf(source.CursorKind, "offset", "fingerprint") {
violations = append(violations, prefix+".cursorKind is invalid")
}
if source.RetentionDays < 0 || source.RetentionDays > 365 {
violations = append(violations, prefix+".retentionDays is invalid")
}
return violations
}
func validateRuntimeProcessLogSourcePlanForJob(prefix string, source domain.RuntimeLogSource) []string {
var violations []string
violations = append(violations, validateProfileKey(prefix+".key", source.Key)...)
if source.TargetKey != "" {
violations = append(violations, validateProfileKey(prefix+".targetKey", source.TargetKey)...)
}
violations = append(violations, validateProfileKey(prefix+".streamKey", source.StreamKey)...)
if source.Kind != "process.stdout" && source.Kind != "process.stderr" {
violations = append(violations, prefix+".kind must be process.stdout or process.stderr")
}
if source.CursorKind != "" && source.CursorKind != "sequence" {
violations = append(violations, prefix+".cursorKind is invalid")
}
if source.RetentionDays < 0 || source.RetentionDays > 365 {
violations = append(violations, prefix+".retentionDays is invalid")
}
return violations
}
func ValidateArtifact(artifact domain.Artifact) error {
var violations []string
violations = appendRequired(violations, "id", artifact.ID)
violations = appendRequired(violations, "ownerId", artifact.OwnerID)
violations = appendRequired(violations, "checksum", artifact.Checksum)
if !validArtifactOwnerKind(artifact.OwnerKind) {
violations = append(violations, "ownerKind is invalid")
}
if !validArtifactState(artifact.State) {
violations = append(violations, "state is invalid")
}
if artifact.SizeBytes < 0 {
violations = append(violations, "sizeBytes must not be negative")
}
return finish(violations)
}
func ValidateLogStream(stream domain.LogStream) error {
var violations []string
violations = appendRequired(violations, "id", stream.ID)
violations = appendRequired(violations, "serverInstanceId", stream.ServerInstanceID)
violations = appendRequired(violations, "streamKey", stream.StreamKey)
violations = appendRequired(violations, "retentionPolicy", stream.RetentionPolicy)
if !validLogStreamSource(stream.Source) {
violations = append(violations, "source is invalid")
}
if !validLogStorageBackend(stream.StorageBackend) {
violations = append(violations, "storageBackend is invalid")
}
hasSessionID := strings.TrimSpace(stream.LogSessionID) != ""
hasSessionStart := !stream.SessionStartedAt.IsZero()
// Keep legacy process streams with only a persisted start time valid while
// requiring the complete pair for new generation-scoped streams.
if hasSessionID && !hasSessionStart {
violations = append(violations, "logSessionId and sessionStartedAt must be provided together")
}
if (hasSessionID || hasSessionStart) && stream.Source != domain.LogStreamSourceProcess {
violations = append(violations, "log session metadata is only valid for process streams")
}
return finish(violations)
}
func MissingCapabilities(actual []string, required []string) []string {
actualSet := make(map[string]struct{}, len(actual))
for _, capability := range actual {
actualSet[capability] = struct{}{}
}
var missing []string
for _, capability := range required {
if _, exists := actualSet[capability]; !exists {
missing = append(missing, capability)
}
}
return missing
}
type fieldString struct {
field string
value string
}
func duplicateViolations(field string, values []string) []string {
seen := map[string]struct{}{}
var violations []string
for i, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
violations = append(violations, fmt.Sprintf("%s[%d] is required", field, i))
continue
}
if _, exists := seen[trimmed]; exists {
violations = append(violations, fmt.Sprintf("%s[%d] duplicates an earlier value", field, i))
}
seen[trimmed] = struct{}{}
}
return violations
}
func validateDeclaredPluginPermissions(permissions []string) []string {
var violations []string
for i, permission := range permissions {
if !validPluginPermission(permission) {
violations = append(violations, fmt.Sprintf("permissions[%d] is not allowed", i))
}
}
violations = append(violations, duplicateViolations("permissions", permissions)...)
return violations
}
func validateLifecycleActions(actions domain.PluginLifecycleActions) []string {
var violations []string
required := []fieldString{
{field: "actions.install", value: actions.Install},
{field: "actions.start", value: actions.Start},
{field: "actions.stop", value: actions.Stop},
}
for _, action := range required {
violations = appendRequired(violations, action.field, action.value)
}
for _, action := range []fieldString{
{field: "actions.install", value: actions.Install},
{field: "actions.start", value: actions.Start},
{field: "actions.stop", value: actions.Stop},
{field: "actions.restart", value: actions.Restart},
{field: "actions.status", value: actions.Status},
} {
if strings.TrimSpace(action.value) != "" && !safeRelativeJSONRef(action.value) {
violations = append(violations, action.field+" must be a safe relative JSON reference")
}
}
return violations
}
func validatePluginPages(pages []domain.GamePluginPage) []string {
var violations []string
seenKeys := map[string]struct{}{}
for i, page := range pages {
prefix := fmt.Sprintf("pages[%d]", i)
violations = appendRequired(violations, prefix+".key", page.Key)
violations = appendRequired(violations, prefix+".title", page.Title)
violations = appendRequired(violations, prefix+".path", page.Path)
if len(page.Title) > maxPluginPageTitleLength {
violations = append(violations, prefix+".title is too long")
}
if !validPluginPagePath(page.Path) {
violations = append(violations, prefix+".path is invalid")
}
if page.Bundle.Key != "" || page.Bundle.Version != "" || page.Bundle.IntegritySHA256 != "" {
violations = appendRequired(violations, prefix+".bundle.key", page.Bundle.Key)
violations = appendRequired(violations, prefix+".bundle.version", page.Bundle.Version)
violations = appendRequired(violations, prefix+".bundle.integritySha256", page.Bundle.IntegritySHA256)
if !validDistributionLogicalKey(page.Bundle.Key) || !validPluginPageBundleVersion(page.Bundle.Version) || !validPluginPageBundleIntegrity(page.Bundle.IntegritySHA256) {
violations = append(violations, prefix+".bundle is invalid")
}
}
if page.Key != "" {
if _, exists := seenKeys[page.Key]; exists {
violations = append(violations, prefix+".key duplicates another page")
}
seenKeys[page.Key] = struct{}{}
}
for permissionIndex, permission := range page.Permissions {
if !validPluginPermission(permission) {
violations = append(violations, fmt.Sprintf("%s.permissions[%d] is not allowed", prefix, permissionIndex))
}
}
violations = append(violations, duplicateViolations(prefix+".permissions", page.Permissions)...)
violations = append(violations, validateBridgeActions(prefix+".bridgeActions", page.BridgeActions)...)
for _, key := range page.FeatureKeys {
if !clientManagerIdentifierPattern.MatchString(key) {
violations = append(violations, prefix+".featureKeys contains an invalid feature key")
}
}
violations = append(violations, duplicateViolations(prefix+".featureKeys", page.FeatureKeys)...)
}
return violations
}
func validPluginPageBundleVersion(value string) bool {
if len(value) == 0 || len(value) > 80 {
return false
}
for _, item := range value {
if !(item >= 'a' && item <= 'z' || item >= 'A' && item <= 'Z' || item >= '0' && item <= '9' || item == '.' || item == '_' || item == '-') {
return false
}
}
return true
}
func validPluginPageBundleIntegrity(value string) bool {
if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") {
return false
}
for _, item := range value[len("sha256:"):] {
if !(item >= 'a' && item <= 'f' || item >= '0' && item <= '9') {
return false
}
}
return true
}
func validatePluginFileWorkspace(prefix string, workspace domain.PluginFileWorkspace) []string {
if workspace.DefaultDirectoryKey == "" && len(workspace.Directories) == 0 && len(workspace.Files) == 0 && len(workspace.ConfigFields) == 0 {
return nil
}
var violations []string
directories := map[string]bool{}
files := map[string]domain.PluginLogicalFile{}
for i, item := range workspace.Directories {
field := fmt.Sprintf("%s.directories[%d]", prefix, i)
if !validDistributionLogicalKey(item.Key) || item.Label == "" || !oneOf(item.Scope, "config", "logs") {
violations = append(violations, field+" is invalid")
}
if directories[item.Key] {
violations = append(violations, field+".key duplicates another directory")
}
directories[item.Key] = true
}
if !directories[workspace.DefaultDirectoryKey] {
violations = append(violations, prefix+".defaultDirectoryKey must reference a declared directory")
}
for i, item := range workspace.Files {
field := fmt.Sprintf("%s.files[%d]", prefix, i)
if !validDistributionLogicalKey(item.Key) || !directories[item.DirectoryKey] || item.Label == "" || !oneOf(item.Kind, "config", "log") || (item.Kind == "log" && item.StreamKey == "") {
violations = append(violations, field+" is invalid")
}
if _, exists := files[item.Key]; exists {
violations = append(violations, field+".key duplicates another file")
}
files[item.Key] = item
}
for i, item := range workspace.ConfigFields {
field := fmt.Sprintf("%s.configFields[%d]", prefix, i)
file, exists := files[item.FileKey]
if !validDistributionLogicalKey(item.Key) || !exists || file.Kind != "config" || !file.Editable || item.ConfigKey == "" || item.Label == "" || item.Description == "" || !oneOf(item.Control, "text", "number", "boolean", "port") || !oneOf(item.RestartImpact, "none", "restart-required") || (item.Minimum > 0 && item.Maximum > 0 && item.Minimum > item.Maximum) {
violations = append(violations, field+" is invalid")
}
}
return violations
}
func validateBridgeActions(field string, actions []string) []string {
var violations []string
for i, action := range actions {
if !validPluginBridgeAction(domain.PluginBridgeAction(action)) {
violations = append(violations, fmt.Sprintf("%s[%d] is not supported", field, i))
}
}
violations = append(violations, duplicateViolations(field, actions)...)
return violations
}
func validateAIPurposes(purposes []string) []string {
var violations []string
for i, purpose := range purposes {
if !validAIPurpose(purpose) {
violations = append(violations, fmt.Sprintf("aiPurposes[%d] is not allowed", i))
}
}
violations = append(violations, duplicateViolations("aiPurposes", purposes)...)
return violations
}
func validateRemoteAccess(field string, remote domain.GamePluginRemoteAccess, declaredCapabilities []string) []string {
var violations []string
if len(remote.Methods) == 0 && len(remote.RunCapabilities) == 0 && len(remote.DatabaseEngines) == 0 && !remote.RCON && !remote.LogTransfer {
return violations
}
if len(remote.Methods) == 0 {
violations = append(violations, field+".methods must not be empty when remote access is declared")
}
for i, method := range remote.Methods {
if !validRemoteAccessMethod(method) {
violations = append(violations, fmt.Sprintf("%s.methods[%d] is not allowed", field, i))
}
}
violations = append(violations, duplicateViolations(field+".methods", remote.Methods)...)
for i, capability := range remote.RunCapabilities {
if !validPluginRunCapability(capability) || !isRemoteRunCapability(capability) {
violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] is not allowed", field, i))
continue
}
if !containsString(declaredCapabilities, capability) {
violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] must also be declared in capabilities", field, i))
}
}
violations = append(violations, duplicateViolations(field+".runCapabilities", remote.RunCapabilities)...)
for i, engine := range remote.DatabaseEngines {
if !validRemoteDatabaseEngine(engine) {
violations = append(violations, fmt.Sprintf("%s.databaseEngines[%d] is not allowed", field, i))
}
}
violations = append(violations, duplicateViolations(field+".databaseEngines", remote.DatabaseEngines)...)
if containsString(remote.Methods, "run") && len(remote.RunCapabilities) == 0 {
violations = append(violations, field+".runCapabilities must not be empty when run access is declared")
}
if containsString(remote.Methods, "ftp") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite}) {
violations = append(violations, field+" requires remote.ftp.read or remote.ftp.write when ftp is declared")
}
if containsString(remote.Methods, "rsync") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite}) {
violations = append(violations, field+" requires remote.rsync.read or remote.rsync.write when rsync is declared")
}
if remote.RCON && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) {
violations = append(violations, field+".rcon requires remote.run.rcon.command")
}
if remote.LogTransfer && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunLogsTransfer) {
violations = append(violations, field+".logTransfer requires remote.run.logs.transfer")
}
for _, engine := range remote.DatabaseEngines {
queryCapability := domain.JobCapabilityRemoteRunDBMySQLQuery
executeCapability := domain.JobCapabilityRemoteRunDBMySQLExecute
if engine == "sqlite" {
queryCapability = domain.JobCapabilityRemoteRunDBSQLiteQuery
executeCapability = domain.JobCapabilityRemoteRunDBSQLiteExecute
}
if !containsString(remote.RunCapabilities, queryCapability) && !containsString(remote.RunCapabilities, executeCapability) {
violations = append(violations, fmt.Sprintf("%s.databaseEngines requires %s or %s", field, queryCapability, executeCapability))
}
}
return violations
}
func validateSafePluginStrings(prefix string, values []fieldString) []string {
var violations []string
for _, value := range values {
for _, reason := range unsafePluginStringReasons(value.value) {
violations = append(violations, prefix+"."+value.field+": "+reason)
}
}
return violations
}
func pluginSafeStrings(plugin domain.GamePlugin) []fieldString {
values := []fieldString{
{field: "id", value: plugin.ID},
{field: "name", value: plugin.Name},
{field: "description", value: plugin.Description},
{field: "serverType", value: plugin.ServerType},
{field: "serverDisplayName", value: plugin.ServerDisplayName},
{field: "manifestRef", value: plugin.ManifestRef},
{field: "createFormSchemaRef", value: plugin.CreateFormSchemaRef},
{field: "actions.install", value: plugin.LifecycleActions.Install},
{field: "actions.start", value: plugin.LifecycleActions.Start},
{field: "actions.stop", value: plugin.LifecycleActions.Stop},
{field: "actions.restart", value: plugin.LifecycleActions.Restart},
{field: "actions.status", value: plugin.LifecycleActions.Status},
}
values = appendStringSliceFields(values, "supportedOs", plugin.SupportedOS)
values = appendStringSliceFields(values, "requiredRunCapabilities", plugin.RequiredRunCapabilities)
values = appendStringSliceFields(values, "declaredPermissions", plugin.DeclaredPermissions)
values = appendStringSliceFields(values, "tags", plugin.Tags)
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
values = appendStringSliceFields(values, "productionLifecycle.operations", plugin.ProductionLifecycle.Operations)
values = append(values, fieldString{field: "productionLifecycle.dependencyPolicy", value: plugin.ProductionLifecycle.DependencyPolicy})
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities)
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", plugin.RemoteAccess.DatabaseEngines)
for i, file := range plugin.LifecycleAssets {
values = append(values, fieldString{field: fmt.Sprintf("lifecycleAssets[%d].path", i), value: file.Path})
}
for i, page := range plugin.Pages {
prefix := fmt.Sprintf("pages[%d]", i)
values = append(values,
fieldString{field: prefix + ".key", value: page.Key},
fieldString{field: prefix + ".title", value: page.Title},
fieldString{field: prefix + ".path", value: page.Path},
)
values = appendStringSliceFields(values, prefix+".permissions", page.Permissions)
values = appendStringSliceFields(values, prefix+".bridgeActions", page.BridgeActions)
}
return values
}
func manifestSafeStrings(registration domain.GamePluginManifestRegistration) []fieldString {
manifest := registration.Manifest
values := []fieldString{
{field: "manifestRef", value: registration.ManifestRef},
{field: "id", value: manifest.ID},
{field: "name", value: manifest.Name},
{field: "description", value: manifest.Description},
{field: "kind", value: manifest.Kind},
{field: "server.type", value: manifest.Server.Type},
{field: "server.displayName", value: manifest.Server.DisplayName},
{field: "server.createFormSchema", value: manifest.Server.CreateFormSchema},
{field: "actions.install", value: manifest.Actions.Install},
{field: "actions.start", value: manifest.Actions.Start},
{field: "actions.stop", value: manifest.Actions.Stop},
{field: "actions.restart", value: manifest.Actions.Restart},
{field: "actions.status", value: manifest.Actions.Status},
}
values = appendStringSliceFields(values, "tags", manifest.Tags)
values = appendStringSliceFields(values, "server.supportedOs", manifest.Server.SupportedOS)
values = appendStringSliceFields(values, "bridge.actions", manifest.Bridge.Actions)
values = appendStringSliceFields(values, "capabilities", manifest.Capabilities)
values = appendStringSliceFields(values, "permissions", manifest.Permissions)
values = appendStringSliceFields(values, "ai.purposes", manifest.AI.Purposes)
values = append(values, fieldString{field: "ai.mediation", value: manifest.AI.Mediation}, fieldString{field: "ai.configWritePolicy", value: manifest.AI.ConfigWritePolicy}, fieldString{field: "productionLifecycle.dependencyPolicy", value: manifest.ProductionLifecycle.DependencyPolicy})
values = appendStringSliceFields(values, "productionLifecycle.operations", manifest.ProductionLifecycle.Operations)
values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities)
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines)
for i, file := range manifest.AssetFiles {
values = append(values, fieldString{field: fmt.Sprintf("assetFiles[%d].path", i), value: file.Path})
}
for i, page := range manifest.Pages {
prefix := fmt.Sprintf("pages[%d]", i)
values = append(values,
fieldString{field: prefix + ".key", value: page.Key},
fieldString{field: prefix + ".title", value: page.Title},
fieldString{field: prefix + ".path", value: page.Path},
)
values = appendStringSliceFields(values, prefix+".permissions", page.Permissions)
values = appendStringSliceFields(values, prefix+".bridgeActions", page.BridgeActions)
}
return values
}
func marketplacePluginSafeStrings(plugin domain.PluginMarketplacePlugin) []fieldString {
values := []fieldString{
{field: "id", value: plugin.ID},
{field: "name", value: plugin.Name},
{field: "description", value: plugin.Description},
{field: "serverType", value: plugin.ServerType},
{field: "serverDisplayName", value: plugin.ServerDisplayName},
{field: "manifestRef", value: plugin.ManifestRef},
{field: "createFormSchemaRef", value: plugin.CreateFormSchemaRef},
{field: "actions.install", value: plugin.LifecycleActions.Install},
{field: "actions.start", value: plugin.LifecycleActions.Start},
{field: "actions.stop", value: plugin.LifecycleActions.Stop},
{field: "actions.restart", value: plugin.LifecycleActions.Restart},
{field: "actions.status", value: plugin.LifecycleActions.Status},
{field: "source", value: plugin.Source},
}
values = appendStringSliceFields(values, "supportedOs", plugin.SupportedOS)
values = appendStringSliceFields(values, "capabilities", plugin.Capabilities)
values = appendStringSliceFields(values, "declaredPermissions", plugin.DeclaredPermissions)
values = appendStringSliceFields(values, "tags", plugin.Tags)
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities)
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", plugin.RemoteAccess.DatabaseEngines)
for i, page := range plugin.Pages {
prefix := fmt.Sprintf("pages[%d]", i)
values = append(values,
fieldString{field: prefix + ".key", value: page.Key},
fieldString{field: prefix + ".title", value: page.Title},
fieldString{field: prefix + ".path", value: page.Path},
)
values = appendStringSliceFields(values, prefix+".permissions", page.Permissions)
values = appendStringSliceFields(values, prefix+".bridgeActions", page.BridgeActions)
}
return values
}
func appendStringSliceFields(values []fieldString, field string, items []string) []fieldString {
for i, item := range items {
values = append(values, fieldString{field: fmt.Sprintf("%s[%d]", field, i), value: item})
}
return values
}
func unsafePluginStringReasons(value string) []string {
trimmed := strings.TrimSpace(value)
lowered := strings.ToLower(trimmed)
if trimmed == "" {
return nil
}
var reasons []string
if looksLikeRawSecret(trimmed) || strings.Contains(lowered, "raw api key") || strings.Contains(lowered, "provider key") || strings.Contains(lowered, "ai key") || strings.Contains(lowered, "raw credential") {
reasons = append(reasons, "raw credential or AI/provider key content is not allowed")
}
if strings.Contains(lowered, "direct run") || strings.Contains(lowered, "run socket") || strings.Contains(lowered, "run credential") || strings.Contains(lowered, "run token") || strings.Contains(lowered, "direct socket") {
reasons = append(reasons, "direct run access request is not allowed")
}
if strings.HasPrefix(lowered, "file://") || strings.HasPrefix(trimmed, `\\`) || looksLikeRawHostPath(trimmed) || strings.Contains(lowered, "host path") || strings.Contains(lowered, "raw host path") {
reasons = append(reasons, "raw host path access is not allowed")
}
return reasons
}
func looksLikeRawHostPath(value string) bool {
if len(value) >= 3 && ((value[1] == ':' && value[2] == '\\') || (value[1] == ':' && value[2] == '/')) {
return true
}
lowered := strings.ToLower(value)
for _, prefix := range []string{"/users/", "/etc/", "/var/", "/tmp/", "/home/", "/root/", "/private/", "/volumes/", "/opt/"} {
if strings.HasPrefix(lowered, prefix) {
return true
}
}
return false
}
func safeRef(value string) bool {
trimmed := strings.TrimSpace(value)
return strings.HasPrefix(trimmed, "artifact://") || strings.HasPrefix(trimmed, "manifest://") || safeRelativeJSONRef(trimmed)
}
func safeRelativeJSONRef(value string) bool {
trimmed := strings.TrimSpace(value)
lowered := strings.ToLower(trimmed)
if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.Contains(trimmed, "..") || strings.Contains(trimmed, "://") || strings.Contains(trimmed, `\`) || !strings.HasSuffix(lowered, ".json") {
return false
}
if len(trimmed) >= 2 && trimmed[1] == ':' {
return false
}
return true
}
func safeRelativeSQLRef(value string) bool {
trimmed := strings.TrimSpace(value)
lowered := strings.ToLower(trimmed)
if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.Contains(trimmed, "..") || strings.Contains(trimmed, "://") || strings.Contains(trimmed, `\`) || !strings.HasSuffix(lowered, ".sql") {
return false
}
return len(trimmed) < 2 || trimmed[1] != ':'
}
func looksLikeRawSecret(value string) bool {
trimmed := strings.TrimSpace(strings.ToLower(value))
if trimmed == "" {
return false
}
if strings.HasPrefix(trimmed, "secret://") || strings.HasPrefix(trimmed, "vault://") || strings.HasPrefix(trimmed, "env://") {
return false
}
return strings.HasPrefix(trimmed, "sk-") ||
strings.HasPrefix(trimmed, "sk_") ||
strings.Contains(trimmed, "api_key=") ||
strings.Contains(trimmed, "apikey=") ||
strings.Contains(trimmed, "bearer ")
}
func validUserStatus(status domain.UserStatus) bool {
switch status {
case domain.UserStatusActive, domain.UserStatusDisabled, domain.UserStatusPending:
return true
default:
return false
}
}
func validUserRole(role string) bool {
switch strings.ToLower(strings.TrimSpace(role)) {
case "admin", "platform-admin", "platformadmin", "server-owner", "owner", "server-admin", "operator":
return true
default:
return false
}
}
func validAIProviderKind(kind domain.AIProviderKind) bool {
switch kind {
case domain.AIProviderKindOpenAICompatible, domain.AIProviderKindOpenAI, domain.AIProviderKindClaude, domain.AIProviderKindGemini, domain.AIProviderKindOllama, domain.AIProviderKindCustom:
return true
default:
return false
}
}
func validAIRelayMode(mode domain.AIRelayMode) bool {
switch mode {
case domain.AIRelayModeDirect, domain.AIRelayModeRelay, domain.AIRelayModeLocal:
return true
default:
return false
}
}
func validAIProviderStatus(status domain.AIProviderStatus) bool {
switch status {
case domain.AIProviderStatusActive, domain.AIProviderStatusDisabled, domain.AIProviderStatusError:
return true
default:
return false
}
}
func validGamePluginStatus(status domain.GamePluginStatus) bool {
switch status {
case domain.GamePluginStatusInstalled, domain.GamePluginStatusDisabled, domain.GamePluginStatusInvalid, domain.GamePluginStatusUpdating:
return true
default:
return false
}
}
func validPluginMarketplaceStateAction(action domain.PluginMarketplaceStateAction) bool {
switch action {
case domain.PluginMarketplaceStateActionInstall, domain.PluginMarketplaceStateActionEnable, domain.PluginMarketplaceStateActionDisable:
return true
default:
return false
}
}
func validPluginRunCapability(capability string) bool {
switch capability {
case "process.install", "process.start", "process.stop", "process.restart", "process.status",
"config.write",
"files.list", "files.read", "files.write", "files.patch",
"file.list", "file.read", "file.write", "file.patch",
"logs.read", "log.query", domain.JobCapabilityLogsBackfill,
domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite,
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBMySQLExecute, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunDBSQLiteExecute,
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
domain.JobCapabilityRemoteRunProgram,
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate,
domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall,
"artifacts.read", "artifacts.write", "artifact.read", "artifact.write",
"ai.invoke":
return true
default:
return false
}
}
func isRemoteRunCapability(capability string) bool {
return strings.HasPrefix(capability, "remote.")
}
func remoteCapabilityRequiresTargetKey(capability string) bool {
switch capability {
case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop:
return false
default:
return isRemoteRunCapability(capability)
}
}
func remoteCapabilityRequiresInputRef(capability string) bool {
switch capability {
case domain.JobCapabilityRemoteFTPWrite,
domain.JobCapabilityRemoteRsyncWrite,
domain.JobCapabilityRemoteRunFilesWrite,
domain.JobCapabilityRemoteRunDBMySQLQuery,
domain.JobCapabilityRemoteRunDBMySQLExecute,
domain.JobCapabilityRemoteRunDBSQLiteQuery,
domain.JobCapabilityRemoteRunDBSQLiteExecute,
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProgram:
return true
default:
return false
}
}
func validRemoteAccessMethod(method string) bool {
switch method {
case "ftp", "rsync", "run":
return true
default:
return false
}
}
func validRemoteDatabaseEngine(engine string) bool {
switch engine {
case "mysql", "sqlite":
return true
default:
return false
}
}
func validFileOperationKind(operation domain.FileOperationKind) bool {
switch operation {
case domain.FileOperationList, domain.FileOperationRead, domain.FileOperationWrite:
return true
default:
return false
}
}
func validUploadFilename(name string) bool {
trimmed := strings.TrimSpace(name)
if trimmed == "" || trimmed != name || name == "." || name == ".." || len([]rune(name)) > 255 || !utf8.ValidString(name) || strings.Contains(name, "/") || strings.Contains(name, `\`) || strings.Contains(name, ":") || strings.Contains(name, "://") || looksLikeRawHostPath(name) {
return false
}
for _, char := range name {
if unicode.IsControl(char) || char == 0 {
return false
}
}
return true
}
func validConfigFileKey(key string) bool {
switch key {
case "server.properties", "config/server.properties":
return true
default:
return strings.HasPrefix(key, "config/") && (strings.HasSuffix(key, ".properties") || strings.HasSuffix(key, ".json"))
}
}
func validLogicalFileKey(key string) bool {
trimmed := strings.TrimSpace(key)
if trimmed == "" || trimmed != key || len([]rune(key)) > maxLogicalFileKeyLength {
return false
}
if !utf8.ValidString(key) || strings.HasPrefix(key, "/") || strings.Contains(key, `\`) || strings.Contains(key, ":") || strings.Contains(key, "://") || looksLikeRawHostPath(key) {
return false
}
for _, segment := range strings.Split(key, "/") {
if segment == "" || segment == "." || segment == ".." {
return false
}
}
for _, char := range key {
if unicode.IsControl(char) || char == 0 {
return false
}
}
return true
}
func validScopedInputRef(ref string) bool {
trimmed := strings.TrimSpace(ref)
if trimmed == "" || trimmed != ref || containsUnsafeRuntimeSecret(ref) || looksLikeRawHostPath(ref) {
return false
}
return strings.HasPrefix(ref, "input://") || strings.HasPrefix(ref, "artifact://")
}
func validPluginPermission(permission string) bool {
switch permission {
case "server.create", "server.read", "server.lifecycle", "server.files.read", "server.files.write", "server.logs.read", "server.artifacts.read", "server.artifacts.write", "server.remote.access", "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage", "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "ai.invoke":
return true
default:
return false
}
}
func validPluginBridgeAction(action domain.PluginBridgeAction) bool {
switch action {
case domain.PluginBridgeActionServerInstancesRead,
domain.PluginBridgeActionJobsDispatch,
domain.PluginBridgeActionLogsQuery,
domain.PluginBridgeActionArtifactsOpen,
domain.PluginBridgeActionFilesRequest,
domain.PluginBridgeActionRemoteAccessRequest,
domain.PluginBridgeActionRunDistribution,
domain.PluginBridgeActionDependenciesRequest,
domain.PluginBridgeActionLogsBackfillRequest,
domain.PluginBridgeActionClientManager,
domain.PluginBridgeActionPluginLifecycle,
domain.PluginBridgeActionAIInvoke:
return true
default:
return false
}
}
func requiredBridgePermissions(action domain.PluginBridgeAction) []string {
switch action {
case domain.PluginBridgeActionServerInstancesRead:
return []string{"server.read"}
case domain.PluginBridgeActionJobsDispatch:
return []string{"server.lifecycle"}
case domain.PluginBridgeActionLogsQuery:
return []string{"server.logs.read"}
case domain.PluginBridgeActionArtifactsOpen:
return []string{"server.artifacts.read"}
case domain.PluginBridgeActionFilesRequest:
return []string{"server.files.read"}
case domain.PluginBridgeActionRemoteAccessRequest:
return []string{"server.remote.access"}
case domain.PluginBridgeActionRunDistribution:
return []string{"server.run.distribution"}
case domain.PluginBridgeActionDependenciesRequest:
return []string{"server.dependencies.manage"}
case domain.PluginBridgeActionLogsBackfillRequest:
return []string{"server.logs.read"}
case domain.PluginBridgeActionClientManager:
return []string{"server.client-manager.manage"}
case domain.PluginBridgeActionPluginLifecycle:
return []string{"server.lifecycle"}
case domain.PluginBridgeActionAIInvoke:
return []string{"ai.invoke"}
default:
return nil
}
}
func validateProductionLifecycle(field string, lifecycle domain.GamePluginProductionLifecycle, required bool) []string {
var violations []string
if required && len(lifecycle.Operations) == 0 {
violations = append(violations, field+".operations must not be empty")
}
for i, operation := range lifecycle.Operations {
switch domain.PluginLifecycleOperation(operation) {
case domain.PluginLifecycleOperationInstall, domain.PluginLifecycleOperationEnable, domain.PluginLifecycleOperationDisable, domain.PluginLifecycleOperationUpgrade, domain.PluginLifecycleOperationRollback, domain.PluginLifecycleOperationRetire, domain.PluginLifecycleOperationDependencyCheck:
default:
violations = append(violations, fmt.Sprintf("%s.operations[%d] is invalid", field, i))
}
}
violations = append(violations, duplicateViolations(field+".operations", lifecycle.Operations)...)
if lifecycle.DependencyPolicy != "required" && lifecycle.DependencyPolicy != "optional" {
violations = append(violations, field+".dependencyPolicy must be required or optional")
}
return violations
}
func effectivePagePermissions(plugin domain.GamePlugin, routeKey string) []string {
declared := plugin.DeclaredPermissions
page, found := findPluginPage(plugin.Pages, routeKey)
if !found || len(page.Permissions) == 0 {
return domain.CopyStringSlice(declared)
}
var effective []string
for _, permission := range page.Permissions {
if containsString(declared, permission) {
effective = append(effective, permission)
}
}
return effective
}
func findPluginPage(pages []domain.GamePluginPage, routeKey string) (domain.GamePluginPage, bool) {
for _, page := range pages {
if page.Key == routeKey {
return page, true
}
}
return domain.GamePluginPage{}, false
}
func containsAll(values []string, required []string) bool {
for _, value := range required {
if !containsString(values, value) {
return false
}
}
return true
}
func containsAny(values []string, candidates []string) bool {
for _, candidate := range candidates {
if containsString(values, candidate) {
return true
}
}
return false
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func validPluginSupportedOS(osName string) bool {
switch osName {
case "windows", "linux", "darwin":
return true
default:
return false
}
}
func validPluginPagePath(path string) bool {
if !strings.HasPrefix(path, "/") || strings.Contains(path, "..") || strings.Contains(path, `\`) || strings.Contains(path, "://") {
return false
}
for _, char := range path[1:] {
if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' || char == '.' || char == '/' || char == '-' {
continue
}
return false
}
return true
}
func validAIPurpose(purpose string) bool {
switch purpose {
case "config.read", "config.generate", "config.suggest", "logs.diagnose", "files.suggest":
return true
default:
return false
}
}
func validServerInstanceState(state domain.ServerInstanceState) bool {
switch state {
case domain.ServerInstanceStateDraft, domain.ServerInstanceStateInstalling, domain.ServerInstanceStateReady, domain.ServerInstanceStateRunning, domain.ServerInstanceStateStopped, domain.ServerInstanceStateFailed, domain.ServerInstanceStateDeleted:
return true
default:
return false
}
}
func validRunEndpointStatus(status domain.RunEndpointStatus) bool {
switch status {
case domain.RunEndpointStatusOnline, domain.RunEndpointStatusOffline, domain.RunEndpointStatusDegraded, domain.RunEndpointStatusDisabled:
return true
default:
return false
}
}
func validJobState(state domain.JobState) bool {
switch state {
case domain.JobStateQueued, domain.JobStateAccepted, domain.JobStateRunning, domain.JobStateRetrying, domain.JobStateSucceeded, domain.JobStateFailed, domain.JobStateCancelled:
return true
default:
return false
}
}
func validArtifactOwnerKind(kind domain.ArtifactOwnerKind) bool {
switch kind {
case domain.ArtifactOwnerKindPlatform, domain.ArtifactOwnerKindPlugin, domain.ArtifactOwnerKindServerInstance, domain.ArtifactOwnerKindJob:
return true
default:
return false
}
}
func validArtifactState(state domain.ArtifactState) bool {
switch state {
case domain.ArtifactStateUploading, domain.ArtifactStateAvailable, domain.ArtifactStateExpired, domain.ArtifactStateFailed:
return true
default:
return false
}
}
func validLogStreamSource(source domain.LogStreamSource) bool {
switch source {
case domain.LogStreamSourceProcess, domain.LogStreamSourceFile, domain.LogStreamSourcePlugin, domain.LogStreamSourceManagementProgram:
return true
default:
return strings.TrimSpace(string(source)) != ""
}
}
func validLogStorageBackend(backend domain.LogStorageBackend) bool {
switch backend {
case domain.LogStorageBackendLocalSegments, domain.LogStorageBackendLoki, domain.LogStorageBackendClickHouse, domain.LogStorageBackendOpenSearch, domain.LogStorageBackendElasticsearch:
return true
default:
return false
}
}