Files
browser/openspec/changes/persist-runtime-profiles-and-server-bindings/design.md
T

7.3 KiB

Context

The plugin JSON schema and TypeScript SDK already define discovery probes, lifecycle profiles, dependency probes/install plans, log sources, transports, and client-manager profiles. Platform manifest DTOs do not decode runtimeProfiles, GamePlugin does not store them, and durable snapshots omit RuntimeBinding, so those declarations and bindings disappear before they can drive a server workflow. The existing action gate also reports complete when no binding rows exist.

The platform uses typed domain/model records behind repository interfaces. File and MySQL backends durably serialize the same StoreSnapshot, while tests use MemoryStore. The web console consumes platform-owned safe DTOs and must never receive raw host paths, direct sockets, credentials, or secret storage values.

Goals / Non-Goals

Goals:

  • Preserve every supported safe runtime-profile declaration during manifest registration and durable reload.
  • Select exactly one declared lifecycle profile per server runtime binding and derive its required logical keys deterministically.
  • Create or update bindings through owner/admin-authorized APIs, persist them in every durable store, and expose only redacted readiness metadata.
  • Require a complete binding before lifecycle and runtime actions whose execution depends on the selected profile.
  • Make profile selection and binding completion available in the create-server and server-detail workflows.

Non-Goals:

  • Storing raw credentials, direct sockets, or host filesystem paths in platform metadata.
  • Implementing a general secret vault, run-side path resolver, durable scheduler, process supervisor, log/artifact backend, dependency installer, self-update system, or client-manager deployment lifecycle.
  • Changing the independent run repository or declaring the wider production-readiness roadmap complete.

Decisions

Decision 1: Persist typed profiles on the installed plugin record

Platform will mirror the existing manifest/SDK runtime profile structures in domain, DTO, and model packages and copy them into GamePlugin at registration. File/MySQL snapshots already persist plugin records, so this preserves the immutable installed-version contract without reaching across repository roots or storing arbitrary manifest JSON.

Alternative considered: keep only the manifest artifact reference and re-read the artifact for every request. This was rejected because artifact availability is a separate lifecycle, it makes validation/reload behavior non-deterministic, and it leaves action gating dependent on external content.

Decision 2: One server-scoped binding aggregate selects one lifecycle profile

Each server has one RuntimeBinding identified deterministically from its server ID. It records the plugin ID/version contract, selected lifecycle profileKey, profile mode, logical binding refs, derived missing keys, and readiness status. Required keys are derived from the selected lifecycle profile and the referenced discovery, dependency, log, transport, and client-manager declarations; callers cannot self-assert complete or missingKeys.

Alternative considered: one row per logical key. This was rejected for now because profile changes need atomic validation and readiness projection, while the existing repository abstraction has aggregate create/update semantics.

Decision 3: Accept safe opaque values, return redacted metadata

Write requests accept logical references and secret:// references only. Values containing raw absolute paths, URI sockets/DSNs, inline credentials, traversal, or other unsafe material are rejected. Read responses return each logical key with a configured boolean and secret boolean, never the stored value or internal secret-storage location. Missing reasons name only declared logical keys.

Alternative considered: return stored logical refs directly. This was rejected because even non-secret refs can encode topology or storage details and the browser does not need them to review readiness.

Decision 4: Create workflow persists the binding before dispatch

Server creation requires a declared profile key and optional initial binding values. The service validates the plugin/profile, creates the server and its binding, verifies completeness, and only then queues install. If required keys are missing, the request fails with logical missing-key details and no install job is dispatched. Repository rollback is limited by the current non-transactional abstraction; validation is therefore completed before the first write, and a binding persistence failure prevents dispatch and is surfaced explicitly.

Existing stored servers without a binding remain readable but are action-gated with a safe runtime profile is not configured reason until an owner or platform admin configures one.

Decision 5: Authorization reuses server ownership rules

Listing a binding uses server visibility; changing it requires server ownership or platform-admin authority. Plugin pages receive no direct binding mutation surface. Lifecycle services independently check binding readiness so bypassing the UI or runtime-action projection cannot dispatch work.

Decision 6: Web forms use declared contract data

GamePluginResponse exposes safe runtime-profile declarations required for selection and labels. The create form renders the selected plugin's lifecycle profiles and declared logical keys, submits the real profile and bindings, and avoids path/socket/credential terminology. The server detail view loads the redacted binding, supports profile changes and logical-key updates, and shows safe missing reasons.

Risks / Trade-offs

  • [Snapshot writes are aggregate and not transactional across server and binding repositories] -> Perform all validation before writes, persist the binding before job dispatch, and add failure/reload tests; a later durable-job change can introduce transactions.
  • [Profile key derivation can over-require unrelated declarations] -> Scope derivation to the selected lifecycle profile and directly referenced transports/client manager, plus required global discovery/dependency/log targets.
  • [Opaque safe refs cannot prove run-side resolvability] -> Treat platform completeness as contract completeness only; run-side resolution/health remains a later lifecycle responsibility.
  • [Existing servers become gated after upgrade] -> Keep them readable and return a safe configuration-required reason; operators can select a profile in server detail.
  • [Changing an active server profile could invalidate running work] -> Reject binding updates while the server is installing or running; require a stable non-active state.

Migration Plan

  1. Deploy profile-aware decoding and snapshot fields with backward-compatible empty defaults.
  2. Existing plugin records without persisted profiles remain listable but cannot configure a runtime binding until the manifest is re-registered.
  3. Existing servers without bindings remain visible with lifecycle/runtime actions disabled.
  4. Re-register manifests, then configure each server binding through the authorized detail workflow.
  5. Rollback can ignore the additive JSON fields; no raw secret values are introduced by this change.

Open Questions

  • Transactional multi-resource creation and encrypted secret material persistence are deferred to the next security/persistence task rather than being represented as complete here.