Remove manual runtime controls from server detail
This commit is contained in:
+1
-1
@@ -102,4 +102,4 @@ Client Manager installations are durable aggregates separate from Run distributi
|
||||
|
||||
Component registration uses the current client-manager key generation, a timestamped nonce, and a short-lived hashed component session. It never reuses a Run session or job lease. Key reset revokes old sessions/artifacts and marks the installation for current-generation rebuild/redeploy. Run reports only logical health, phase, and bounded execution evidence; host paths, PIDs, sockets, raw keys, and credential material are not operator or plugin projections. Production KMS/code-signing, private source credentials, and fleet rollout remain explicit non-goals.
|
||||
|
||||
Validated plugin runtime profiles and per-server runtime bindings are part of durable metadata. Server creation requires only the plugin type and server name; operators set a declared profile and complete logical bindings after creation, before any gated lifecycle/runtime action. Browser and plugin-facing responses expose readiness only, not binding values. Platform-owned Docker builds need no registered Run endpoint with `distribution.build`; component keys remain in platform-controlled per-job input. This change uses controlled secret references and an injectable AES-GCM component-key envelope. The built-in envelope key is a disposable-development compatibility fallback; deployments must set `PLATFORM_SECRET_ENVELOPE_KEY`. This is not a production vault/KMS or machine-side runtime resolver. Durable scheduling, process supervision, durable log/artifact bodies, bounded metrics/backups, declaration-backed remote adapter envelopes, typed dependency installation, and transactional Run self-update are implemented. Client-manager lifecycle, production signing/fleet rollout, external provider/storage adapters, production scaling/alerts, plugin lifecycle, and real AI-provider integration remain separate tasks.
|
||||
Validated plugin runtime profiles and per-server runtime bindings are part of durable metadata for advanced logical transports. Server creation requires only the plugin type and server name, and plugin-declared deployment/lifecycle actions must be enough for user-facing start/stop and generated Run package flows without forcing operators through a manual runtime-profile binding screen. Browser and plugin-facing responses expose readiness only, not binding values. Platform-owned Docker builds need no registered Run endpoint with `distribution.build`; component keys remain in platform-controlled per-job input. This change uses controlled secret references and an injectable AES-GCM component-key envelope. The built-in envelope key is a disposable-development compatibility fallback; deployments must set `PLATFORM_SECRET_ENVELOPE_KEY`. This is not a production vault/KMS or machine-side runtime resolver. Durable scheduling, process supervision, durable log/artifact bodies, bounded metrics/backups, declaration-backed remote adapter envelopes, typed dependency installation, and transactional Run self-update are implemented. Client-manager lifecycle, production signing/fleet rollout, external provider/storage adapters, production scaling/alerts, plugin lifecycle, and real AI-provider integration remain separate tasks.
|
||||
|
||||
@@ -1601,8 +1601,10 @@ func TestRuntimeBindingAPIIsAuthorizedValidatedAndRedacted(t *testing.T) {
|
||||
if unconfigured.Configured || unconfigured.Reason != "runtime profile is not configured" {
|
||||
t.Fatalf("unexpected unconfigured projection: %+v", unconfigured)
|
||||
}
|
||||
missingStart := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+server.ID+"/start", dto.ServerLifecycleCommandRequest{ExpectedConfigVersion: server.ConfigVersion, IdempotencyKey: "api-start-missing-binding"}, adminSession)
|
||||
assertErrorResponse(t, missingStart, http.StatusBadRequest, errorCodeValidation)
|
||||
missingStart := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/"+server.ID+"/start", dto.ServerLifecycleCommandRequest{ExpectedConfigVersion: server.ConfigVersion, IdempotencyKey: "api-start-missing-binding"}, adminSession)
|
||||
if !missingStart.Accepted || missingStart.Job.TargetKey != "actions/start.json" {
|
||||
t.Fatalf("expected plugin lifecycle start without manual runtime binding, got %+v", missingStart)
|
||||
}
|
||||
|
||||
incompleteRecorder := requestJSONWithAuth(t, router, http.MethodPut, "/api/v1/server-instances/"+server.ID+"/runtime-binding", dto.RuntimeBindingUpdateRequest{ProfileKey: "local", Bindings: map[string]string{"server-root": "runtime.server-root"}}, adminSession)
|
||||
assertStatus(t, incompleteRecorder, http.StatusOK)
|
||||
|
||||
@@ -163,9 +163,9 @@ Lifecycle workflow responses include accepted status, action, bounded server ins
|
||||
- `GET /api/v1/server-instances/{id}/logs/events`: streams selected server log metadata and entries as `text/event-stream`; the optional `historyLimit` query replays recent stored entries before live push events.
|
||||
- `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 platform-builder readiness. Run-side lifecycle commands separately require run endpoint capability support. 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 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.
|
||||
|
||||
`POST /api/v1/server-instances/workflows/create` requires only the plugin type and server name. A runtime binding is set later through `PUT /api/v1/server-instances/{id}/runtime-binding`; until then, lifecycle and runtime-dependent actions return a safe configuration-required reason. Platform builds distributions itself and never needs a registered Run endpoint with `distribution.build` to do so.
|
||||
`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
|
||||
|
||||
|
||||
@@ -542,8 +542,10 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
||||
if err := svc.validateDistributionPluginPermission(user.ID, plugin, instance.ID, "server.run.distribution", "run.update.denied"); err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "run.update.denied"); err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
if deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "run.update.denied"); err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
}
|
||||
artifact, err := svc.GetArtifactForSession(sessionID, request.ArtifactID)
|
||||
if err != nil {
|
||||
|
||||
@@ -61,16 +61,18 @@ func TestRuntimeBindingValidationAndLifecycleGating(t *testing.T) {
|
||||
if err := svc.store.RuntimeBindings().Create(domain.RuntimeBinding{ID: "runtime-binding-" + forged.ID, ServerInstanceID: forged.ID, PluginID: plugin.ID, PluginVersion: plugin.Version, ProfileKey: "local", Mode: "local-process", Bindings: map[string]string{}, Status: domain.RuntimeBindingStatusComplete, CreatedAt: fixedTime, UpdatedAt: fixedTime}); err != nil {
|
||||
t.Fatalf("store forged complete binding: %v", err)
|
||||
}
|
||||
if _, err := svc.StartServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: forged.ID, ExpectedConfigVersion: forged.ConfigVersion, IdempotencyKey: "start-forged-complete"}); err == nil || !strings.Contains(err.Error(), "rcon.password") {
|
||||
t.Fatalf("expected derived missing keys to override stored complete status, got %v", err)
|
||||
forgedView, err := svc.GetServerRuntimeBindingForSession(ownerSession, forged.ID)
|
||||
if err != nil || forgedView.Status != domain.RuntimeBindingStatusIncomplete || len(forgedView.MissingKeys) != 2 || !containsString(forgedView.MissingKeys, "server-root") || !containsString(forgedView.MissingKeys, "rcon.password") {
|
||||
t.Fatalf("expected derived missing keys to override stored complete status in the readiness view, view=%+v err=%v", forgedView, err)
|
||||
}
|
||||
|
||||
view, err := svc.GetServerRuntimeBindingForSession(ownerSession, instance.ID)
|
||||
if err != nil || view.Configured || view.Reason != "runtime profile is not configured" {
|
||||
t.Fatalf("unexpected unconfigured view: view=%+v err=%v", view, err)
|
||||
}
|
||||
if _, err := svc.StartServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, IdempotencyKey: "start-without-binding"}); err == nil || !strings.Contains(err.Error(), "runtime profile is not configured") {
|
||||
t.Fatalf("expected missing binding to block start, got %v", err)
|
||||
withoutBinding, err := svc.StartServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, IdempotencyKey: "start-without-binding"})
|
||||
if err != nil || withoutBinding.Job.TargetKey != "actions/start.json" || withoutBinding.Job.ExecutionInput.WorkspaceScope != "" {
|
||||
t.Fatalf("expected plugin lifecycle start without manual runtime binding, result=%+v err=%v", withoutBinding, err)
|
||||
}
|
||||
if _, err := svc.UpdateServerRuntimeBindingForSession(otherSession, instance.ID, domain.RuntimeBindingUpdate{ProfileKey: "local"}); err != ErrForbidden {
|
||||
t.Fatalf("expected non-owner update forbidden, got %v", err)
|
||||
@@ -86,8 +88,9 @@ func TestRuntimeBindingValidationAndLifecycleGating(t *testing.T) {
|
||||
if err != nil || view.Status != domain.RuntimeBindingStatusIncomplete || len(view.MissingKeys) != 1 || view.MissingKeys[0] != "rcon.password" {
|
||||
t.Fatalf("unexpected incomplete binding: view=%+v err=%v", view, err)
|
||||
}
|
||||
if _, err := svc.StartServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, IdempotencyKey: "start-incomplete-binding"}); err == nil || !strings.Contains(err.Error(), "rcon.password") {
|
||||
t.Fatalf("expected missing logical key to block start, got %v", err)
|
||||
incompleteStart, err := svc.StartServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, IdempotencyKey: "start-incomplete-binding"})
|
||||
if err != nil || incompleteStart.Job.TargetKey != "actions/start.json" || incompleteStart.Job.ExecutionInput.WorkspaceScope != "local" {
|
||||
t.Fatalf("expected plugin lifecycle start to tolerate incomplete optional bindings, result=%+v err=%v", incompleteStart, err)
|
||||
}
|
||||
|
||||
view, err = svc.UpdateServerRuntimeBindingForSession(ownerSession, instance.ID, domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{"rcon.password": "secret://runtime-server/rcon"}})
|
||||
|
||||
@@ -252,11 +252,6 @@ func (svc *CoreService) dispatchExistingServerLifecycle(command domain.ServerLif
|
||||
}
|
||||
}
|
||||
}
|
||||
if deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
||||
if err := svc.requireCompleteRuntimeBindings(instance.OwnerUserID, instance.ID, "server.lifecycle."+string(action)+".denied"); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
}
|
||||
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user