Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+10
View File
@@ -14,6 +14,16 @@ func ValidateRunControlHello(hello domain.RunControlHello) error {
violations = appendRequired(violations, "displayName", hello.DisplayName)
violations = appendRequired(violations, "version", hello.Version)
violations = appendRequired(violations, "capabilityReport.fingerprint", hello.CapabilityReport.Fingerprint)
if hello.ServerInstanceID != "" || hello.PluginID != "" || hello.ComponentKind != "" || hello.ComponentKey != "" || hello.KeyGeneration != 0 {
violations = appendRequired(violations, "serverInstanceId", hello.ServerInstanceID)
violations = appendRequired(violations, "pluginId", hello.PluginID)
if hello.ComponentKind != domain.DistributionComponentRun && hello.ComponentKind != domain.DistributionComponentClientManager {
violations = append(violations, "componentKind is invalid")
}
if hello.KeyGeneration <= 0 {
violations = append(violations, "keyGeneration must be positive")
}
}
if !validRunControlStatus(hello.Status) {
violations = append(violations, "status is invalid")
}
+433
View File
@@ -0,0 +1,433 @@
package validator
import (
"fmt"
"strings"
"browser.local/platform/domain"
)
const maxDistributionMessageLength = 256
func ValidateRuntimeBinding(binding domain.RuntimeBinding) error {
var violations []string
violations = appendRequired(violations, "id", binding.ID)
violations = appendRequired(violations, "serverInstanceId", binding.ServerInstanceID)
violations = appendRequired(violations, "pluginId", binding.PluginID)
violations = appendRequired(violations, "profileKey", binding.ProfileKey)
violations = appendRequired(violations, "mode", binding.Mode)
if !validRuntimeBindingStatus(binding.Status) {
violations = append(violations, "status is invalid")
}
for key, value := range binding.Bindings {
if !validDistributionLogicalKey(key) {
violations = append(violations, "bindings key is invalid")
}
if containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "://") && !strings.HasPrefix(value, "secret://") {
violations = append(violations, "bindings."+key+" must use safe logical or secret refs")
}
}
for i, key := range binding.MissingKeys {
if !validDistributionLogicalKey(key) {
violations = append(violations, fmt.Sprintf("missingKeys[%d] is invalid", i))
}
}
if binding.CreatedAt.IsZero() {
violations = append(violations, "createdAt is required")
}
if binding.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
return finish(violations)
}
func ValidateEncryptedComponentKey(key domain.EncryptedComponentKey) error {
var violations []string
violations = appendRequired(violations, "id", key.ID)
violations = appendRequired(violations, "serverInstanceId", key.ServerInstanceID)
violations = appendRequired(violations, "encryptedKey", key.EncryptedKey)
violations = appendRequired(violations, "keyHash", key.KeyHash)
violations = appendRequired(violations, "fingerprint", key.Fingerprint)
violations = appendRequired(violations, "secretRef", key.SecretRef)
if !validDistributionComponentKind(key.ComponentKind) {
violations = append(violations, "componentKind is invalid")
}
if key.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(key.ComponentKey) == "" {
violations = append(violations, "componentKey is required for client-manager keys")
}
if key.ComponentKey != "" && !validDistributionLogicalKey(key.ComponentKey) {
violations = append(violations, "componentKey is invalid")
}
if !strings.HasPrefix(key.EncryptedKey, "enc:v1:") {
violations = append(violations, "encryptedKey must be encrypted")
}
if key.KeyHash != "" && !validSHA256Checksum(key.KeyHash) {
violations = append(violations, "keyHash must be sha256:<hex>")
}
if !strings.HasPrefix(key.SecretRef, "secret://runtime-keys/") {
violations = append(violations, "secretRef must be a runtime key secret ref")
}
if key.Generation <= 0 {
violations = append(violations, "generation must be positive")
}
if !validComponentKeyStatus(key.Status) {
violations = append(violations, "status is invalid")
}
if key.CreatedAt.IsZero() {
violations = append(violations, "createdAt is required")
}
if key.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
return finish(violations)
}
func ValidateRunDistribution(distribution domain.RunDistribution) error {
var violations []string
violations = appendRequired(violations, "id", distribution.ID)
violations = appendRequired(violations, "serverInstanceId", distribution.ServerInstanceID)
violations = appendRequired(violations, "pluginId", distribution.PluginID)
violations = appendRequired(violations, "runEndpointId", distribution.RunEndpointID)
violations = appendRequired(violations, "targetOs", distribution.TargetOS)
violations = appendRequired(violations, "targetArch", distribution.TargetArch)
violations = appendRequired(violations, "packageFormat", distribution.PackageFormat)
violations = appendRequired(violations, "artifactId", distribution.ArtifactID)
violations = appendRequired(violations, "checksum", distribution.Checksum)
violations = appendRequired(violations, "secretRef", distribution.SecretRef)
violations = appendDistributionTargetViolations(violations, distribution.TargetOS, distribution.TargetArch)
violations = appendDistributionStatusViolations(violations, distribution.Status)
if distribution.Checksum != "" && !validSHA256Checksum(distribution.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if distribution.KeyGeneration <= 0 {
violations = append(violations, "keyGeneration must be positive")
}
if distribution.PackageFormat != "zip" && distribution.PackageFormat != "tar.gz" {
violations = append(violations, "packageFormat is invalid")
}
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
violations = append(violations, "secretRef must be redacted runtime key ref")
}
if distribution.CreatedAt.IsZero() {
violations = append(violations, "createdAt is required")
}
if distribution.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
return finish(violations)
}
func ValidateClientManagerDistribution(distribution domain.ClientManagerDistribution) error {
var violations []string
violations = appendRequired(violations, "id", distribution.ID)
violations = appendRequired(violations, "serverInstanceId", distribution.ServerInstanceID)
violations = appendRequired(violations, "pluginId", distribution.PluginID)
violations = appendRequired(violations, "profileKey", distribution.ProfileKey)
violations = appendRequired(violations, "repositoryUrl", distribution.RepositoryURL)
violations = appendRequired(violations, "sourceRevision", distribution.SourceRevision)
violations = appendRequired(violations, "buildJobId", distribution.BuildJobID)
violations = appendRequired(violations, "artifactId", distribution.ArtifactID)
violations = appendRequired(violations, "checksum", distribution.Checksum)
violations = appendRequired(violations, "secretRef", distribution.SecretRef)
violations = appendDistributionTargetViolations(violations, distribution.TargetOS, distribution.TargetArch)
violations = appendDistributionStatusViolations(violations, distribution.Status)
if !validDistributionLogicalKey(distribution.ProfileKey) {
violations = append(violations, "profileKey is invalid")
}
if distribution.Checksum != "" && !validSHA256Checksum(distribution.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if distribution.KeyGeneration <= 0 {
violations = append(violations, "keyGeneration must be positive")
}
violations = append(violations, validateRepositoryURL("repositoryUrl", distribution.RepositoryURL)...)
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
violations = append(violations, "secretRef must be redacted runtime key ref")
}
if distribution.CreatedAt.IsZero() {
violations = append(violations, "createdAt is required")
}
if distribution.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
return finish(violations)
}
func ValidateDependencyStatus(status domain.DependencyStatus) error {
var violations []string
violations = appendRequired(violations, "id", status.ID)
violations = appendRequired(violations, "serverInstanceId", status.ServerInstanceID)
violations = appendRequired(violations, "pluginId", status.PluginID)
violations = appendRequired(violations, "probeKey", status.ProbeKey)
if !validDistributionLogicalKey(status.ProbeKey) {
violations = append(violations, "probeKey is invalid")
}
if status.TargetOS != "" || status.TargetArch != "" {
violations = appendDistributionTargetViolations(violations, status.TargetOS, status.TargetArch)
}
if !validDependencyState(status.State) {
violations = append(violations, "state is invalid")
}
if status.InstallPlanKey != "" && !validDistributionLogicalKey(status.InstallPlanKey) {
violations = append(violations, "installPlanKey is invalid")
}
if len(status.Message) > maxDistributionMessageLength || containsUnsafeRuntimeSecret(status.Message) || looksLikeRawHostPath(status.Message) {
violations = append(violations, "message is unsafe or too long")
}
if status.CheckedAt.IsZero() {
violations = append(violations, "checkedAt is required")
}
if status.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
return finish(violations)
}
func ValidateClientManagerBuildJob(job domain.ClientManagerBuildJob) error {
var violations []string
violations = appendRequired(violations, "id", job.ID)
violations = appendRequired(violations, "serverInstanceId", job.ServerInstanceID)
violations = appendRequired(violations, "pluginId", job.PluginID)
violations = appendRequired(violations, "profileKey", job.ProfileKey)
violations = appendRequired(violations, "repositoryUrl", job.RepositoryURL)
violations = appendRequired(violations, "sourceRevision", job.SourceRevision)
violations = appendDistributionTargetViolations(violations, job.TargetOS, job.TargetArch)
if !validDistributionLogicalKey(job.ProfileKey) {
violations = append(violations, "profileKey is invalid")
}
if job.Checksum != "" && !validSHA256Checksum(job.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if job.LogsRef != "" && !validScopedInputRef(job.LogsRef) {
violations = append(violations, "logsRef is not allowed")
}
if job.KeyGeneration < 0 {
violations = append(violations, "keyGeneration must not be negative")
}
if !validDistributionJobStatus(job.Status) {
violations = append(violations, "status is invalid")
}
violations = append(violations, validateRepositoryURL("repositoryUrl", job.RepositoryURL)...)
if job.CreatedAt.IsZero() {
violations = append(violations, "createdAt is required")
}
if job.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
return finish(violations)
}
func ValidateRunUpdateJob(job domain.RunUpdateJob) error {
var violations []string
violations = appendRequired(violations, "id", job.ID)
violations = appendRequired(violations, "serverInstanceId", job.ServerInstanceID)
violations = appendRequired(violations, "runEndpointId", job.RunEndpointID)
violations = appendRequired(violations, "artifactId", job.ArtifactID)
violations = appendRequired(violations, "checksum", job.Checksum)
violations = appendRequired(violations, "idempotencyKey", job.IdempotencyKey)
if job.Checksum != "" && !validSHA256Checksum(job.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if !validDistributionJobStatus(job.Status) {
violations = append(violations, "status is invalid")
}
if containsUnsafeRuntimeSecret(job.IdempotencyKey) || looksLikeRawHostPath(job.IdempotencyKey) {
violations = append(violations, "idempotencyKey is unsafe")
}
if job.CreatedAt.IsZero() {
violations = append(violations, "createdAt is required")
}
if job.UpdatedAt.IsZero() {
violations = append(violations, "updatedAt is required")
}
return finish(violations)
}
func ValidateRunDistributionGenerateRequest(request domain.RunDistributionGenerateRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "targetOs", request.TargetOS)
violations = appendRequired(violations, "targetArch", request.TargetArch)
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
violations = appendDistributionTargetViolations(violations, request.TargetOS, request.TargetArch)
if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) {
violations = append(violations, "idempotencyKey is unsafe")
}
return finish(violations)
}
func ValidateClientManagerBuildRequest(request domain.ClientManagerBuildRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "profileKey", request.ProfileKey)
violations = appendRequired(violations, "targetOs", request.TargetOS)
violations = appendRequired(violations, "targetArch", request.TargetArch)
violations = appendRequired(violations, "repositoryUrl", request.RepositoryURL)
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
violations = appendDistributionTargetViolations(violations, request.TargetOS, request.TargetArch)
if !validDistributionLogicalKey(request.ProfileKey) {
violations = append(violations, "profileKey is invalid")
}
if request.SourceRevision != "" && !validDistributionLogicalKey(request.SourceRevision) {
violations = append(violations, "sourceRevision is invalid")
}
if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) {
violations = append(violations, "idempotencyKey is unsafe")
}
violations = append(violations, validateRepositoryURL("repositoryUrl", request.RepositoryURL)...)
return finish(violations)
}
func ValidateComponentKeyResetRequest(request domain.ComponentKeyResetRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
if !validDistributionComponentKind(request.ComponentKind) {
violations = append(violations, "componentKind is invalid")
}
if request.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(request.ComponentKey) == "" {
violations = append(violations, "componentKey is required for client-manager")
}
if request.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) {
violations = append(violations, "componentKey is invalid")
}
return finish(violations)
}
func ValidateComponentAuthenticationRequest(request domain.ComponentAuthenticationRequest) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "key", request.Key)
if !validDistributionComponentKind(request.ComponentKind) {
violations = append(violations, "componentKind is invalid")
}
if request.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(request.ComponentKey) == "" {
violations = append(violations, "componentKey is required for client-manager")
}
if request.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) {
violations = append(violations, "componentKey is invalid")
}
if request.Generation <= 0 {
violations = append(violations, "generation must be positive")
}
if len(request.Key) > 256 || looksLikeRawHostPath(request.Key) || strings.Contains(strings.ToLower(request.Key), "://") {
violations = append(violations, "key is unsafe")
}
return finish(violations)
}
func appendDistributionTargetViolations(violations []string, targetOS string, targetArch string) []string {
if !validDistributionTargetOS(targetOS) {
violations = append(violations, "targetOs is invalid")
}
if !validDistributionTargetArch(targetArch) {
violations = append(violations, "targetArch is invalid")
}
return violations
}
func appendDistributionStatusViolations(violations []string, status domain.DistributionStatus) []string {
if !validDistributionStatus(status) {
return append(violations, "status is invalid")
}
return violations
}
func validateRepositoryURL(field string, value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
lowered := strings.ToLower(strings.TrimSpace(value))
if !strings.HasPrefix(lowered, "https://") || !strings.HasSuffix(lowered, ".git") {
return []string{field + " must be an HTTPS git repository URL"}
}
for _, reason := range unsafePluginStringReasons(value) {
return []string{field + ": " + reason}
}
return nil
}
func validDistributionComponentKind(kind domain.DistributionComponentKind) bool {
switch kind {
case domain.DistributionComponentRun, domain.DistributionComponentClientManager:
return true
default:
return false
}
}
func validComponentKeyStatus(status domain.ComponentKeyStatus) bool {
switch status {
case domain.ComponentKeyStatusActive, domain.ComponentKeyStatusRevoked:
return true
default:
return false
}
}
func validDistributionStatus(status domain.DistributionStatus) bool {
switch status {
case domain.DistributionStatusAvailable, domain.DistributionStatusRevoked, domain.DistributionStatusBuilding, domain.DistributionStatusFailed:
return true
default:
return false
}
}
func validRuntimeBindingStatus(status domain.RuntimeBindingStatus) bool {
switch status {
case domain.RuntimeBindingStatusComplete, domain.RuntimeBindingStatusIncomplete:
return true
default:
return false
}
}
func validDependencyState(state domain.DependencyState) bool {
switch state {
case domain.DependencyStateUnknown, domain.DependencyStatePresent, domain.DependencyStateMissing, domain.DependencyStateInstalling, domain.DependencyStateFailed:
return true
default:
return false
}
}
func validDistributionJobStatus(status domain.DistributionJobStatus) bool {
switch status {
case domain.DistributionJobStatusQueued, domain.DistributionJobStatusRunning, domain.DistributionJobStatusSucceeded, domain.DistributionJobStatusFailed, domain.DistributionJobStatusDenied:
return true
default:
return false
}
}
func validDistributionTargetOS(targetOS string) bool {
switch targetOS {
case "linux", "windows", "darwin":
return true
default:
return false
}
}
func validDistributionTargetArch(targetArch string) bool {
switch targetArch {
case "amd64", "arm64":
return true
default:
return false
}
}
func validDistributionLogicalKey(value string) bool {
trimmed := strings.TrimSpace(value)
if trimmed == "" || trimmed != value || len([]rune(value)) > 96 {
return false
}
for _, char := range value {
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' {
continue
}
return false
}
return !strings.Contains(value, "..") && !strings.Contains(value, "://") && !looksLikeRawHostPath(value) && !containsUnsafeRuntimeSecret(value)
}
+181 -3
View File
@@ -145,6 +145,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
violations = append(violations, validatePluginPages(plugin.Pages)...)
violations = append(violations, duplicateViolations("tags", plugin.Tags)...)
violations = append(violations, validateAIPurposes(plugin.AIPurposes)...)
violations = append(violations, validateRemoteAccess("remoteAccess", plugin.RemoteAccess, plugin.RequiredRunCapabilities)...)
violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
return finish(violations)
}
@@ -196,6 +197,7 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
violations = append(violations, validatePluginPages(manifest.Pages)...)
violations = append(violations, duplicateViolations("manifest.tags", manifest.Tags)...)
violations = append(violations, validateAIPurposes(manifest.AI.Purposes)...)
violations = append(violations, validateRemoteAccess("manifest.remoteAccess", manifest.RemoteAccess, manifest.Capabilities)...)
violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...)
return finish(violations)
}
@@ -348,6 +350,7 @@ func validatePluginMarketplacePlugin(prefix string, plugin domain.PluginMarketpl
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
}
@@ -401,6 +404,14 @@ func AuthorizePluginBridgeAction(plugin domain.GamePlugin, request domain.Plugin
}
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)
@@ -425,7 +436,7 @@ func ValidateServerInstance(instance domain.ServerInstance) error {
if !validServerInstanceState(instance.State) {
violations = append(violations, "state is invalid")
}
if instance.State == domain.ServerInstanceStateDeleted {
if instance.State == domain.ServerInstanceStateDeleted && !allowDeleted {
violations = append(violations, "state must not be deleted on create")
}
if instance.ConfigVersion < 0 {
@@ -434,6 +445,20 @@ func ValidateServerInstance(instance domain.ServerInstance) error {
return finish(violations)
}
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 {
var violations []string
if plugin.ID == "" {
@@ -698,6 +723,17 @@ func ValidateJob(job domain.Job) error {
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)
}
@@ -877,6 +913,63 @@ func validateAIPurposes(purposes []string) []string {
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 {
required := domain.JobCapabilityRemoteRunDBMySQLQuery
if engine == "sqlite" {
required = domain.JobCapabilityRemoteRunDBSQLiteQuery
}
if !containsString(remote.RunCapabilities, required) {
violations = append(violations, fmt.Sprintf("%s.databaseEngines requires %s", field, required))
}
}
return violations
}
func validateSafePluginStrings(prefix string, values []fieldString) []string {
var violations []string
for _, value := range values {
@@ -908,6 +1001,9 @@ func pluginSafeStrings(plugin domain.GamePlugin) []fieldString {
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,
@@ -944,6 +1040,9 @@ func manifestSafeStrings(registration domain.GamePluginManifestRegistration) []f
values = appendStringSliceFields(values, "capabilities", manifest.Capabilities)
values = appendStringSliceFields(values, "permissions", manifest.Permissions)
values = appendStringSliceFields(values, "ai.purposes", manifest.AI.Purposes)
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, page := range manifest.Pages {
prefix := fmt.Sprintf("pages[%d]", i)
values = append(values,
@@ -979,6 +1078,9 @@ func marketplacePluginSafeStrings(plugin domain.PluginMarketplacePlugin) []field
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,
@@ -1132,7 +1234,14 @@ func validPluginRunCapability(capability string) bool {
"config.write",
"files.list", "files.read", "files.write", "files.patch",
"file.list", "file.read", "file.write", "file.patch",
"logs.read", "log.query",
"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.JobCapabilityRemoteRunDBSQLiteQuery,
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
"artifacts.read", "artifacts.write", "artifact.read", "artifact.write",
"ai.invoke":
return true
@@ -1141,6 +1250,51 @@ func validPluginRunCapability(capability string) bool {
}
}
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.JobCapabilityRemoteRunDBSQLiteQuery,
domain.JobCapabilityRemoteRunRCONCommand:
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.FileOperationRead, domain.FileOperationWrite:
@@ -1186,7 +1340,7 @@ func validScopedInputRef(ref string) bool {
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", "ai.invoke":
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", "ai.invoke":
return true
default:
return false
@@ -1200,6 +1354,11 @@ func validPluginBridgeAction(action domain.PluginBridgeAction) bool {
domain.PluginBridgeActionLogsQuery,
domain.PluginBridgeActionArtifactsOpen,
domain.PluginBridgeActionFilesRequest,
domain.PluginBridgeActionRemoteAccessRequest,
domain.PluginBridgeActionRunDistribution,
domain.PluginBridgeActionDependenciesRequest,
domain.PluginBridgeActionLogsBackfillRequest,
domain.PluginBridgeActionClientManager,
domain.PluginBridgeActionAIInvoke:
return true
default:
@@ -1219,6 +1378,16 @@ func requiredBridgePermissions(action domain.PluginBridgeAction) []string {
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.PluginBridgeActionAIInvoke:
return []string{"ai.invoke"}
default:
@@ -1259,6 +1428,15 @@ func containsAll(values []string, required []string) bool {
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 {