10 KiB
Context
Platform already persists server runtime profiles/bindings and durable Job scheduling metadata. Run already persists active assignments and pending terminal results, reconciles them after session rotation, signs Run-service requests, and keeps control/jobs/logs/artifacts on separate routes. The missing execution layer is narrower but security-sensitive: lifecycle start/stop are currently one-shot exec.CommandContext calls, config reads are platform-derived, and config/file jobs do not carry durable approved bytes or produce typed execution results.
The independent Run repository must remain separate. Browser/plugin callers name only a server, declared capability, logical target, and scoped input/artifact reference. Platform remains the authority for ownership, profile selection, approval, job attempt/session fencing, and safe projection. Run alone maps the logical scope into its private workspace and may persist PID or filesystem details, none of which can cross into platform_web or plugin DTOs.
Goals / Non-Goals
Goals:
- Supervise one declared local game process per server/profile scope with real spawn, liveness, graceful stop, exit observation, idempotent operations, and restart reconciliation.
- Execute approved config/file reads and writes with strict containment, symlink/device rejection, bounded I/O, atomic replacement, and compare-and-swap version/checksum semantics.
- Preserve private execution input and typed result state across Platform/Run restarts without weakening task 04 lease, attempt, session-generation, signature, cancellation, or reconciliation rules.
- Project only safe process state, exit classification, config/file version, checksum, size, and audit summary to authorized owners/admins and platform_web.
- Keep current visual language and existing 401/403 handling.
Non-Goals:
- General shell execution, arbitrary interpreters, host path selection, direct sockets, credential injection, multiple unmanaged processes per server, or OS service/container orchestration.
- Durable log/artifact/metric/backup storage, remote FTP/rsync/database/RCON adapters, dependency installation, Run self-update, client-manager lifecycle, production scaling/alerts/plugin lifecycle, or real AI-provider integration.
- Claiming that example action declarations install or ship commercial game binaries.
Decisions
Decision 1: Extend the durable Job aggregate with private execution input and typed result
Job will persist a nested execution input containing the workspace profile key, approved content, expected version/checksum, and bounded read limit, plus a nested typed result containing kind, process state, exit classification, version, checksum, size, and audit summary. Model conversions and file/MySQL snapshot tests will cover the fields. The user JobResponse will omit approved content and expose only the safe result subset; the signed Run assignment carries the private input only after a matching claim.
This is preferred over an in-memory input map because queued/retrying work must survive Platform restart. A separate input repository was considered, but the input has the same lifecycle, idempotency, ownership, and retention as its Job and would add cross-record transaction failure without providing reuse.
Decision 2: Resolve lifecycle action ref and workspace scope at dispatch
Platform will load the server's persisted runtime binding and selected plugin lifecycle profile, verify the requested capability/action is declared, set the Job target to that action's relative JSON ref, and set the private workspace scope to the selected profile key. Config/file dispatch uses the same binding-derived scope and validates the endpoint, plugin permission, server ownership, logical target, and scoped input/artifact ref before Job creation.
This replaces the current mismatch where lifecycle jobs send the profile key as targetKey while Run expects an action file ref. Platform will never send a host path or binding value to browser/plugin callers.
Decision 3: Typed action files distinguish one-shot and supervised operations
Run will decode action JSON with unknown-field rejection, bounded size, and an explicit action matching the Job capability. start requires a workspace-relative executable key and argument vector; stop and status operate only on the persisted supervised identity for that server/profile; install remains a bounded one-shot declaration and does not represent dependency installation. Executable resolution rejects symlinks, non-regular/non-executable files, shells, interpreter escape forms, unsafe environment names/values, and paths outside the scoped workspace.
This keeps plugins declarative and supports actual process execution without accepting shell source or a generic command string. Keeping the existing unrestricted PATH lookup was rejected because names such as interpreters can turn a safe-looking vector into arbitrary execution.
Decision 4: Run owns a private process journal and liveness probe
Run will atomically persist owner-only process records under its state directory. Records include logical server/profile identity, private PID, start time, command fingerprint, state, exit classification, and the latest owning Job attempt/lease hash, but not raw session tokens or environment credentials. A live in-process waiter records unexpected exits. On Run startup, reconciliation probes each recorded PID, retains only matching live identities, and marks missing identities exited before new claims execute.
Start is idempotent when a matching process is live; stop is idempotent when absent/stopped. Cancellation or timeout before start commit terminates the child. Attempt evidence prevents an older recovered assignment from replacing or stopping a newer process record. PID is intentionally private and never appears in Platform protocol results.
OS liveness primitives provide bounded best-effort identity validation. Strong native start-token adapters for every supported OS remain future hardening; command fingerprint and journal start metadata reduce PID-reuse ambiguity in this implementation.
Decision 5: Secure workspace access validates every path component
The effective workspace is <private-root>/instances/<server-id>/<profile-key>, constructed only from validated logical identifiers. Reads and writes walk components with Lstat, reject symlinks, absolute/traversal/backslash keys, reserved Run state/action targets for generic writes, non-directory parents, device/FIFO/socket files, and any resolved path outside the scope. Reads require regular files and enforce a configured maximum before and during reading.
Writes compare current file metadata with expected version/checksum, create an owner-only temporary regular file in the same verified directory, write and fsync bounded bytes, recheck containment, rename atomically, fsync the parent where supported, and atomically update an owner-only file metadata journal. A conflict performs no rename. This is preferred over os.WriteFile, which can follow symlinks and expose partial content.
Decision 6: Platform applies terminal typed results only after existing fencing
Run returns typed execution results on the existing Job result route with Job ID, attempt, lease token, and current signed session. Platform validates type/capability consistency, bounds/redacts fields, includes the typed result in the terminal fingerprint, and stores/applies it only after the existing endpoint/session-generation/attempt/lease/deadline/cancel checks pass.
A successful config write advances the persisted server config version and checksum and stores the approved content already attached to that Job. File reads store private bounded content on the Job but expose only checksum/size/version in the normal job projection. Lifecycle process state updates the server projection without trusting Run-supplied PID or path data. Stale/conflicting terminal results cannot mutate server config or process projection.
Decision 7: Frontend changes are projection-only
platform_web will add schema validation for the safe execution result, show config checksum/version and process/file audit outcomes in existing server detail/job surfaces, and continue to use the current preview-then-approve flow for AI or manual config changes. It will not render approved private bytes from Job records, PID, host paths, Run tokens, leases, secret refs, sockets, or credentials, and it will reuse shared theme surfaces.
Risks / Trade-offs
- [A process can exit between a liveness probe and an idempotent response] -> Record the latest observed state, keep probes bounded, and treat later status as authoritative rather than claiming continuous availability.
- [PID reuse after a long Run outage can produce ambiguous recovery on some OSes] -> Persist start metadata and command fingerprint, reject inconsistent identities, document best-effort recovery, and leave stronger per-OS birth-token adapters as follow-up hardening.
- [Platform snapshot persistence of approved config bytes increases metadata size] -> Keep input/read limits at 64 KiB and never store large artifacts in Job input; larger payloads stay artifact-referenced and are outside this change's durable artifact claim.
- [Atomic rename durability differs by filesystem] -> fsync file and parent where supported, keep same-directory temporary files, and test visibility/cleanup semantics without claiming distributed-filesystem guarantees.
- [Existing example workspaces may not contain supervised executables] -> Preserve bounded install behavior, make start failures explicit and retry policy-aware, and use controlled executable fixtures in Run tests.
Migration Plan
- Add backward-compatible zero-value execution input/result and config checksum/content fields to Platform domain/model/snapshot conversions.
- Add protocol DTOs/validators and result projection before enabling Run capabilities.
- Deploy Run secure workspace, process/file journals, executor, and worker dispatch; old journals load with empty new fields.
- Update plugin action schema/examples and Platform lifecycle target resolution.
- Enable safe frontend projections after API fields are available.
- Rollback ignores additive snapshot fields and stops advertising new Run capabilities; supervised child processes must be stopped through the old Run instance or operator-controlled host procedure before removing its private journal.
Open Questions
- Strong native process birth-token verification for every supported OS is intentionally deferred; this change uses the bounded liveness/fingerprint strategy described above.