feat: 完整游戏运维功能

This commit is contained in:
npc0-hue
2026-07-18 09:04:01 +08:00
parent f3b14b7945
commit 48b8ad8d6c
187 changed files with 16607 additions and 1140 deletions
+306 -100
View File
@@ -31,6 +31,7 @@ type Core interface {
RegisterUser(domain.UserRegistration) (domain.AuthSession, error)
LoginUser(domain.UserLogin) (domain.AuthSession, error)
LogoutUser(string) error
RotateUserSession(string) (domain.AuthSession, error)
GetCurrentUser(string) (domain.User, error)
UpdateCurrentUserProfile(string, domain.UserProfile) (domain.User, error)
UpdateCurrentUserTheme(string, domain.UserThemePreference) (domain.UserThemePreference, error)
@@ -50,12 +51,14 @@ type Core interface {
GetMarketplacePlugin(string) (domain.PluginMarketplacePlugin, error)
SetMarketplacePluginState(string, domain.PluginMarketplaceStateAction) (domain.PluginMarketplacePlugin, error)
AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest) (domain.PluginBridgeAuthorization, error)
AuthorizePluginBridgeActionForSession(string, domain.PluginBridgeAuthorizeRequest) (domain.PluginBridgeAuthorization, error)
ExecutePluginBridgeAction(string, domain.PluginBridgeExecuteRequest) (domain.PluginBridgeExecuteResponse, error)
CreateRunEndpoint(domain.RunEndpoint) (domain.RunEndpoint, error)
GetRunEndpoint(string) (domain.RunEndpoint, error)
ListRunEndpoints(domain.RunEndpointFilter) ([]domain.RunEndpoint, error)
RegisterRunHello(domain.RunControlHello) (domain.RunControlHelloResult, error)
AcceptRunHeartbeat(domain.RunControlHeartbeat) (domain.RunControlHeartbeatResult, error)
AuthorizeRunRequestSignature(domain.RunRequestSignature) error
CreateServerInstance(domain.ServerInstance) (domain.ServerInstance, error)
CreateServerInstanceForSession(string, domain.ServerInstance) (domain.ServerInstance, error)
CreateServerInstanceWorkflow(domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error)
@@ -64,6 +67,7 @@ type Core interface {
StartServerInstanceForSession(string, domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
StopServerInstance(domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
StopServerInstanceForSession(string, domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
QueryServerInstanceProcessForSession(string, domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
GetServerInstance(string) (domain.ServerInstance, error)
GetServerInstanceForSession(string, string) (domain.ServerInstance, error)
UpdateServerInstanceForSession(string, string, domain.ServerInstanceUpdate) (domain.ServerInstance, error)
@@ -75,6 +79,13 @@ type Core interface {
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
IngestMetricBatch(domain.MetricBatchIngest) (domain.MetricBatchIngestResult, error)
ListMetricSamplesForSession(string, domain.MetricSampleFilter) ([]domain.MetricSample, error)
CreateBackupForSession(string, domain.BackupRecord) (domain.BackupRecord, error)
GetBackupForSession(string, string) (domain.BackupRecord, error)
ListBackupsForSession(string, domain.BackupFilter) ([]domain.BackupRecord, error)
ListRemoteAdapterDeclarationsForSession(string, string) ([]domain.RemoteAdapterDeclaration, error)
RequestRemoteAdapterForSession(string, domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error)
GetServerConfigForSession(string, string) (domain.ServerConfig, error)
PreviewServerConfigWriteForSession(string, domain.ServerConfigDiffRequest) (domain.ServerConfigDiffPreview, error)
ApproveServerConfigWriteForSession(string, domain.ServerConfigWriteApproval) (domain.ServerConfigWriteDispatch, error)
@@ -82,28 +93,53 @@ type Core interface {
CreateJob(domain.Job) (domain.Job, error)
GetJob(string) (domain.Job, error)
ListJobs(domain.JobFilter) ([]domain.Job, error)
GetJobForSession(string, string) (domain.Job, error)
ListJobsForSession(string, domain.JobFilter) ([]domain.Job, error)
RequestRunJobCancelForSession(string, domain.RunJobCancelRequest) (domain.RunJobCancelRequestResult, error)
ClaimRunJob(domain.RunJobClaim) (domain.RunJobClaimResult, error)
AckRunJob(domain.RunJobAck) (domain.RunJobAckResult, error)
UpdateRunJobProgress(domain.RunJobProgress) (domain.RunJobProgressResult, error)
CompleteRunJob(domain.RunJobResult) (domain.RunJobResultResult, error)
GetDistributionBuildInput(domain.DistributionBuildInputRequest) (domain.DistributionBuildInput, error)
GetDependencyExecutionInput(domain.DependencyExecutionInputRequest) (domain.DependencyExecutionInput, error)
GetRunUpdateInput(domain.RunUpdateInputRequest) (domain.RunUpdateInput, error)
ReadRunUpdateChunk(domain.RunUpdateChunkRequest) (domain.RunUpdateChunk, error)
ReportRunUpdateHealth(domain.RunUpdateHealthReport) (domain.RunUpdateHealthResult, error)
RequestRunJobCancel(domain.RunJobCancelRequest) (domain.RunJobCancelRequestResult, error)
PollRunJobCancel(domain.RunJobCancelPoll) (domain.RunJobCancelPollResult, error)
ReconcileRunJobs(domain.RunJobReconcile) (domain.RunJobReconcileResult, error)
CreateArtifact(domain.Artifact) (domain.Artifact, error)
GetArtifact(string) (domain.Artifact, error)
ListArtifacts(domain.ArtifactFilter) ([]domain.Artifact, error)
ListArtifactsForSession(string, domain.ArtifactFilter) ([]domain.Artifact, error)
GetArtifactForSession(string, string) (domain.Artifact, error)
OpenArtifactDownloadForSession(string, domain.ArtifactDownloadReferenceRequest) (domain.ArtifactDownloadReference, error)
ReadArtifactContentForSession(string, domain.ArtifactContentRequest) (domain.ArtifactContent, error)
GetServerRuntimeActionsForSession(string, string) (domain.ServerRuntimeActions, error)
GetServerRuntimeBindingForSession(string, string) (domain.RuntimeBindingView, error)
UpdateServerRuntimeBindingForSession(string, string, domain.RuntimeBindingUpdate) (domain.RuntimeBindingView, error)
GenerateRunDistributionForSession(string, domain.RunDistributionGenerateRequest) (domain.RunDistribution, error)
GenerateClientManagerDistributionForSession(string, domain.ClientManagerBuildRequest) (domain.ClientManagerDistribution, error)
OpenLatestRunDistributionDownloadForSession(string, string) (domain.ArtifactDownloadReference, error)
OpenLatestClientManagerDistributionDownloadForSession(string, string, string) (domain.ArtifactDownloadReference, error)
ResetComponentKeyForSession(string, domain.ComponentKeyResetRequest) (domain.EncryptedComponentKey, error)
AuthenticateComponent(domain.ComponentAuthenticationRequest) (domain.ComponentAuthenticationResult, error)
DeployClientManagerForSession(string, domain.ClientManagerDeployRequest) (domain.ClientManagerLifecycleView, error)
ControlClientManagerForSession(string, domain.ClientManagerControlRequest) (domain.ClientManagerLifecycleView, error)
UpdateClientManagerForSession(string, domain.ClientManagerUpdateRequest) (domain.ClientManagerLifecycleView, error)
UninstallClientManagerForSession(string, domain.ClientManagerUninstallRequest) (domain.ClientManagerLifecycleView, error)
RetryClientManagerLifecycleForSession(string, domain.ClientManagerRetryRequest) (domain.ClientManagerLifecycleView, error)
RevokeClientManagerSessionForSession(string, domain.ClientManagerRevokeSessionRequest) (domain.ClientManagerLifecycleView, error)
GetClientManagerLifecycleForSession(string, string, string) (domain.ClientManagerLifecycleView, error)
ListClientManagerLifecyclesForSession(string, string) ([]domain.ClientManagerLifecycleView, error)
GetClientManagerLifecycleInput(domain.ClientManagerLifecycleInputRequest) (domain.ClientManagerLifecycleInput, error)
ReadClientManagerLifecycleChunk(domain.RunUpdateChunkRequest) (domain.RunUpdateChunk, error)
RegisterClientManager(domain.ClientManagerRegisterRequest) (domain.ClientManagerRegisterResult, error)
AcceptClientManagerHeartbeat(domain.ClientManagerHeartbeat) (domain.ClientManagerHeartbeatResult, error)
ReconcileClientManagerLifecycle() error
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
ListRunUpdateJobsForSession(string, string) ([]domain.RunUpdateJob, error)
GetDependencyCatalogForSession(string, string) (domain.DependencyCatalog, error)
QueueDependencyJobForSession(string, domain.DependencyJobRequest) (domain.Job, error)
QueueLogBackfillForSession(string, domain.LogBackfillRequest) (domain.Job, error)
OpenArtifactTransfer(domain.ArtifactTransferOpen) (domain.ArtifactTransferOpenResult, error)
@@ -113,11 +149,15 @@ type Core interface {
CreateLogStream(domain.LogStream) (domain.LogStream, error)
GetLogStream(string) (domain.LogStream, error)
ListLogStreams(domain.LogStreamFilter) ([]domain.LogStream, error)
GetLogStreamForSession(string, string) (domain.LogStream, error)
ListLogStreamsForSession(string, domain.LogStreamFilter) ([]domain.LogStream, error)
QueryLogStreamForSession(string, domain.LogStreamCursorQuery) (domain.LogStreamCursorResult, error)
IngestLogBatch(domain.LogBatchIngest) (domain.LogBatchIngestResult, error)
QueryLogStream(domain.LogStreamCursorQuery) (domain.LogStreamCursorResult, error)
CreateAuditEvent(domain.AuditEvent) (domain.AuditEvent, error)
GetAuditEvent(string) (domain.AuditEvent, error)
ListAuditEvents(domain.AuditEventFilter) ([]domain.AuditEvent, error)
SeedPlatformAdmin(string, string) error
}
type CoreService struct {
@@ -129,9 +169,8 @@ type CoreService struct {
runSessions map[string]domain.RunControlSession
runSessionSeq uint64
jobMu sync.Mutex
jobLeases map[string]domain.RunJobLease
jobLeaseSeq uint64
logStore LogBodyStore
artifactStore ArtifactBodyStore
artifactMu sync.Mutex
artifactTransfers map[string]domain.ArtifactTransferSession
artifactPayloads map[string][]byte
@@ -139,6 +178,7 @@ type CoreService struct {
auditMu sync.Mutex
auditSeq uint64
aiProviderClient AIProviderClient
secretEnvelope SecretEnvelope
}
var _ Core = (*CoreService)(nil)
@@ -159,17 +199,71 @@ func newCoreServiceWithLogStore(store repo.Store, logStore LogBodyStore, now fun
if logStore == nil {
logStore = NewMemoryLogBodyStore()
}
return &CoreService{
artifactStore := NewMemoryArtifactBodyStore()
service := &CoreService{
store: store,
now: now,
authSessions: map[string]string{},
runSessions: map[string]domain.RunControlSession{},
jobLeases: map[string]domain.RunJobLease{},
logStore: logStore,
artifactStore: artifactStore,
artifactTransfers: map[string]domain.ArtifactTransferSession{},
artifactPayloads: map[string][]byte{},
aiProviderClient: MockAIProviderClient{},
secretEnvelope: newSecretEnvelope(developmentSecretEnvelopeKey),
}
return service
}
func NewCoreServiceWithDurableStores(store repo.Store, logStore LogBodyStore, artifactStore ArtifactBodyStore) (*CoreService, error) {
if artifactStore == nil {
artifactStore = NewMemoryArtifactBodyStore()
}
service := newCoreServiceWithLogStore(store, logStore, func() time.Time { return time.Now().UTC() })
service.artifactStore = artifactStore
sessions, err := artifactStore.LoadTransfers()
if err != nil {
return nil, err
}
for _, session := range sessions {
service.artifactTransfers[session.TransferID] = domain.CopyArtifactTransferSession(session)
}
if err := service.recoverLogCursors(); err != nil {
return nil, err
}
if err := service.RecoverIncompleteBackups(); err != nil {
return nil, err
}
if err := service.ReconcileClientManagerLifecycle(); err != nil {
return nil, err
}
return service, nil
}
func (svc *CoreService) recoverLogCursors() error {
store, ok := svc.logStore.(interface{ LatestSeq(string) (uint64, error) })
if !ok {
return nil
}
streams, err := svc.store.LogStreams().List(domain.LogStreamFilter{})
if err != nil {
return err
}
for _, stream := range streams {
latest, latestErr := store.LatestSeq(stream.ID)
if latestErr != nil || latest <= stream.LatestSeq {
if latestErr != nil {
return latestErr
}
continue
}
stream.LatestSeq = latest
stream.UpdatedAt = svc.now()
if err := svc.store.LogStreams().Update(stream); err != nil {
return err
}
}
return nil
}
func (svc *CoreService) CreateUser(user domain.User) (domain.User, error) {
@@ -240,6 +334,11 @@ func (svc *CoreService) UpdateUser(id string, user domain.User) (domain.User, er
if err := svc.store.Users().Update(user); err != nil {
return domain.User{}, err
}
if user.Status != domain.UserStatusActive {
if err := svc.revokeUserSessions(user.ID); err != nil {
return domain.User{}, err
}
}
return domain.CopyUser(user), nil
}
@@ -282,19 +381,7 @@ func (svc *CoreService) RegisterUser(registration domain.UserRegistration) (doma
return domain.AuthSession{}, err
}
if firstUser {
sessionID, err := randomToken()
if err != nil {
return domain.AuthSession{}, err
}
svc.authMu.Lock()
svc.authSessions[sessionID] = created.ID
svc.authMu.Unlock()
return domain.AuthSession{
SessionID: sessionID,
User: created,
Status: "authenticated",
Message: "首个账号已创建为平台管理员。",
}, nil
return svc.issueAuthSession(created, "首个账号已创建为平台管理员。")
}
return domain.AuthSession{
User: created,
@@ -325,27 +412,11 @@ func (svc *CoreService) LoginUser(login domain.UserLogin) (domain.AuthSession, e
if matched.Status == domain.UserStatusDisabled {
return domain.AuthSession{}, ErrForbidden
}
sessionID, err := randomToken()
if err != nil {
return domain.AuthSession{}, err
}
svc.authMu.Lock()
svc.authSessions[sessionID] = matched.ID
svc.authMu.Unlock()
return domain.AuthSession{SessionID: sessionID, User: matched, Status: "authenticated", Message: "登录成功"}, nil
return svc.issueAuthSession(matched, "登录成功")
}
func (svc *CoreService) LogoutUser(sessionID string) error {
if strings.TrimSpace(sessionID) == "" {
return ErrUnauthorized
}
svc.authMu.Lock()
defer svc.authMu.Unlock()
if _, exists := svc.authSessions[sessionID]; !exists {
return ErrUnauthorized
}
delete(svc.authSessions, sessionID)
return nil
return svc.revokeAuthSession(sessionID)
}
func (svc *CoreService) GetCurrentUser(sessionID string) (domain.User, error) {
@@ -381,7 +452,17 @@ func (svc *CoreService) UpdateCurrentUserTheme(sessionID string, preference doma
}
func (svc *CoreService) SeedLocalPlatformAdmin() error {
const adminEmail = "operator.local@example.test"
return svc.SeedPlatformAdmin("operator.local@example.test", "operator-local")
}
func (svc *CoreService) SeedPlatformAdmin(email string, password string) error {
email = strings.TrimSpace(email)
if email == "" {
email = "operator.local@example.test"
}
if len([]rune(password)) < 12 {
return validationError("bootstrap admin password must be at least 12 characters")
}
_, err := svc.store.Users().Get("user-admin")
if err == nil {
return nil
@@ -392,11 +473,11 @@ func (svc *CoreService) SeedLocalPlatformAdmin() error {
return svc.store.Users().Create(domain.User{
ID: "user-admin",
DisplayName: "Operator",
Email: adminEmail,
Email: email,
Status: domain.UserStatusActive,
Roles: []string{"platform-admin"},
PasswordHash: mustHashPassword("operator-local"),
Profile: domain.UserProfile{ContactNote: "local development admin"},
PasswordHash: mustHashPassword(password),
Profile: domain.UserProfile{ContactNote: "bootstrap platform admin"},
CreatedAt: svc.now(),
UpdatedAt: svc.now(),
})
@@ -541,6 +622,7 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
Tags: manifest.Tags,
AIPurposes: manifest.AI.Purposes,
RemoteAccess: manifest.RemoteAccess,
RuntimeProfiles: manifest.RuntimeProfiles,
Status: domain.GamePluginStatusInstalled,
}
}
@@ -623,11 +705,11 @@ func (svc *CoreService) ExecutePluginBridgeAction(sessionID string, request doma
case domain.PluginBridgeActionFilesRequest:
base = svc.executeBridgeFileRequest(sessionID, base, request)
case domain.PluginBridgeActionRemoteAccessRequest:
base = svc.executeBridgeRemoteAccessRequest(base, plugin, instance, request.Payload)
base = svc.executeBridgeRemoteAccessRequest(sessionID, base, plugin, instance, request.Payload)
case domain.PluginBridgeActionRunDistribution:
base = svc.executeBridgeRunDistribution(sessionID, base, request)
case domain.PluginBridgeActionDependenciesRequest:
base = svc.executeBridgeDependenciesRequest(base, plugin, instance, request.Payload)
base = svc.executeBridgeDependenciesRequest(sessionID, base, plugin, instance, request.Payload)
case domain.PluginBridgeActionLogsBackfillRequest:
base = svc.executeBridgeLogsBackfillRequest(base, plugin, instance, request.Payload)
case domain.PluginBridgeActionClientManager:
@@ -843,39 +925,44 @@ func (svc *CoreService) executeBridgeFileRequest(sessionID string, base domain.P
return base
}
func (svc *CoreService) executeBridgeRemoteAccessRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
capability := strings.TrimSpace(payload["capability"])
if capability == "" {
base.Status = "error"
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "capability is required"}
return base
}
if !containsString(plugin.RequiredRunCapabilities, capability) {
if !containsString(plugin.RequiredRunCapabilities, capability) || !containsString(plugin.RemoteAccess.RunCapabilities, capability) {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "requested remote capability is not declared by plugin"}
return base
}
job := domain.Job{
ID: jobIDFromParts("job-remote", base.RequestID, capability),
ServerInstanceID: instance.ID,
RunEndpointID: instance.RunEndpointID,
Capability: capability,
TargetKey: payload["targetKey"],
InputRef: payload["inputRef"],
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
Progress: domain.JobProgress{Percent: 0, Message: "remote access job queued"},
declarationKey := strings.TrimSpace(payload["declarationKey"])
if declarationKey == "" {
for _, profile := range plugin.RuntimeProfiles.TransportProfiles {
if profile.TargetKey == payload["targetKey"] && containsString(profile.Capabilities, capability) {
declarationKey = profile.Key
break
}
}
}
created, err := svc.CreateJob(job)
if declarationKey == "" {
declarationKey = "legacy-" + string(remoteAdapterKindForCapability(capability))
}
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)})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "queued"
base.Result = map[string]string{
"jobId": created.ID,
"state": string(created.State),
"capability": created.Capability,
"targetKey": created.TargetKey,
"serverInstanceId": created.ServerInstanceID,
"jobId": result.RequestID,
"state": result.Status,
"capability": capability,
"targetKey": result.TargetKey,
"serverInstanceId": result.ServerInstanceID,
"adapterKind": string(result.Kind),
}
return base
}
@@ -896,60 +983,116 @@ func (svc *CoreService) executeBridgeRunDistribution(sessionID string, base doma
"artifactId": distribution.ArtifactID,
"checksum": distribution.Checksum,
"keyGeneration": strconv.Itoa(distribution.KeyGeneration),
"secretRef": distribution.SecretRef,
"status": string(distribution.Status),
}
return base
}
func (svc *CoreService) executeBridgeClientManager(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
distribution, err := svc.GenerateClientManagerDistributionForSession(sessionID, domain.ClientManagerBuildRequest{
ServerInstanceID: request.ServerInstanceID,
ProfileKey: request.Payload["profileKey"],
TargetOS: defaultBridgeValue(request.Payload["targetOs"], "windows"),
TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"),
RepositoryURL: request.Payload["repositoryUrl"],
SourceRevision: request.Payload["sourceRevision"],
IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID),
})
profileKey := request.Payload["profileKey"]
operation := defaultBridgeValue(request.Payload["operation"], "status")
idempotencyKey := defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID)
generation, _ := strconv.Atoi(request.Payload["expectedDeploymentGeneration"])
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
if err != nil {
return bridgeExecutionError(base, err)
}
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return bridgeExecutionError(base, err)
}
profile, err := findRuntimeClientManagerProfile(plugin, profileKey)
if err != nil {
return bridgeExecutionError(base, ErrForbidden)
}
var view domain.ClientManagerLifecycleView
switch operation {
case "generate":
distribution, err := svc.GenerateClientManagerDistributionForSession(sessionID, domain.ClientManagerBuildRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, TargetOS: defaultBridgeValue(request.Payload["targetOs"], "windows"), TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"), RepositoryURL: profile.RepositoryURL, SourceRevision: clientManagerProfileRevision(profile), IdempotencyKey: idempotencyKey})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "queued"
base.Result = map[string]string{"distributionId": distribution.ID, "buildJobId": distribution.BuildJobID, "artifactId": distribution.ArtifactID, "checksum": distribution.Checksum, "keyGeneration": strconv.Itoa(distribution.KeyGeneration), "version": distribution.Version, "status": string(distribution.Status)}
return base
case "download":
reference, err := svc.OpenLatestClientManagerDistributionDownloadForSession(sessionID, instance.ID, profileKey)
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "ok"
base.Result = map[string]string{"artifactId": reference.ArtifactID, "downloadUrl": reference.DownloadURL, "checksum": reference.Checksum, "sizeBytes": strconv.FormatInt(reference.SizeBytes, 10), "expiresAt": reference.ExpiresAt.Format(time.RFC3339), "rangeSupported": strconv.FormatBool(reference.RangeSupported), "chunkSizeBytes": strconv.Itoa(reference.ChunkSizeBytes)}
return base
case "reset-key":
key, err := svc.ResetComponentKeyForSession(sessionID, domain.ComponentKeyResetRequest{ServerInstanceID: instance.ID, ComponentKind: domain.DistributionComponentClientManager, ComponentKey: profileKey})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "ok"
base.Result = map[string]string{"profileKey": profileKey, "keyGeneration": strconv.Itoa(key.Generation), "status": string(key.Status), "requiresRedeploy": "true"}
return base
case "status":
view, err = svc.GetClientManagerLifecycleForSession(sessionID, instance.ID, profileKey)
case "deploy":
distributionID, resolveErr := svc.resolveClientManagerDistributionID(instance.ID, profileKey, request.Payload["artifactId"])
if resolveErr != nil {
return bridgeExecutionError(base, resolveErr)
}
view, err = svc.DeployClientManagerForSession(sessionID, domain.ClientManagerDeployRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, DistributionID: distributionID, ExpectedDeploymentGeneration: generation, IdempotencyKey: idempotencyKey})
case "start", "stop", "restart", "rollback":
view, err = svc.ControlClientManagerForSession(sessionID, domain.ClientManagerControlRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, Operation: domain.ClientManagerLifecycleOperation(operation), ExpectedDeploymentGeneration: generation, IdempotencyKey: idempotencyKey})
case "update":
distributionID, resolveErr := svc.resolveClientManagerDistributionID(instance.ID, profileKey, request.Payload["artifactId"])
if resolveErr != nil {
return bridgeExecutionError(base, resolveErr)
}
view, err = svc.UpdateClientManagerForSession(sessionID, domain.ClientManagerUpdateRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, DistributionID: distributionID, ExpectedDeploymentGeneration: generation, Approved: true, IdempotencyKey: idempotencyKey})
case "retry":
view, err = svc.RetryClientManagerLifecycleForSession(sessionID, domain.ClientManagerRetryRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, ExpectedDeploymentGeneration: generation, IdempotencyKey: idempotencyKey})
case "revoke-session":
view, err = svc.RevokeClientManagerSessionForSession(sessionID, domain.ClientManagerRevokeSessionRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, Reason: "plugin bridge operator request"})
case "uninstall":
view, err = svc.UninstallClientManagerForSession(sessionID, domain.ClientManagerUninstallRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, ExpectedDeploymentGeneration: generation, Confirmed: true, IdempotencyKey: idempotencyKey})
default:
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "invalid_client_manager_operation", Message: "client-manager operation is not supported"}
return base
}
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "ok"
base.Result = map[string]string{
"distributionId": distribution.ID,
"buildJobId": distribution.BuildJobID,
"artifactId": distribution.ArtifactID,
"checksum": distribution.Checksum,
"keyGeneration": strconv.Itoa(distribution.KeyGeneration),
"secretRef": distribution.SecretRef,
"status": string(distribution.Status),
if view.Job.ID != "" && !isTerminalJobState(view.Job.State) {
base.Status = "queued"
}
base.Result = safeClientManagerBridgeResult(view)
return base
}
func (svc *CoreService) executeBridgeDependenciesRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
action := defaultBridgeValue(payload["action"], "check")
func (svc *CoreService) executeBridgeDependenciesRequest(sessionID string, base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
action := defaultBridgeValue(payload["operation"], "check")
capability := domain.JobCapabilityDependenciesCheck
message := "dependency check queued"
if action == "install" {
capability = domain.JobCapabilityDependenciesInstall
message = "dependency install queued"
} else if action != "check" {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "invalid_dependency_operation", Message: "dependency operation must be check or install"}
return base
}
if !containsString(plugin.RequiredRunCapabilities, capability) {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "dependency capability is not declared by plugin"}
return base
}
job, err := svc.CreateJob(domain.Job{
ID: jobIDFromParts("job-dependencies", base.RequestID, capability),
job, err := svc.QueueDependencyJobForSession(sessionID, domain.DependencyJobRequest{
ServerInstanceID: instance.ID,
RunEndpointID: instance.RunEndpointID,
Capability: capability,
TargetKey: defaultBridgeValue(payload["probeKey"], "dependencies/default"),
InputRef: payload["inputRef"],
ProbeKey: payload["probeKey"],
InstallPlanKey: payload["planKey"],
PlanDigest: payload["planDigest"],
TargetOS: payload["targetOS"],
TargetArch: payload["targetArch"],
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
Progress: domain.JobProgress{Percent: 0, Message: message},
Install: action == "install",
})
if err != nil {
return bridgeExecutionError(base, err)
@@ -1136,6 +1279,7 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
Tags: plugin.Tags,
AIPurposes: plugin.AIPurposes,
RemoteAccess: plugin.RemoteAccess,
RuntimeProfiles: plugin.RuntimeProfiles,
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
Source: "platform-registry",
@@ -1390,11 +1534,22 @@ func (svc *CoreService) GetServerConfigForSession(sessionID string, serverInstan
Key: "server.properties",
Source: "platform-derived",
UpdatedAt: instance.UpdatedAt,
Checksum: instance.ConfigChecksum,
}
if config.UpdatedAt.IsZero() {
config.UpdatedAt = svc.now()
}
config.Content = buildLogicalServerConfig(instance)
config.Key = instance.ConfigKey
if config.Key == "" {
config.Key = "server.properties"
}
config.Content = instance.ConfigContent
if config.Content == "" {
config.Content = buildLogicalServerConfig(instance)
}
if config.Checksum == "" {
config.Checksum = validator.BytesChecksum([]byte(config.Content))
}
if err := validator.ValidateServerConfig(config); err != nil {
return domain.ServerConfig{}, err
}
@@ -1415,12 +1570,16 @@ func (svc *CoreService) PreviewServerConfigWriteForSession(sessionID string, req
if config.ConfigVersion != request.ExpectedConfigVersion {
return domain.ServerConfigDiffPreview{}, validationError("expectedConfigVersion must match server instance")
}
if request.ExpectedChecksum != "" && config.Checksum != request.ExpectedChecksum {
return domain.ServerConfigDiffPreview{}, validationError("expectedChecksum must match server config")
}
if config.Key != request.Key {
return domain.ServerConfigDiffPreview{}, validationError("key must match server config")
}
preview := domain.ServerConfigDiffPreview{
ServerInstanceID: request.ServerInstanceID,
ConfigVersion: config.ConfigVersion,
Checksum: config.Checksum,
Key: request.Key,
CurrentContent: config.Content,
ProposedContent: request.ProposedContent,
@@ -1446,6 +1605,7 @@ func (svc *CoreService) ApproveServerConfigWriteForSession(sessionID string, app
preview, err := svc.PreviewServerConfigWriteForSession(sessionID, domain.ServerConfigDiffRequest{
ServerInstanceID: approval.ServerInstanceID,
ExpectedConfigVersion: approval.ExpectedConfigVersion,
ExpectedChecksum: approval.ExpectedChecksum,
Key: approval.Key,
ProposedContent: approval.ProposedContent,
ProposedContentInputRef: approval.ProposedContentInputRef,
@@ -1460,6 +1620,13 @@ func (svc *CoreService) ApproveServerConfigWriteForSession(sessionID string, app
if err != nil {
return domain.ServerConfigWriteDispatch{}, err
}
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.ServerConfigWriteDispatch{}, err
}
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "config.write.denied"); err != nil {
return domain.ServerConfigWriteDispatch{}, err
}
job, err := svc.CreateJob(domain.Job{
ID: jobIDFromParts("job-config-write", approval.ServerInstanceID, approval.IdempotencyKey),
ServerInstanceID: instance.ID,
@@ -1467,6 +1634,7 @@ func (svc *CoreService) ApproveServerConfigWriteForSession(sessionID string, app
Capability: domain.JobCapabilityConfigWrite,
TargetKey: approval.Key,
InputRef: approval.ProposedContentInputRef,
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), Content: approval.ProposedContent, ExpectedVersion: approval.ExpectedConfigVersion, ExpectedChecksum: preview.Checksum, MaxReadBytes: 64 * 1024},
IdempotencyKey: approval.IdempotencyKey,
Progress: domain.JobProgress{Percent: 0, Message: "config write queued"},
})
@@ -1487,6 +1655,29 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
if request.ExpectedConfigVersion > 0 && request.ExpectedConfigVersion != instance.ConfigVersion {
return domain.FileOperationDispatchResult{}, validationError("expectedConfigVersion must match server instance")
}
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.FileOperationDispatchResult{}, err
}
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "file.operation.denied"); err != nil {
return domain.FileOperationDispatchResult{}, err
}
content := request.Content
if request.Operation == domain.FileOperationWrite && content == "" && strings.HasPrefix(request.InputRef, "artifact://") {
artifactID := strings.TrimPrefix(request.InputRef, "artifact://")
artifact, artifactErr := svc.store.Artifacts().Get(artifactID)
if artifactErr != nil {
return domain.FileOperationDispatchResult{}, artifactErr
}
if artifact.OwnerKind != domain.ArtifactOwnerKindServerInstance || artifact.OwnerID != instance.ID || artifact.State != domain.ArtifactStateAvailable {
return domain.FileOperationDispatchResult{}, ErrForbidden
}
payload, payloadErr := svc.artifactPayload(artifactID)
if payloadErr != nil {
return domain.FileOperationDispatchResult{}, payloadErr
}
content = string(payload)
}
if request.PluginID != "" {
plugin, err := svc.store.GamePlugins().Get(request.PluginID)
if err != nil {
@@ -1518,6 +1709,7 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
Capability: capability,
TargetKey: request.Key,
InputRef: request.InputRef,
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), Content: content, ExpectedVersion: request.ExpectedConfigVersion, ExpectedChecksum: request.ExpectedChecksum, MaxReadBytes: 64 * 1024},
IdempotencyKey: request.IdempotencyKey,
Progress: domain.JobProgress{Percent: 0, Message: message},
})
@@ -1535,6 +1727,14 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
}), nil
}
func (svc *CoreService) runtimeProfileScope(serverInstanceID string) string {
binding, err := svc.runtimeBindingForServer(serverInstanceID)
if err != nil {
return "default"
}
return binding.ProfileKey
}
func (svc *CoreService) metricsForServer(instance domain.ServerInstance) domain.ServerMetrics {
metrics := domain.ServerMetrics{
ServerInstanceID: instance.ID,
@@ -1732,6 +1932,7 @@ func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
if job.UpdatedAt.IsZero() {
job.UpdatedAt = stamp
}
job = normalizeJobScheduling(job, stamp)
if err := validator.ValidateJob(job); err != nil {
return domain.Job{}, err
}
@@ -1772,11 +1973,22 @@ func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
}
func (svc *CoreService) GetJob(id string) (domain.Job, error) {
return svc.store.Jobs().Get(id)
job, err := svc.store.Jobs().Get(id)
if err != nil {
return domain.Job{}, err
}
return normalizeJobScheduling(job, svc.now()), nil
}
func (svc *CoreService) ListJobs(filter domain.JobFilter) ([]domain.Job, error) {
return svc.store.Jobs().List(filter)
jobs, err := svc.store.Jobs().List(filter)
if err != nil {
return nil, err
}
for i := range jobs {
jobs[i] = normalizeJobScheduling(jobs[i], svc.now())
}
return jobs, nil
}
func (svc *CoreService) CreateArtifact(artifact domain.Artifact) (domain.Artifact, error) {
@@ -1891,17 +2103,11 @@ func validationError(violation string) error {
}
func (svc *CoreService) userIDForSession(sessionID string) (string, error) {
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return "", ErrUnauthorized
session, err := svc.authenticatedSession(sessionID)
if err != nil {
return "", err
}
svc.authMu.Lock()
defer svc.authMu.Unlock()
userID, exists := svc.authSessions[sessionID]
if !exists {
return "", ErrUnauthorized
}
return userID, nil
return session.UserID, nil
}
func userIDFromEmail(email string) string {