Remove pre-1.0 audit and protected request scaffolding
This commit is contained in:
@@ -59,8 +59,7 @@ func platformAdminRequest(r *http.Request) bool {
|
||||
if path == "/api/v1/users" || strings.HasPrefix(path, "/api/v1/users/") ||
|
||||
path == "/api/v1/ai-providers" || strings.HasPrefix(path, "/api/v1/ai-providers/") ||
|
||||
path == "/api/v1/metrics/platform" ||
|
||||
path == "/api/v1/run/endpoints" || strings.HasPrefix(path, "/api/v1/run/endpoints/") ||
|
||||
path == "/api/v1/audit-events" || strings.HasPrefix(path, "/api/v1/audit-events/") {
|
||||
path == "/api/v1/run/endpoints" || strings.HasPrefix(path, "/api/v1/run/endpoints/") {
|
||||
return true
|
||||
}
|
||||
if r.Method != http.MethodGet && (path == "/api/v1/game-plugins" || strings.HasPrefix(path, "/api/v1/game-plugins/") || strings.Contains(path, "/plugin-marketplace/plugins/")) {
|
||||
|
||||
@@ -142,13 +142,12 @@ func TestRunHTTPEnvelopeRequiresValidSignatureAndRejectsReplay(t *testing.T) {
|
||||
assertErrorResponse(t, staleClaim, http.StatusUnauthorized, errorCodeUnauthorized)
|
||||
|
||||
privateUpdateBodies := map[string]any{
|
||||
"/api/v1/run/lifecycle/report": dto.RunLifecycleReportRequest{RunEndpointID: "run-local", SessionToken: token, ServerInstanceID: "server-signed", Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: dto.JobProgressBody{Percent: 100}, ExecutionResult: dto.RunJobExecutionResultBody{Kind: "process", ProcessState: "running"}},
|
||||
"/api/v1/run/jobs/dependency-input": dto.DependencyExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
|
||||
"/api/v1/run/jobs/protected-request-input": dto.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1, FencingToken: 1},
|
||||
"/api/v1/run/jobs/source-rcon-input": dto.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
|
||||
"/api/v1/run/jobs/update-input": dto.RunUpdateInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
|
||||
"/api/v1/run/jobs/update-chunk": dto.RunUpdateChunkRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1, Offset: 0, Length: 8},
|
||||
"/api/v1/run/jobs/update-health": dto.RunUpdateHealthRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1, Outcome: "succeeded", Version: "0.1.1"},
|
||||
"/api/v1/run/lifecycle/report": dto.RunLifecycleReportRequest{RunEndpointID: "run-local", SessionToken: token, ServerInstanceID: "server-signed", Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: dto.JobProgressBody{Percent: 100}, ExecutionResult: dto.RunJobExecutionResultBody{Kind: "process", ProcessState: "running"}},
|
||||
"/api/v1/run/jobs/dependency-input": dto.DependencyExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
|
||||
"/api/v1/run/jobs/source-rcon-input": dto.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
|
||||
"/api/v1/run/jobs/update-input": dto.RunUpdateInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
|
||||
"/api/v1/run/jobs/update-chunk": dto.RunUpdateChunkRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1, Offset: 0, Length: 8},
|
||||
"/api/v1/run/jobs/update-health": dto.RunUpdateHealthRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1, Outcome: "succeeded", Version: "0.1.1"},
|
||||
}
|
||||
nonce := 10
|
||||
for path, request := range privateUpdateBodies {
|
||||
|
||||
@@ -206,7 +206,7 @@ func (h *coreHandlers) serverClientManagerRevokeSession(w http.ResponseWriter, r
|
||||
|
||||
// serverClientManagerUninstall godoc
|
||||
// @Summary Safely uninstall a Client Manager
|
||||
// @Description Stops the supervised process and removes only the controlled installation workspace while retaining audit and distribution history.
|
||||
// @Description Stops the supervised process and removes the Client Manager installation workspace while retaining build and distribution records.
|
||||
// @Tags client-managers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestRunLifecycleReportAPIProjectsServerState(t *testing.T) {
|
||||
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, helloRequest))
|
||||
server := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-lifecycle-report-api", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Lifecycle Report API", State: domain.ServerInstanceStateReady}, adminSession)
|
||||
|
||||
recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/lifecycle/report", dto.RunLifecycleReportRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, ServerInstanceID: server.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: dto.JobProgressBody{Percent: 100, Message: "autonomous start complete"}, Message: "autonomous start complete", ExecutionResult: dto.RunJobExecutionResultBody{Kind: "process", ProcessState: "running", AuditSummary: "private supervised process identity"}})
|
||||
recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/lifecycle/report", dto.RunLifecycleReportRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, ServerInstanceID: server.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: dto.JobProgressBody{Percent: 100, Message: "autonomous start complete"}, Message: "autonomous start complete", ExecutionResult: dto.RunJobExecutionResultBody{Kind: "process", ProcessState: "running", Summary: "private supervised process identity"}})
|
||||
assertStatus(t, recorder, http.StatusOK)
|
||||
response := decodeBody[dto.RunLifecycleReportResponse](t, recorder)
|
||||
if !response.Accepted || response.ProjectedState != domain.ServerInstanceStateRunning {
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestGameClientBridgeOperatorRoutes(t *testing.T) {
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall, domain.JobCapabilityRemoteRunDBSQLiteQuery)
|
||||
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "sqlite-db", Kind: "sqlite", TargetKey: "db/sqlite", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}}}
|
||||
plugin.RuntimeProfiles.ClientManagers = []domain.RuntimeClientManagerProfile{{Key: "scum-client", DisplayName: "SCUM Client", Version: "1.0.0", RepositoryURL: "https://github.com/example/scum-client.git", RevisionPolicy: "pinned", Revision: "0123456789abcdef", SupportedTargets: []domain.RuntimeTarget{{OS: "linux", Arch: "amd64"}}, BuildSystem: "go", EntryRef: "main.go", OutputArtifacts: []string{"scum-client"}, Deployment: domain.RuntimeClientManagerDeployment{Mode: "run-supervised", ExecutableRef: "scum-client", RequiredRunCapabilities: []string{domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall}}, Lifecycle: domain.RuntimeClientManagerLifecycle{Actions: []string{"start", "stop", "restart", "status", "update", "rollback", "uninstall"}, StartupTimeoutSeconds: 60, StopTimeoutSeconds: 30}, Health: domain.RuntimeClientManagerHealth{Mode: "component-heartbeat", IntervalSeconds: 15, DegradedAfterSeconds: 45, OfflineAfterSeconds: 120, RequiredCapabilities: []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"}}, Compatibility: domain.RuntimeClientManagerCompatibility{MinimumVersion: "1.0.0"}, UpdatePolicy: domain.RuntimeClientManagerUpdatePolicy{Strategy: "manual-staged", RequireApproval: true, HealthConfirmationSeconds: 60, RetainPrevious: true}}}
|
||||
plugin.GameClientBridge = domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "announcement.send", Title: "Send announcement", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, PayloadSchemaRef: "schemas/bridge/announcement.schema.json", ResultSchemaRef: "schemas/bridge/announcement-result.schema.json", TimeoutSeconds: 3600, MaxPayloadBytes: 4096}}, QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{{Key: "player.lookup", Title: "Player lookup", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "sqlite-db", TargetKey: "db/sqlite", ParameterSchemaRef: "schemas/bridge/query/player-lookup.parameters.schema.json", ResultSchemaRef: "schemas/bridge/query/player-lookup.result.schema.json", MaxRows: 50, TimeoutSeconds: 10}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}
|
||||
plugin.GameClientBridge = domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", Title: "Diagnostic ping", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelNone, PayloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", ResultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", TimeoutSeconds: 3600, MaxPayloadBytes: 4096}}, QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{{Key: "player.lookup", Title: "Player lookup", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "sqlite-db", TargetKey: "db/sqlite", ParameterSchemaRef: "schemas/bridge/query/player-lookup.parameters.schema.json", ResultSchemaRef: "schemas/bridge/query/player-lookup.result.schema.json", MaxRows: 50, TimeoutSeconds: 10}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}
|
||||
if _, err := coreService.CreateGamePlugin(plugin); err != nil {
|
||||
t.Fatalf("create bridge plugin: %v", err)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func TestGameClientBridgeOperatorRoutes(t *testing.T) {
|
||||
t.Fatalf("bridge status did not safely expose query template availability: %#v", status)
|
||||
}
|
||||
|
||||
queue := dto.GameClientBridgeQueueRequest{ProfileKey: "scum-client", CommandType: "announcement.send", Payload: map[string]any{"message": "hello"}, IdempotencyKey: "announce-1", ExpiresAt: time.Now().UTC().Add(time.Hour)}
|
||||
queue := dto.GameClientBridgeQueueRequest{ProfileKey: "scum-client", CommandType: "diagnostic.ping", Payload: map[string]any{"message": "hello"}, IdempotencyKey: "diag-1", ExpiresAt: time.Now().UTC().Add(time.Hour)}
|
||||
queuedRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/server-bridge/game-client-bridge/commands", queue, adminSession)
|
||||
assertStatus(t, queuedRecorder, http.StatusAccepted)
|
||||
queued := decodeBody[dto.GameClientBridgeCommandResponse](t, queuedRecorder)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"browser.local/platform/dto"
|
||||
)
|
||||
|
||||
// runProtectedRequestInput godoc
|
||||
// @Summary Read one protected request for the active Run lease
|
||||
// @Description Returns approved SQL, RCON, or management-program text exactly once to its signed, fenced Run lease. Browser and plugin clients never receive this payload.
|
||||
// @Tags run-job-channel
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body dto.ProtectedRequestExecutionInputRequest true "Fenced protected request input request"
|
||||
// @Success 200 {object} dto.ProtectedRequestExecutionInputResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/run/jobs/protected-request-input [post]
|
||||
func (h *coreHandlers) runProtectedRequestInput(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.ProtectedRequestExecutionInputRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
input, err := h.core.GetProtectedRequestExecutionInput(request.ToDomain())
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ProtectedRequestExecutionInputFromDomain(input))
|
||||
}
|
||||
@@ -47,12 +47,6 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/game-plugins/{id}", h.gamePluginDetail)
|
||||
mux.HandleFunc("/api/v1/metrics/platform", h.platformMetrics)
|
||||
mux.HandleFunc("/api/v1/metrics/server-instances", h.serverInstanceMetrics)
|
||||
mux.HandleFunc("/api/v1/production/capacity", h.productionCapacity)
|
||||
mux.HandleFunc("/api/v1/production/capacity/admission", h.productionCapacityAdmission)
|
||||
mux.HandleFunc("/api/v1/alerts", h.alerts)
|
||||
mux.HandleFunc("/api/v1/alerts/{id}/acknowledge", h.alertAcknowledge)
|
||||
mux.HandleFunc("/api/v1/alerts/{id}/resolve", h.alertResolve)
|
||||
mux.HandleFunc("/api/v1/alerts/{id}/retry", h.alertRetry)
|
||||
mux.HandleFunc("/api/v1/plugin-lifecycles", h.pluginLifecycles)
|
||||
mux.HandleFunc("/api/v1/plugin-lifecycles/{pluginId}/actions", h.pluginLifecycleAction)
|
||||
mux.HandleFunc("/api/v1/ai/config-diffs", h.aiConfigDiffs)
|
||||
@@ -98,6 +92,7 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies/install", h.serverDependenciesInstall)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies", h.serverDependencies)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/logs/events", h.serverLogEvents)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/rcon/commands", h.sourceRCONCommands)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/administrators/candidates", h.serverAdministratorCandidates)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/administrators", h.serverAdministrators)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/administrators/{userId}", h.serverAdministratorDetail)
|
||||
@@ -112,7 +107,6 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/run/jobs/build-input", h.requireRunSignature(h.runJobBuildInput))
|
||||
mux.HandleFunc("/api/v1/run/jobs/dependency-input", h.requireRunSignature(h.runJobDependencyInput))
|
||||
mux.HandleFunc("/api/v1/run/jobs/source-rcon-input", h.requireRunSignature(h.runSourceRCONInput))
|
||||
mux.HandleFunc("/api/v1/run/jobs/protected-request-input", h.requireRunSignature(h.runProtectedRequestInput))
|
||||
mux.HandleFunc("/api/v1/run/jobs/update-input", h.requireRunSignature(h.runJobUpdateInput))
|
||||
mux.HandleFunc("/api/v1/run/jobs/update-chunk", h.requireRunSignature(h.runJobUpdateChunk))
|
||||
mux.HandleFunc("/api/v1/run/jobs/update-health", h.requireRunSignature(h.runJobUpdateHealth))
|
||||
@@ -139,8 +133,6 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/log-streams", h.logStreams)
|
||||
mux.HandleFunc("/api/v1/log-streams/query", h.logStreamQuery)
|
||||
mux.HandleFunc("/api/v1/log-streams/{id}", h.logStreamDetail)
|
||||
mux.HandleFunc("/api/v1/audit-events", h.auditEvents)
|
||||
mux.HandleFunc("/api/v1/audit-events/{id}", h.auditEventDetail)
|
||||
mux.HandleFunc("/api/v1/client-managers/register", h.clientManagerRegister)
|
||||
mux.HandleFunc("/api/v1/client-managers/heartbeat", h.clientManagerHeartbeat)
|
||||
mux.HandleFunc("/api/v1/game-client-bridge/companion/commands/claim", h.gameClientBridgeCompanionClaim)
|
||||
@@ -150,180 +142,9 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/game-client-bridge/companion/diagnostics", h.gameClientBridgeCompanionDiagnostics)
|
||||
}
|
||||
|
||||
// productionCapacity godoc
|
||||
// @Summary Get production capacity governance state
|
||||
// @Description Returns bounded Run endpoint capacity and durable pressure counts visible to the current operator.
|
||||
// @Tags production-operations
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ProductionCapacitySummaryResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/production/capacity [get]
|
||||
func (h *coreHandlers) productionCapacity(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
summary, err := h.core.GetProductionCapacityForSession(bearerToken(r))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.ProductionCapacityFromDomain(summary))
|
||||
}
|
||||
|
||||
// productionCapacityAdmission godoc
|
||||
// @Summary Check production capacity admission
|
||||
// @Description Evaluates endpoint heartbeat, capability, durable jobs, and bounded backlog pressure without dispatching work.
|
||||
// @Tags production-operations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body dto.CapacityAdmissionRequest true "Capacity admission request"
|
||||
// @Success 200 {object} dto.CapacityAdmissionDecisionResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 403 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/production/capacity/admission [post]
|
||||
func (h *coreHandlers) productionCapacityAdmission(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.CapacityAdmissionRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
decision, err := h.core.CheckCapacityAdmissionForSession(bearerToken(r), request.ToDomain())
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.CapacityDecisionFromDomain(decision))
|
||||
}
|
||||
|
||||
// alerts godoc
|
||||
// @Summary List durable production alerts
|
||||
// @Description Lists alerts visible to the current operator with optional safe state/source/severity filters.
|
||||
// @Tags production-operations
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.AlertListResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/alerts [get]
|
||||
func (h *coreHandlers) alerts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
alerts, err := h.core.ListAlertsForSession(bearerToken(r), domain.AlertFilter{State: domain.AlertState(r.URL.Query().Get("state")), SourceKind: r.URL.Query().Get("sourceKind"), SourceID: r.URL.Query().Get("sourceId"), Severity: domain.AlertSeverity(r.URL.Query().Get("severity"))})
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.AlertListFromDomain(alerts))
|
||||
}
|
||||
|
||||
// alertAcknowledge godoc
|
||||
// @Summary Acknowledge a durable alert
|
||||
// @Description Persists acknowledgement actor, timestamp, and linked audit evidence for one alert.
|
||||
// @Tags production-operations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Alert ID"
|
||||
// @Param body body dto.AlertAcknowledgeRequest true "Acknowledgement request"
|
||||
// @Success 200 {object} dto.AlertResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 403 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/alerts/{id}/acknowledge [post]
|
||||
func (h *coreHandlers) alertAcknowledge(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.AlertAcknowledgeRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
alert, err := h.core.AcknowledgeAlertForSession(bearerToken(r), domain.AlertAcknowledgeRequest{AlertID: r.PathValue("id"), Note: request.Note})
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.AlertFromDomain(alert))
|
||||
}
|
||||
|
||||
// alertResolve godoc
|
||||
// @Summary Resolve a durable alert
|
||||
// @Description Resolves one alert with a safe operator note and linked audit evidence.
|
||||
// @Tags production-operations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Alert ID"
|
||||
// @Param body body dto.AlertResolveRequest true "Resolution request"
|
||||
// @Success 200 {object} dto.AlertResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 403 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/alerts/{id}/resolve [post]
|
||||
func (h *coreHandlers) alertResolve(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.AlertResolveRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
alert, err := h.core.ResolveAlertForSession(bearerToken(r), domain.AlertResolveRequest{AlertID: r.PathValue("id"), Note: request.Note})
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.AlertFromDomain(alert))
|
||||
}
|
||||
|
||||
// alertRetry godoc
|
||||
// @Summary Retry one durable alert source
|
||||
// @Description Retries only the bounded source represented by an alert and preserves idempotency.
|
||||
// @Tags production-operations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Alert ID"
|
||||
// @Param body body dto.AlertRetryRequest true "Scoped retry request"
|
||||
// @Success 202 {object} dto.AlertRetryResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 403 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/alerts/{id}/retry [post]
|
||||
func (h *coreHandlers) alertRetry(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.AlertRetryRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
result, err := h.core.RetryAlertForSession(bearerToken(r), domain.AlertRetryRequest{AlertID: r.PathValue("id"), IdempotencyKey: request.IdempotencyKey})
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, dto.AlertRetryFromDomain(result))
|
||||
}
|
||||
|
||||
// pluginLifecycles godoc
|
||||
// @Summary List server-bound plugin lifecycle state
|
||||
// @Description Lists durable plugin installation, desired/current state, compatibility, dependency, job, alert, and audit metadata.
|
||||
// @Description Lists durable plugin installation, desired/current state, compatibility, dependency, and job metadata.
|
||||
// @Tags production-operations
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.PluginLifecycleListResponse
|
||||
@@ -2598,70 +2419,3 @@ func (h *coreHandlers) logStreamDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.LogStreamFromDomain(stream))
|
||||
}
|
||||
|
||||
// auditEvents godoc
|
||||
// @Summary Create or list audit events
|
||||
// @Description Creates or lists audit event metadata.
|
||||
// @Tags audit-events
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body dto.AuditEventCreateRequest false "Audit event create request"
|
||||
// @Success 200 {object} dto.AuditEventListResponse
|
||||
// @Success 201 {object} dto.AuditEventResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/audit-events [get]
|
||||
// @Router /api/v1/audit-events [post]
|
||||
func (h *coreHandlers) auditEvents(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
events, err := h.core.ListAuditEvents(domain.AuditEventFilter{
|
||||
ActorID: r.URL.Query().Get("actorId"),
|
||||
ResourceKind: r.URL.Query().Get("resourceKind"),
|
||||
ResourceID: r.URL.Query().Get("resourceId"),
|
||||
Result: domain.AuditResult(r.URL.Query().Get("result")),
|
||||
})
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.AuditEventListFromDomain(events))
|
||||
case http.MethodPost:
|
||||
request, err := decodeJSON[dto.AuditEventCreateRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
event, err := h.core.CreateAuditEvent(request.ToDomain())
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, dto.AuditEventFromDomain(event))
|
||||
default:
|
||||
writeMethodNotAllowed(w, "GET, POST")
|
||||
}
|
||||
}
|
||||
|
||||
// auditEventDetail godoc
|
||||
// @Summary Get audit event
|
||||
// @Description Returns one audit event by ID.
|
||||
// @Tags audit-events
|
||||
// @Produce json
|
||||
// @Param id path string true "Audit event ID"
|
||||
// @Success 200 {object} dto.AuditEventResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Failure 405 {object} dto.ErrorResponse
|
||||
// @Router /api/v1/audit-events/{id} [get]
|
||||
func (h *coreHandlers) auditEventDetail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
event, err := h.core.GetAuditEvent(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.AuditEventFromDomain(event))
|
||||
}
|
||||
|
||||
@@ -127,21 +127,6 @@ func TestCoreAPICreateListDetailWorkflows(t *testing.T) {
|
||||
t.Fatalf("expected explicitly created stream in list, got %+v", streams)
|
||||
}
|
||||
|
||||
auditResponse := postJSON[dto.AuditEventResponse](t, router, "/api/v1/audit-events", dto.AuditEventCreateRequest{
|
||||
ID: "audit-1",
|
||||
ActorID: "user-1",
|
||||
Action: "server.create",
|
||||
ResourceKind: "server-instance",
|
||||
ResourceID: "server-1",
|
||||
Result: domain.AuditResultSuccess,
|
||||
Summary: "created server instance",
|
||||
})
|
||||
if auditResponse.Result != domain.AuditResultSuccess {
|
||||
t.Fatalf("expected successful audit event, got %+v", auditResponse)
|
||||
}
|
||||
getJSON[dto.AuditEventResponse](t, router, "/api/v1/audit-events/audit-1")
|
||||
auditEvents := getJSON[dto.AuditEventListResponse](t, router, "/api/v1/audit-events?actorId=user-1&resourceKind=server-instance&resourceId=server-1&result=success")
|
||||
assertListCount(t, auditEvents.Count, 1)
|
||||
}
|
||||
|
||||
func TestMetricsAndConfigReadAPIAreSafeAndRoleScoped(t *testing.T) {
|
||||
@@ -349,16 +334,6 @@ func TestCoreAPIServerRuntimeDistributionAndJobWorkflows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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", "client-manager.build", "dependency.install", "runtime-key.reset"} {
|
||||
if !auditActions[action] {
|
||||
t.Fatalf("expected audit action %q in %+v", action, audits.Items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreAPIRunDistributionDenialNamesMissingPluginPermission(t *testing.T) {
|
||||
@@ -1538,31 +1513,19 @@ func TestGamePluginRegistryResponseDoesNotExposeRawInternals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionOperationsGovernanceRoutesAreDurableAndRedacted(t *testing.T) {
|
||||
func TestPluginLifecycleAndAIConfigRoutesAreDurableAndRedacted(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
serverID := createRuntimeAPIFixtures(t, router, adminSession)
|
||||
createAIProviderFixture(t, router, adminSession)
|
||||
|
||||
capacity := getJSONWithAuth[dto.ProductionCapacitySummaryResponse](t, router, "/api/v1/production/capacity", adminSession)
|
||||
if len(capacity.Endpoints) == 0 {
|
||||
t.Fatalf("expected persisted capacity endpoints, got %+v", capacity)
|
||||
lifecycle := postOKJSONWithAuth[dto.PluginLifecycleActionResponse](t, router, "/api/v1/plugin-lifecycles/server.runtime/actions", dto.PluginLifecycleActionRequest{ServerInstanceID: serverID, Operation: "install", TargetVersion: "1.0.0", IdempotencyKey: "api-plugin-install", Confirmed: false}, adminSession)
|
||||
if lifecycle.Status != "queued" || lifecycle.Job.ID == "" || lifecycle.Installation.ID == "" {
|
||||
t.Fatalf("expected queued plugin lifecycle job, got %+v", lifecycle)
|
||||
}
|
||||
decision := postOKJSONWithAuth[dto.CapacityAdmissionDecisionResponse](t, router, "/api/v1/production/capacity/admission", dto.CapacityAdmissionRequest{ServerInstanceID: serverID, Capability: domain.JobCapabilityRemoteRunRCONCommand, IdempotencyKey: "api-capacity-check"}, adminSession)
|
||||
if decision.Accepted || decision.AlertID == "" || decision.AuditEventID == "" {
|
||||
t.Fatalf("expected unavailable capability admission to create durable evidence, got %+v", decision)
|
||||
}
|
||||
alerts := getJSONWithAuth[dto.AlertListResponse](t, router, "/api/v1/alerts?state=active", adminSession)
|
||||
if alerts.Count == 0 {
|
||||
t.Fatalf("expected durable alert list, got %+v", alerts)
|
||||
}
|
||||
acknowledged := postOKJSONWithAuth[dto.AlertResponse](t, router, "/api/v1/alerts/"+decision.AlertID+"/acknowledge", dto.AlertAcknowledgeRequest{Note: "operator review"}, adminSession)
|
||||
if acknowledged.State != string(domain.AlertStateAcknowledged) {
|
||||
t.Fatalf("expected acknowledged state, got %+v", acknowledged)
|
||||
}
|
||||
resolved := postOKJSONWithAuth[dto.AlertResponse](t, router, "/api/v1/alerts/"+decision.AlertID+"/resolve", dto.AlertResolveRequest{Note: "review complete"}, adminSession)
|
||||
if resolved.State != string(domain.AlertStateResolved) {
|
||||
t.Fatalf("expected resolved state, got %+v", resolved)
|
||||
lifecycles := getJSONWithAuth[dto.PluginLifecycleListResponse](t, router, "/api/v1/plugin-lifecycles?serverInstanceId="+serverID, adminSession)
|
||||
if lifecycles.Count != 1 || lifecycles.Items[0].JobID != lifecycle.Job.ID {
|
||||
t.Fatalf("expected persisted plugin lifecycle, got %+v", lifecycles)
|
||||
}
|
||||
|
||||
invocation := postOKJSONWithAuth[dto.AIInvocationResponse](t, router, "/api/v1/ai/invocations", dto.AIInvocationRequest{RequestID: "api-ai-config-diff", ServerInstanceID: serverID, Purpose: "config.suggest", ProviderID: "ai.openai", Prompt: "disable pvp"}, adminSession)
|
||||
@@ -1580,10 +1543,10 @@ func TestProductionOperationsGovernanceRoutesAreDurableAndRedacted(t *testing.T)
|
||||
t.Fatalf("expected approved diff with config write job, got %+v", approval)
|
||||
}
|
||||
|
||||
evidence := fmt.Sprintf("%+v %+v %+v %+v %+v %+v", capacity, decision, alerts, resolved, diffs, approval)
|
||||
evidence := fmt.Sprintf("%+v %+v %+v %+v", lifecycle, lifecycles, diffs, approval)
|
||||
for _, forbidden := range []string{"/Users/", "/private/", "unix://", "tcp://", "Bearer ", "sk-", "password=", "apiKeyRef", "rawApiKey", "https://api.openai.com"} {
|
||||
if strings.Contains(evidence, forbidden) {
|
||||
t.Fatalf("production governance response leaked forbidden fragment %q: %s", forbidden, evidence)
|
||||
t.Fatalf("production operations response leaked forbidden fragment %q: %s", forbidden, evidence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-13
@@ -23,7 +23,6 @@ All routes use JSON request and response bodies. Collection routes support `GET`
|
||||
| Jobs | `GET /api/v1/jobs`, `POST /api/v1/jobs` | `GET /api/v1/jobs/{id}` | `JobCreateRequest`, `JobResponse`, `JobListResponse` |
|
||||
| Artifacts | `GET /api/v1/artifacts`, `POST /api/v1/artifacts` | `GET /api/v1/artifacts/{id}`, `POST /api/v1/artifacts/{id}/download`, `GET /api/v1/artifacts/{id}/content` | `ArtifactCreateRequest`, `ArtifactResponse`, `ArtifactListResponse`, `ArtifactDownloadReferenceResponse`, `ArtifactContentRequest` |
|
||||
| Log streams | `GET /api/v1/log-streams`, `POST /api/v1/log-streams` | `GET /api/v1/log-streams/{id}` | `LogStreamCreateRequest`, `LogStreamResponse`, `LogStreamListResponse` |
|
||||
| Audit events | `GET /api/v1/audit-events`, `POST /api/v1/audit-events` | `GET /api/v1/audit-events/{id}` | `AuditEventCreateRequest`, `AuditEventResponse`, `AuditEventListResponse` |
|
||||
|
||||
Client Manager lifecycle routes are grouped under the server instance and return only the safe installation projection: `GET /api/v1/server-instances/{id}/client-managers`, `GET .../{profileKey}`, and typed `POST` routes for `deploy`, `control`, `update`, `retry`, `revoke-session`, and confirmed `uninstall`. Component-only `POST /api/v1/client-managers/register` and `/heartbeat` use the separate signed component identity/session contract. Run-only input/chunk routes are fenced by the active Run job lease. None of these DTOs return raw component keys, bearer sessions, secret refs/values, host paths, PIDs, sockets, or endpoint addresses.
|
||||
|
||||
@@ -40,7 +39,6 @@ Client Manager lifecycle routes are grouped under the server instance and return
|
||||
- `GET /api/v1/jobs?serverInstanceId=server-1&runEndpointId=run-local&state=queued`
|
||||
- `GET /api/v1/artifacts?ownerKind=job&ownerId=job-1&state=uploading`
|
||||
- `GET /api/v1/log-streams?serverInstanceId=server-1&streamKey=stdout`
|
||||
- `GET /api/v1/audit-events?actorId=user-1&resourceKind=server-instance&resourceId=server-1&result=success`
|
||||
|
||||
## Implemented Authentication And Current User Actions
|
||||
|
||||
@@ -52,7 +50,7 @@ Client Manager lifecycle routes are grouped under the server instance and return
|
||||
- `PUT /api/v1/users/current/profile`: update bounded current-user profile fields using `UserProfileBody`.
|
||||
- `PUT /api/v1/users/current/theme`: persist current-user console theme preferences using `UserThemePreferenceRequest`.
|
||||
|
||||
Bearer sessions are stored as SHA-256 verifiers with issued/expiry/revocation timestamps and rotation generation; raw tokens are never written to FileStore/MySQLStore snapshots. Browser sessions use HttpOnly SameSite cookies, while explicit CLI bearer mode returns the token once. Production router construction requires authentication for sensitive API paths, reserves user/provider/plugin install/Run endpoint/audit/global create operations for platform administrators, and repeats server/job/log/artifact ownership checks in services. After the first account exists, public registration defaults to `pending` plus server-scoped roles and does not grant platform administrator privileges. A bootstrap administrator is created only when `PLATFORM_BOOTSTRAP_ADMIN_PASSWORD` is explicitly configured; local debug scripts provide their own development-only value.
|
||||
Bearer sessions are stored as SHA-256 verifiers with issued/expiry/revocation timestamps and rotation generation; raw tokens are never written to FileStore/MySQLStore snapshots. Browser sessions use HttpOnly SameSite cookies, while explicit CLI bearer mode returns the token once. Production router construction requires authentication for sensitive API paths, reserves user/provider/plugin install/Run endpoint/global create operations for platform administrators, and repeats server/job/log/artifact ownership checks in services. After the first account exists, public registration defaults to `pending` plus server-scoped roles and does not grant platform administrator privileges. A bootstrap administrator is created only when `PLATFORM_BOOTSTRAP_ADMIN_PASSWORD` is explicitly configured; local debug scripts provide their own development-only value.
|
||||
|
||||
## Implemented Role-Scoped Server Access
|
||||
|
||||
@@ -91,7 +89,7 @@ File dispatch responses expose only logical target keys, scoped input/artifact r
|
||||
AI invocation is platform-mediated. `PLATFORM_AI_PROVIDER_MODE=live` uses the Platform-owned HTTP client and environment secret resolver; local verification explicitly uses `mock`. Invocation responses do not expose provider base URLs, API key refs, raw keys, bearer tokens, host paths, run sockets, or storage credentials. Config suggestions persist an expiring diff and never dispatch run-side writes before separate approval.
|
||||
AI Provider management responses also return only `baseUrlConfigured` and `apiKeyConfigured`; an empty base URL or secret reference in an update preserves the Platform-owned value instead of round-tripping it through the browser.
|
||||
|
||||
AI provider management responses expose `apiKeyConfigured` only. Create/update requests may carry a controlled secret reference, and a blank update preserves an existing configured secret; the stored reference is not returned to the browser.
|
||||
AI provider management responses expose `apiKeyConfigured` only. Create/update requests may carry a scoped secret reference, and a blank update preserves an existing configured secret; the stored reference is not returned to the browser.
|
||||
|
||||
## Implemented Game Plugin Registry Actions
|
||||
|
||||
@@ -111,18 +109,18 @@ Marketplace state actions are metadata-only in this change. `install` and `enabl
|
||||
|
||||
Marketplace catalog state remains separate from production lifecycle installations. Server-bound install/enable/disable/upgrade/rollback/retire operations use the production lifecycle routes below.
|
||||
|
||||
## Production Operations Governance
|
||||
## Production Operations
|
||||
|
||||
- `GET /api/v1/production/capacity`: return bounded endpoint capacity, durable job pressure, backlog counts, pressure codes, and active-alert count visible to the session.
|
||||
- `POST /api/v1/production/capacity/admission`: evaluate server binding, endpoint heartbeat/capability, job limits, queue pressure, and spool pressure without dispatching work.
|
||||
- `GET /api/v1/alerts`: list durable alerts with state/source/severity filters.
|
||||
- `POST /api/v1/alerts/{id}/acknowledge`, `/resolve`, and `/retry`: persist one scoped alert transition or source retry with actor/audit evidence.
|
||||
- `POST /api/v1/alerts/{id}/acknowledge`, `/resolve`, and `/retry`: persist one scoped alert transition or source retry with actor evidence.
|
||||
- `GET /api/v1/plugin-lifecycles`: list server-bound plugin lifecycle installations.
|
||||
- `POST /api/v1/plugin-lifecycles/{pluginId}/actions`: validate manifest declaration, compatibility, confirmation, idempotency, and capacity before creating one durable Run job.
|
||||
- `GET /api/v1/ai/config-diffs`: list reviewable AI config recommendations visible to the session.
|
||||
- `POST /api/v1/ai/config-diffs/{id}/approve`: revalidate actor/server/config revision/checksum/expiry and dispatch exactly one bounded `config.write` job.
|
||||
|
||||
These responses expose logical IDs, counts, states, pressure codes, safe diagnostics, and job/audit links only. They never project raw credentials, provider transport configuration, Run sessions/endpoints, host paths, PIDs, sockets, DSNs, or RCON material.
|
||||
These responses expose logical IDs, counts, states, pressure codes, safe diagnostics, and job links only. They never project raw credentials, provider transport configuration, Run sessions/endpoints, host paths, PIDs, sockets, DSNs, or RCON material.
|
||||
|
||||
## Implemented Plugin Bridge Actions
|
||||
|
||||
@@ -150,7 +148,7 @@ Lifecycle workflow responses include accepted status, action, bounded server ins
|
||||
- `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`.
|
||||
- `GET /api/v1/server-instances/{id}/run/update`: lists safe update phase, target, progress message, artifact checksum, release identity, rollback, and audit summary for the authorized server.
|
||||
- `GET /api/v1/server-instances/{id}/run/update`: lists safe update phase, target, progress message, artifact checksum, release identity, rollback, and summary for the authorized server.
|
||||
- `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`.
|
||||
@@ -159,15 +157,15 @@ Lifecycle workflow responses include accepted status, action, bounded server ins
|
||||
- `POST /api/v1/server-instances/{id}/dependencies/install`: accepts `DependencyJobRequest` with an install plan key and the exact catalog `planDigest`; stale/missing digests are denied before job creation.
|
||||
Server-scoped terminal log streaming (`GET /api/v1/server-instances/{id}/logs/events`) is registered for the server detail terminal drawer and emits platform-accepted log SSE history/live events only. The raw log list/backfill routes (`logs/live` and `logs/backfill`) remain unavailable as product APIs; internal log ingest and cursor query remain available for run/platform maintenance flows.
|
||||
|
||||
Runtime distribution and client-manager APIs require the current bearer session, server visibility, plugin-declared permissions, complete runtime bindings only for actions that truly depend on external logical bindings, and platform-builder readiness. Run-side lifecycle commands separately require run endpoint capability support and use plugin-declared lifecycle actions without making manual runtime-profile binding a user prerequisite. 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.
|
||||
Runtime distribution and client-manager APIs require the current bearer session, server visibility, plugin-declared permissions, complete runtime bindings only for actions that truly depend on external logical bindings, and platform-builder readiness. Run-side lifecycle commands separately require run endpoint capability support and use plugin-declared lifecycle actions without making manual runtime-profile binding a user prerequisite. Responses and 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.
|
||||
|
||||
SCUM product APIs expose only safe local projections, typed operation/workflow requests, approval status, confirmation status, blocker reasons, and audit-safe summaries. They never expose SCUM.db SQL text, DB paths, DSNs, RCON command text, raw protected request payloads, run sockets, host paths, or credentials.
|
||||
SCUM product APIs expose only safe local projections, typed operation/workflow requests, approval status, confirmation status, blocker reasons, and bounded summaries. They never expose SCUM.db SQL text, DB paths, DSNs, RCON command text, raw protected request payloads, run sockets, host paths, or credentials.
|
||||
|
||||
`POST /api/v1/server-instances/workflows/create` requires only the plugin type and server name. A runtime binding may still be maintained internally for advanced logical transports, but browser lifecycle controls must not force operators to choose a runtime profile before start/stop or run-package generation when the plugin deployment/lifecycle declaration is sufficient. Platform builds distributions itself and never needs a registered Run endpoint with `distribution.build` to do so.
|
||||
|
||||
## Private Run Dependency And Update Routes
|
||||
|
||||
The following signed routes are Run-only and never part of browser/plugin DTOs: `POST /api/v1/run/jobs/dependency-input`, `POST /api/v1/run/jobs/protected-request-input`, `POST /api/v1/run/jobs/update-input`, `POST /api/v1/run/jobs/update-chunk`, and `POST /api/v1/run/jobs/update-health`. They require the current endpoint/session signature; input/chunk calls additionally require active attempt/lease/cancel fencing. Protected-request input additionally requires the current fencing token and returns approved text exactly once for the server-bound logical transport; the text is not persisted in a job, bridge command, journal, response projection, or audit summary. Update chunks are bounded to 1 MiB and resolve only an available same-server target-matched Run distribution. Health reports are accepted only after the terminal staged job, matching attempt/lease proof, current online endpoint release, and reconciliation-capable session are verified. These routes never return raw artifact paths, browser download tokens, host paths, credentials, secret refs, or session/lease hashes.
|
||||
The following signed routes are Run-only and never part of browser/plugin DTOs: `POST /api/v1/run/jobs/dependency-input`, `POST /api/v1/run/jobs/source-rcon-input`, `POST /api/v1/run/jobs/update-input`, `POST /api/v1/run/jobs/update-chunk`, and `POST /api/v1/run/jobs/update-health`. They require the current endpoint/session signature; input/chunk calls additionally require active attempt/lease/cancel fencing. Source RCON input returns one queued command exactly once to the active Run lease. Update chunks are bounded to 1 MiB and resolve only an available same-server target-matched Run distribution. Health reports are accepted only after the terminal staged job, matching attempt/lease proof, current online endpoint release, and reconciliation-capable session are verified. These routes never return raw artifact paths, browser download tokens, host paths, credentials, secret refs, or session/lease hashes.
|
||||
|
||||
## Implemented Run Control Actions
|
||||
|
||||
@@ -185,7 +183,6 @@ Control is the highest-priority run-facing channel; artifact/file transfer press
|
||||
- `POST /api/v1/run/jobs/result`: accept `RunJobResultRequest` and write an idempotent terminal result or durable retry-wait transition with capped exponential backoff.
|
||||
- `POST /api/v1/run/jobs/cancel`: accept fenced `RunJobCancelPollRequest` and return durable pending cancellation intent for the current attempt.
|
||||
- `POST /api/v1/run/jobs/reconcile`: accept persisted Run journal evidence (`jobId`, `attempt`, `leaseToken`), rebind only matching active attempts to the current authenticated session generation, persist reconciliation metadata, retry/cancel platform-active missing work, and return confirmed assignments plus discard IDs.
|
||||
- `POST /api/v1/run/jobs/protected-request-input`: accept `ProtectedRequestExecutionInputRequest`, fence endpoint/session/attempt/lease/token, and return one approved, unexpired SQL, RCON, or management-program request only for its exact server-bound logical transport. The route never returns credentials, DSNs, paths, sockets, raw connections, or host shell material.
|
||||
- `POST /api/v1/jobs/{id}/cancel`: authorize the server owner/administrator or platform administrator and durably record cancellation intent; queued/retrying work becomes cancelled immediately while active work completes through fenced Run polling/result.
|
||||
|
||||
Run job actions carry bounded job metadata only: job ID, run endpoint ID, server instance ID, capability, idempotency key, lease token, attempt/retry limits, deadlines, progress, terminal state, message, error code, result reference, and timing hints. Raw lease tokens exist only on the signed Run job channel; platform persistence stores their hashes. User-facing Job responses expose safe attempt, retry, cancel, terminal, and reconcile projections but never raw/hashed leases, Run sessions, secret refs, host paths, sockets, or credentials.
|
||||
@@ -200,7 +197,7 @@ Server-scoped SSE log streaming is removed from product routes. `POST /api/v1/lo
|
||||
Log ingest actions carry durable log metadata and bounded entries only: run endpoint ID, session token, stream identity, source, sequence range, compression metadata, checksum, entries, and cursor limits. They do not carry artifact chunks, host paths, raw credentials, direct sockets, or unbounded inline data.
|
||||
Log ingest is durable and independently retried. Artifact/file transfer backlog must not prevent log acknowledgement, duplicate acknowledgement, cursor state updates, or spool cleanup.
|
||||
|
||||
Platform storage is configured by `PLATFORM_STORAGE_BACKEND`. The default `file` backend writes metadata snapshots to `PLATFORM_METADATA_PATH` and log bodies to segmented files in `PLATFORM_LOG_DIR`; `memory` remains available for tests and ephemeral local runs. Relational stores such as MySQL/Postgres are reserved for metadata, stream cursors, indexes, retention state, and audit trails. High-volume log bodies for hundreds or thousands of servers should use a log-optimized backend behind `LogBodyStore`, such as ClickHouse, Loki, OpenSearch/Elasticsearch, or object-storage segments.
|
||||
Platform storage is configured by `PLATFORM_STORAGE_BACKEND`. The default `file` backend writes metadata snapshots to `PLATFORM_METADATA_PATH` and log bodies to segmented files in `PLATFORM_LOG_DIR`; `memory` remains available for tests and ephemeral local runs. Relational stores such as MySQL/Postgres are reserved for metadata, stream cursors, indexes, retention state, and operational records. High-volume log bodies for hundreds or thousands of servers should use a log-optimized backend behind `LogBodyStore`, such as ClickHouse, Loki, OpenSearch/Elasticsearch, or object-storage segments.
|
||||
|
||||
## Implemented Run Artifact Actions
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"browser.local/platform/dto"
|
||||
)
|
||||
|
||||
// sourceRCONCommands is kept as legacy service plumbing but is not registered as a browser product route.
|
||||
// sourceRCONCommands dispatches a one-time SCUM Source RCON command through Run.
|
||||
func (h *coreHandlers) sourceRCONCommands(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
|
||||
Reference in New Issue
Block a user