功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+292 -7
View File
@@ -20,6 +20,7 @@ const (
maxServerConfigContentSize = 64 * 1024
maxJobExecutionContentSize = 64 * 1024
maxLogicalFileKeyLength = 160
maxProductionMessageLength = 320
)
type ValidationError struct {
@@ -146,11 +147,14 @@ 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, 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, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
return finish(violations)
}
@@ -202,15 +206,262 @@ 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, 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, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...)
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.Pages) == 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")
}
}
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 command.ApprovalLevel != domain.GameClientBridgeApprovalLevelNone && command.ApprovalLevel != domain.GameClientBridgeApprovalLevelOperator && command.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin {
violations = append(violations, prefix+".approvalLevel is invalid")
}
if !safeRelativeJSONRef(command.PayloadSchemaRef) || command.ResultSchemaRef != "" && !safeRelativeJSONRef(command.ResultSchemaRef) {
violations = append(violations, prefix+" schema references must be safe relative JSON references")
}
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{}
transports := map[string]domain.RuntimeTransportProfile{}
for _, transport := range runtimeProfiles.TransportProfiles {
transports[transport.Key] = transport
}
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")
}
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")
}
}
pageDeclarations := map[string]domain.GamePluginPage{}
for _, page := range pages {
pageDeclarations[page.Key] = page
}
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")
}
}
}
return violations
}
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)
@@ -701,12 +952,7 @@ func ValidateRunEndpoint(endpoint domain.RunEndpoint) error {
if !validRunEndpointStatus(endpoint.Status) {
violations = append(violations, "status is invalid")
}
if endpoint.Capacity.MaxJobs < 0 || endpoint.Capacity.RunningJobs < 0 || endpoint.Capacity.QueuedJobs < 0 {
violations = append(violations, "capacity counts must not be negative")
}
if endpoint.Capacity.MaxJobs > 0 && endpoint.Capacity.RunningJobs > endpoint.Capacity.MaxJobs {
violations = append(violations, "runningJobs must not exceed maxJobs")
}
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))
@@ -766,6 +1012,7 @@ func ValidateJob(job domain.Job) error {
if job.ExecutionInput.ExpectedChecksum != "" && !validSHA256Checksum(job.ExecutionInput.ExpectedChecksum) {
violations = append(violations, "executionInput.expectedChecksum must be sha256:<hex>")
}
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>")
}
@@ -1065,6 +1312,9 @@ func pluginSafeStrings(plugin domain.GamePlugin) []fieldString {
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 = appendStringSliceFields(values, "productionLifecycle.approvalRequired", plugin.ProductionLifecycle.ApprovalRequired)
values = append(values, fieldString{field: "productionLifecycle.dependencyPolicy", value: plugin.ProductionLifecycle.DependencyPolicy})
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities)
@@ -1105,6 +1355,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 = append(values, fieldString{field: "ai.mediation", value: manifest.AI.Mediation}, fieldString{field: "ai.configWritePolicy", value: manifest.AI.ConfigWritePolicy}, fieldString{field: "productionLifecycle.dependencyPolicy", value: manifest.ProductionLifecycle.DependencyPolicy})
values = appendStringSliceFields(values, "productionLifecycle.operations", manifest.ProductionLifecycle.Operations)
values = appendStringSliceFields(values, "productionLifecycle.approvalRequired", manifest.ProductionLifecycle.ApprovalRequired)
values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities)
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines)
@@ -1407,7 +1660,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", "server.remote.access", "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage", "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", "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "ai.invoke":
return true
default:
return false
@@ -1426,6 +1679,7 @@ func validPluginBridgeAction(action domain.PluginBridgeAction) bool {
domain.PluginBridgeActionDependenciesRequest,
domain.PluginBridgeActionLogsBackfillRequest,
domain.PluginBridgeActionClientManager,
domain.PluginBridgeActionPluginLifecycle,
domain.PluginBridgeActionAIInvoke:
return true
default:
@@ -1455,6 +1709,8 @@ func requiredBridgePermissions(action domain.PluginBridgeAction) []string {
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:
@@ -1462,6 +1718,35 @@ func requiredBridgePermissions(action domain.PluginBridgeAction) []string {
}
}
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")
}
for i, operation := range lifecycle.ApprovalRequired {
if operation != string(domain.PluginLifecycleOperationDisable) && operation != string(domain.PluginLifecycleOperationRollback) && operation != string(domain.PluginLifecycleOperationRetire) {
violations = append(violations, fmt.Sprintf("%s.approvalRequired[%d] is invalid", field, i))
}
}
for _, operation := range []string{string(domain.PluginLifecycleOperationDisable), string(domain.PluginLifecycleOperationRollback), string(domain.PluginLifecycleOperationRetire)} {
if containsString(lifecycle.Operations, operation) && !containsString(lifecycle.ApprovalRequired, operation) {
violations = append(violations, field+".approvalRequired must include "+operation)
}
}
return violations
}
func effectivePagePermissions(plugin domain.GamePlugin, routeKey string) []string {
declared := plugin.DeclaredPermissions
page, found := findPluginPage(plugin.Pages, routeKey)