功能修改
This commit is contained in:
@@ -23,6 +23,28 @@ var (
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
)
|
||||
|
||||
type ForbiddenError struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (err ForbiddenError) Error() string {
|
||||
if strings.TrimSpace(err.Reason) == "" {
|
||||
return ErrForbidden.Error()
|
||||
}
|
||||
return ErrForbidden.Error() + ": " + err.Reason
|
||||
}
|
||||
|
||||
func (err ForbiddenError) Is(target error) bool {
|
||||
return target == ErrForbidden
|
||||
}
|
||||
|
||||
func forbiddenError(reason string) error {
|
||||
if strings.TrimSpace(reason) == "" {
|
||||
return ErrForbidden
|
||||
}
|
||||
return ForbiddenError{Reason: reason}
|
||||
}
|
||||
|
||||
type Core interface {
|
||||
CreateUser(domain.User) (domain.User, error)
|
||||
UpdateUser(string, domain.User) (domain.User, error)
|
||||
@@ -79,6 +101,16 @@ type Core interface {
|
||||
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
|
||||
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
|
||||
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
|
||||
GetProductionCapacityForSession(string) (domain.ProductionCapacitySummary, error)
|
||||
CheckCapacityAdmissionForSession(string, domain.CapacityAdmissionRequest) (domain.CapacityAdmissionDecision, error)
|
||||
ListAlertsForSession(string, domain.AlertFilter) ([]domain.AlertRecord, error)
|
||||
AcknowledgeAlertForSession(string, domain.AlertAcknowledgeRequest) (domain.AlertRecord, error)
|
||||
ResolveAlertForSession(string, domain.AlertResolveRequest) (domain.AlertRecord, error)
|
||||
RetryAlertForSession(string, domain.AlertRetryRequest) (domain.AlertRetryResult, error)
|
||||
ListPluginLifecyclesForSession(string, domain.PluginLifecycleFilter) ([]domain.PluginLifecycleInstallation, error)
|
||||
RunPluginLifecycleForSession(string, domain.PluginLifecycleRequest) (domain.PluginLifecycleResult, error)
|
||||
ListAIConfigDiffsForSession(string, domain.AIConfigDiffFilter) ([]domain.AIConfigDiffPreview, error)
|
||||
ApproveAIConfigDiffForSession(string, domain.AIConfigDiffApprovalRequest) (domain.AIConfigDiffApprovalResult, error)
|
||||
IngestMetricBatch(domain.MetricBatchIngest) (domain.MetricBatchIngestResult, error)
|
||||
ListMetricSamplesForSession(string, domain.MetricSampleFilter) ([]domain.MetricSample, error)
|
||||
CreateBackupForSession(string, domain.BackupRecord) (domain.BackupRecord, error)
|
||||
@@ -137,6 +169,17 @@ type Core interface {
|
||||
RegisterClientManager(domain.ClientManagerRegisterRequest) (domain.ClientManagerRegisterResult, error)
|
||||
AcceptClientManagerHeartbeat(domain.ClientManagerHeartbeat) (domain.ClientManagerHeartbeatResult, error)
|
||||
ReconcileClientManagerLifecycle() error
|
||||
QueueGameClientBridgeCommandForSession(string, domain.GameClientBridgeQueueRequest) (domain.GameClientBridgeCommand, error)
|
||||
ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest) ([]domain.GameClientBridgeCommand, error)
|
||||
AckGameClientBridgeCommand(domain.GameClientBridgeAckRequest) (domain.GameClientBridgeCommand, error)
|
||||
CompleteGameClientBridgeCommand(domain.GameClientBridgeResultRequest) (domain.GameClientBridgeCommand, error)
|
||||
CancelGameClientBridgeCommandForSession(string, domain.GameClientBridgeCancelRequest) (domain.GameClientBridgeCommand, error)
|
||||
UploadGameClientBridgeSnapshot(domain.GameClientBridgeSnapshotIngestRequest) (domain.GameClientBridgeSnapshot, error)
|
||||
ReconcileGameClientBridgeCommands() error
|
||||
GetGameClientBridgeStatusForSession(string, string) (domain.GameClientBridgeStatus, error)
|
||||
ListGameClientBridgeCommandsForSession(string, domain.GameClientBridgeCommandFilter) ([]domain.GameClientBridgeCommand, error)
|
||||
GetGameClientBridgeCommandForSession(string, string) (domain.GameClientBridgeCommand, error)
|
||||
QueryGameClientBridgeSnapshotsForSession(string, domain.GameClientBridgeSnapshotQuery) ([]domain.GameClientBridgeSnapshot, error)
|
||||
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
|
||||
ListRunUpdateJobsForSession(string, string) ([]domain.RunUpdateJob, error)
|
||||
GetDependencyCatalogForSession(string, string) (domain.DependencyCatalog, error)
|
||||
@@ -169,6 +212,8 @@ type CoreService struct {
|
||||
runSessions map[string]domain.RunControlSession
|
||||
runSessionSeq uint64
|
||||
jobMu sync.Mutex
|
||||
bridgeMu sync.Mutex
|
||||
bridgeSeq uint64
|
||||
logStore LogBodyStore
|
||||
artifactStore ArtifactBodyStore
|
||||
artifactMu sync.Mutex
|
||||
@@ -177,6 +222,7 @@ type CoreService struct {
|
||||
artifactTransferSeq uint64
|
||||
auditMu sync.Mutex
|
||||
auditSeq uint64
|
||||
productionMu sync.Mutex
|
||||
aiProviderClient AIProviderClient
|
||||
secretEnvelope SecretEnvelope
|
||||
}
|
||||
@@ -237,6 +283,9 @@ func NewCoreServiceWithDurableStores(store repo.Store, logStore LogBodyStore, ar
|
||||
if err := service.ReconcileClientManagerLifecycle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := service.ReconcileGameClientBridgeCommands(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
@@ -538,13 +587,13 @@ func (svc *CoreService) TestAIProvider(id string) (domain.AIProviderTestResult,
|
||||
|
||||
result := domain.AIProviderTestResult{
|
||||
ProviderID: provider.ID,
|
||||
Mode: "metadata",
|
||||
Mode: "provider",
|
||||
Success: true,
|
||||
Message: "metadata validation passed",
|
||||
Message: "provider invocation passed",
|
||||
}
|
||||
if err := validator.ValidateAIProvider(provider); err != nil {
|
||||
result.Success = false
|
||||
result.Message = "metadata validation failed"
|
||||
result.Message = "provider validation failed"
|
||||
var validationErr validator.ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
result.Violations = append(result.Violations, validationErr.Violations...)
|
||||
@@ -554,9 +603,35 @@ func (svc *CoreService) TestAIProvider(id string) (domain.AIProviderTestResult,
|
||||
}
|
||||
if provider.Status != domain.AIProviderStatusActive {
|
||||
result.Success = false
|
||||
result.Message = "metadata validation failed"
|
||||
result.Message = "provider validation failed"
|
||||
result.Violations = append(result.Violations, "provider must be active")
|
||||
}
|
||||
if !result.Success {
|
||||
return domain.CopyAIProviderTestResult(result), nil
|
||||
}
|
||||
_, invokeErr := svc.aiProviderClient.Invoke(provider, domain.AIInvocationRequest{RequestID: "provider-test-" + provider.ID, Purpose: "provider.health", Prompt: "Return a short health acknowledgement.", Model: provider.DefaultModel})
|
||||
if invokeErr != nil {
|
||||
result.Success = false
|
||||
result.Message = "provider invocation failed safely"
|
||||
result.Violations = []string{"provider invocation failed safely"}
|
||||
auditID, auditErr := svc.recordAuditEventWithID("platform", "ai.provider.test.failed", "ai-provider", provider.ID, domain.AuditResultFailed, result.Message)
|
||||
if auditErr != nil {
|
||||
return domain.AIProviderTestResult{}, auditErr
|
||||
}
|
||||
svc.productionMu.Lock()
|
||||
_, alertErr := svc.upsertAlert(domain.AlertRecord{SourceKind: "ai-provider", SourceID: provider.ID, RuleKey: "ai.provider.failed", Severity: domain.AlertSeverityWarning, Title: "AI provider health check failed", Message: result.Message, Retryable: false, LastAuditEventID: auditID})
|
||||
svc.productionMu.Unlock()
|
||||
if alertErr != nil {
|
||||
return domain.AIProviderTestResult{}, alertErr
|
||||
}
|
||||
} else {
|
||||
svc.productionMu.Lock()
|
||||
resolveErr := svc.resolveAlertForSource("ai-provider", provider.ID, "ai.provider.failed", "platform", "AI provider health check passed", "")
|
||||
svc.productionMu.Unlock()
|
||||
if resolveErr != nil {
|
||||
return domain.AIProviderTestResult{}, resolveErr
|
||||
}
|
||||
}
|
||||
return domain.CopyAIProviderTestResult(result), nil
|
||||
}
|
||||
|
||||
@@ -584,6 +659,7 @@ func (svc *CoreService) CreateGamePlugin(plugin domain.GamePlugin) (domain.GameP
|
||||
if plugin.Status == "" {
|
||||
plugin.Status = domain.GamePluginStatusInstalled
|
||||
}
|
||||
plugin.ProductionLifecycle = normalizedProductionLifecycle(plugin.ProductionLifecycle)
|
||||
if err := validator.ValidateGamePlugin(plugin); err != nil {
|
||||
return domain.GamePlugin{}, err
|
||||
}
|
||||
@@ -621,8 +697,10 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
|
||||
Pages: manifest.Pages,
|
||||
Tags: manifest.Tags,
|
||||
AIPurposes: manifest.AI.Purposes,
|
||||
ProductionLifecycle: manifest.ProductionLifecycle,
|
||||
RemoteAccess: manifest.RemoteAccess,
|
||||
RuntimeProfiles: manifest.RuntimeProfiles,
|
||||
GameClientBridge: manifest.GameClientBridge,
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
}
|
||||
@@ -714,6 +792,8 @@ func (svc *CoreService) ExecutePluginBridgeAction(sessionID string, request doma
|
||||
base = svc.executeBridgeLogsBackfillRequest(base, plugin, instance, request.Payload)
|
||||
case domain.PluginBridgeActionClientManager:
|
||||
base = svc.executeBridgeClientManager(sessionID, base, request)
|
||||
case domain.PluginBridgeActionPluginLifecycle:
|
||||
base = svc.executeBridgePluginLifecycle(sessionID, base, request)
|
||||
case domain.PluginBridgeActionArtifactsOpen:
|
||||
base = svc.executeBridgeArtifactOpen(sessionID, base, request)
|
||||
case domain.PluginBridgeActionAIInvoke:
|
||||
@@ -775,7 +855,6 @@ func (svc *CoreService) executeBridgeAIInvoke(sessionID string, base domain.Plug
|
||||
RouteKey: request.RouteKey,
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
Purpose: request.AIPurpose,
|
||||
ProviderID: request.Payload["providerId"],
|
||||
Model: request.Payload["model"],
|
||||
Prompt: defaultBridgeValue(request.Payload["prompt"], "Review the current server context and provide a safe recommendation."),
|
||||
CurrentConfig: request.Payload["currentConfig"],
|
||||
@@ -795,6 +874,9 @@ func (svc *CoreService) executeBridgeAIInvoke(sessionID string, base domain.Plug
|
||||
if response.ConfigRecommendation != nil {
|
||||
base.Result["suggestedConfig"] = response.ConfigRecommendation.SuggestedConfig
|
||||
base.Result["diffSummary"] = response.ConfigRecommendation.DiffSummary
|
||||
base.Result["diffId"] = response.ConfigRecommendation.DiffID
|
||||
base.Result["key"] = response.ConfigRecommendation.Key
|
||||
base.Result["expiresAt"] = response.ConfigRecommendation.ExpiresAt
|
||||
}
|
||||
if response.Error != nil {
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: response.Error.Code, Message: response.Error.Message, Details: response.Error.Details}
|
||||
@@ -802,6 +884,22 @@ func (svc *CoreService) executeBridgeAIInvoke(sessionID string, base domain.Plug
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgePluginLifecycle(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
|
||||
confirmed, err := strconv.ParseBool(defaultBridgeValue(request.Payload["confirmed"], "false"))
|
||||
if err != nil {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "confirmed must be true or false"}
|
||||
return base
|
||||
}
|
||||
result, err := svc.RunPluginLifecycleForSession(sessionID, domain.PluginLifecycleRequest{PluginID: request.PluginID, ServerInstanceID: request.ServerInstanceID, Operation: domain.PluginLifecycleOperation(request.Payload["operation"]), TargetVersion: request.Payload["targetVersion"], IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID), Confirmed: confirmed})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = result.Status
|
||||
base.Result = map[string]string{"installationId": result.Installation.ID, "currentState": string(result.Installation.CurrentState), "desiredState": string(result.Installation.DesiredState), "jobId": result.Job.ID, "alertId": result.Installation.AlertID, "auditEventId": result.Installation.AuditEventID, "admissionState": string(result.Decision.State), "admissionReason": result.Decision.Reason}
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeJobDispatch(sessionID string, base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
capability := strings.TrimSpace(payload["capability"])
|
||||
if capability == "" {
|
||||
@@ -951,7 +1049,53 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
|
||||
}
|
||||
timeoutSeconds, _ := strconv.Atoi(payload["timeoutSeconds"])
|
||||
maxAttempts, _ := strconv.Atoi(payload["maxAttempts"])
|
||||
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: declarationKey, TargetKey: payload["targetKey"], Capability: capability, TimeoutSeconds: timeoutSeconds, MaxAttempts: maxAttempts, IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID)})
|
||||
inputs := map[string]string{}
|
||||
for key, value := range payload {
|
||||
if strings.HasPrefix(key, "input.") {
|
||||
inputs[strings.TrimPrefix(key, "input.")] = value
|
||||
}
|
||||
}
|
||||
if capability == domain.JobCapabilityRemoteRunDBSQLiteQuery {
|
||||
templateKey := strings.TrimSpace(inputs["templateKey"])
|
||||
if templateKey == "" {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "input.templateKey is required for sqlite query requests"}
|
||||
return base
|
||||
}
|
||||
template, reason := findBridgeQueryTemplate(plugin, base.RouteKey, templateKey)
|
||||
if reason != "" {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "query_template_denied", Message: reason}
|
||||
return base
|
||||
}
|
||||
if template.Engine != "sqlite" || template.TransportKey != declarationKey || template.TargetKey != payload["targetKey"] {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "query_template_denied", Message: "query template transport or target is not approved"}
|
||||
return base
|
||||
}
|
||||
if timeoutSeconds == 0 {
|
||||
timeoutSeconds = template.TimeoutSeconds
|
||||
} else if timeoutSeconds > template.TimeoutSeconds {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "query template timeout limit exceeded"}
|
||||
return base
|
||||
}
|
||||
maxRows := template.MaxRows
|
||||
if requestedRows, ok := inputs["maxRows"]; ok && strings.TrimSpace(requestedRows) != "" {
|
||||
parsedRows, parseErr := strconv.Atoi(requestedRows)
|
||||
if parseErr != nil || parsedRows <= 0 {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "input.maxRows must be a positive integer"}
|
||||
return base
|
||||
}
|
||||
if parsedRows < maxRows {
|
||||
maxRows = parsedRows
|
||||
}
|
||||
}
|
||||
inputs["templateKey"] = template.Key
|
||||
inputs["maxRows"] = strconv.Itoa(maxRows)
|
||||
}
|
||||
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: declarationKey, TargetKey: payload["targetKey"], Capability: capability, TimeoutSeconds: timeoutSeconds, MaxAttempts: maxAttempts, IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID), InputRef: payload["inputRef"], Inputs: inputs})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
@@ -967,6 +1111,46 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
|
||||
return base
|
||||
}
|
||||
|
||||
func findBridgeQueryTemplate(plugin domain.GamePlugin, routeKey string, templateKey string) (domain.GameClientBridgeQueryTemplateDeclaration, string) {
|
||||
pageFound := false
|
||||
pageAllowsTemplate := false
|
||||
for _, page := range plugin.GameClientBridge.Pages {
|
||||
if page.PageKey != routeKey {
|
||||
continue
|
||||
}
|
||||
pageFound = true
|
||||
if containsString(page.QueryTemplateKeys, templateKey) {
|
||||
pageAllowsTemplate = true
|
||||
}
|
||||
}
|
||||
if !pageFound || !pageAllowsTemplate {
|
||||
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template is not declared by the bridge page"
|
||||
}
|
||||
var selected domain.GameClientBridgeQueryTemplateDeclaration
|
||||
for _, template := range plugin.GameClientBridge.QueryTemplates {
|
||||
if template.Key == templateKey {
|
||||
selected = template
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected.Key == "" {
|
||||
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template is not declared by the plugin"
|
||||
}
|
||||
for _, page := range plugin.Pages {
|
||||
if page.Key != routeKey {
|
||||
continue
|
||||
}
|
||||
if !containsString(page.Permissions, selected.Permission) {
|
||||
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template permission is not declared by the plugin page"
|
||||
}
|
||||
if !containsString(page.BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest)) {
|
||||
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template page does not declare remote access"
|
||||
}
|
||||
return selected, ""
|
||||
}
|
||||
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template plugin page is not declared"
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeRunDistribution(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
|
||||
distribution, err := svc.GenerateRunDistributionForSession(sessionID, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
@@ -1278,14 +1462,29 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
|
||||
Pages: plugin.Pages,
|
||||
Tags: plugin.Tags,
|
||||
AIPurposes: plugin.AIPurposes,
|
||||
ProductionLifecycle: plugin.ProductionLifecycle,
|
||||
RemoteAccess: plugin.RemoteAccess,
|
||||
RuntimeProfiles: plugin.RuntimeProfiles,
|
||||
GameClientBridge: plugin.GameClientBridge,
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
Source: "platform-registry",
|
||||
}
|
||||
}
|
||||
|
||||
func normalizedProductionLifecycle(lifecycle domain.GamePluginProductionLifecycle) domain.GamePluginProductionLifecycle {
|
||||
if len(lifecycle.Operations) == 0 {
|
||||
lifecycle.Operations = []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}
|
||||
}
|
||||
if lifecycle.DependencyPolicy == "" {
|
||||
lifecycle.DependencyPolicy = "optional"
|
||||
}
|
||||
if len(lifecycle.ApprovalRequired) == 0 {
|
||||
lifecycle.ApprovalRequired = []string{"disable", "rollback", "retire"}
|
||||
}
|
||||
return lifecycle
|
||||
}
|
||||
|
||||
func marketplacePluginMatchesKeyword(plugin domain.PluginMarketplacePlugin, keyword string) bool {
|
||||
keyword = strings.ToLower(strings.TrimSpace(keyword))
|
||||
if keyword == "" {
|
||||
|
||||
Reference in New Issue
Block a user