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

76 lines
7.3 KiB
Markdown

## 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.