Complete platform management workflows
This commit is contained in:
@@ -49,6 +49,18 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/server-instances/workflows/create", h.serverInstanceCreateWorkflow)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/start", h.serverInstanceStart)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/stop", h.serverInstanceStop)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/runtime/actions", h.serverRuntimeActions)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/run/generate", h.serverRunGenerate)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/run/download", h.serverRunDownload)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/run/key/reset", h.serverRunKeyReset)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/run/update", h.serverRunUpdate)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/generate", h.serverClientManagerGenerate)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/download", h.serverClientManagerDownload)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/key/reset", h.serverClientManagerKeyReset)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies/check", h.serverDependenciesCheck)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies/install", h.serverDependenciesInstall)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/logs/live", h.serverLiveLogs)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/logs/backfill", h.serverLogsBackfill)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/config/diff", h.serverInstanceConfigDiff)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/config/approve", h.serverInstanceConfigApprove)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/config", h.serverInstanceConfig)
|
||||
@@ -893,26 +905,53 @@ func (h *coreHandlers) serverInstances(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// serverInstanceDetail godoc
|
||||
// @Summary Get server instance
|
||||
// @Description Returns one server instance by ID.
|
||||
// @Summary Get, update, or archive server instance
|
||||
// @Description Returns one server instance by ID, updates safe metadata, or archives it by marking the instance deleted after safety validation.
|
||||
// @Tags server-instances
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Server instance ID"
|
||||
// @Param body body dto.ServerInstanceUpdateRequest false "Server metadata update request"
|
||||
// @Success 204
|
||||
// @Success 200 {object} dto.ServerInstanceResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 403 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/server-instances/{id} [get]
|
||||
// @Router /api/v1/server-instances/{id} [put]
|
||||
// @Router /api/v1/server-instances/{id} [delete]
|
||||
func (h *coreHandlers) serverInstanceDetail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
instance, err := h.core.GetServerInstanceForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ServerInstanceFromDomain(instance))
|
||||
case http.MethodPut:
|
||||
request, err := decodeJSON[dto.ServerInstanceUpdateRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
instance, err := h.core.UpdateServerInstanceForSession(bearerToken(r), r.PathValue("id"), request.ToDomain())
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ServerInstanceFromDomain(instance))
|
||||
case http.MethodDelete:
|
||||
_, err := h.core.ArchiveServerInstanceForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
writeMethodNotAllowed(w, http.MethodGet+", "+http.MethodPut+", "+http.MethodDelete)
|
||||
}
|
||||
instance, err := h.core.GetServerInstanceForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ServerInstanceFromDomain(instance))
|
||||
}
|
||||
|
||||
// platformMetrics godoc
|
||||
|
||||
@@ -211,7 +211,9 @@ func TestConfigWriteAndFileDispatchAPIAreScopedAndSafe(t *testing.T) {
|
||||
ownerSession := postOKJSON[dto.AuthSessionResponse](t, router, "/api/v1/auth/login", dto.LoginRequest{Account: "owner-config-api@example.test", Password: "secret-password"}).SessionID
|
||||
otherSession := postOKJSON[dto.AuthSessionResponse](t, router, "/api/v1/auth/login", dto.LoginRequest{Account: "other-config-api@example.test", Password: "secret-password"}).SessionID
|
||||
|
||||
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
|
||||
pluginRequest := validGamePluginRequest()
|
||||
pluginRequest.RequiredRunCapabilities = append(pluginRequest.RequiredRunCapabilities, domain.JobCapabilityConfigWrite, domain.JobCapabilityFilesRead, domain.JobCapabilityFilesWrite)
|
||||
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", pluginRequest)
|
||||
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", validRunEndpointRequest())
|
||||
instance := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||
ID: "server-config-api",
|
||||
@@ -286,6 +288,101 @@ func TestConfigWriteAndFileDispatchAPIAreScopedAndSafe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreAPIServerRuntimeDistributionAndJobWorkflows(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
serverID := createRuntimeAPIFixtures(t, router, adminSession)
|
||||
|
||||
actions := getJSONWithAuth[dto.ServerRuntimeActionsResponse](t, router, "/api/v1/server-instances/"+serverID+"/runtime/actions", adminSession)
|
||||
availability := map[string]bool{}
|
||||
for _, action := range actions.Actions {
|
||||
availability[action.Key] = action.Available
|
||||
}
|
||||
for _, key := range []string{"generate-run", "push-run-update", "generate-client-manager", "dependencies-check", "dependencies-install", "historical-logs"} {
|
||||
if !availability[key] {
|
||||
t.Fatalf("expected action %q available in %+v", key, actions.Actions)
|
||||
}
|
||||
}
|
||||
|
||||
runDistribution := postJSONWithAuth[dto.RunDistributionResponse](t, router, "/api/v1/server-instances/"+serverID+"/run/generate", dto.RunDistributionGenerateRequest{TargetOS: "linux", TargetArch: "amd64", IdempotencyKey: "api-run-generate"}, adminSession)
|
||||
if runDistribution.ArtifactID == "" || runDistribution.KeyGeneration != 1 || runDistribution.SecretRef == "" {
|
||||
t.Fatalf("unexpected run distribution: %+v", runDistribution)
|
||||
}
|
||||
runDownload := postOKJSONWithAuth[dto.ArtifactDownloadReferenceResponse](t, router, "/api/v1/server-instances/"+serverID+"/run/download", map[string]string{}, adminSession)
|
||||
if runDownload.ArtifactID != runDistribution.ArtifactID || runDownload.DownloadURL == "" {
|
||||
t.Fatalf("unexpected run download: %+v", runDownload)
|
||||
}
|
||||
updateRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/run/update", dto.RunUpdateRequest{ArtifactID: runDistribution.ArtifactID, Checksum: runDistribution.Checksum, IdempotencyKey: "api-run-update"}, adminSession)
|
||||
assertStatus(t, updateRecorder, http.StatusAccepted)
|
||||
update := decodeBody[dto.RunUpdateJobResponse](t, updateRecorder)
|
||||
if update.JobID == "" || update.ArtifactID != runDistribution.ArtifactID || update.Status != string(domain.DistributionJobStatusQueued) {
|
||||
t.Fatalf("unexpected run update job: %+v", update)
|
||||
}
|
||||
|
||||
clientDistribution := postJSONWithAuth[dto.ClientManagerDistributionResponse](t, router, "/api/v1/server-instances/"+serverID+"/client-managers/generate", dto.ClientManagerBuildRequest{ProfileKey: "scum-client-manager", TargetOS: "windows", TargetArch: "amd64", RepositoryURL: "https://github.com/F88888/scum_client.git", SourceRevision: "main", IdempotencyKey: "api-client-manager"}, adminSession)
|
||||
if clientDistribution.ArtifactID == "" || clientDistribution.BuildJobID == "" || clientDistribution.SecretRef == runDistribution.SecretRef {
|
||||
t.Fatalf("unexpected client distribution: %+v", clientDistribution)
|
||||
}
|
||||
clientDownload := postOKJSONWithAuth[dto.ArtifactDownloadReferenceResponse](t, router, "/api/v1/server-instances/"+serverID+"/client-managers/download", dto.ClientManagerDownloadRequest{ProfileKey: "scum-client-manager"}, adminSession)
|
||||
if clientDownload.ArtifactID != clientDistribution.ArtifactID {
|
||||
t.Fatalf("unexpected client download: %+v", clientDownload)
|
||||
}
|
||||
|
||||
dependencyCheckRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/dependencies/check", dto.DependencyJobRequest{ProbeKey: "java-runtime", IdempotencyKey: "api-dependency-check"}, adminSession)
|
||||
assertStatus(t, dependencyCheckRecorder, http.StatusAccepted)
|
||||
dependencyCheck := decodeBody[dto.JobResponse](t, dependencyCheckRecorder)
|
||||
if dependencyCheck.Capability != domain.JobCapabilityDependenciesCheck || dependencyCheck.TargetKey != "dependencies/java-runtime" {
|
||||
t.Fatalf("unexpected dependency check job: %+v", dependencyCheck)
|
||||
}
|
||||
dependencyInstallRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/dependencies/install", dto.DependencyJobRequest{ProbeKey: "java-runtime", InstallPlanKey: "java-install", IdempotencyKey: "api-dependency-install"}, adminSession)
|
||||
assertStatus(t, dependencyInstallRecorder, http.StatusAccepted)
|
||||
dependencyInstall := decodeBody[dto.JobResponse](t, dependencyInstallRecorder)
|
||||
if dependencyInstall.Capability != domain.JobCapabilityDependenciesInstall || dependencyInstall.TargetKey != "dependencies/install/java-install" {
|
||||
t.Fatalf("unexpected dependency install job: %+v", dependencyInstall)
|
||||
}
|
||||
unsafeDependency := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/dependencies/install", dto.DependencyJobRequest{ProbeKey: "java-runtime", InstallPlanKey: "bash -c whoami", IdempotencyKey: "api-dependency-unsafe"}, adminSession)
|
||||
assertErrorResponse(t, unsafeDependency, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
backfillRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/logs/backfill", dto.LogBackfillRequest{SourceKey: "latest", CheckpointRef: "input://logs/" + serverID + "/latest/v1", Limit: 500, IdempotencyKey: "api-logs-backfill"}, adminSession)
|
||||
assertStatus(t, backfillRecorder, http.StatusAccepted)
|
||||
backfill := decodeBody[dto.JobResponse](t, backfillRecorder)
|
||||
if backfill.Capability != domain.JobCapabilityLogsBackfill || backfill.ResultRef != "" || backfill.InputRef == "" {
|
||||
t.Fatalf("unexpected log backfill job: %+v", backfill)
|
||||
}
|
||||
liveLogs := getJSONWithAuth[dto.LogStreamListResponse](t, router, "/api/v1/server-instances/"+serverID+"/logs/live", adminSession)
|
||||
if liveLogs.Count != 1 || liveLogs.Items[0].StreamKey != "stdout" {
|
||||
t.Fatalf("unexpected live logs: %+v", liveLogs)
|
||||
}
|
||||
|
||||
runReset := postOKJSONWithAuth[dto.ComponentKeyResponse](t, router, "/api/v1/server-instances/"+serverID+"/run/key/reset", map[string]string{}, adminSession)
|
||||
if runReset.Generation != 2 || runReset.SecretRef == "" {
|
||||
t.Fatalf("unexpected run key reset: %+v", runReset)
|
||||
}
|
||||
clientReset := postOKJSONWithAuth[dto.ComponentKeyResponse](t, router, "/api/v1/server-instances/"+serverID+"/client-managers/key/reset", dto.ComponentKeyResetRequest{ComponentKey: "scum-client-manager"}, adminSession)
|
||||
if clientReset.Generation != 2 || clientReset.SecretRef == runReset.SecretRef {
|
||||
t.Fatalf("unexpected client key reset: %+v", clientReset)
|
||||
}
|
||||
|
||||
for _, body := range []string{mustJSON(t, runDistribution), mustJSON(t, clientDistribution), mustJSON(t, runDownload), mustJSON(t, clientDownload), mustJSON(t, runReset), mustJSON(t, clientReset), mustJSON(t, dependencyInstall), mustJSON(t, backfill)} {
|
||||
for _, forbidden := range []string{"authKey", "enc:v1", "password=", "unix://", "tcp://", "/Users/", "mysql://", "sqlite://"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("runtime API response exposed forbidden fragment %q: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audits := getJSONWithAuth[dto.AuditEventListResponse](t, router, "/api/v1/audit-events?resourceId="+serverID, adminSession)
|
||||
auditActions := map[string]bool{}
|
||||
for _, audit := range audits.Items {
|
||||
auditActions[audit.Action] = true
|
||||
}
|
||||
for _, action := range []string{"run.generate", "run.download", "run.update", "client-manager.build", "client-manager.download", "dependency.install", "logs.backfill", "runtime-key.reset"} {
|
||||
if !auditActions[action] {
|
||||
t.Fatalf("expected audit action %q in %+v", action, audits.Items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreAPIErrorResponses(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
@@ -562,6 +659,54 @@ func TestServerLifecycleWorkflowAPI(t *testing.T) {
|
||||
assertErrorResponse(t, invalidStop, http.StatusBadRequest, errorCodeValidation)
|
||||
}
|
||||
|
||||
func TestServerInstanceManagementAPI(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
|
||||
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", validRunEndpointRequest())
|
||||
|
||||
ready := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||
ID: "server-management",
|
||||
PluginID: "server.scum",
|
||||
RunEndpointID: "run-local",
|
||||
Name: "SCUM Ops",
|
||||
State: domain.ServerInstanceStateReady,
|
||||
}, adminSession)
|
||||
|
||||
newName := "SCUM Ops Renamed"
|
||||
updated := putJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances/server-management", dto.ServerInstanceUpdateRequest{Name: &newName}, adminSession)
|
||||
if updated.Name != newName || updated.PluginID != ready.PluginID || updated.RunEndpointID != ready.RunEndpointID {
|
||||
t.Fatalf("unexpected server update: %+v", updated)
|
||||
}
|
||||
|
||||
running := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||
ID: "server-running-archive",
|
||||
PluginID: "server.scum",
|
||||
RunEndpointID: "run-local",
|
||||
Name: "SCUM Running Archive",
|
||||
State: domain.ServerInstanceStateRunning,
|
||||
}, adminSession)
|
||||
unsafeArchive := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/"+running.ID, "", adminSession)
|
||||
assertErrorResponse(t, unsafeArchive, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
archived := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/server-management", "", adminSession)
|
||||
assertStatus(t, archived, http.StatusNoContent)
|
||||
activeList := getJSONWithAuth[dto.ServerInstanceListResponse](t, router, "/api/v1/server-instances", adminSession)
|
||||
for _, item := range activeList.Items {
|
||||
if item.ID == "server-management" {
|
||||
t.Fatalf("archived server should be hidden from normal list: %+v", activeList)
|
||||
}
|
||||
}
|
||||
deletedList := getJSONWithAuth[dto.ServerInstanceListResponse](t, router, "/api/v1/server-instances?state=deleted", adminSession)
|
||||
if deletedList.Count != 1 || deletedList.Items[0].ID != "server-management" || deletedList.Items[0].State != domain.ServerInstanceStateDeleted {
|
||||
t.Fatalf("expected explicit deleted filter to return archived server, got %+v", deletedList)
|
||||
}
|
||||
|
||||
blank := ""
|
||||
invalidUpdate := requestJSONWithAuth(t, router, http.MethodPut, "/api/v1/server-instances/server-running-archive", dto.ServerInstanceUpdateRequest{Name: &blank}, adminSession)
|
||||
assertErrorResponse(t, invalidUpdate, http.StatusBadRequest, errorCodeValidation)
|
||||
}
|
||||
|
||||
func TestServerAccessAPIScopesOwnersAndAdministrators(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
@@ -1313,6 +1458,67 @@ func anyJSON(t *testing.T, value any) map[string]any {
|
||||
return body
|
||||
}
|
||||
|
||||
func createRuntimeAPIFixtures(t *testing.T, router http.Handler, adminSession string) string {
|
||||
t.Helper()
|
||||
pluginRequest := validGamePluginRequest()
|
||||
pluginRequest.ID = "server.runtime"
|
||||
pluginRequest.Name = "Runtime Test Plugin"
|
||||
pluginRequest.ServerType = "runtime-test"
|
||||
pluginRequest.SupportedOS = []string{"linux", "windows"}
|
||||
pluginRequest.RequiredRunCapabilities = []string{
|
||||
"process.install",
|
||||
"process.start",
|
||||
"process.stop",
|
||||
"logs.read",
|
||||
domain.JobCapabilityRunSelfUpdate,
|
||||
domain.JobCapabilityDependenciesCheck,
|
||||
domain.JobCapabilityDependenciesInstall,
|
||||
domain.JobCapabilityLogsBackfill,
|
||||
}
|
||||
pluginRequest.DeclaredPermissions = []string{
|
||||
"server.read",
|
||||
"server.logs.read",
|
||||
"server.run.distribution",
|
||||
"server.client-manager.manage",
|
||||
"server.dependencies.manage",
|
||||
"server.artifacts.read",
|
||||
}
|
||||
pluginRequest.BridgeActions = []string{
|
||||
string(domain.PluginBridgeActionRunDistribution),
|
||||
string(domain.PluginBridgeActionClientManager),
|
||||
string(domain.PluginBridgeActionDependenciesRequest),
|
||||
string(domain.PluginBridgeActionLogsBackfillRequest),
|
||||
}
|
||||
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", pluginRequest)
|
||||
|
||||
endpoint := validRunEndpointRequest()
|
||||
endpoint.ID = "run-runtime"
|
||||
endpoint.Capabilities = append(endpoint.Capabilities,
|
||||
domain.JobCapabilityRunSelfUpdate,
|
||||
domain.JobCapabilityDependenciesCheck,
|
||||
domain.JobCapabilityDependenciesInstall,
|
||||
domain.JobCapabilityLogsBackfill,
|
||||
)
|
||||
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", endpoint)
|
||||
|
||||
server := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||
ID: "server-runtime-api",
|
||||
PluginID: "server.runtime",
|
||||
RunEndpointID: "run-runtime",
|
||||
Name: "Runtime API Server",
|
||||
State: domain.ServerInstanceStateReady,
|
||||
}, adminSession)
|
||||
postJSON[dto.LogStreamResponse](t, router, "/api/v1/log-streams", dto.LogStreamCreateRequest{
|
||||
ID: "log-runtime-api",
|
||||
ServerInstanceID: server.ID,
|
||||
Source: domain.LogStreamSourceProcess,
|
||||
StreamKey: "stdout",
|
||||
StorageBackend: domain.LogStorageBackendLocalSegments,
|
||||
RetentionPolicy: "default",
|
||||
})
|
||||
return server.ID
|
||||
}
|
||||
|
||||
func validAIProviderRequest() dto.AIProviderCreateRequest {
|
||||
return dto.AIProviderCreateRequest{
|
||||
ID: "ai.openai",
|
||||
|
||||
+21
-2
@@ -13,7 +13,8 @@ All routes use JSON request and response bodies. Collection routes support `GET`
|
||||
| Game plugins | `GET /api/v1/game-plugins`, `POST /api/v1/game-plugins` | `GET /api/v1/game-plugins/{id}` | `GamePluginCreateRequest`, `GamePluginResponse`, `GamePluginListResponse` |
|
||||
| Plugin marketplace | `GET /api/v1/plugin-marketplace/plugins` | `GET /api/v1/plugin-marketplace/plugins/{id}`, `POST /api/v1/plugin-marketplace/plugins/{id}/state` | `MarketplacePluginResponse`, `MarketplacePluginListResponse`, `MarketplacePluginStateRequest` |
|
||||
| Plugin bridge | `POST /api/v1/plugin-bridge/authorize`, `POST /api/v1/plugin-bridge/execute` | n/a | `PluginBridgeAuthorizeRequest`, `PluginBridgeAuthorizeResponse`, `PluginBridgeExecuteRequest`, `PluginBridgeExecuteResponse` |
|
||||
| Server instances | `GET /api/v1/server-instances`, `POST /api/v1/server-instances` | `GET /api/v1/server-instances/{id}` | `ServerInstanceCreateRequest`, `ServerInstanceResponse`, `ServerInstanceListResponse` |
|
||||
| Server instances | `GET /api/v1/server-instances`, `POST /api/v1/server-instances` | `GET /api/v1/server-instances/{id}`, `PUT /api/v1/server-instances/{id}`, `DELETE /api/v1/server-instances/{id}` | `ServerInstanceCreateRequest`, `ServerInstanceUpdateRequest`, `ServerInstanceResponse`, `ServerInstanceListResponse` |
|
||||
| Server runtime distribution | n/a | `GET /api/v1/server-instances/{id}/runtime/actions`, `POST /api/v1/server-instances/{id}/run/generate`, `POST /api/v1/server-instances/{id}/run/download`, `POST /api/v1/server-instances/{id}/run/key/reset`, `POST /api/v1/server-instances/{id}/run/update`, `POST /api/v1/server-instances/{id}/client-managers/generate`, `POST /api/v1/server-instances/{id}/client-managers/download`, `POST /api/v1/server-instances/{id}/client-managers/key/reset`, `POST /api/v1/server-instances/{id}/dependencies/check`, `POST /api/v1/server-instances/{id}/dependencies/install`, `GET /api/v1/server-instances/{id}/logs/live`, `POST /api/v1/server-instances/{id}/logs/backfill` | `ServerRuntimeActionsResponse`, `RunDistributionGenerateRequest`, `RunDistributionResponse`, `RunUpdateRequest`, `RunUpdateJobResponse`, `ClientManagerBuildRequest`, `ClientManagerDistributionResponse`, `ClientManagerDownloadRequest`, `ComponentKeyResetRequest`, `ComponentKeyResponse`, `DependencyJobRequest`, `LogBackfillRequest` |
|
||||
| Metrics | `GET /api/v1/metrics/platform`, `GET /api/v1/metrics/server-instances` | n/a | `PlatformResourceUsageResponse`, `ServerMetricsResponse`, `ServerMetricsListResponse` |
|
||||
| Server config | n/a | `GET /api/v1/server-instances/{id}/config`, `POST /api/v1/server-instances/{id}/config/diff`, `POST /api/v1/server-instances/{id}/config/approve` | `ServerConfigResponse`, `ServerConfigDiffPreviewRequest`, `ServerConfigDiffPreviewResponse`, `ServerConfigWriteApprovalRequest`, `ServerConfigWriteDispatchResponse` |
|
||||
| File operations | `POST /api/v1/file-operations/dispatch` | n/a | `FileOperationDispatchRequest`, `FileOperationDispatchResponse` |
|
||||
@@ -31,6 +32,7 @@ All routes use JSON request and response bodies. Collection routes support `GET`
|
||||
- `GET /api/v1/game-plugins?serverType=scum&status=installed`
|
||||
- `GET /api/v1/plugin-marketplace/plugins?serverType=scum&status=installed&capability=logs.read&keyword=scum`
|
||||
- `GET /api/v1/server-instances?pluginId=server.scum&runEndpointId=run-local&state=draft`
|
||||
- `GET /api/v1/server-instances?state=deleted`
|
||||
- `GET /api/v1/metrics/server-instances`
|
||||
- `GET /api/v1/run/endpoints?status=online`
|
||||
- `GET /api/v1/jobs?serverInstanceId=server-1&runEndpointId=run-local&state=queued`
|
||||
@@ -120,6 +122,23 @@ Artifact bridge execution returns safe metadata and platform content routes only
|
||||
|
||||
Lifecycle workflow responses include accepted status, action, bounded server instance metadata, and bounded job metadata. They do not expose run credentials, host paths, raw credentials, AI provider keys, direct sockets, plugin action file contents, or large result bodies.
|
||||
|
||||
## Implemented Runtime Distribution And Client Manager Actions
|
||||
|
||||
- `GET /api/v1/server-instances/{id}/runtime/actions`: returns the current user-visible runtime action matrix for the server, including run endpoint status, action availability, and safe unavailable reasons.
|
||||
- `POST /api/v1/server-instances/{id}/run/generate`: accepts `RunDistributionGenerateRequest`, creates or reuses the server's current encrypted run key, writes that key into the secret-bearing generated package config, publishes an artifact, and returns `RunDistributionResponse` with checksum, key generation, artifact ID, and redacted secret ref only.
|
||||
- `POST /api/v1/server-instances/{id}/run/download`: opens the latest available run package through `ArtifactDownloadReferenceResponse` after server-scoped authorization.
|
||||
- `POST /api/v1/server-instances/{id}/run/key/reset`: resets the server's single active run key, increments generation, revokes previous run packages, and returns `ComponentKeyResponse`.
|
||||
- `POST /api/v1/server-instances/{id}/run/update`: accepts `RunUpdateRequest` with an approved artifact ID/checksum and queues a bounded `run.self-update` job through `RunUpdateJobResponse`.
|
||||
- `POST /api/v1/server-instances/{id}/client-managers/generate`: accepts `ClientManagerBuildRequest`, validates the plugin-declared client-manager profile and target platform, injects a distinct current client-manager key into the package config, publishes a downloadable artifact, and returns `ClientManagerDistributionResponse`.
|
||||
- `POST /api/v1/server-instances/{id}/client-managers/download`: accepts `ClientManagerDownloadRequest` and opens the latest authorized client-manager artifact through `ArtifactDownloadReferenceResponse`.
|
||||
- `POST /api/v1/server-instances/{id}/client-managers/key/reset`: accepts `ComponentKeyResetRequest`, resets only the named client-manager component key, increments generation, revokes older client-manager packages, and returns `ComponentKeyResponse`.
|
||||
- `POST /api/v1/server-instances/{id}/dependencies/check`: accepts `DependencyJobRequest` and queues a `dependencies.check` run job for a declared logical probe key.
|
||||
- `POST /api/v1/server-instances/{id}/dependencies/install`: accepts `DependencyJobRequest` with an install plan key and queues `dependencies.install` only for typed plugin-declared plans.
|
||||
- `GET /api/v1/server-instances/{id}/logs/live`: returns safe live log stream metadata for the selected server using `LogStreamListResponse`.
|
||||
- `POST /api/v1/server-instances/{id}/logs/backfill`: accepts `LogBackfillRequest`, queues a `logs.backfill` job with source key, checkpoint ref, limit, and idempotency metadata, and keeps log bodies out of job results.
|
||||
|
||||
Runtime distribution and client-manager APIs require the current bearer session, server visibility, plugin-declared permissions, complete runtime bindings where required, and run endpoint capability support for run-side jobs. Responses and audit summaries expose artifact IDs, job IDs, checksums, key generations, fingerprints, status, and redacted `secret://runtime-keys/.../current` refs only. They do not expose raw run keys, client-manager keys, FTP passwords, database DSNs, RCON passwords, host paths, direct sockets, run endpoint private addresses, build workspace paths, or large inline logs.
|
||||
|
||||
## Implemented Run Control Actions
|
||||
|
||||
- `POST /api/v1/run/control/hello`: accept `RunControlHelloRequest`, create or update run endpoint metadata, and return `RunControlHelloResponse` with a platform-issued session token.
|
||||
@@ -191,7 +210,7 @@ These route groups remain documented future work beyond the currently implemente
|
||||
- Browser artifact upload, external artifact storage backends, presigned URLs, and production throttling policies.
|
||||
- Plugin page iframe packaging and remote hosting policies beyond SDK-mediated bridge contracts.
|
||||
- Live AI provider connectivity tests and remote model discovery.
|
||||
- Server restart/update/delete routes.
|
||||
- Server restart/delete routes beyond the currently implemented lifecycle, metadata update, and archive actions.
|
||||
|
||||
## Core Service Boundary
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/dto"
|
||||
)
|
||||
|
||||
func (h *coreHandlers) serverRuntimeActions(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
actions, err := h.core.GetServerRuntimeActionsForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ServerRuntimeActionsFromDomain(actions))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverRunGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.RunDistributionGenerateRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
distribution, err := h.core.GenerateRunDistributionForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, dto.RunDistributionFromDomain(distribution))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverRunDownload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
reference, err := h.core.OpenLatestRunDistributionDownloadForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ArtifactDownloadReferenceFromDomain(reference))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverRunKeyReset(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
key, err := h.core.ResetComponentKeyForSession(bearerToken(r), domain.ComponentKeyResetRequest{ServerInstanceID: r.PathValue("id"), ComponentKind: domain.DistributionComponentRun})
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ComponentKeyFromDomain(key))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverRunUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.RunUpdateRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
job, err := h.core.PushRunUpdateForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, dto.RunUpdateJobFromDomain(job))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverClientManagerGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.ClientManagerBuildRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
distribution, err := h.core.GenerateClientManagerDistributionForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, dto.ClientManagerDistributionFromDomain(distribution))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverClientManagerDownload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.ClientManagerDownloadRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
reference, err := h.core.OpenLatestClientManagerDistributionDownloadForSession(bearerToken(r), r.PathValue("id"), request.ProfileKey)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ArtifactDownloadReferenceFromDomain(reference))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverClientManagerKeyReset(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.ComponentKeyResetRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
reset := request.ToDomain(r.PathValue("id"))
|
||||
reset.ComponentKind = domain.DistributionComponentClientManager
|
||||
key, err := h.core.ResetComponentKeyForSession(bearerToken(r), reset)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ComponentKeyFromDomain(key))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverDependenciesCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.DependencyJobRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
job, err := h.core.QueueDependencyJobForSession(bearerToken(r), request.ToDomain(r.PathValue("id"), false))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, dto.JobFromDomain(job))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverDependenciesInstall(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.DependencyJobRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
job, err := h.core.QueueDependencyJobForSession(bearerToken(r), request.ToDomain(r.PathValue("id"), true))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, dto.JobFromDomain(job))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverLiveLogs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
instance, err := h.core.GetServerInstanceForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
streams, err := h.core.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.LogStreamListFromDomain(streams))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverLogsBackfill(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.LogBackfillRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
job, err := h.core.QueueLogBackfillForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, dto.JobFromDomain(job))
|
||||
}
|
||||
@@ -10,6 +10,11 @@ type RunCapabilityReport struct {
|
||||
type RunControlHello struct {
|
||||
RegistrationToken string
|
||||
RunEndpointID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ComponentKind DistributionComponentKind
|
||||
ComponentKey string
|
||||
KeyGeneration int
|
||||
DisplayName string
|
||||
Version string
|
||||
Status RunEndpointStatus
|
||||
|
||||
@@ -104,6 +104,56 @@ const (
|
||||
ArtifactStateFailed ArtifactState = "failed"
|
||||
)
|
||||
|
||||
type DistributionComponentKind string
|
||||
|
||||
const (
|
||||
DistributionComponentRun DistributionComponentKind = "run"
|
||||
DistributionComponentClientManager DistributionComponentKind = "client-manager"
|
||||
)
|
||||
|
||||
type ComponentKeyStatus string
|
||||
|
||||
const (
|
||||
ComponentKeyStatusActive ComponentKeyStatus = "active"
|
||||
ComponentKeyStatusRevoked ComponentKeyStatus = "revoked"
|
||||
)
|
||||
|
||||
type DistributionStatus string
|
||||
|
||||
const (
|
||||
DistributionStatusAvailable DistributionStatus = "available"
|
||||
DistributionStatusRevoked DistributionStatus = "revoked"
|
||||
DistributionStatusBuilding DistributionStatus = "building"
|
||||
DistributionStatusFailed DistributionStatus = "failed"
|
||||
)
|
||||
|
||||
type RuntimeBindingStatus string
|
||||
|
||||
const (
|
||||
RuntimeBindingStatusComplete RuntimeBindingStatus = "complete"
|
||||
RuntimeBindingStatusIncomplete RuntimeBindingStatus = "incomplete"
|
||||
)
|
||||
|
||||
type DependencyState string
|
||||
|
||||
const (
|
||||
DependencyStateUnknown DependencyState = "unknown"
|
||||
DependencyStatePresent DependencyState = "present"
|
||||
DependencyStateMissing DependencyState = "missing"
|
||||
DependencyStateInstalling DependencyState = "installing"
|
||||
DependencyStateFailed DependencyState = "failed"
|
||||
)
|
||||
|
||||
type DistributionJobStatus string
|
||||
|
||||
const (
|
||||
DistributionJobStatusQueued DistributionJobStatus = "queued"
|
||||
DistributionJobStatusRunning DistributionJobStatus = "running"
|
||||
DistributionJobStatusSucceeded DistributionJobStatus = "succeeded"
|
||||
DistributionJobStatusFailed DistributionJobStatus = "failed"
|
||||
DistributionJobStatusDenied DistributionJobStatus = "denied"
|
||||
)
|
||||
|
||||
type LogStreamSource string
|
||||
|
||||
const (
|
||||
@@ -208,11 +258,12 @@ type AIProviderModels struct {
|
||||
}
|
||||
|
||||
type PluginPermissions struct {
|
||||
AI bool
|
||||
Logs bool
|
||||
Files bool
|
||||
Jobs bool
|
||||
Artifacts bool
|
||||
AI bool
|
||||
Logs bool
|
||||
Files bool
|
||||
Jobs bool
|
||||
Artifacts bool
|
||||
RemoteAccess bool
|
||||
}
|
||||
|
||||
type PluginLifecycleActions struct {
|
||||
@@ -246,6 +297,14 @@ type GamePluginManifestAI struct {
|
||||
Purposes []string
|
||||
}
|
||||
|
||||
type GamePluginRemoteAccess struct {
|
||||
Methods []string
|
||||
RunCapabilities []string
|
||||
DatabaseEngines []string
|
||||
RCON bool
|
||||
LogTransfer bool
|
||||
}
|
||||
|
||||
type GamePluginManifest struct {
|
||||
ID string
|
||||
Name string
|
||||
@@ -260,6 +319,7 @@ type GamePluginManifest struct {
|
||||
Actions PluginLifecycleActions
|
||||
Pages []GamePluginPage
|
||||
AI GamePluginManifestAI
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
}
|
||||
|
||||
type GamePluginManifestRegistration struct {
|
||||
@@ -285,6 +345,7 @@ type GamePlugin struct {
|
||||
Pages []GamePluginPage
|
||||
Tags []string
|
||||
AIPurposes []string
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
ValidationViolations []string
|
||||
Status GamePluginStatus
|
||||
}
|
||||
@@ -307,6 +368,7 @@ type PluginMarketplacePlugin struct {
|
||||
Pages []GamePluginPage
|
||||
Tags []string
|
||||
AIPurposes []string
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
ValidationViolations []string
|
||||
Status GamePluginStatus
|
||||
Source string
|
||||
@@ -320,6 +382,11 @@ const (
|
||||
PluginBridgeActionLogsQuery PluginBridgeAction = "logs.query"
|
||||
PluginBridgeActionArtifactsOpen PluginBridgeAction = "artifacts.open"
|
||||
PluginBridgeActionFilesRequest PluginBridgeAction = "files.request"
|
||||
PluginBridgeActionRemoteAccessRequest PluginBridgeAction = "remote.access.request"
|
||||
PluginBridgeActionRunDistribution PluginBridgeAction = "run.distribution.request"
|
||||
PluginBridgeActionDependenciesRequest PluginBridgeAction = "dependencies.request"
|
||||
PluginBridgeActionLogsBackfillRequest PluginBridgeAction = "logs.backfill.request"
|
||||
PluginBridgeActionClientManager PluginBridgeAction = "client-manager.request"
|
||||
PluginBridgeActionAIInvoke PluginBridgeAction = "ai.invoke"
|
||||
)
|
||||
|
||||
@@ -383,6 +450,10 @@ type ServerInstance struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ServerInstanceUpdate struct {
|
||||
Name *string
|
||||
}
|
||||
|
||||
type PlatformResourceUsage struct {
|
||||
CPUPercent float64
|
||||
MemoryPercent float64
|
||||
@@ -493,9 +564,25 @@ type RunCapacity struct {
|
||||
}
|
||||
|
||||
const (
|
||||
JobCapabilityConfigWrite = "config.write"
|
||||
JobCapabilityFilesRead = "files.read"
|
||||
JobCapabilityFilesWrite = "files.write"
|
||||
JobCapabilityConfigWrite = "config.write"
|
||||
JobCapabilityFilesRead = "files.read"
|
||||
JobCapabilityFilesWrite = "files.write"
|
||||
JobCapabilityRemoteFTPRead = "remote.ftp.read"
|
||||
JobCapabilityRemoteFTPWrite = "remote.ftp.write"
|
||||
JobCapabilityRemoteRsyncRead = "remote.rsync.read"
|
||||
JobCapabilityRemoteRsyncWrite = "remote.rsync.write"
|
||||
JobCapabilityRemoteRunFilesRead = "remote.run.files.read"
|
||||
JobCapabilityRemoteRunFilesWrite = "remote.run.files.write"
|
||||
JobCapabilityRemoteRunProcessStart = "remote.run.process.start"
|
||||
JobCapabilityRemoteRunProcessStop = "remote.run.process.stop"
|
||||
JobCapabilityRemoteRunDBMySQLQuery = "remote.run.db.mysql.query"
|
||||
JobCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
|
||||
JobCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
|
||||
JobCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
|
||||
JobCapabilityRunSelfUpdate = "run.self-update"
|
||||
JobCapabilityDependenciesCheck = "dependencies.check"
|
||||
JobCapabilityDependenciesInstall = "dependencies.install"
|
||||
JobCapabilityLogsBackfill = "logs.backfill"
|
||||
)
|
||||
|
||||
type RunEndpoint struct {
|
||||
@@ -539,6 +626,197 @@ type Artifact struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type RuntimeBinding struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
Mode string
|
||||
Bindings map[string]string
|
||||
MissingKeys []string
|
||||
Status RuntimeBindingStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type EncryptedComponentKey struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
ComponentKind DistributionComponentKind
|
||||
ComponentKey string
|
||||
EncryptedKey string
|
||||
KeyHash string
|
||||
Fingerprint string
|
||||
SecretRef string
|
||||
Generation int
|
||||
Status ComponentKeyStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ResetAt time.Time
|
||||
}
|
||||
|
||||
type RunDistribution struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
RunEndpointID string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
PackageFormat string
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
KeyGeneration int
|
||||
SecretRef string
|
||||
Status DistributionStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerDistribution struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
RepositoryURL string
|
||||
SourceRevision string
|
||||
BuildJobID string
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
KeyGeneration int
|
||||
SecretRef string
|
||||
Status DistributionStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type DependencyStatus struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProbeKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
State DependencyState
|
||||
Required bool
|
||||
InstallPlanKey string
|
||||
Message string
|
||||
CheckedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerBuildJob struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
RepositoryURL string
|
||||
SourceRevision string
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
KeyGeneration int
|
||||
LogsRef string
|
||||
Status DistributionJobStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type RunUpdateJob struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
RunEndpointID string
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
JobID string
|
||||
IdempotencyKey string
|
||||
Status DistributionJobStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type RunDistributionGenerateRequest struct {
|
||||
ServerInstanceID string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ClientManagerBuildRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
RepositoryURL string
|
||||
SourceRevision string
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ComponentKeyResetRequest struct {
|
||||
ServerInstanceID string
|
||||
ComponentKind DistributionComponentKind
|
||||
ComponentKey string
|
||||
}
|
||||
|
||||
type ComponentAuthenticationRequest struct {
|
||||
ServerInstanceID string
|
||||
ComponentKind DistributionComponentKind
|
||||
ComponentKey string
|
||||
Generation int
|
||||
Key string
|
||||
}
|
||||
|
||||
type ComponentAuthenticationResult struct {
|
||||
ServerInstanceID string
|
||||
ComponentKind DistributionComponentKind
|
||||
ComponentKey string
|
||||
Generation int
|
||||
Allowed bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ServerRuntimeAction struct {
|
||||
Key string
|
||||
Label string
|
||||
Available bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ServerRuntimeActions struct {
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
RunEndpointID string
|
||||
RunStatus RunEndpointStatus
|
||||
Actions []ServerRuntimeAction
|
||||
}
|
||||
|
||||
type RunUpdateRequest struct {
|
||||
ServerInstanceID string
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type DependencyJobRequest struct {
|
||||
ServerInstanceID string
|
||||
ProbeKey string
|
||||
InstallPlanKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
IdempotencyKey string
|
||||
Install bool
|
||||
}
|
||||
|
||||
type LogBackfillRequest struct {
|
||||
ServerInstanceID string
|
||||
SourceKey string
|
||||
CheckpointRef string
|
||||
IdempotencyKey string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type LogStream struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
@@ -606,6 +884,51 @@ type ArtifactFilter struct {
|
||||
State ArtifactState
|
||||
}
|
||||
|
||||
type RuntimeBindingFilter struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
Status RuntimeBindingStatus
|
||||
}
|
||||
|
||||
type EncryptedComponentKeyFilter struct {
|
||||
ServerInstanceID string
|
||||
ComponentKind DistributionComponentKind
|
||||
ComponentKey string
|
||||
Status ComponentKeyStatus
|
||||
}
|
||||
|
||||
type RunDistributionFilter struct {
|
||||
ServerInstanceID string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
Status DistributionStatus
|
||||
}
|
||||
|
||||
type ClientManagerDistributionFilter struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
Status DistributionStatus
|
||||
}
|
||||
|
||||
type DependencyStatusFilter struct {
|
||||
ServerInstanceID string
|
||||
ProbeKey string
|
||||
State DependencyState
|
||||
}
|
||||
|
||||
type ClientManagerBuildJobFilter struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
Status DistributionJobStatus
|
||||
}
|
||||
|
||||
type RunUpdateJobFilter struct {
|
||||
ServerInstanceID string
|
||||
Status DistributionJobStatus
|
||||
}
|
||||
|
||||
type LogStreamFilter struct {
|
||||
ServerInstanceID string
|
||||
StreamKey string
|
||||
@@ -666,6 +989,7 @@ func CopyGamePlugin(plugin GamePlugin) GamePlugin {
|
||||
plugin.Pages = CopyGamePluginPageSlice(plugin.Pages)
|
||||
plugin.Tags = CopyStringSlice(plugin.Tags)
|
||||
plugin.AIPurposes = CopyStringSlice(plugin.AIPurposes)
|
||||
plugin.RemoteAccess = CopyGamePluginRemoteAccess(plugin.RemoteAccess)
|
||||
plugin.ValidationViolations = CopyStringSlice(plugin.ValidationViolations)
|
||||
return plugin
|
||||
}
|
||||
@@ -678,6 +1002,7 @@ func CopyPluginMarketplacePlugin(plugin PluginMarketplacePlugin) PluginMarketpla
|
||||
plugin.Pages = CopyGamePluginPageSlice(plugin.Pages)
|
||||
plugin.Tags = CopyStringSlice(plugin.Tags)
|
||||
plugin.AIPurposes = CopyStringSlice(plugin.AIPurposes)
|
||||
plugin.RemoteAccess = CopyGamePluginRemoteAccess(plugin.RemoteAccess)
|
||||
plugin.ValidationViolations = CopyStringSlice(plugin.ValidationViolations)
|
||||
return plugin
|
||||
}
|
||||
@@ -706,9 +1031,17 @@ func CopyGamePluginManifest(manifest GamePluginManifest) GamePluginManifest {
|
||||
manifest.Permissions = CopyStringSlice(manifest.Permissions)
|
||||
manifest.Pages = CopyGamePluginPageSlice(manifest.Pages)
|
||||
manifest.AI.Purposes = CopyStringSlice(manifest.AI.Purposes)
|
||||
manifest.RemoteAccess = CopyGamePluginRemoteAccess(manifest.RemoteAccess)
|
||||
return manifest
|
||||
}
|
||||
|
||||
func CopyGamePluginRemoteAccess(remote GamePluginRemoteAccess) GamePluginRemoteAccess {
|
||||
remote.Methods = CopyStringSlice(remote.Methods)
|
||||
remote.RunCapabilities = CopyStringSlice(remote.RunCapabilities)
|
||||
remote.DatabaseEngines = CopyStringSlice(remote.DatabaseEngines)
|
||||
return remote
|
||||
}
|
||||
|
||||
func CopyGamePluginPageSlice(pages []GamePluginPage) []GamePluginPage {
|
||||
if pages == nil {
|
||||
return nil
|
||||
@@ -807,6 +1140,86 @@ func CopyArtifact(artifact Artifact) Artifact {
|
||||
return artifact
|
||||
}
|
||||
|
||||
func CopyRuntimeBinding(binding RuntimeBinding) RuntimeBinding {
|
||||
binding.Bindings = CopyStringMap(binding.Bindings)
|
||||
binding.MissingKeys = CopyStringSlice(binding.MissingKeys)
|
||||
return binding
|
||||
}
|
||||
|
||||
func CopyEncryptedComponentKey(key EncryptedComponentKey) EncryptedComponentKey {
|
||||
return key
|
||||
}
|
||||
|
||||
func CopyRunDistribution(distribution RunDistribution) RunDistribution {
|
||||
return distribution
|
||||
}
|
||||
|
||||
func CopyClientManagerDistribution(distribution ClientManagerDistribution) ClientManagerDistribution {
|
||||
return distribution
|
||||
}
|
||||
|
||||
func CopyDependencyStatus(status DependencyStatus) DependencyStatus {
|
||||
return status
|
||||
}
|
||||
|
||||
func CopyClientManagerBuildJob(job ClientManagerBuildJob) ClientManagerBuildJob {
|
||||
return job
|
||||
}
|
||||
|
||||
func CopyRunUpdateJob(job RunUpdateJob) RunUpdateJob {
|
||||
return job
|
||||
}
|
||||
|
||||
func CopyRunDistributionGenerateRequest(request RunDistributionGenerateRequest) RunDistributionGenerateRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyClientManagerBuildRequest(request ClientManagerBuildRequest) ClientManagerBuildRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyComponentKeyResetRequest(request ComponentKeyResetRequest) ComponentKeyResetRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyComponentAuthenticationRequest(request ComponentAuthenticationRequest) ComponentAuthenticationRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyComponentAuthenticationResult(result ComponentAuthenticationResult) ComponentAuthenticationResult {
|
||||
return result
|
||||
}
|
||||
|
||||
func CopyServerRuntimeAction(action ServerRuntimeAction) ServerRuntimeAction {
|
||||
return action
|
||||
}
|
||||
|
||||
func CopyServerRuntimeActions(actions ServerRuntimeActions) ServerRuntimeActions {
|
||||
actions.Actions = CopyServerRuntimeActionSlice(actions.Actions)
|
||||
return actions
|
||||
}
|
||||
|
||||
func CopyServerRuntimeActionSlice(actions []ServerRuntimeAction) []ServerRuntimeAction {
|
||||
if actions == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]ServerRuntimeAction, len(actions))
|
||||
copy(out, actions)
|
||||
return out
|
||||
}
|
||||
|
||||
func CopyRunUpdateRequest(request RunUpdateRequest) RunUpdateRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyDependencyJobRequest(request DependencyJobRequest) DependencyJobRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyLogBackfillRequest(request LogBackfillRequest) LogBackfillRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyLogStream(stream LogStream) LogStream {
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -48,16 +48,21 @@ This file defines the first platform resource contracts. Concrete Go domain stru
|
||||
- `createFormSchemaRef`: create form schema reference.
|
||||
- `requiredRunCapabilities`: run capabilities required by this plugin.
|
||||
- `declaredPermissions`: scoped manifest permission keys used by plugin bridge and marketplace views.
|
||||
- `permissions`: aggregate platform ability declarations for AI, logs, files, jobs, and artifacts.
|
||||
- `permissions`: aggregate platform ability declarations for AI, logs, files, jobs, artifacts, and remote access.
|
||||
- `lifecycleActions`: manifest action contract references for install/start/stop and optional restart/status.
|
||||
- `pages`: plugin-local page metadata with scoped permission requirements.
|
||||
- `tags`: bounded catalog tags.
|
||||
- `aiPurposes`: platform-mediated AI purposes such as config suggestions or log diagnosis.
|
||||
- `remoteAccess`: plugin-declared remote access methods (`ftp`, `rsync`, `run`), run capabilities, database engines, RCON, and log transfer flags.
|
||||
- `validationViolations`: safe validation findings for invalid plugin records.
|
||||
- `status`: `installed`, `disabled`, `invalid`, or `updating`.
|
||||
|
||||
Manifest registration uses `GamePluginManifestRegistrationRequest` at `POST /api/v1/game-plugins/register-manifest`. Platform validation repeats plugin workspace safety checks and rejects raw host paths, direct run sockets, raw credentials, and raw AI/provider keys before metadata reaches the registry.
|
||||
|
||||
Remote access jobs are enabled only when both the selected run endpoint reports the capability and the server instance's installed plugin declares it. Plugin pages must use `remote.access.request` with `server.remote.access`; platform rejects undeclared database, RCON, log transfer, or remote file capabilities before creating jobs.
|
||||
|
||||
Runtime profile and distribution permissions are declared by plugins, then gated again by platform routes and services. `server.run.distribution` enables run package generation/download/reset/update operations, `server.dependencies.manage` enables dependency check/install jobs, and `server.client-manager.manage` enables plugin-declared companion client-manager generation/download/reset operations. Plugin metadata stores only declarations and safe refs; raw run/client-manager keys and transport credentials are stored through platform secret resources, never in plugin records.
|
||||
|
||||
## ServerInstance
|
||||
|
||||
- `id`: server instance ID.
|
||||
@@ -80,6 +85,46 @@ Manifest registration uses `GamePluginManifestRegistrationRequest` at `POST /api
|
||||
- `capacity`: current queue and resource summary.
|
||||
- `lastHeartbeatAt`: last control heartbeat time.
|
||||
|
||||
Run control hello can include server/component identity from a generated package config. When `serverInstanceId`, `pluginId`, `componentKind`, `componentKey`, and `keyGeneration` are present, platform authenticates the provided key against the current encrypted component key before issuing a session token. Stale generations after reset are rejected without returning raw key material.
|
||||
|
||||
## RuntimeBinding
|
||||
|
||||
- `id`: runtime binding ID.
|
||||
- `serverInstanceId`: server instance using the binding.
|
||||
- `pluginId`: installed plugin that declared the logical runtime profile.
|
||||
- `profileKey`: declared lifecycle/runtime profile key.
|
||||
- `mode`: runtime mode such as `local-process`, `hosted-ftp-rcon`, `ftp-only`, or `custom-client`.
|
||||
- `bindings`: logical binding keys to operator-provided settings.
|
||||
- `missingKeys`: logical keys that must be completed before dependent actions are available.
|
||||
- `status`: `complete`, `incomplete`, or `invalid`.
|
||||
|
||||
Bindings are used for action gating and run-side profile resolution. API responses and logs must use logical keys and safe reasons only; they must not expose raw host paths, direct sockets, FTP/RCON passwords, SQL DSNs, or component auth keys.
|
||||
|
||||
## Runtime Component Keys And Distributions
|
||||
|
||||
- `EncryptedComponentKey`: stores exactly one active encrypted key per server/component plus hash, fingerprint, redacted secret ref, generation, status, and reset time.
|
||||
- `RunDistribution`: records a generated run package for one server, target OS/architecture, package format, artifact ID, checksum, key generation, secret ref, and status.
|
||||
- `ClientManagerDistribution`: records a generated plugin-declared client-manager package with profile key, repository/source revision metadata, build job ID, artifact ID, checksum, key generation, secret ref, and status.
|
||||
- `ClientManagerBuildJob`: records source checkout/build status, target platform, artifact ID, checksum, redacted build log ref, key generation, and status.
|
||||
- `RunUpdateJob`: records platform-created run self-update orchestration with server, run endpoint, artifact ID, checksum, job ID, idempotency key, and status.
|
||||
|
||||
Run and client-manager keys are isolated singleton credentials. Reset replaces the encrypted database value, increments generation, marks older distributions revoked, and requires regenerating and redeploying that component. API DTOs may expose key generation, fingerprint, status, artifact ID, checksum, job ID, and `secret://runtime-keys/.../current` refs, but never the raw key.
|
||||
|
||||
## DependencyStatus
|
||||
|
||||
- `id`: dependency status ID.
|
||||
- `serverInstanceId`: server instance checked by run.
|
||||
- `pluginId`: plugin that declared the probe.
|
||||
- `probeKey`: logical dependency probe key.
|
||||
- `targetOs`, `targetArch`: target platform metadata.
|
||||
- `state`: dependency state such as present, missing, failed, or unknown.
|
||||
- `required`: whether the probe is required for the runtime profile.
|
||||
- `installPlanKey`: optional typed install plan key.
|
||||
- `message`: bounded safe status.
|
||||
- `checkedAt`, `updatedAt`: observation times.
|
||||
|
||||
Dependency checks and installs are queued as run jobs with logical `dependencies/...` or `dependencies/install/...` target keys. Install jobs must use typed plugin-declared plans and must not carry arbitrary shell snippets.
|
||||
|
||||
## Job
|
||||
|
||||
- `id`: job ID.
|
||||
@@ -96,6 +141,10 @@ Lifecycle workflow jobs use fixed capabilities:
|
||||
- `process.install`: dispatched by server create workflow and projects successful terminal results to `ready`.
|
||||
- `process.start`: dispatched by server start workflow and projects successful terminal results to `running`.
|
||||
- `process.stop`: dispatched by server stop workflow and projects successful terminal results to `stopped`.
|
||||
- `run.self-update`: dispatched by runtime distribution APIs with an approved artifact ref and checksum.
|
||||
- `dependencies.check`: dispatched by dependency check APIs for a declared probe key.
|
||||
- `dependencies.install`: dispatched by dependency install APIs for a declared typed install plan.
|
||||
- `logs.backfill`: dispatched by historical log APIs for a declared source key and checkpoint ref.
|
||||
|
||||
Failed or cancelled lifecycle jobs project the server instance to `failed`. Active start/stop jobs are visible through job metadata; this change does not add separate `starting` or `stopping` server states.
|
||||
|
||||
|
||||
+18
-8
@@ -12,14 +12,19 @@ type RunCapabilityReport struct {
|
||||
}
|
||||
|
||||
type RunControlHelloRequest struct {
|
||||
RegistrationToken string `json:"registrationToken"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Version string `json:"version"`
|
||||
Status domain.RunEndpointStatus `json:"status"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
CapabilityReport RunCapabilityReport `json:"capabilityReport"`
|
||||
Capacity RunCapacityResponse `json:"capacity"`
|
||||
RegistrationToken string `json:"registrationToken"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
ServerInstanceID string `json:"serverInstanceId,omitempty"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
ComponentKind domain.DistributionComponentKind `json:"componentKind,omitempty"`
|
||||
ComponentKey string `json:"componentKey,omitempty"`
|
||||
KeyGeneration int `json:"keyGeneration,omitempty"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Version string `json:"version"`
|
||||
Status domain.RunEndpointStatus `json:"status"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
CapabilityReport RunCapabilityReport `json:"capabilityReport"`
|
||||
Capacity RunCapacityResponse `json:"capacity"`
|
||||
}
|
||||
|
||||
type RunControlHelloResponse struct {
|
||||
@@ -52,6 +57,11 @@ func (request RunControlHelloRequest) ToDomain() domain.RunControlHello {
|
||||
return domain.RunControlHello{
|
||||
RegistrationToken: request.RegistrationToken,
|
||||
RunEndpointID: request.RunEndpointID,
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
PluginID: request.PluginID,
|
||||
ComponentKind: request.ComponentKind,
|
||||
ComponentKey: request.ComponentKey,
|
||||
KeyGeneration: request.KeyGeneration,
|
||||
DisplayName: request.DisplayName,
|
||||
Version: request.Version,
|
||||
Status: request.Status,
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type RunDistributionGenerateRequest struct {
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
}
|
||||
|
||||
type RunUpdateRequest struct {
|
||||
ArtifactID string `json:"artifactId"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
}
|
||||
|
||||
type DependencyJobRequest struct {
|
||||
ProbeKey string `json:"probeKey"`
|
||||
InstallPlanKey string `json:"installPlanKey,omitempty"`
|
||||
TargetOS string `json:"targetOs,omitempty"`
|
||||
TargetArch string `json:"targetArch,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
}
|
||||
|
||||
type LogBackfillRequest struct {
|
||||
SourceKey string `json:"sourceKey"`
|
||||
CheckpointRef string `json:"checkpointRef,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerDownloadRequest struct {
|
||||
ProfileKey string `json:"profileKey,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerBuildRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
RepositoryURL string `json:"repositoryUrl"`
|
||||
SourceRevision string `json:"sourceRevision,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
}
|
||||
|
||||
type ComponentKeyResetRequest struct {
|
||||
ComponentKind string `json:"componentKind"`
|
||||
ComponentKey string `json:"componentKey,omitempty"`
|
||||
}
|
||||
|
||||
type ComponentKeyResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
ComponentKind string `json:"componentKind"`
|
||||
ComponentKey string `json:"componentKey,omitempty"`
|
||||
SecretRef string `json:"secretRef"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Generation int `json:"generation"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ResetAt time.Time `json:"resetAt,omitempty"`
|
||||
}
|
||||
|
||||
type ServerRuntimeActionResponse struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type ServerRuntimeActionsResponse struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
RunStatus string `json:"runStatus"`
|
||||
Actions []ServerRuntimeActionResponse `json:"actions"`
|
||||
}
|
||||
|
||||
type RunDistributionResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
PackageFormat string `json:"packageFormat"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
Checksum string `json:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
SecretRef string `json:"secretRef"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ClientManagerDistributionResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
RepositoryURL string `json:"repositoryUrl"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
BuildJobID string `json:"buildJobId"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
Checksum string `json:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
SecretRef string `json:"secretRef"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type DependencyStatusResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProbeKey string `json:"probeKey"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
State string `json:"state"`
|
||||
Required bool `json:"required"`
|
||||
InstallPlanKey string `json:"installPlanKey,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ClientManagerBuildJobResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
RepositoryURL string `json:"repositoryUrl"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
ArtifactID string `json:"artifactId,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
KeyGeneration int `json:"keyGeneration,omitempty"`
|
||||
LogsRef string `json:"logsRef,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type RunUpdateJobResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
Checksum string `json:"checksum"`
|
||||
JobID string `json:"jobId,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (request RunDistributionGenerateRequest) ToDomain(serverInstanceID string) domain.RunDistributionGenerateRequest {
|
||||
return domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (request RunUpdateRequest) ToDomain(serverInstanceID string) domain.RunUpdateRequest {
|
||||
return domain.RunUpdateRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
ArtifactID: request.ArtifactID,
|
||||
Checksum: request.Checksum,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (request DependencyJobRequest) ToDomain(serverInstanceID string, install bool) domain.DependencyJobRequest {
|
||||
return domain.DependencyJobRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
ProbeKey: request.ProbeKey,
|
||||
InstallPlanKey: request.InstallPlanKey,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Install: install,
|
||||
}
|
||||
}
|
||||
|
||||
func (request LogBackfillRequest) ToDomain(serverInstanceID string) domain.LogBackfillRequest {
|
||||
return domain.LogBackfillRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
SourceKey: request.SourceKey,
|
||||
CheckpointRef: request.CheckpointRef,
|
||||
Limit: request.Limit,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (request ClientManagerBuildRequest) ToDomain(serverInstanceID string) domain.ClientManagerBuildRequest {
|
||||
return domain.ClientManagerBuildRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
SourceRevision: request.SourceRevision,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (request ComponentKeyResetRequest) ToDomain(serverInstanceID string) domain.ComponentKeyResetRequest {
|
||||
return domain.ComponentKeyResetRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
ComponentKind: domain.DistributionComponentKind(request.ComponentKind),
|
||||
ComponentKey: request.ComponentKey,
|
||||
}
|
||||
}
|
||||
|
||||
func ComponentKeyFromDomain(key domain.EncryptedComponentKey) ComponentKeyResponse {
|
||||
return ComponentKeyResponse{
|
||||
ID: key.ID,
|
||||
ServerInstanceID: key.ServerInstanceID,
|
||||
ComponentKind: string(key.ComponentKind),
|
||||
ComponentKey: key.ComponentKey,
|
||||
SecretRef: key.SecretRef,
|
||||
Fingerprint: key.Fingerprint,
|
||||
Generation: key.Generation,
|
||||
Status: string(key.Status),
|
||||
CreatedAt: key.CreatedAt,
|
||||
UpdatedAt: key.UpdatedAt,
|
||||
ResetAt: key.ResetAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ServerRuntimeActionsFromDomain(actions domain.ServerRuntimeActions) ServerRuntimeActionsResponse {
|
||||
actions = domain.CopyServerRuntimeActions(actions)
|
||||
items := make([]ServerRuntimeActionResponse, len(actions.Actions))
|
||||
for i, action := range actions.Actions {
|
||||
items[i] = ServerRuntimeActionResponse{Key: action.Key, Label: action.Label, Available: action.Available, Reason: action.Reason}
|
||||
}
|
||||
return ServerRuntimeActionsResponse{
|
||||
ServerInstanceID: actions.ServerInstanceID,
|
||||
PluginID: actions.PluginID,
|
||||
RunEndpointID: actions.RunEndpointID,
|
||||
RunStatus: string(actions.RunStatus),
|
||||
Actions: items,
|
||||
}
|
||||
}
|
||||
|
||||
func RunDistributionFromDomain(distribution domain.RunDistribution) RunDistributionResponse {
|
||||
return RunDistributionResponse{
|
||||
ID: distribution.ID,
|
||||
ServerInstanceID: distribution.ServerInstanceID,
|
||||
PluginID: distribution.PluginID,
|
||||
RunEndpointID: distribution.RunEndpointID,
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
PackageFormat: distribution.PackageFormat,
|
||||
ArtifactID: distribution.ArtifactID,
|
||||
Checksum: distribution.Checksum,
|
||||
KeyGeneration: distribution.KeyGeneration,
|
||||
SecretRef: distribution.SecretRef,
|
||||
Status: string(distribution.Status),
|
||||
CreatedAt: distribution.CreatedAt,
|
||||
UpdatedAt: distribution.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ClientManagerDistributionFromDomain(distribution domain.ClientManagerDistribution) ClientManagerDistributionResponse {
|
||||
return ClientManagerDistributionResponse{
|
||||
ID: distribution.ID,
|
||||
ServerInstanceID: distribution.ServerInstanceID,
|
||||
PluginID: distribution.PluginID,
|
||||
ProfileKey: distribution.ProfileKey,
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
RepositoryURL: distribution.RepositoryURL,
|
||||
SourceRevision: distribution.SourceRevision,
|
||||
BuildJobID: distribution.BuildJobID,
|
||||
ArtifactID: distribution.ArtifactID,
|
||||
Checksum: distribution.Checksum,
|
||||
KeyGeneration: distribution.KeyGeneration,
|
||||
SecretRef: distribution.SecretRef,
|
||||
Status: string(distribution.Status),
|
||||
CreatedAt: distribution.CreatedAt,
|
||||
UpdatedAt: distribution.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func DependencyStatusFromDomain(status domain.DependencyStatus) DependencyStatusResponse {
|
||||
return DependencyStatusResponse{
|
||||
ID: status.ID,
|
||||
ServerInstanceID: status.ServerInstanceID,
|
||||
PluginID: status.PluginID,
|
||||
ProbeKey: status.ProbeKey,
|
||||
TargetOS: status.TargetOS,
|
||||
TargetArch: status.TargetArch,
|
||||
State: string(status.State),
|
||||
Required: status.Required,
|
||||
InstallPlanKey: status.InstallPlanKey,
|
||||
Message: status.Message,
|
||||
CheckedAt: status.CheckedAt,
|
||||
UpdatedAt: status.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ClientManagerBuildJobFromDomain(job domain.ClientManagerBuildJob) ClientManagerBuildJobResponse {
|
||||
return ClientManagerBuildJobResponse{
|
||||
ID: job.ID,
|
||||
ServerInstanceID: job.ServerInstanceID,
|
||||
PluginID: job.PluginID,
|
||||
ProfileKey: job.ProfileKey,
|
||||
TargetOS: job.TargetOS,
|
||||
TargetArch: job.TargetArch,
|
||||
RepositoryURL: job.RepositoryURL,
|
||||
SourceRevision: job.SourceRevision,
|
||||
ArtifactID: job.ArtifactID,
|
||||
Checksum: job.Checksum,
|
||||
KeyGeneration: job.KeyGeneration,
|
||||
LogsRef: job.LogsRef,
|
||||
Status: string(job.Status),
|
||||
CreatedAt: job.CreatedAt,
|
||||
UpdatedAt: job.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func RunUpdateJobFromDomain(job domain.RunUpdateJob) RunUpdateJobResponse {
|
||||
return RunUpdateJobResponse{
|
||||
ID: job.ID,
|
||||
ServerInstanceID: job.ServerInstanceID,
|
||||
RunEndpointID: job.RunEndpointID,
|
||||
ArtifactID: job.ArtifactID,
|
||||
Checksum: job.Checksum,
|
||||
JobID: job.JobID,
|
||||
IdempotencyKey: job.IdempotencyKey,
|
||||
Status: string(job.Status),
|
||||
CreatedAt: job.CreatedAt,
|
||||
UpdatedAt: job.UpdatedAt,
|
||||
}
|
||||
}
|
||||
+63
-15
@@ -158,11 +158,12 @@ type AIProviderModelsResponse struct {
|
||||
}
|
||||
|
||||
type PluginPermissionsResponse struct {
|
||||
AI bool `json:"ai"`
|
||||
Logs bool `json:"logs"`
|
||||
Files bool `json:"files"`
|
||||
Jobs bool `json:"jobs"`
|
||||
Artifacts bool `json:"artifacts"`
|
||||
AI bool `json:"ai"`
|
||||
Logs bool `json:"logs"`
|
||||
Files bool `json:"files"`
|
||||
Jobs bool `json:"jobs"`
|
||||
Artifacts bool `json:"artifacts"`
|
||||
RemoteAccess bool `json:"remoteAccess"`
|
||||
}
|
||||
|
||||
type PluginLifecycleActionsBody struct {
|
||||
@@ -196,6 +197,14 @@ type GamePluginManifestAIBody struct {
|
||||
Purposes []string `json:"purposes,omitempty"`
|
||||
}
|
||||
|
||||
type GamePluginRemoteAccessBody struct {
|
||||
Methods []string `json:"methods,omitempty"`
|
||||
RunCapabilities []string `json:"runCapabilities,omitempty"`
|
||||
DatabaseEngines []string `json:"databaseEngines,omitempty"`
|
||||
RCON bool `json:"rcon,omitempty"`
|
||||
LogTransfer bool `json:"logTransfer,omitempty"`
|
||||
}
|
||||
|
||||
type GamePluginManifestBody struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -210,6 +219,7 @@ type GamePluginManifestBody struct {
|
||||
Actions PluginLifecycleActionsBody `json:"actions"`
|
||||
Pages []GamePluginPageBody `json:"pages,omitempty"`
|
||||
AI GamePluginManifestAIBody `json:"ai,omitempty"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
}
|
||||
|
||||
type GamePluginManifestRegistrationRequest struct {
|
||||
@@ -235,6 +245,7 @@ type GamePluginCreateRequest struct {
|
||||
Pages []GamePluginPageBody `json:"pages,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
AIPurposes []string `json:"aiPurposes,omitempty"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
}
|
||||
|
||||
@@ -256,6 +267,7 @@ type GamePluginResponse struct {
|
||||
Pages []GamePluginPageBody `json:"pages"`
|
||||
Tags []string `json:"tags"`
|
||||
AIPurposes []string `json:"aiPurposes"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
}
|
||||
@@ -283,6 +295,7 @@ type MarketplacePluginResponse struct {
|
||||
Pages []GamePluginPageBody `json:"pages"`
|
||||
Tags []string `json:"tags"`
|
||||
AIPurposes []string `json:"aiPurposes"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
Source string `json:"source"`
|
||||
@@ -353,6 +366,10 @@ type ServerInstanceCreateRequest struct {
|
||||
State domain.ServerInstanceState `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
type ServerInstanceUpdateRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type ServerMemberRequest struct {
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
@@ -764,6 +781,7 @@ func (request GamePluginManifestRegistrationRequest) ToDomain() domain.GamePlugi
|
||||
Actions: request.Manifest.Actions.ToDomain(),
|
||||
Pages: pagesToDomain(request.Manifest.Pages),
|
||||
AI: request.Manifest.AI.ToDomain(),
|
||||
RemoteAccess: request.Manifest.RemoteAccess.ToDomain(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -785,6 +803,16 @@ func (ai GamePluginManifestAIBody) ToDomain() domain.GamePluginManifestAI {
|
||||
return domain.GamePluginManifestAI{Purposes: domain.CopyStringSlice(ai.Purposes)}
|
||||
}
|
||||
|
||||
func (remote GamePluginRemoteAccessBody) ToDomain() domain.GamePluginRemoteAccess {
|
||||
return domain.GamePluginRemoteAccess{
|
||||
Methods: domain.CopyStringSlice(remote.Methods),
|
||||
RunCapabilities: domain.CopyStringSlice(remote.RunCapabilities),
|
||||
DatabaseEngines: domain.CopyStringSlice(remote.DatabaseEngines),
|
||||
RCON: remote.RCON,
|
||||
LogTransfer: remote.LogTransfer,
|
||||
}
|
||||
}
|
||||
|
||||
func (actions PluginLifecycleActionsBody) ToDomain() domain.PluginLifecycleActions {
|
||||
return domain.PluginLifecycleActions{
|
||||
Install: actions.Install,
|
||||
@@ -814,6 +842,7 @@ func (request GamePluginCreateRequest) ToDomain() domain.GamePlugin {
|
||||
Pages: pagesToDomain(request.Pages),
|
||||
Tags: domain.CopyStringSlice(request.Tags),
|
||||
AIPurposes: domain.CopyStringSlice(request.AIPurposes),
|
||||
RemoteAccess: request.RemoteAccess.ToDomain(),
|
||||
ValidationViolations: domain.CopyStringSlice(request.ValidationViolations),
|
||||
}
|
||||
}
|
||||
@@ -830,6 +859,10 @@ func (request ServerInstanceCreateRequest) ToDomain() domain.ServerInstance {
|
||||
}
|
||||
}
|
||||
|
||||
func (request ServerInstanceUpdateRequest) ToDomain() domain.ServerInstanceUpdate {
|
||||
return domain.ServerInstanceUpdate{Name: request.Name}
|
||||
}
|
||||
|
||||
func (request ServerConfigDiffPreviewRequest) ToDomain(serverInstanceID string) domain.ServerConfigDiffRequest {
|
||||
return domain.ServerConfigDiffRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
@@ -1045,6 +1078,7 @@ func GamePluginFromDomain(plugin domain.GamePlugin) GamePluginResponse {
|
||||
Pages: pagesFromDomain(plugin.Pages),
|
||||
Tags: plugin.Tags,
|
||||
AIPurposes: plugin.AIPurposes,
|
||||
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
}
|
||||
@@ -1132,6 +1166,7 @@ func MarketplacePluginFromDomain(plugin domain.PluginMarketplacePlugin) Marketpl
|
||||
Pages: pagesFromDomain(plugin.Pages),
|
||||
Tags: plugin.Tags,
|
||||
AIPurposes: plugin.AIPurposes,
|
||||
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
Source: plugin.Source,
|
||||
@@ -1403,21 +1438,34 @@ func AuditEventListFromDomain(events []domain.AuditEvent) AuditEventListResponse
|
||||
|
||||
func permissionsFromDomain(permissions domain.PluginPermissions) PluginPermissionsResponse {
|
||||
return PluginPermissionsResponse{
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
RemoteAccess: permissions.RemoteAccess,
|
||||
}
|
||||
}
|
||||
|
||||
func permissionsToDomain(permissions PluginPermissionsResponse) domain.PluginPermissions {
|
||||
return domain.PluginPermissions{
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
RemoteAccess: permissions.RemoteAccess,
|
||||
}
|
||||
}
|
||||
|
||||
func remoteAccessFromDomain(remote domain.GamePluginRemoteAccess) GamePluginRemoteAccessBody {
|
||||
remote = domain.CopyGamePluginRemoteAccess(remote)
|
||||
return GamePluginRemoteAccessBody{
|
||||
Methods: remote.Methods,
|
||||
RunCapabilities: remote.RunCapabilities,
|
||||
DatabaseEngines: remote.DatabaseEngines,
|
||||
RCON: remote.RCON,
|
||||
LogTransfer: remote.LogTransfer,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type RuntimeBinding struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
ProfileKey string `json:"profileKey" db:"profile_key"`
|
||||
Mode string `json:"mode" db:"mode"`
|
||||
Bindings map[string]string `json:"bindings" db:"bindings"`
|
||||
MissingKeys []string `json:"missingKeys" db:"missing_keys"`
|
||||
Status domain.RuntimeBindingStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (RuntimeBinding) TableName() string { return "runtime_bindings" }
|
||||
|
||||
type EncryptedComponentKey struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
ComponentKind domain.DistributionComponentKind `json:"componentKind" db:"component_kind"`
|
||||
ComponentKey string `json:"componentKey,omitempty" db:"component_key"`
|
||||
EncryptedKey string `json:"encryptedKey" db:"encrypted_key"`
|
||||
KeyHash string `json:"keyHash" db:"key_hash"`
|
||||
Fingerprint string `json:"fingerprint" db:"fingerprint"`
|
||||
SecretRef string `json:"secretRef" db:"secret_ref"`
|
||||
Generation int `json:"generation" db:"generation"`
|
||||
Status domain.ComponentKeyStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
ResetAt time.Time `json:"resetAt,omitempty" db:"reset_at"`
|
||||
}
|
||||
|
||||
func (EncryptedComponentKey) TableName() string { return "encrypted_component_keys" }
|
||||
|
||||
type RunDistribution struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
RunEndpointID string `json:"runEndpointId" db:"run_endpoint_id"`
|
||||
TargetOS string `json:"targetOs" db:"target_os"`
|
||||
TargetArch string `json:"targetArch" db:"target_arch"`
|
||||
PackageFormat string `json:"packageFormat" db:"package_format"`
|
||||
ArtifactID string `json:"artifactId" db:"artifact_id"`
|
||||
Checksum string `json:"checksum" db:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration" db:"key_generation"`
|
||||
SecretRef string `json:"secretRef" db:"secret_ref"`
|
||||
Status domain.DistributionStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (RunDistribution) TableName() string { return "run_distributions" }
|
||||
|
||||
type ClientManagerDistribution struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
ProfileKey string `json:"profileKey" db:"profile_key"`
|
||||
TargetOS string `json:"targetOs" db:"target_os"`
|
||||
TargetArch string `json:"targetArch" db:"target_arch"`
|
||||
RepositoryURL string `json:"repositoryUrl" db:"repository_url"`
|
||||
SourceRevision string `json:"sourceRevision" db:"source_revision"`
|
||||
BuildJobID string `json:"buildJobId" db:"build_job_id"`
|
||||
ArtifactID string `json:"artifactId" db:"artifact_id"`
|
||||
Checksum string `json:"checksum" db:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration" db:"key_generation"`
|
||||
SecretRef string `json:"secretRef" db:"secret_ref"`
|
||||
Status domain.DistributionStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (ClientManagerDistribution) TableName() string { return "client_manager_distributions" }
|
||||
|
||||
type DependencyStatus struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
ProbeKey string `json:"probeKey" db:"probe_key"`
|
||||
TargetOS string `json:"targetOs" db:"target_os"`
|
||||
TargetArch string `json:"targetArch" db:"target_arch"`
|
||||
State domain.DependencyState `json:"state" db:"state"`
|
||||
Required bool `json:"required" db:"required"`
|
||||
InstallPlanKey string `json:"installPlanKey,omitempty" db:"install_plan_key"`
|
||||
Message string `json:"message,omitempty" db:"message"`
|
||||
CheckedAt time.Time `json:"checkedAt" db:"checked_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (DependencyStatus) TableName() string { return "dependency_statuses" }
|
||||
|
||||
type ClientManagerBuildJob struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
ProfileKey string `json:"profileKey" db:"profile_key"`
|
||||
TargetOS string `json:"targetOs" db:"target_os"`
|
||||
TargetArch string `json:"targetArch" db:"target_arch"`
|
||||
RepositoryURL string `json:"repositoryUrl" db:"repository_url"`
|
||||
SourceRevision string `json:"sourceRevision" db:"source_revision"`
|
||||
ArtifactID string `json:"artifactId" db:"artifact_id"`
|
||||
Checksum string `json:"checksum" db:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration" db:"key_generation"`
|
||||
LogsRef string `json:"logsRef,omitempty" db:"logs_ref"`
|
||||
Status domain.DistributionJobStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (ClientManagerBuildJob) TableName() string { return "client_manager_build_jobs" }
|
||||
|
||||
type RunUpdateJob struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
RunEndpointID string `json:"runEndpointId" db:"run_endpoint_id"`
|
||||
ArtifactID string `json:"artifactId" db:"artifact_id"`
|
||||
Checksum string `json:"checksum" db:"checksum"`
|
||||
JobID string `json:"jobId" db:"job_id"`
|
||||
IdempotencyKey string `json:"idempotencyKey" db:"idempotency_key"`
|
||||
Status domain.DistributionJobStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (RunUpdateJob) TableName() string { return "run_update_jobs" }
|
||||
|
||||
func RuntimeBindingFromDomain(binding domain.RuntimeBinding) RuntimeBinding {
|
||||
binding = domain.CopyRuntimeBinding(binding)
|
||||
return RuntimeBinding{
|
||||
ID: binding.ID,
|
||||
ServerInstanceID: binding.ServerInstanceID,
|
||||
PluginID: binding.PluginID,
|
||||
ProfileKey: binding.ProfileKey,
|
||||
Mode: binding.Mode,
|
||||
Bindings: binding.Bindings,
|
||||
MissingKeys: binding.MissingKeys,
|
||||
Status: binding.Status,
|
||||
CreatedAt: binding.CreatedAt,
|
||||
UpdatedAt: binding.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (binding RuntimeBinding) ToDomain() domain.RuntimeBinding {
|
||||
return domain.RuntimeBinding{
|
||||
ID: binding.ID,
|
||||
ServerInstanceID: binding.ServerInstanceID,
|
||||
PluginID: binding.PluginID,
|
||||
ProfileKey: binding.ProfileKey,
|
||||
Mode: binding.Mode,
|
||||
Bindings: domain.CopyStringMap(binding.Bindings),
|
||||
MissingKeys: domain.CopyStringSlice(binding.MissingKeys),
|
||||
Status: binding.Status,
|
||||
CreatedAt: binding.CreatedAt,
|
||||
UpdatedAt: binding.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func EncryptedComponentKeyFromDomain(key domain.EncryptedComponentKey) EncryptedComponentKey {
|
||||
return EncryptedComponentKey(key)
|
||||
}
|
||||
|
||||
func (key EncryptedComponentKey) ToDomain() domain.EncryptedComponentKey {
|
||||
return domain.EncryptedComponentKey(key)
|
||||
}
|
||||
|
||||
func RunDistributionFromDomain(distribution domain.RunDistribution) RunDistribution {
|
||||
return RunDistribution(distribution)
|
||||
}
|
||||
|
||||
func (distribution RunDistribution) ToDomain() domain.RunDistribution {
|
||||
return domain.RunDistribution(distribution)
|
||||
}
|
||||
|
||||
func ClientManagerDistributionFromDomain(distribution domain.ClientManagerDistribution) ClientManagerDistribution {
|
||||
return ClientManagerDistribution(distribution)
|
||||
}
|
||||
|
||||
func (distribution ClientManagerDistribution) ToDomain() domain.ClientManagerDistribution {
|
||||
return domain.ClientManagerDistribution(distribution)
|
||||
}
|
||||
|
||||
func DependencyStatusFromDomain(status domain.DependencyStatus) DependencyStatus {
|
||||
return DependencyStatus(status)
|
||||
}
|
||||
|
||||
func (status DependencyStatus) ToDomain() domain.DependencyStatus {
|
||||
return domain.DependencyStatus(status)
|
||||
}
|
||||
|
||||
func ClientManagerBuildJobFromDomain(job domain.ClientManagerBuildJob) ClientManagerBuildJob {
|
||||
return ClientManagerBuildJob(job)
|
||||
}
|
||||
|
||||
func (job ClientManagerBuildJob) ToDomain() domain.ClientManagerBuildJob {
|
||||
return domain.ClientManagerBuildJob(job)
|
||||
}
|
||||
|
||||
func RunUpdateJobFromDomain(job domain.RunUpdateJob) RunUpdateJob {
|
||||
return RunUpdateJob(job)
|
||||
}
|
||||
|
||||
func (job RunUpdateJob) ToDomain() domain.RunUpdateJob {
|
||||
return domain.RunUpdateJob(job)
|
||||
}
|
||||
+52
-10
@@ -69,6 +69,21 @@ type PluginPermissions struct {
|
||||
Jobs bool `json:"jobs" db:"jobs"`
|
||||
// Artifacts allows artifact metadata and transfer references.
|
||||
Artifacts bool `json:"artifacts" db:"artifacts"`
|
||||
// RemoteAccess allows platform-mediated remote server operations.
|
||||
RemoteAccess bool `json:"remoteAccess" db:"remote_access"`
|
||||
}
|
||||
|
||||
type GamePluginRemoteAccess struct {
|
||||
// Methods lists declared remote access transports such as ftp, rsync, or run.
|
||||
Methods []string `json:"methods" db:"methods"`
|
||||
// RunCapabilities lists remote run job capabilities enabled by the plugin.
|
||||
RunCapabilities []string `json:"runCapabilities" db:"run_capabilities"`
|
||||
// DatabaseEngines lists database engines supported through run-mediated reads.
|
||||
DatabaseEngines []string `json:"databaseEngines" db:"database_engines"`
|
||||
// RCON indicates that platform-mediated RCON commands are supported.
|
||||
RCON bool `json:"rcon" db:"rcon"`
|
||||
// LogTransfer indicates that run-mediated log transfer is supported.
|
||||
LogTransfer bool `json:"logTransfer" db:"log_transfer"`
|
||||
}
|
||||
|
||||
type PluginLifecycleActions struct {
|
||||
@@ -128,6 +143,8 @@ type GamePlugin struct {
|
||||
Tags []string `json:"tags" db:"tags"`
|
||||
// AIPurposes stores platform-mediated AI usage purposes.
|
||||
AIPurposes []string `json:"aiPurposes" db:"ai_purposes"`
|
||||
// RemoteAccess stores plugin-declared remote access metadata.
|
||||
RemoteAccess GamePluginRemoteAccess `json:"remoteAccess" db:"remote_access"`
|
||||
// ValidationViolations stores safe validation findings for invalid plugins.
|
||||
ValidationViolations []string `json:"validationViolations" db:"validation_violations"`
|
||||
// Status is the plugin lifecycle status.
|
||||
@@ -377,6 +394,7 @@ func GamePluginFromDomain(plugin domain.GamePlugin) GamePlugin {
|
||||
Pages: pagesFromDomain(plugin.Pages),
|
||||
Tags: plugin.Tags,
|
||||
AIPurposes: plugin.AIPurposes,
|
||||
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
}
|
||||
@@ -400,6 +418,7 @@ func (plugin GamePlugin) ToDomain() domain.GamePlugin {
|
||||
Pages: pagesToDomain(plugin.Pages),
|
||||
Tags: domain.CopyStringSlice(plugin.Tags),
|
||||
AIPurposes: domain.CopyStringSlice(plugin.AIPurposes),
|
||||
RemoteAccess: plugin.RemoteAccess.ToDomain(),
|
||||
ValidationViolations: domain.CopyStringSlice(plugin.ValidationViolations),
|
||||
Status: plugin.Status,
|
||||
}
|
||||
@@ -459,21 +478,44 @@ func pagesFromDomain(pages []domain.GamePluginPage) []GamePluginPage {
|
||||
|
||||
func (permissions PluginPermissions) ToDomain() domain.PluginPermissions {
|
||||
return domain.PluginPermissions{
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
RemoteAccess: permissions.RemoteAccess,
|
||||
}
|
||||
}
|
||||
|
||||
func permissionsFromDomain(permissions domain.PluginPermissions) PluginPermissions {
|
||||
return PluginPermissions{
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
AI: permissions.AI,
|
||||
Logs: permissions.Logs,
|
||||
Files: permissions.Files,
|
||||
Jobs: permissions.Jobs,
|
||||
Artifacts: permissions.Artifacts,
|
||||
RemoteAccess: permissions.RemoteAccess,
|
||||
}
|
||||
}
|
||||
|
||||
func (remote GamePluginRemoteAccess) ToDomain() domain.GamePluginRemoteAccess {
|
||||
return domain.GamePluginRemoteAccess{
|
||||
Methods: domain.CopyStringSlice(remote.Methods),
|
||||
RunCapabilities: domain.CopyStringSlice(remote.RunCapabilities),
|
||||
DatabaseEngines: domain.CopyStringSlice(remote.DatabaseEngines),
|
||||
RCON: remote.RCON,
|
||||
LogTransfer: remote.LogTransfer,
|
||||
}
|
||||
}
|
||||
|
||||
func remoteAccessFromDomain(remote domain.GamePluginRemoteAccess) GamePluginRemoteAccess {
|
||||
remote = domain.CopyGamePluginRemoteAccess(remote)
|
||||
return GamePluginRemoteAccess{
|
||||
Methods: remote.Methods,
|
||||
RunCapabilities: remote.RunCapabilities,
|
||||
DatabaseEngines: remote.DatabaseEngines,
|
||||
RCON: remote.RCON,
|
||||
LogTransfer: remote.LogTransfer,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+161
-2
@@ -63,6 +63,55 @@ type ArtifactRepository interface {
|
||||
Update(domain.Artifact) error
|
||||
}
|
||||
|
||||
type RuntimeBindingRepository interface {
|
||||
Create(domain.RuntimeBinding) error
|
||||
Get(id string) (domain.RuntimeBinding, error)
|
||||
List(domain.RuntimeBindingFilter) ([]domain.RuntimeBinding, error)
|
||||
Update(domain.RuntimeBinding) error
|
||||
}
|
||||
|
||||
type EncryptedComponentKeyRepository interface {
|
||||
Create(domain.EncryptedComponentKey) error
|
||||
Get(id string) (domain.EncryptedComponentKey, error)
|
||||
List(domain.EncryptedComponentKeyFilter) ([]domain.EncryptedComponentKey, error)
|
||||
Update(domain.EncryptedComponentKey) error
|
||||
}
|
||||
|
||||
type RunDistributionRepository interface {
|
||||
Create(domain.RunDistribution) error
|
||||
Get(id string) (domain.RunDistribution, error)
|
||||
List(domain.RunDistributionFilter) ([]domain.RunDistribution, error)
|
||||
Update(domain.RunDistribution) error
|
||||
}
|
||||
|
||||
type ClientManagerDistributionRepository interface {
|
||||
Create(domain.ClientManagerDistribution) error
|
||||
Get(id string) (domain.ClientManagerDistribution, error)
|
||||
List(domain.ClientManagerDistributionFilter) ([]domain.ClientManagerDistribution, error)
|
||||
Update(domain.ClientManagerDistribution) error
|
||||
}
|
||||
|
||||
type DependencyStatusRepository interface {
|
||||
Create(domain.DependencyStatus) error
|
||||
Get(id string) (domain.DependencyStatus, error)
|
||||
List(domain.DependencyStatusFilter) ([]domain.DependencyStatus, error)
|
||||
Update(domain.DependencyStatus) error
|
||||
}
|
||||
|
||||
type ClientManagerBuildJobRepository interface {
|
||||
Create(domain.ClientManagerBuildJob) error
|
||||
Get(id string) (domain.ClientManagerBuildJob, error)
|
||||
List(domain.ClientManagerBuildJobFilter) ([]domain.ClientManagerBuildJob, error)
|
||||
Update(domain.ClientManagerBuildJob) error
|
||||
}
|
||||
|
||||
type RunUpdateJobRepository interface {
|
||||
Create(domain.RunUpdateJob) error
|
||||
Get(id string) (domain.RunUpdateJob, error)
|
||||
List(domain.RunUpdateJobFilter) ([]domain.RunUpdateJob, error)
|
||||
Update(domain.RunUpdateJob) error
|
||||
}
|
||||
|
||||
type LogStreamRepository interface {
|
||||
Create(domain.LogStream) error
|
||||
Get(id string) (domain.LogStream, error)
|
||||
@@ -85,6 +134,13 @@ type Store interface {
|
||||
RunEndpoints() RunEndpointRepository
|
||||
Jobs() JobRepository
|
||||
Artifacts() ArtifactRepository
|
||||
RuntimeBindings() RuntimeBindingRepository
|
||||
EncryptedComponentKeys() EncryptedComponentKeyRepository
|
||||
RunDistributions() RunDistributionRepository
|
||||
ClientManagerDistributions() ClientManagerDistributionRepository
|
||||
DependencyStatuses() DependencyStatusRepository
|
||||
ClientManagerBuildJobs() ClientManagerBuildJobRepository
|
||||
RunUpdateJobs() RunUpdateJobRepository
|
||||
LogStreams() LogStreamRepository
|
||||
AuditEvents() AuditEventRepository
|
||||
}
|
||||
@@ -97,6 +153,13 @@ type MemoryStore struct {
|
||||
runEndpoints *memoryRepository[domain.RunEndpoint, domain.RunEndpointFilter]
|
||||
jobs *memoryJobRepository
|
||||
artifacts *memoryRepository[domain.Artifact, domain.ArtifactFilter]
|
||||
runtimeBindings *memoryRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]
|
||||
componentKeys *memoryRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]
|
||||
runDists *memoryRepository[domain.RunDistribution, domain.RunDistributionFilter]
|
||||
clientDists *memoryRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]
|
||||
dependencies *memoryRepository[domain.DependencyStatus, domain.DependencyStatusFilter]
|
||||
buildJobs *memoryRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]
|
||||
updateJobs *memoryRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]
|
||||
logStreams *memoryRepository[domain.LogStream, domain.LogStreamFilter]
|
||||
auditEvents *memoryRepository[domain.AuditEvent, domain.AuditEventFilter]
|
||||
}
|
||||
@@ -134,6 +197,41 @@ func NewMemoryStore() *MemoryStore {
|
||||
domain.CopyArtifact,
|
||||
matchArtifact,
|
||||
),
|
||||
runtimeBindings: newMemoryRepository(
|
||||
func(binding domain.RuntimeBinding) string { return binding.ID },
|
||||
domain.CopyRuntimeBinding,
|
||||
matchRuntimeBinding,
|
||||
),
|
||||
componentKeys: newMemoryRepository(
|
||||
func(key domain.EncryptedComponentKey) string { return key.ID },
|
||||
domain.CopyEncryptedComponentKey,
|
||||
matchEncryptedComponentKey,
|
||||
),
|
||||
runDists: newMemoryRepository(
|
||||
func(distribution domain.RunDistribution) string { return distribution.ID },
|
||||
domain.CopyRunDistribution,
|
||||
matchRunDistribution,
|
||||
),
|
||||
clientDists: newMemoryRepository(
|
||||
func(distribution domain.ClientManagerDistribution) string { return distribution.ID },
|
||||
domain.CopyClientManagerDistribution,
|
||||
matchClientManagerDistribution,
|
||||
),
|
||||
dependencies: newMemoryRepository(
|
||||
func(status domain.DependencyStatus) string { return status.ID },
|
||||
domain.CopyDependencyStatus,
|
||||
matchDependencyStatus,
|
||||
),
|
||||
buildJobs: newMemoryRepository(
|
||||
func(job domain.ClientManagerBuildJob) string { return job.ID },
|
||||
domain.CopyClientManagerBuildJob,
|
||||
matchClientManagerBuildJob,
|
||||
),
|
||||
updateJobs: newMemoryRepository(
|
||||
func(job domain.RunUpdateJob) string { return job.ID },
|
||||
domain.CopyRunUpdateJob,
|
||||
matchRunUpdateJob,
|
||||
),
|
||||
logStreams: newMemoryRepository(
|
||||
func(stream domain.LogStream) string { return stream.ID },
|
||||
domain.CopyLogStream,
|
||||
@@ -154,8 +252,21 @@ func (store *MemoryStore) ServerInstances() ServerInstanceRepository { return st
|
||||
func (store *MemoryStore) RunEndpoints() RunEndpointRepository { return store.runEndpoints }
|
||||
func (store *MemoryStore) Jobs() JobRepository { return store.jobs }
|
||||
func (store *MemoryStore) Artifacts() ArtifactRepository { return store.artifacts }
|
||||
func (store *MemoryStore) LogStreams() LogStreamRepository { return store.logStreams }
|
||||
func (store *MemoryStore) AuditEvents() AuditEventRepository { return store.auditEvents }
|
||||
func (store *MemoryStore) RuntimeBindings() RuntimeBindingRepository { return store.runtimeBindings }
|
||||
func (store *MemoryStore) EncryptedComponentKeys() EncryptedComponentKeyRepository {
|
||||
return store.componentKeys
|
||||
}
|
||||
func (store *MemoryStore) RunDistributions() RunDistributionRepository { return store.runDists }
|
||||
func (store *MemoryStore) ClientManagerDistributions() ClientManagerDistributionRepository {
|
||||
return store.clientDists
|
||||
}
|
||||
func (store *MemoryStore) DependencyStatuses() DependencyStatusRepository { return store.dependencies }
|
||||
func (store *MemoryStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
|
||||
return store.buildJobs
|
||||
}
|
||||
func (store *MemoryStore) RunUpdateJobs() RunUpdateJobRepository { return store.updateJobs }
|
||||
func (store *MemoryStore) LogStreams() LogStreamRepository { return store.logStreams }
|
||||
func (store *MemoryStore) AuditEvents() AuditEventRepository { return store.auditEvents }
|
||||
|
||||
type memoryRepository[T any, F any] struct {
|
||||
mu sync.RWMutex
|
||||
@@ -271,6 +382,9 @@ func matchGamePlugin(plugin domain.GamePlugin, filter domain.GamePluginFilter) b
|
||||
}
|
||||
|
||||
func matchServerInstance(instance domain.ServerInstance, filter domain.ServerInstanceFilter) bool {
|
||||
if instance.State == domain.ServerInstanceStateDeleted && filter.State != domain.ServerInstanceStateDeleted {
|
||||
return false
|
||||
}
|
||||
return (filter.PluginID == "" || instance.PluginID == filter.PluginID) &&
|
||||
(filter.RunEndpointID == "" || instance.RunEndpointID == filter.RunEndpointID) &&
|
||||
(filter.State == "" || instance.State == filter.State) &&
|
||||
@@ -302,6 +416,51 @@ func matchArtifact(artifact domain.Artifact, filter domain.ArtifactFilter) bool
|
||||
(filter.State == "" || artifact.State == filter.State)
|
||||
}
|
||||
|
||||
func matchRuntimeBinding(binding domain.RuntimeBinding, filter domain.RuntimeBindingFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || binding.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProfileKey == "" || binding.ProfileKey == filter.ProfileKey) &&
|
||||
(filter.Status == "" || binding.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchEncryptedComponentKey(key domain.EncryptedComponentKey, filter domain.EncryptedComponentKeyFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || key.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ComponentKind == "" || key.ComponentKind == filter.ComponentKind) &&
|
||||
(filter.ComponentKey == "" || key.ComponentKey == filter.ComponentKey) &&
|
||||
(filter.Status == "" || key.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchRunDistribution(distribution domain.RunDistribution, filter domain.RunDistributionFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || distribution.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.TargetOS == "" || distribution.TargetOS == filter.TargetOS) &&
|
||||
(filter.TargetArch == "" || distribution.TargetArch == filter.TargetArch) &&
|
||||
(filter.Status == "" || distribution.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchClientManagerDistribution(distribution domain.ClientManagerDistribution, filter domain.ClientManagerDistributionFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || distribution.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProfileKey == "" || distribution.ProfileKey == filter.ProfileKey) &&
|
||||
(filter.TargetOS == "" || distribution.TargetOS == filter.TargetOS) &&
|
||||
(filter.TargetArch == "" || distribution.TargetArch == filter.TargetArch) &&
|
||||
(filter.Status == "" || distribution.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchDependencyStatus(status domain.DependencyStatus, filter domain.DependencyStatusFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || status.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProbeKey == "" || status.ProbeKey == filter.ProbeKey) &&
|
||||
(filter.State == "" || status.State == filter.State)
|
||||
}
|
||||
|
||||
func matchClientManagerBuildJob(job domain.ClientManagerBuildJob, filter domain.ClientManagerBuildJobFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || job.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProfileKey == "" || job.ProfileKey == filter.ProfileKey) &&
|
||||
(filter.Status == "" || job.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchRunUpdateJob(job domain.RunUpdateJob, filter domain.RunUpdateJobFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || job.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.Status == "" || job.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchLogStream(stream domain.LogStream, filter domain.LogStreamFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || stream.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.StreamKey == "" || stream.StreamKey == filter.StreamKey)
|
||||
|
||||
@@ -55,6 +55,9 @@ func (svc *CoreService) OpenArtifactDownloadForSession(sessionID string, request
|
||||
if err := validator.ValidateArtifactDownloadReference(reference); err != nil {
|
||||
return domain.ArtifactDownloadReference{}, err
|
||||
}
|
||||
if err := svc.auditArtifactDownload(sessionID, artifact); err != nil {
|
||||
return domain.ArtifactDownloadReference{}, err
|
||||
}
|
||||
return domain.CopyArtifactDownloadReference(reference), nil
|
||||
}
|
||||
|
||||
@@ -154,6 +157,10 @@ func (svc *CoreService) artifactPayload(artifactID string) ([]byte, error) {
|
||||
svc.artifactMu.Lock()
|
||||
defer svc.artifactMu.Unlock()
|
||||
|
||||
if payload, exists := svc.artifactPayloads[artifactID]; exists {
|
||||
return domain.CopyBytes(payload), nil
|
||||
}
|
||||
|
||||
sessions := make([]domain.ArtifactTransferSession, 0, len(svc.artifactTransfers))
|
||||
for _, session := range svc.artifactTransfers {
|
||||
if session.ArtifactID == artifactID && session.Completed {
|
||||
|
||||
@@ -19,6 +19,27 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
if err := validator.ValidateRunControlHello(hello); err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
if hasComponentAuthIdentity(hello) {
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: hello.ServerInstanceID,
|
||||
ComponentKind: hello.ComponentKind,
|
||||
ComponentKey: hello.ComponentKey,
|
||||
Generation: hello.KeyGeneration,
|
||||
Key: hello.RegistrationToken,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
if !auth.Allowed {
|
||||
return domain.CopyRunControlHelloResult(domain.RunControlHelloResult{
|
||||
Accepted: false,
|
||||
RunEndpointID: hello.RunEndpointID,
|
||||
ServerTime: svc.now(),
|
||||
HeartbeatIntervalSeconds: defaultHeartbeatIntervalSeconds,
|
||||
FeatureFlags: []string{"runtime-key.auth.denied"},
|
||||
}), nil
|
||||
}
|
||||
}
|
||||
|
||||
stamp := svc.now()
|
||||
endpoint := domain.RunEndpoint{
|
||||
@@ -60,6 +81,10 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
}), nil
|
||||
}
|
||||
|
||||
func hasComponentAuthIdentity(hello domain.RunControlHello) bool {
|
||||
return hello.ServerInstanceID != "" || hello.PluginID != "" || hello.ComponentKind != "" || hello.ComponentKey != "" || hello.KeyGeneration != 0
|
||||
}
|
||||
|
||||
func (svc *CoreService) AcceptRunHeartbeat(heartbeat domain.RunControlHeartbeat) (domain.RunControlHeartbeatResult, error) {
|
||||
heartbeat = domain.CopyRunControlHeartbeat(heartbeat)
|
||||
if err := validator.ValidateRunControlHeartbeat(heartbeat); err != nil {
|
||||
|
||||
@@ -134,6 +134,49 @@ func TestCoreServiceRejectsInvalidRunControlHello(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunHelloRejectsStalePackageKeyAfterReset(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "idem-control-auth",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate run distribution: %v", err)
|
||||
}
|
||||
pkg := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
||||
hello := validRunControlHello()
|
||||
hello.RunEndpointID = instance.RunEndpointID
|
||||
hello.RegistrationToken = pkg.AuthKey
|
||||
hello.ServerInstanceID = instance.ID
|
||||
hello.PluginID = instance.PluginID
|
||||
hello.ComponentKind = domain.DistributionComponentRun
|
||||
hello.KeyGeneration = pkg.KeyGeneration
|
||||
|
||||
result, err := svc.RegisterRunHello(hello)
|
||||
if err != nil {
|
||||
t.Fatalf("register current package hello: %v", err)
|
||||
}
|
||||
if !result.Accepted || result.SessionToken == "" {
|
||||
t.Fatalf("expected current package hello to be accepted, got %+v", result)
|
||||
}
|
||||
|
||||
if _, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
}); err != nil {
|
||||
t.Fatalf("reset run key: %v", err)
|
||||
}
|
||||
result, err = svc.RegisterRunHello(hello)
|
||||
if err != nil {
|
||||
t.Fatalf("register stale package hello: %v", err)
|
||||
}
|
||||
if result.Accepted || result.SessionToken != "" {
|
||||
t.Fatalf("expected stale package hello to be rejected, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRequestsCapabilityRefreshOnFingerprintDrift(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
hello, err := svc.RegisterRunHello(validRunControlHello())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "idem-run-generate",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate run distribution: %v", err)
|
||||
}
|
||||
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.Status != domain.DistributionStatusAvailable {
|
||||
t.Fatalf("unexpected run distribution: %+v", distribution)
|
||||
}
|
||||
|
||||
keys, err := svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
Status: domain.ComponentKeyStatusActive,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list component keys: %v", err)
|
||||
}
|
||||
if len(keys) != 1 || keys[0].Generation != 1 || !strings.HasPrefix(keys[0].EncryptedKey, "enc:v1:") {
|
||||
t.Fatalf("expected one active encrypted run key, got %+v", keys)
|
||||
}
|
||||
|
||||
config := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
||||
if config.AuthKey == "" || config.AuthKey == keys[0].EncryptedKey || strings.Contains(distribution.SecretRef, config.AuthKey) {
|
||||
t.Fatalf("run package key leaked through metadata or was not encrypted, config=%+v key=%+v distribution=%+v", config, keys[0], distribution)
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
Generation: config.KeyGeneration,
|
||||
Key: config.AuthKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("authenticate run: %v", err)
|
||||
}
|
||||
if !auth.Allowed {
|
||||
t.Fatalf("expected current run key to authenticate, got %+v", auth)
|
||||
}
|
||||
|
||||
second, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "idem-run-generate-second",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate second run distribution: %v", err)
|
||||
}
|
||||
if second.KeyGeneration != 1 || second.SecretRef != distribution.SecretRef {
|
||||
t.Fatalf("expected second package to reuse current singleton key, got first=%+v second=%+v", distribution, second)
|
||||
}
|
||||
keys, err = svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
Status: domain.ComponentKeyStatusActive,
|
||||
})
|
||||
if err != nil || len(keys) != 1 {
|
||||
t.Fatalf("expected one active key after second generation, keys=%+v err=%v", keys, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "idem-run-before-reset",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate run distribution: %v", err)
|
||||
}
|
||||
oldConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
||||
|
||||
reset, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("reset run key: %v", err)
|
||||
}
|
||||
if reset.Generation != 2 || reset.Status != domain.ComponentKeyStatusActive {
|
||||
t.Fatalf("expected reset key generation 2, got %+v", reset)
|
||||
}
|
||||
oldDistribution, err := svc.store.RunDistributions().Get(distribution.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get old distribution: %v", err)
|
||||
}
|
||||
if oldDistribution.Status != domain.DistributionStatusRevoked {
|
||||
t.Fatalf("expected old distribution revoked, got %+v", oldDistribution)
|
||||
}
|
||||
if _, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: distribution.ArtifactID}); err == nil || !strings.Contains(err.Error(), "available") {
|
||||
t.Fatalf("expected old artifact download to be unavailable, got %v", err)
|
||||
}
|
||||
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
Generation: oldConfig.KeyGeneration,
|
||||
Key: oldConfig.AuthKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("authenticate old key: %v", err)
|
||||
}
|
||||
if auth.Allowed || !strings.Contains(auth.Reason, "generation") {
|
||||
t.Fatalf("expected old package authentication denial, got %+v", auth)
|
||||
}
|
||||
|
||||
newDistribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "idem-run-after-reset",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate run distribution after reset: %v", err)
|
||||
}
|
||||
newConfig := readGeneratedPackageConfig(t, svc, session, newDistribution.ArtifactID)
|
||||
if newDistribution.KeyGeneration != 2 || newConfig.AuthKey == oldConfig.AuthKey {
|
||||
t.Fatalf("expected regenerated package with new generation/key, old=%+v new=%+v", oldConfig, newConfig)
|
||||
}
|
||||
auth, err = svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
Generation: newConfig.KeyGeneration,
|
||||
Key: newConfig.AuthKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("authenticate new key: %v", err)
|
||||
}
|
||||
if !auth.Allowed {
|
||||
t.Fatalf("expected regenerated package to authenticate, got %+v", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceBuildsClientManagerWithDistinctKeyAndAuditsSensitiveOperations(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
runDistribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "idem-run-for-client",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate run distribution: %v", err)
|
||||
}
|
||||
if _, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: runDistribution.ArtifactID}); err != nil {
|
||||
t.Fatalf("open run download: %v", err)
|
||||
}
|
||||
runConfig := readGeneratedPackageConfig(t, svc, session, runDistribution.ArtifactID)
|
||||
|
||||
clientDistribution, err := svc.GenerateClientManagerDistributionForSession(session, domain.ClientManagerBuildRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ProfileKey: "scum-client-manager",
|
||||
TargetOS: "windows",
|
||||
TargetArch: "amd64",
|
||||
RepositoryURL: "https://github.com/F88888/scum_client.git",
|
||||
SourceRevision: "main",
|
||||
IdempotencyKey: "idem-client-manager",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate client-manager distribution: %v", err)
|
||||
}
|
||||
clientConfig := readGeneratedPackageConfig(t, svc, session, clientDistribution.ArtifactID)
|
||||
if clientDistribution.KeyGeneration != 1 || clientDistribution.BuildJobID == "" || clientDistribution.Status != domain.DistributionStatusAvailable {
|
||||
t.Fatalf("unexpected client-manager distribution: %+v", clientDistribution)
|
||||
}
|
||||
if clientConfig.AuthKey == runConfig.AuthKey || clientDistribution.SecretRef == runDistribution.SecretRef {
|
||||
t.Fatalf("client-manager must use a distinct key/ref, run=%+v client=%+v", runConfig, clientConfig)
|
||||
}
|
||||
build, err := svc.store.ClientManagerBuildJobs().Get(clientDistribution.BuildJobID)
|
||||
if err != nil {
|
||||
t.Fatalf("get build job: %v", err)
|
||||
}
|
||||
if build.Status != domain.DistributionJobStatusSucceeded || build.RepositoryURL != "https://github.com/F88888/scum_client.git" || build.SourceRevision != "main" {
|
||||
t.Fatalf("unexpected build job: %+v", build)
|
||||
}
|
||||
if build.LogsRef == "" || !strings.HasPrefix(build.LogsRef, "artifact://") {
|
||||
t.Fatalf("expected redacted build log artifact ref, got %+v", build)
|
||||
}
|
||||
packagePayload := readClientManagerPackage(t, svc, session, clientDistribution.ArtifactID)
|
||||
if packagePayload.Checkout.CheckoutRef != "branch/main" || packagePayload.Config.AuthKey != clientConfig.AuthKey || packagePayload.KeyFingerprint == "" {
|
||||
t.Fatalf("expected package checkout metadata and injected config, got %+v", packagePayload)
|
||||
}
|
||||
if len(packagePayload.OutputArtifacts) == 0 || packagePayload.BuildLogRef != build.LogsRef {
|
||||
t.Fatalf("expected output artifacts and build log ref, got %+v build=%+v", packagePayload, build)
|
||||
}
|
||||
buildLog := readArtifactString(t, svc, session, strings.TrimPrefix(build.LogsRef, "artifact://"))
|
||||
for _, expected := range []string{"client-manager checkout prepared", "checkoutRef=branch/main", "dependencyCheck=typed build profile accepted", "configInjection=secret ref"} {
|
||||
if !strings.Contains(buildLog, expected) {
|
||||
t.Fatalf("expected build log to contain %q, got %q", expected, buildLog)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{runConfig.AuthKey, clientConfig.AuthKey, "password=", "unix://", "tcp://", "/Users/", "mysql://", "sqlite://"} {
|
||||
if strings.Contains(buildLog, forbidden) {
|
||||
t.Fatalf("build log leaked forbidden fragment %q: %s", forbidden, buildLog)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = svc.GenerateClientManagerDistributionForSession(session, domain.ClientManagerBuildRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ProfileKey: "scum-client-manager",
|
||||
TargetOS: "darwin",
|
||||
TargetArch: "amd64",
|
||||
RepositoryURL: "https://github.com/F88888/scum_client.git",
|
||||
IdempotencyKey: "idem-client-manager-denied",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "targetOs") {
|
||||
t.Fatalf("expected unsupported target denial, got %v", err)
|
||||
}
|
||||
|
||||
if _, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentClientManager,
|
||||
ComponentKey: "scum-client-manager",
|
||||
}); err != nil {
|
||||
t.Fatalf("reset client-manager key: %v", err)
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentClientManager,
|
||||
ComponentKey: "scum-client-manager",
|
||||
Generation: clientConfig.KeyGeneration,
|
||||
Key: clientConfig.AuthKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("authenticate old client key: %v", err)
|
||||
}
|
||||
if auth.Allowed {
|
||||
t.Fatalf("expected old client-manager key to be denied after reset, got %+v", auth)
|
||||
}
|
||||
|
||||
audits, err := svc.ListAuditEvents(domain.AuditEventFilter{ResourceID: instance.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("list audits: %v", err)
|
||||
}
|
||||
actions := map[string]bool{}
|
||||
for _, audit := range audits {
|
||||
actions[audit.Action] = true
|
||||
for _, forbidden := range []string{runConfig.AuthKey, clientConfig.AuthKey, "password=", "unix://", "/Users/"} {
|
||||
if strings.Contains(audit.Summary, forbidden) {
|
||||
t.Fatalf("audit leaked forbidden fragment %q in %+v", forbidden, audit)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, action := range []string{"run.generate", "run.download", "client-manager.build", "client-manager.build.denied", "runtime-key.reset"} {
|
||||
if !actions[action] {
|
||||
t.Fatalf("expected audit action %q in %+v", action, audits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newDistributionTestFixture(t *testing.T) (*CoreService, string, domain.ServerInstance) {
|
||||
t.Helper()
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
plugin.SupportedOS = []string{"linux", "windows"}
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions,
|
||||
"server.run.distribution",
|
||||
"server.client-manager.manage",
|
||||
"server.dependencies.manage",
|
||||
)
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities,
|
||||
domain.JobCapabilityRunSelfUpdate,
|
||||
domain.JobCapabilityDependenciesCheck,
|
||||
domain.JobCapabilityDependenciesInstall,
|
||||
domain.JobCapabilityLogsBackfill,
|
||||
)
|
||||
plugin.BridgeActions = append(plugin.BridgeActions,
|
||||
string(domain.PluginBridgeActionRunDistribution),
|
||||
string(domain.PluginBridgeActionClientManager),
|
||||
string(domain.PluginBridgeActionDependenciesRequest),
|
||||
string(domain.PluginBridgeActionLogsBackfillRequest),
|
||||
)
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin fixture: %v", err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities,
|
||||
domain.JobCapabilityRunSelfUpdate,
|
||||
domain.JobCapabilityDependenciesCheck,
|
||||
domain.JobCapabilityDependenciesInstall,
|
||||
domain.JobCapabilityLogsBackfill,
|
||||
)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("update endpoint fixture: %v", err)
|
||||
}
|
||||
session := createServiceUserAndLogin(t, svc, domain.User{
|
||||
ID: "user-distribution-owner",
|
||||
DisplayName: "Distribution Owner",
|
||||
Email: "distribution-owner@example.test",
|
||||
Roles: []string{"server-owner"},
|
||||
PasswordHash: "secret-password",
|
||||
})
|
||||
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
|
||||
ID: "server-distribution",
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: endpoint.ID,
|
||||
Name: "Distribution Server",
|
||||
State: domain.ServerInstanceStateReady,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create distribution server: %v", err)
|
||||
}
|
||||
return svc, session, instance
|
||||
}
|
||||
|
||||
func readGeneratedPackageConfig(t *testing.T, svc *CoreService, session string, artifactID string) generatedPackageConfig {
|
||||
t.Helper()
|
||||
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: artifactID})
|
||||
if err != nil {
|
||||
t.Fatalf("read artifact content: %v", err)
|
||||
}
|
||||
var config generatedPackageConfig
|
||||
if err := json.Unmarshal(content.Payload, &config); err != nil {
|
||||
t.Fatalf("unmarshal generated config: %v", err)
|
||||
}
|
||||
if config.AuthKey == "" {
|
||||
var packagePayload generatedClientManagerPackage
|
||||
if err := json.Unmarshal(content.Payload, &packagePayload); err != nil {
|
||||
t.Fatalf("unmarshal generated client-manager package: %v", err)
|
||||
}
|
||||
config = packagePayload.Config
|
||||
}
|
||||
if config.AuthKey == "" || config.SecretRef == "" || config.KeyGeneration <= 0 {
|
||||
t.Fatalf("generated package config is incomplete: %+v", config)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func readClientManagerPackage(t *testing.T, svc *CoreService, session string, artifactID string) generatedClientManagerPackage {
|
||||
t.Helper()
|
||||
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: artifactID})
|
||||
if err != nil {
|
||||
t.Fatalf("read client-manager package content: %v", err)
|
||||
}
|
||||
var packagePayload generatedClientManagerPackage
|
||||
if err := json.Unmarshal(content.Payload, &packagePayload); err != nil {
|
||||
t.Fatalf("unmarshal generated client-manager package: %v", err)
|
||||
}
|
||||
return packagePayload
|
||||
}
|
||||
|
||||
func readArtifactString(t *testing.T, svc *CoreService, session string, artifactID string) string {
|
||||
t.Helper()
|
||||
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: artifactID})
|
||||
if err != nil {
|
||||
t.Fatalf("read artifact content: %v", err)
|
||||
}
|
||||
return string(content.Payload)
|
||||
}
|
||||
|
||||
func TestCoreServiceDeniesRunDistributionWithoutPluginDeclaration(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
session := createServiceUserAndLogin(t, svc, domain.User{
|
||||
ID: "user-distribution-denied",
|
||||
DisplayName: "Distribution Denied",
|
||||
Email: "distribution-denied@example.test",
|
||||
Roles: []string{"server-owner"},
|
||||
PasswordHash: "secret-password",
|
||||
})
|
||||
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
|
||||
ID: "server-distribution-denied",
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: endpoint.ID,
|
||||
Name: "Distribution Denied Server",
|
||||
State: domain.ServerInstanceStateReady,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create denied server: %v", err)
|
||||
}
|
||||
_, err = svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "idem-run-denied",
|
||||
})
|
||||
if !errors.Is(err, ErrForbidden) {
|
||||
t.Fatalf("expected plugin declaration denial, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -66,11 +66,13 @@ type Core interface {
|
||||
StopServerInstanceForSession(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)
|
||||
ListServerInstances(domain.ServerInstanceFilter) ([]domain.ServerInstance, error)
|
||||
ListServerInstancesForSession(string, domain.ServerInstanceFilter) ([]domain.ServerInstance, error)
|
||||
ListServerAdministratorCandidates(string, string) ([]domain.User, error)
|
||||
AddServerAdministrator(string, string, string) (domain.ServerInstance, error)
|
||||
RemoveServerAdministrator(string, string, string) (domain.ServerInstance, error)
|
||||
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
|
||||
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
|
||||
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
|
||||
GetServerConfigForSession(string, string) (domain.ServerConfig, error)
|
||||
@@ -93,6 +95,16 @@ type Core interface {
|
||||
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)
|
||||
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)
|
||||
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
|
||||
QueueDependencyJobForSession(string, domain.DependencyJobRequest) (domain.Job, error)
|
||||
QueueLogBackfillForSession(string, domain.LogBackfillRequest) (domain.Job, error)
|
||||
OpenArtifactTransfer(domain.ArtifactTransferOpen) (domain.ArtifactTransferOpenResult, error)
|
||||
UploadArtifactChunk(domain.ArtifactChunkUpload) (domain.ArtifactChunkUploadResult, error)
|
||||
QueryArtifactTransferStatus(domain.ArtifactTransferStatusQuery) (domain.ArtifactTransferStatusResult, error)
|
||||
@@ -121,7 +133,10 @@ type CoreService struct {
|
||||
logStore LogBodyStore
|
||||
artifactMu sync.Mutex
|
||||
artifactTransfers map[string]domain.ArtifactTransferSession
|
||||
artifactPayloads map[string][]byte
|
||||
artifactTransferSeq uint64
|
||||
auditMu sync.Mutex
|
||||
auditSeq uint64
|
||||
aiProviderClient AIProviderClient
|
||||
}
|
||||
|
||||
@@ -151,6 +166,7 @@ func newCoreServiceWithLogStore(store repo.Store, logStore LogBodyStore, now fun
|
||||
jobLeases: map[string]domain.RunJobLease{},
|
||||
logStore: logStore,
|
||||
artifactTransfers: map[string]domain.ArtifactTransferSession{},
|
||||
artifactPayloads: map[string][]byte{},
|
||||
aiProviderClient: MockAIProviderClient{},
|
||||
}
|
||||
}
|
||||
@@ -523,6 +539,7 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
|
||||
Pages: manifest.Pages,
|
||||
Tags: manifest.Tags,
|
||||
AIPurposes: manifest.AI.Purposes,
|
||||
RemoteAccess: manifest.RemoteAccess,
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
}
|
||||
@@ -604,6 +621,16 @@ func (svc *CoreService) ExecutePluginBridgeAction(sessionID string, request doma
|
||||
base = svc.executeBridgeLogsQuery(base, instance, request.Payload)
|
||||
case domain.PluginBridgeActionFilesRequest:
|
||||
base = svc.executeBridgeFileRequest(sessionID, base, request)
|
||||
case domain.PluginBridgeActionRemoteAccessRequest:
|
||||
base = svc.executeBridgeRemoteAccessRequest(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)
|
||||
case domain.PluginBridgeActionLogsBackfillRequest:
|
||||
base = svc.executeBridgeLogsBackfillRequest(base, plugin, instance, request.Payload)
|
||||
case domain.PluginBridgeActionClientManager:
|
||||
base = svc.executeBridgeClientManager(sessionID, base, request)
|
||||
case domain.PluginBridgeActionArtifactsOpen:
|
||||
base = svc.executeBridgeArtifactOpen(sessionID, base, request)
|
||||
case domain.PluginBridgeActionAIInvoke:
|
||||
@@ -815,6 +842,146 @@ 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 {
|
||||
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) {
|
||||
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"},
|
||||
}
|
||||
created, err := svc.CreateJob(job)
|
||||
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,
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeRunDistribution(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
|
||||
distribution, err := svc.GenerateRunDistributionForSession(sessionID, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
TargetOS: defaultBridgeValue(request.Payload["targetOs"], "linux"),
|
||||
TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"),
|
||||
IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID),
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "ok"
|
||||
base.Result = map[string]string{
|
||||
"distributionId": distribution.ID,
|
||||
"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),
|
||||
})
|
||||
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),
|
||||
}
|
||||
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")
|
||||
capability := domain.JobCapabilityDependenciesCheck
|
||||
message := "dependency check queued"
|
||||
if action == "install" {
|
||||
capability = domain.JobCapabilityDependenciesInstall
|
||||
message = "dependency install queued"
|
||||
}
|
||||
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),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: capability,
|
||||
TargetKey: defaultBridgeValue(payload["probeKey"], "dependencies/default"),
|
||||
InputRef: payload["inputRef"],
|
||||
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
|
||||
Progress: domain.JobProgress{Percent: 0, Message: message},
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "queued"
|
||||
base.Result = map[string]string{"jobId": job.ID, "state": string(job.State), "capability": job.Capability, "targetKey": job.TargetKey}
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeLogsBackfillRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
if !containsString(plugin.RequiredRunCapabilities, domain.JobCapabilityLogsBackfill) {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "log backfill capability is not declared by plugin"}
|
||||
return base
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: jobIDFromParts("job-logs-backfill", base.RequestID, payload["sourceKey"]),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: domain.JobCapabilityLogsBackfill,
|
||||
TargetKey: defaultBridgeValue(payload["sourceKey"], "logs/default"),
|
||||
InputRef: payload["checkpointRef"],
|
||||
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "historical log backfill queued"},
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "queued"
|
||||
base.Result = map[string]string{"jobId": job.ID, "state": string(job.State), "capability": job.Capability, "sourceKey": job.TargetKey}
|
||||
return base
|
||||
}
|
||||
|
||||
func bridgeExecutionError(base domain.PluginBridgeExecuteResponse, err error) domain.PluginBridgeExecuteResponse {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "execution_failed", Message: safeBridgeReason(err.Error())}
|
||||
@@ -855,6 +1022,11 @@ func pluginPermissionsFromManifest(permissions []string) domain.PluginPermission
|
||||
aggregate.Jobs = true
|
||||
case "server.artifacts.read", "server.artifacts.write":
|
||||
aggregate.Artifacts = true
|
||||
case "server.remote.access":
|
||||
aggregate.RemoteAccess = true
|
||||
case "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage":
|
||||
aggregate.Jobs = true
|
||||
aggregate.Artifacts = true
|
||||
}
|
||||
}
|
||||
return aggregate
|
||||
@@ -962,6 +1134,7 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
|
||||
Pages: plugin.Pages,
|
||||
Tags: plugin.Tags,
|
||||
AIPurposes: plugin.AIPurposes,
|
||||
RemoteAccess: plugin.RemoteAccess,
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
Source: "platform-registry",
|
||||
@@ -1077,6 +1250,37 @@ func (svc *CoreService) GetServerInstanceForSession(sessionID string, id string)
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) UpdateServerInstanceForSession(sessionID string, id string, update domain.ServerInstanceUpdate) (domain.ServerInstance, error) {
|
||||
if err := validator.ValidateServerInstanceUpdate(update); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(id)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
|
||||
return domain.ServerInstance{}, ErrForbidden
|
||||
}
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return domain.ServerInstance{}, validationError("deleted server instances cannot be edited")
|
||||
}
|
||||
if update.Name != nil {
|
||||
instance.Name = *update.Name
|
||||
}
|
||||
instance.UpdatedAt = svc.now()
|
||||
if err := validator.ValidateStoredServerInstance(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListServerInstances(filter domain.ServerInstanceFilter) ([]domain.ServerInstance, error) {
|
||||
return svc.store.ServerInstances().List(filter)
|
||||
}
|
||||
@@ -1487,6 +1691,35 @@ func (svc *CoreService) RemoveServerAdministrator(sessionID string, serverInstan
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ArchiveServerInstanceForSession(sessionID string, serverInstanceID string) (domain.ServerInstance, error) {
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
|
||||
return domain.ServerInstance{}, ErrForbidden
|
||||
}
|
||||
if instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateInstalling {
|
||||
return domain.ServerInstance{}, validationError("running or installing server instances must be stopped before archive")
|
||||
}
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
instance.State = domain.ServerInstanceStateDeleted
|
||||
instance.UpdatedAt = svc.now()
|
||||
if err := validator.ValidateStoredServerInstance(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
|
||||
if job.State == "" {
|
||||
job.State = domain.JobStateQueued
|
||||
@@ -1522,7 +1755,11 @@ func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
|
||||
if err != nil {
|
||||
return domain.Job{}, fmt.Errorf("get server instance dependency: %w", err)
|
||||
}
|
||||
if err := validateJobServerTarget(job, instance); err != nil {
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.Job{}, fmt.Errorf("get server plugin dependency: %w", err)
|
||||
}
|
||||
if err := validateJobServerTarget(job, instance, plugin); err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
}
|
||||
@@ -1632,13 +1869,19 @@ func validateRunnableEndpoint(endpoint domain.RunEndpoint, capability string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateJobServerTarget(job domain.Job, instance domain.ServerInstance) error {
|
||||
func validateJobServerTarget(job domain.Job, instance domain.ServerInstance, plugin domain.GamePlugin) error {
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return validationError("server instance must not be deleted")
|
||||
}
|
||||
if instance.RunEndpointID != job.RunEndpointID {
|
||||
return validationError("job runEndpointId must match server instance")
|
||||
}
|
||||
if plugin.ID != instance.PluginID {
|
||||
return validationError("job plugin must match server instance")
|
||||
}
|
||||
if !containsString(plugin.RequiredRunCapabilities, job.Capability) {
|
||||
return validationError("plugin missing required capability: " + job.Capability)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -876,6 +876,105 @@ func TestCoreServiceAuthorizesPluginBridgeActions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRemoteAccessRequiresPluginDeclaration(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
registration := validPluginManifestRegistration()
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities,
|
||||
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer,
|
||||
domain.JobCapabilityRemoteRunRCONCommand,
|
||||
)
|
||||
registration.Manifest.Permissions = append(registration.Manifest.Permissions, "server.remote.access")
|
||||
registration.Manifest.Bridge.Actions = append(registration.Manifest.Bridge.Actions, string(domain.PluginBridgeActionRemoteAccessRequest))
|
||||
registration.Manifest.Pages = append(registration.Manifest.Pages, domain.GamePluginPage{
|
||||
Key: "remote",
|
||||
Title: "Remote",
|
||||
Path: "/remote",
|
||||
Permissions: []string{"server.remote.access"},
|
||||
BridgeActions: []string{string(domain.PluginBridgeActionRemoteAccessRequest)},
|
||||
})
|
||||
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{
|
||||
Methods: []string{"run"},
|
||||
RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand},
|
||||
DatabaseEngines: []string{"sqlite"},
|
||||
RCON: true,
|
||||
LogTransfer: true,
|
||||
}
|
||||
plugin, err := svc.RegisterGamePluginManifest(registration)
|
||||
if err != nil {
|
||||
t.Fatalf("register remote manifest: %v", err)
|
||||
}
|
||||
if !plugin.Permissions.RemoteAccess || !plugin.RemoteAccess.RCON || plugin.RemoteAccess.DatabaseEngines[0] != "sqlite" {
|
||||
t.Fatalf("expected remote access metadata from manifest, got %+v", plugin)
|
||||
}
|
||||
marketplace, err := svc.GetMarketplacePlugin(plugin.ID)
|
||||
if err != nil || !marketplace.RemoteAccess.LogTransfer || marketplace.RemoteAccess.Methods[0] != "run" {
|
||||
t.Fatalf("expected marketplace remote access projection, got %+v err=%v", marketplace, err)
|
||||
}
|
||||
|
||||
endpoint, err := svc.CreateRunEndpoint(domain.RunEndpoint{
|
||||
ID: "run-remote",
|
||||
DisplayName: "Remote Run",
|
||||
Version: "0.1.0",
|
||||
Capabilities: append([]string{"process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"}, plugin.RemoteAccess.RunCapabilities...),
|
||||
Capacity: domain.RunCapacity{MaxJobs: 2},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create remote endpoint: %v", err)
|
||||
}
|
||||
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
|
||||
ID: "user-remote-owner",
|
||||
DisplayName: "Remote Owner",
|
||||
Email: "remote-owner@example.test",
|
||||
Roles: []string{"server-owner"},
|
||||
PasswordHash: "secret-password",
|
||||
})
|
||||
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
|
||||
ID: "server-remote",
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: endpoint.ID,
|
||||
Name: "Remote Server",
|
||||
State: domain.ServerInstanceStateRunning,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create remote server: %v", err)
|
||||
}
|
||||
|
||||
queued, err := svc.ExecutePluginBridgeAction(ownerSession, domain.PluginBridgeExecuteRequest{
|
||||
RequestID: "remote-rcon-1",
|
||||
PluginID: plugin.ID,
|
||||
RouteKey: "remote",
|
||||
ServerInstanceID: instance.ID,
|
||||
Action: domain.PluginBridgeActionRemoteAccessRequest,
|
||||
Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunRCONCommand,
|
||||
"targetKey": "rcon/command",
|
||||
"inputRef": "input://server-remote/rcon/command/1",
|
||||
"idempotencyKey": "idem-remote-rcon",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute remote bridge action: %v", err)
|
||||
}
|
||||
if queued.Status != "queued" || queued.Result["capability"] != domain.JobCapabilityRemoteRunRCONCommand {
|
||||
t.Fatalf("expected queued remote bridge job, got %+v", queued)
|
||||
}
|
||||
|
||||
plainPlugin, plainEndpoint := createPluginAndRunEndpoint(t, svc)
|
||||
plainEndpoint.Capabilities = append(plainEndpoint.Capabilities, domain.JobCapabilityRemoteRunRCONCommand)
|
||||
if err := svc.store.RunEndpoints().Update(plainEndpoint); err != nil {
|
||||
t.Fatalf("extend plain endpoint: %v", err)
|
||||
}
|
||||
plainInstance, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-plain", PluginID: plainPlugin.ID, RunEndpointID: plainEndpoint.ID, Name: "Plain Server"})
|
||||
if err != nil {
|
||||
t.Fatalf("create plain server: %v", err)
|
||||
}
|
||||
_, err = svc.CreateJob(domain.Job{ID: "job-remote-denied", ServerInstanceID: plainInstance.ID, RunEndpointID: plainEndpoint.ID, Capability: domain.JobCapabilityRemoteRunRCONCommand, TargetKey: "rcon/command", InputRef: "input://plain/rcon/command/1", IdempotencyKey: "idem-denied"})
|
||||
if err == nil || !strings.Contains(err.Error(), "plugin missing required capability") {
|
||||
t.Fatalf("expected undeclared remote job denial, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRejectsDuplicateGamePluginManifest(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
registration := validPluginManifestRegistration()
|
||||
@@ -1034,7 +1133,7 @@ func createPluginAndRunEndpoint(t *testing.T, svc *CoreService) (domain.GamePlug
|
||||
ServerType: "scum",
|
||||
ManifestRef: "artifact://manifests/server.scum/1.0.0",
|
||||
CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0",
|
||||
RequiredRunCapabilities: []string{"process.install", "process.start", "process.stop", "logs.read"},
|
||||
RequiredRunCapabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "config.write", "files.read", "files.write"},
|
||||
DeclaredPermissions: []string{"server.files.read", "server.files.write"},
|
||||
LifecycleActions: domain.PluginLifecycleActions{
|
||||
Install: "actions/install.json",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user