From 19d1d802c47953d1ab0ca1711fce658a78f429a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 21:20:03 +0000 Subject: [PATCH 1/4] docs: renumber standalone panel spec from 023 to 025 specs/023-standalone-panel clashed with specs/023-change-based-test-selection. Move it to specs/025-standalone-panel and update the api/specs.md link. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claude --- api/specs.md | 2 +- specs/{023-standalone-panel => 025-standalone-panel}/spec.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename specs/{023-standalone-panel => 025-standalone-panel}/spec.md (100%) diff --git a/api/specs.md b/api/specs.md index 66edfe09e..fcb5d5f35 100644 --- a/api/specs.md +++ b/api/specs.md @@ -871,4 +871,4 @@ The version cap also applies behind API path prefixes. SPDY TLS/header negotiati Standalone `/modules/*` requests require one explicit registered remote `cluster` selector and cluster-wide module permission on that target. The selected remote operator owns module reconciliation; upload ConfigMaps use the configured operator namespace. Combined installs retain local module ownership and reject remote selectors on this surface. Local node credential minting is incompatible with standalone startup; Kubernetes system-log endpoints return 501 and the dashboard directs administrators to container logs. -See [standalone installation](../docs/standalone-panel.md) and [design](../specs/023-standalone-panel/spec.md). +See [standalone installation](../docs/standalone-panel.md) and [design](../specs/025-standalone-panel/spec.md). diff --git a/specs/023-standalone-panel/spec.md b/specs/025-standalone-panel/spec.md similarity index 100% rename from specs/023-standalone-panel/spec.md rename to specs/025-standalone-panel/spec.md From f2a7993d62c58c457d1bf6fb0978dd0c4b21989a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 21:38:17 +0000 Subject: [PATCH 2/4] docs: complete spec-kit artifacts for spec 025 standalone panel Rewrite spec.md into the spec-kit template, keeping the contributor's design note, and add plan, research, data model, contracts, quickstart, tasks, open decisions and the requirements checklist. They document what PR #630 built: every task is ticked and tied to its commits, and requirements the merged code does not fully meet are listed as known gaps instead of new tasks. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claude --- specs/025-standalone-panel/OPEN-DECISIONS.md | 37 ++++ .../checklists/requirements.md | 36 ++++ .../025-standalone-panel/contracts/api-cli.md | 31 +++ .../contracts/deployment.md | 36 ++++ .../contracts/http-api.md | 50 +++++ specs/025-standalone-panel/data-model.md | 74 +++++++ specs/025-standalone-panel/plan.md | 112 ++++++++++ specs/025-standalone-panel/quickstart.md | 28 +++ specs/025-standalone-panel/research.md | 57 ++++++ specs/025-standalone-panel/spec.md | 193 +++++++++++++++++- specs/025-standalone-panel/tasks.md | 171 ++++++++++++++++ 11 files changed, 821 insertions(+), 4 deletions(-) create mode 100644 specs/025-standalone-panel/OPEN-DECISIONS.md create mode 100644 specs/025-standalone-panel/checklists/requirements.md create mode 100644 specs/025-standalone-panel/contracts/api-cli.md create mode 100644 specs/025-standalone-panel/contracts/deployment.md create mode 100644 specs/025-standalone-panel/contracts/http-api.md create mode 100644 specs/025-standalone-panel/data-model.md create mode 100644 specs/025-standalone-panel/plan.md create mode 100644 specs/025-standalone-panel/quickstart.md create mode 100644 specs/025-standalone-panel/research.md create mode 100644 specs/025-standalone-panel/tasks.md diff --git a/specs/025-standalone-panel/OPEN-DECISIONS.md b/specs/025-standalone-panel/OPEN-DECISIONS.md new file mode 100644 index 000000000..5b0addfad --- /dev/null +++ b/specs/025-standalone-panel/OPEN-DECISIONS.md @@ -0,0 +1,37 @@ +# Open Decisions: 025-standalone-panel + +Do not treat an item as decided until a maintainer rules on it here. + +## OD-1: One API process with `Recreate` — **Settled (valgul, 2026-10-14)** + +Keep one API process and the `Recreate` strategy; no rolling updates or multiple replicas. The audit hash chain and user-management safeguards use process-local locks. Multi-replica HA would be a separate future feature. + +## OD-2: Telemetry marker for standalone installs — **Settled (valgul, 2026-10-14)** + +A follow-up, not part of this feature. No task here. + +## OD-3: Enforce `--skip-crds` for the panel-only Helm profile — **Settled (valgul, PR #630 review)** + +Documented requirement only in this feature; valgul agreed to open a follow-up issue. No task here. + +## OD-4: Legacy panel-key formats — **Open** + +PR #630 merged with two legacy paths still supported: headerless ciphertext (decrypted and re-encrypted with the `gpk1.` header by rotation) and the legacy default key path `/data/panel.key` (on the database volume). This was still an open review thread on PR #630 when it merged. + +- **Keep** (as merged): existing early standalone installs keep working; no change. +- **Drop in a follow-up**: remove headerless decryption and make the key path required, after a release note. + +Recommendation: keep. Asked in the spec 025 thread on 2026-10-14. + +## OD-5: Archive this folder as `done_025-standalone-panel` — **Open** + +Every task is ticked and PR #630 is merged, so Constitution IV and CLAUDE.md rule 16 call for `git mv specs/025-standalone-panel specs/done_025-standalone-panel` in its own `docs:` commit. Constitution I also says a feature is incomplete without a `test/e2e/` test, and standalone mode has none (spec Known gaps). + +- **Keep `025-`**: leave it in flight until a standalone E2E test exists, which would be new work under a new decision. +- **Archive now**: rename to `done_025-`; the missing E2E test stays recorded as a known gap. + +Recommendation: keep `025-`. Asked in the spec 025 thread on 2026-10-14. + +## Moot + +- Splitting the share-links token fix (`834c3ca`) out of PR #630: merged inside PR #630, so there is nothing left to split. diff --git a/specs/025-standalone-panel/checklists/requirements.md b/specs/025-standalone-panel/checklists/requirements.md new file mode 100644 index 000000000..b6adb62be --- /dev/null +++ b/specs/025-standalone-panel/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: Standalone Panel and API + +**Purpose**: Validate specification completeness and quality +**Created**: 2026-10-14 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [ ] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Written retroactively for merged PR #630. Flags, paths and file names appear because the contributor's design note named them and operators configure them; they are the user-facing contract, not implementation choices. +- "Feature meets measurable outcomes" stays unchecked: SC-001 to SC-006 are met by CI jobs and unit tests, but Constitution I requires a `test/e2e/` test, which does not exist (spec Known gaps). +- OD-4 (legacy key formats) and OD-5 (`done_` rename) are open; neither changes a requirement of the merged feature. diff --git a/specs/025-standalone-panel/contracts/api-cli.md b/specs/025-standalone-panel/contracts/api-cli.md new file mode 100644 index 000000000..2c0799b27 --- /dev/null +++ b/specs/025-standalone-panel/contracts/api-cli.md @@ -0,0 +1,31 @@ +# Contract: API flags, environment and commands + +As merged in PR #630 (`api/cmd/main.go`, `api/cmd/panel_key.go`, `api/cmd/management.go`). + +## `api serve` flags + +| Flag | Environment | Default | Meaning | +|---|---|---|---| +| `--standalone` | `GAMEPLANE_STANDALONE=true` | off | Store management records in SQL; never read a kubeconfig or in-cluster credentials; register no `local` cluster | +| `--panel-key-file` | `GAMEPLANE_PANEL_KEY_FILE` | `/data/panel.key` (legacy) | Credential encryption key. Compose and Helm set `/keys/panel.key` | +| `--panel-key-provisioned` | `GAMEPLANE_PANEL_KEY_PROVISIONED=true` | off | Key must already exist; never generated or written | +| `--remote-allowed-cidrs` | `GAMEPLANE_REMOTE_ALLOWED_CIDRS` | empty | Comma-separated CIDRs that narrow standalone Kubernetes and gateway destinations. Invalid entries fail startup | + +Rules: + +- `--standalone` with cluster-ops enabled fails at startup ("cluster-ops requires a local Kubernetes cluster"). +- When credentials or a key-state row exist, a missing, unsafe or wrong key fails startup. + +## `api rotate-panel-key` + +Offline master-key rotation. Stop the API first. + +| Flag | Environment | Required | Meaning | +|---|---|---|---| +| `--old-key-file` | `GAMEPLANE_PANEL_KEY_FILE` | yes | Current key; never removed or overwritten | +| `--new-key-file` | | yes | Separately named replacement; generated unless provisioned | +| `--new-key-provisioned` | | no | Replacement must already exist | +| `--db-driver` | `GAMEPLANE_DB_DRIVER` | no (`sqlite`) | `sqlite` or `postgres` | +| | `GAMEPLANE_DB_DSN` | no | Database DSN. Environment only: `--db-dsn` is rejected so credentials never reach argv or help text | + +Behaviour: validates the old key against every stored row before creating the new key; re-encrypts all rows and updates the key-state row in one transaction; refuses identical paths or identical key material; a retry after a lost commit is recognised through `previous_key_id`. Afterwards, point `--panel-key-file` (or Helm `api.panelKey.fileName`) at the new file. diff --git a/specs/025-standalone-panel/contracts/deployment.md b/specs/025-standalone-panel/contracts/deployment.md new file mode 100644 index 000000000..6d5c42cf5 --- /dev/null +++ b/specs/025-standalone-panel/contracts/deployment.md @@ -0,0 +1,36 @@ +# Contract: Helm values and Docker Compose + +As merged in PR #630. Operator guide: `docs/standalone-panel.md`. + +## Helm values (`charts/gameplane/values.yaml`) + +| Value | Default | Meaning | +|---|---|---| +| `operator.enabled` | `true` | `false` removes every operator resource (Deployment, RBAC, namespaces, network policies, monitors, rules, module source, cosign key, Grafana dashboard; agent mTLS certificates in `mtls.yaml` unless the combined API needs them) | +| `api.standalone` | `false` | Passes `--standalone`; drops API RBAC, agent CA and client mounts, cluster-ops, CRD apply hook and MCP server | +| `api.replicas` | `1` | Must stay 1 in standalone (validated) | +| `api.panelKey.fileName` | `panel.key` | Key file under `/keys`; a simple name of at most 128 characters | +| `api.panelKey.existingSecret` | empty | Mount this Secret read-only at `/keys` (mode `0440`) and pass `--panel-key-provisioned` | +| `api.panelKey.secretKey` | `panel.key` | Entry in `existingSecret` | +| `api.panelKey.storage.existingClaim` | empty | Use an existing PVC for `/keys` | +| `api.panelKey.storage.size` | `1Mi` | Size of the generated `gameplane-api-key` PVC (kept on uninstall) | +| `api.panelKey.storage.storageClassName` | empty | Storage class for the generated PVC | +| `api.remoteAllowedCIDRs` | `[]` | Rendered as `--remote-allowed-cidrs` in standalone only | + +### Render-time guards (`templates/validate.yaml`) + +When `api.standalone=true`, rendering fails unless: the API is enabled; `operator.enabled=false`; `api.replicas=1`; `gateway.enabled` is not set; `panelKey.fileName` is a simple name; `existingSecret` and `storage.existingClaim` are not both set. + +### Standalone pod settings (`templates/api.yaml`) + +`Recreate` strategy, `automountServiceAccountToken: false`, `fsGroupChangePolicy: OnRootMismatch`; no data PVC when `db.driver=postgres`. + +### Panel-only example + +`charts/gameplane/examples/standalone-panel-values.yaml`. Install with `--skip-crds`: values cannot stop Helm from installing `crds/` (documented only; follow-up issue owed). + +## Docker Compose (`deploy/standalone/`) + +- `compose.yaml`: project `gameplane-panel`, services `gameplane-api` and `web` only. Volumes `panel-data` (`/data`) and `panel-keys` (`/keys`). API runs read-only, drops all capabilities, `no-new-privileges`, publishes no port. Web binds `127.0.0.1:${GAMEPLANE_PORT:-8080}`. +- `compose.provisioned-key.yaml`: overlay that mounts `GAMEPLANE_PANEL_KEY_SOURCE_FILE` as secret `panel-key` at `/run/secrets/panel-key` and sets `GAMEPLANE_PANEL_KEY_PROVISIONED=true`. +- Run exactly one `gameplane-api` container (documented; not enforced by Compose). diff --git a/specs/025-standalone-panel/contracts/http-api.md b/specs/025-standalone-panel/contracts/http-api.md new file mode 100644 index 000000000..10da5a93b --- /dev/null +++ b/specs/025-standalone-panel/contracts/http-api.md @@ -0,0 +1,50 @@ +# Contract: HTTP endpoints + +As merged in PR #630. All endpoints sit behind session authentication and the RBAC middleware (`api/internal/rbac/rbac.go`). The full API reference is `api/specs.md`. + +## `GET /admin/installation` + +Any authenticated user. Response `200`: + +```json +{ "standalone": true, "localCluster": false, "version": "v0.3.0" } +``` + +Not reachable before login (Core rule 3). + +## `GET /clusters`, `GET /fleet/*` + +In standalone mode both succeed with no implicit `local` entry; an empty registry returns empty lists. The cluster view exposes only name, display name, phase, message, version and last check time, plus a boolean `canViewInventory`. + +## `POST /clusters` + +Requires `cluster:manage` (`GET /clusters` needs only a session). Body `{ "name", "displayName", "kubeconfig" }`. + +- Standalone: the kubeconfig must pass `kube.ValidateStandaloneKubeconfig` and the remote access policy (HTTPS, verified TLS, no proxy, safe destination). Registration and credential commit in one transaction. Duplicate name returns `409`. +- Combined: unchanged Secret-then-CR path, with cleanup on failure. + +## `PUT /clusters/{name}/kubeconfig` + +Requires `cluster:manage`. Body `{ "kubeconfig" }`. `204` on success, `409` when the registration changed concurrently. Publishes a new immutable credential, swaps the reference under UID and previous-value checks, keeps gateway settings and grants, then deletes the old credential. + +## `PUT /clusters/{name}/gateway`, `DELETE /clusters/{name}/gateway` + +Requires `cluster:manage`. `PUT` body `{ "url", "caCert", "clientCert", "clientKey" }`; the URL must be an HTTPS origin that passes the remote access policy. `204` with no body. `DELETE` clears the gateway and removes only the API-generated gateway Secret. + +## `DELETE /clusters/{name}` + +Requires `cluster:manage`. Removes the registration and its API-owned kubeconfig and gateway credentials with UID and version preconditions. + +## `/modules` and `/modules/*` + +- Standalone: exactly one `?cluster=` is required (`400` when missing, repeated or `local`). `GET` needs `modules:read` and every other method needs `modules:manage`, as a cluster-wide grant on that cluster (`403` otherwise). +- Combined: local catalog as before; a remote selector returns `501`. +- `modules:read`, `modules:manage`, `templates:read` and `templates:write` are now checked as cluster-wide grants on the target cluster in both modes (`api/internal/auth/perms.go`). + +## `GET /admin/system-logs/{component}` + +Standalone: `501` with guidance to read container-runtime logs. + +## Error and log surface + +No response body, error or log line from these endpoints contains kubeconfig, token, certificate or key material. Remote lookup and connection failures use fixed messages. diff --git a/specs/025-standalone-panel/data-model.md b/specs/025-standalone-panel/data-model.md new file mode 100644 index 000000000..8c79a0a31 --- /dev/null +++ b/specs/025-standalone-panel/data-model.md @@ -0,0 +1,74 @@ +# Data Model: Standalone Panel and API + +As merged in PR #630. Source of truth is the code named under each entity. + +## Management object + +Table `management_objects` (`api/internal/db/migrations/common/016_standalone_management.sql`). Used only in standalone mode; the combined install keeps Kubernetes Secrets and Cluster CRs. + +| Field | Type | Notes | +|---|---|---| +| `kind` | text | `secrets` or `clusters` | +| `namespace` | text | Secret namespace; empty for cluster-scoped registrations | +| `name` | text | Object name | +| `uid` | text | Assigned on create; checked by delete and update preconditions | +| `version` | bigint | Starts at 1; compare-and-swap on every update (Kubernetes `resourceVersion` semantics) | +| `payload` | text | `secrets`: ciphertext (see below). `clusters`: plain JSON with references only, never credential values | + +Primary key `(kind, namespace, name)`. List supports label selectors and a continue token (unpadded URL-safe base64 of the last name). Dry-run writes are refused. + +## Credential ciphertext + +`api/internal/controlplane/key_ciphertext.go`, `store.go`. + +- Format: `gpk1..` followed by base64 of nonce and AES-256-GCM output. +- Associated data: kind, namespace, name, UID and version, so a ciphertext cannot be copied to another record. +- Legacy headerless ciphertext from early PR #630 builds still decrypts. + +## Panel key + +`api/internal/controlplane/store.go`, `key_file_unix.go`, `key_acl_linux.go`, `key_directory.go`, `key_publication_unix.go`, `key_options.go`. + +- 32 random bytes in a file outside the database. Path from `--panel-key-file`. +- Accepted only when: regular file, exactly 32 bytes, one hard link, owned by root or the API user, no group or world write, no execute bits, group-readable only through the API process group, no extended POSIX ACL entries (checked on the opened inode). +- Key ID: SHA-256 over a fixed domain string and the key. +- Generated keys: written to a temp file, fsynced, hard-linked into place without overwrite, parent directory fsynced, re-read and compared, all under `.management-key.lock`. New directories are created `0700`. +- Provisioned keys (`--panel-key-provisioned`): read only; never created or written. + +## Key state + +Table `management_key_state` (`017_standalone_key_state.sql`, `api/internal/controlplane/key_state.go`). + +| Field | Type | Notes | +|---|---|---| +| `singleton` | integer | Always 1 (CHECK constraint) | +| `key_id` | text | Active key identity | +| `previous_key_id` | text | Key identity before the last rotation; lets a retried rotation recognise a lost commit | + +Every credential write and every registration locks this row with `UPDATE ... WHERE key_id = ?` and holds it until commit. Zero rows updated means the process holds a retired key, and the write is refused. + +### State transitions + +```text +empty store ──first start──▶ key generated or provisioned, key_state initialised +established ──start with missing or wrong key──▶ startup fails (fail closed) +established ──rotate-panel-key (offline)──▶ all rows re-encrypted, key_id=new, previous_key_id=old +``` + +## Cluster registration + +Stored as a `clusters` management object (standalone) or a `Cluster` CR (combined). Fields written by the API: + +- `spec.kubeconfigSecret.{name, key: "kubeconfig"}`, pointing at an immutable Secret `cluster--kubeconfig-<32 hex>`. +- `spec.agentGateway.{url, tlsSecretRef.name}`, pointing at an immutable Secret `cluster--gateway-<32 hex>` with keys `ca.crt`, `tls.crt`, `tls.key` and label `gameplane.local/agent-gateway-credentials`. +- `status.{phase, message, serverVersion, lastCheckTime}`, written only by the standalone health poller with fixed messages. + +API-owned credentials carry `gameplane.local/managed-by=gameplane-api` and are deleted with UID and version preconditions. + +## Installation capabilities + +`GET /admin/installation` (`api/internal/handlers/installation.go`): `{ "standalone": bool, "localCluster": bool, "version": string }`. `localCluster` is `!standalone`. + +## Notification event + +`api/internal/notify/notify.go`: `Event` gains an optional `cluster` field (`json:"cluster,omitempty"`); titles show `cluster/namespace/name` when it is set. diff --git a/specs/025-standalone-panel/plan.md b/specs/025-standalone-panel/plan.md new file mode 100644 index 000000000..ab4c2dfd4 --- /dev/null +++ b/specs/025-standalone-panel/plan.md @@ -0,0 +1,112 @@ +# Implementation Plan: Standalone Panel and API + +**Branch**: `feat/standalone-panel` (PR #630, merged as `6ed25c1`) | **Date**: 2026-10-14 | **Spec**: [spec.md](spec.md) + +**Input**: Feature specification from `specs/025-standalone-panel/spec.md` + +**Note**: Written retroactively. This plan describes the design PR #630 merged; it plans no new work. + +## Summary + +The API gains a standalone mode that runs without any local Kubernetes cluster. Management records (cluster registrations and labelled credentials) move behind a narrow storage interface. The combined install backs it with Kubernetes; standalone backs it with two new SQL tables, with credentials sealed by AES-256-GCM under a panel key kept on separate storage. Remote clusters are reached through hardened clients (verified HTTPS, no proxy, no redirects, dial-time destination checks, bounded responses). A health poller and a notification poller replace the informers a central operator would otherwise feed. The dashboard reads an authenticated installation endpoint and stops assuming a `local` cluster. Helm gains `operator.enabled` and `api.standalone`, and a two-service Docker Compose profile ships under `deploy/standalone/`. + +## Technical Context + +**Language/Version**: Go (API, `api/go.mod`), TypeScript strict with React 19 (dashboard), Helm templates, Docker Compose, Python 3 (CI smoke and chart checks) + +**Primary Dependencies**: chi, client-go and its SPDY executor, controller-runtime (combined path only), TanStack Query, HeroUI v3 + +**Storage**: Panel SQL database (SQLite, or experimental PostgreSQL) with migrations `016_standalone_management.sql` and `017_standalone_key_state.sql`; panel key file on separate storage (`/keys/panel.key`) + +**Testing**: Go unit tests (including PostgreSQL-tagged tests run by the CI `api-postgres` job), Vitest dashboard tests, `charts/tests/check_standalone.py`, `deploy/standalone/smoke.py` (CI `standalone-compose` job). No `test/e2e/` test (see Constitution Check I) + +**Target Platform**: Linux host with Docker Compose, or a Kubernetes cluster for the panel-only Helm profile; remote game clusters on Kubernetes + +**Project Type**: Web service (API and dashboard) plus deployment profiles + +**Performance Goals**: None stated. Fixed bounds instead: 8 MiB decompressed REST response, 64 KiB `/version`, 1 MiB per watch frame, 1024 objects per kind per notification snapshot, 30 s health poll with 4 workers and 10 s timeout, 5 s notification poll + +**Constraints**: One API process (`Recreate`, one replica); credentials never in responses, errors or logs; fail closed on any key problem; no login-page leak of the install mode + +**Scale/Scope**: Homelab-scale central panel managing a handful of remote clusters; 231 files changed in PR #630 + +## Constitution Check + +*Evaluated against constitution 2.2.0 on the merged code.* + +| Principle | Result | Evidence | +|---|---|---| +| I. E2E-tested delivery | **Not met** | No `test/e2e/` test runs a standalone panel and no bucket in `test/e2e/buckets.sh` covers it. Coverage is the Compose smoke test and render-only chart checks. Recorded as a known gap in [spec.md](spec.md#known-gaps); no task added. | +| II. Design-first | **Partly met** | 19 standalone frames exported to `assets/design-export/` (MANIFEST section "Standalone panel (PR #630, 2026-10-08)"), but design commit `6eb7ac3` came after the first UI commit `0182e16`. | +| III. Language best practice | Met | No `//nolint`, `eslint-disable`, `@ts-ignore` or `#nosec` added; no lint config loosened; errors wrapped with `%w`. | +| IV. Spec-driven development | **Partly met** | Only the contributor's design note came with the first code commit (`0182e16`); spec, plan and tasks were written after the merge, on 2026-10-14. `api/specs.md` and `web/specs.md` were updated in PR #630. | +| V. Delegation | Not applicable | Contributor PR. | +| VI. CI bears the load | Met | All 97 checks green on head `117caf35` before merge. | + +**Gate decision**: The feature is merged, so the gate cannot block it. Every task is ticked, so rule 16 calls for the `done_025-` rename; it is pending OD-5 because of the Principle I gap (see [OPEN-DECISIONS.md](OPEN-DECISIONS.md)). + +## Project Structure + +### Documentation (this feature) + +```text +specs/025-standalone-panel/ +├── spec.md +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +│ ├── api-cli.md +│ ├── http-api.md +│ └── deployment.md +├── OPEN-DECISIONS.md +├── checklists/requirements.md +└── tasks.md +``` + +### Source Code (repository root) + +```text +api/ +├── cmd/ main.go (flags, rotate-panel-key), management.go, panel_key.go +├── internal/controlplane/ SQL management store, key handling, rotation, atomic registration +├── internal/db/migrations/common/016_*.sql, 017_*.sql +├── internal/kube/ management interface, registry sequencing, remote policy, bounded responses, SPDY guard, standalone health poller +├── internal/notify/ per-cluster notification snapshots +├── internal/ws/ gateway client destination guard +├── internal/handlers/ clusters (register, kubeconfig, gateway), installation, modules routing, fleet, managed secrets +└── internal/auth, rbac, telemetry, registry +web/src/ +├── lib/useInstallation.ts, cluster.ts, endpoints.ts, api.ts +├── components/ ClusterSelector, RegisterClusterDialog, ModuleTarget, modules/*, NotificationsPanel, AppLayout +└── routes/ Clusters, Cluster, Modules, AdminSettings, AdminLogs, Users, tabs/Settings +charts/gameplane/ values.yaml, templates/{api,panel-key,validate,...}.yaml, examples/standalone-panel-values.yaml +charts/tests/check_standalone.py +deploy/standalone/ compose.yaml, compose.provisioned-key.yaml, smoke.py, README.md +docs/standalone-panel.md plus install, security, architecture, gateway and multicluster docs +``` + +**Structure Decision**: Existing modules only. Standalone logic lives in the API (`api/internal/controlplane/` is new); the operator and agent are unchanged. This respects Core rule 10: the remote operator stays authoritative for workloads, and the API only stores registrations and credentials. + +## Design (as merged) + +1. **Mode switch** (`api/cmd/management.go`): `managementClients` builds either a Kubernetes-backed client registered as `local`, or the SQL-backed `controlplane` client with no default cluster. Standalone with cluster-ops is a startup error. +2. **Management interface** (`api/internal/kube/management.go`): `SecretStore`, `ClusterStore` and `ClusterRegistrar`. Handlers call `Client.Secrets(ns)` and `Client.Clusters()` and never know which backend they have. +3. **SQL store** (`api/internal/controlplane/`): one `management_objects` table keyed by kind, namespace and name, with UID and version for compare-and-swap; label selectors and continue tokens on list; dry-run refused. +4. **Panel key**: 32 bytes, validated (regular file, one link, safe owner and mode, no extended ACL, read through `os.OpenRoot`). Generated keys are published by temp file, fsync, hard link and directory fsync under a lock file. Provisioned keys are read only. +5. **Ciphertext**: `gpk1..` plus AES-256-GCM, with kind, namespace, name, UID and version as associated data, so ciphertext cannot be moved between records. Legacy headerless ciphertext still decrypts. +6. **Key state and rotation**: the singleton `management_key_state` row is locked by every credential write and checked against the process's key ID. `api rotate-panel-key` re-encrypts all rows in one transaction and records `previous_key_id` so a retry after a lost commit is safe. +7. **Registration**: standalone registration writes credential and registration in one transaction; kubeconfig and gateway rotation publish a new immutable Secret, swap the reference under UID and version checks, then delete the old one. +8. **Remote access policy** (`api/internal/kube/remote_access.go`, `remote_response.go`, `remote_spdy.go`): kubeconfig validation, redirect refusal, dial-time IP checks with optional CIDR allowlist, size-bounded responses and watch frames, guarded SPDY upgrades. The gateway client uses the same dial guard. +9. **Health and notifications**: `watch_standalone.go` polls `/version` and writes a fixed status projection; `notify/poll_standalone.go` takes bounded per-cluster snapshots for alerts. +10. **Dashboard**: `GET /admin/installation` drives every mode decision; module, source, builder and backup-destination screens take an explicit cluster; empty and selection states replace the implicit `local`. +11. **Deployment**: Helm `operator.enabled`, `api.standalone`, `api.panelKey.*`, `api.remoteAllowedCIDRs`, validation guards and a panel-only example; Compose with `gameplane-api` and `web` only. + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| Principle I: no `test/e2e/` coverage | Not justified by PR #630; it shipped with Compose smoke and chart render checks only | Recorded as a known gap. Adding an E2E bucket is new work and out of scope for this retroactive folder. | +| Principle IV: plan and tasks written after the code | Not justified; the contributor shipped a design note only | This folder now documents the merged design; no corrective work is planned. | +| Principle II: UI code before design commit | Not justified; historical ordering in the contributor branch | The final tree carries the design export, so no corrective work is planned. | diff --git a/specs/025-standalone-panel/quickstart.md b/specs/025-standalone-panel/quickstart.md new file mode 100644 index 000000000..8964d025e --- /dev/null +++ b/specs/025-standalone-panel/quickstart.md @@ -0,0 +1,28 @@ +# Quickstart: validating the standalone panel + +Validation runs in CI (CLAUDE.md rule 8, Constitution VI). Locally, only compile checks are allowed (`go build ./...` in `api/`, `npx tsc --noEmit` in `web/`). The operator walkthrough is `docs/standalone-panel.md`. + +## What CI runs + +| Scenario | Where | Proves | +|---|---|---| +| Compose panel starts with no Kubernetes, bootstraps an admin, logs in, lists empty clusters and fleet | `deploy/standalone/smoke.py`, job `standalone-compose` | US1, SC-001 | +| Register a cluster, inspect a stopped-container SQLite snapshot for ciphertext, restart, log in again | same | US2, SC-002 | +| Replace a kubeconfig, rotate the panel key once and re-run the same command as a retry, check key digests | same | US2, US4, SC-004 | +| Provisioned-key mode against a read-only key volume | same (`smoke.py` builds its own read-only override; `compose.provisioned-key.yaml` is not exercised) | US4 | +| Remove and re-register a cluster | same | US2 | +| Render combined, remote-only and standalone profiles; every invalid combination fails | `charts/tests/check_standalone.py`, chart-template job | US5, SC-005 | +| Store, key, rotation and registration behaviour on SQLite and PostgreSQL | `api/internal/controlplane/*_test.go`; `api-postgres` job adds `./internal/controlplane/...` | US2, US4, SC-003 | +| Remote access policy, bounded responses, SPDY guard, health and notification pollers | `api/internal/kube/*_test.go`, `api/internal/notify/*_test.go`, `api/internal/ws/*_test.go` | FR-012 to FR-015 | +| Handlers: registration, kubeconfig and gateway rotation, credential secrecy, module routing | `api/internal/handlers/*_test.go` | US2, US3, SC-006 | +| Dashboard with zero clusters, explicit targets, standalone admin screens | `web/src/routes/Standalone.test.tsx` and related tests | US3 | + +## What is not covered + +No `test/e2e/` test starts a standalone panel, and no CI job installs the Helm panel-only profile in a cluster. See [spec.md](spec.md#known-gaps). + +## Manual check (optional, on a host with Docker) + +1. Follow "Docker Compose" in `docs/standalone-panel.md`. +2. Expect: login works, Clusters says no cluster is registered, and the bell shows no stream. +3. Register a remote cluster with a self-contained kubeconfig; within about 30 s its status shows the server version. diff --git a/specs/025-standalone-panel/research.md b/specs/025-standalone-panel/research.md new file mode 100644 index 000000000..dfa971b97 --- /dev/null +++ b/specs/025-standalone-panel/research.md @@ -0,0 +1,57 @@ +# Research: Standalone Panel and API + +Retroactive. Each entry records a choice PR #630 made, the reason given in its code, docs or review, and the alternative it passed over. Except for R8, which valgul decided on PR #630, the alternatives are inferred while documenting; PR #630 did not record them. There are no open `NEEDS CLARIFICATION` items. + +## R1. Where management records live in standalone mode + +- **Decision**: Two tables in the panel database (`management_objects`, `management_key_state`), behind the same interface the combined install backs with Kubernetes Secrets and Cluster CRs. +- **Rationale**: The panel already owns a database that survives restarts; one interface keeps handlers identical in both modes. +- **Alternatives considered**: A local embedded Kubernetes API (k3s, kine) on the panel host, which reintroduces the cluster the feature removes; a separate key-value store, which is one more service to back up. + +## R2. Credential encryption and key custody + +- **Decision**: AES-256-GCM with a 32-byte key in a file outside the database, authenticated over the record identity, with a versioned `gpk1..` header. Deployment profiles put the key on a separate volume (`/keys`), or provision it read-only. +- **Rationale**: A stolen database backup alone reveals no credentials; binding the record identity stops ciphertext from being swapped between records. +- **Alternatives considered**: Storing the key in the database (defeats separation); deriving it from a passphrase at startup (needs an interactive or secret input on every restart). + +## R3. Key file validation and publication + +- **Decision**: Check type, size, link count, owner, mode and extended ACLs on the opened inode; publish a generated key with fsync, hard link and directory fsync under a lock file; fail closed on any doubt. +- **Rationale**: A key that another user can read or replace silently breaks the separation in R2; a key lost in a crash makes every credential unreadable. +- **Alternatives considered**: Trusting the mount, which offers no guarantee on Compose bind mounts or reused volumes. + +## R4. Master-key rotation + +- **Decision**: Offline command `api rotate-panel-key` that re-encrypts every record in one transaction and updates the singleton key-state row; every credential writer locks that row and checks the active key ID. +- **Rationale**: One transaction means no mixed-key state; the row lock stops a stale process from writing under a retired key. +- **Alternatives considered**: Online rotation with dual-key reads, which needs coordination across processes the single-process design (R8) does not have. + +## R5. Reaching remote clusters safely + +- **Decision**: Validate kubeconfigs (HTTPS, verified TLS, no proxy, no userinfo or query), refuse redirects, resolve once and dial only validated IPs, block loopback, metadata and non-global classes, optionally narrow by operator CIDRs, and bound every response and watch frame. +- **Rationale**: A registration administrator can point the panel anywhere; the panel must not become an SSRF pivot or be exhausted by a hostile endpoint. +- **Alternatives considered**: Reusing `netguard.IsAllowed` alone, which does not cover redirects, proxies or response size. + +## R6. Health and notifications without a central operator + +- **Decision**: A 30 s health poller writes a fixed status projection; a 5 s per-cluster notification poller takes bounded snapshots. +- **Rationale**: Informers would cache unbounded remote state centrally; polling with fixed budgets keeps memory bounded. +- **Alternatives considered**: Running an operator next to the panel, which the feature exists to avoid. + +## R7. Dashboard mode detection + +- **Decision**: An authenticated `GET /admin/installation` returning `standalone`, `localCluster` and `version`. +- **Rationale**: Core rule 3 forbids exposing deployment details before login. +- **Alternatives considered**: A public capabilities endpoint (leaks mode), or inferring mode from the cluster list (ambiguous when it is empty). + +## R8. Process model + +- **Decision**: One API process, `Recreate` strategy, one replica (valgul, 2026-10-14). +- **Rationale**: The audit hash chain and user-management safeguards use process-local locks. +- **Alternatives considered**: Database-coordinated locks with a shared key for rolling updates and replicas; deferred to a possible future HA feature. + +## R9. Deployment profiles + +- **Decision**: Helm `operator.enabled` and `api.standalone` with render-time guards and a panel-only example; Docker Compose with only `gameplane-api` and `web`. +- **Rationale**: Compose covers hosts without Kubernetes; the Helm profile covers operators who already run Kubernetes but want a central panel. +- **Alternatives considered**: Skipping CRDs from values, which Helm cannot do (the `crds/` directory installs before templates), so `--skip-crds` is documented instead (follow-up issue owed). diff --git a/specs/025-standalone-panel/spec.md b/specs/025-standalone-panel/spec.md index 29e172be5..10ecb14d1 100644 --- a/specs/025-standalone-panel/spec.md +++ b/specs/025-standalone-panel/spec.md @@ -1,10 +1,195 @@ -# Standalone panel and API +# Feature Specification: Standalone Panel and API -## Intent +**Feature Branch**: `feat/standalone-panel` (contributor fork `Baylem/Gameplane`, merged as PR #630, merge commit `6ed25c1`) + +**Created**: 2026-10-07 + +**Status**: Implemented (merged to `master` 2026-10-14). This folder was written retroactively; see [Context](#context). + +**Input**: Contributor design note for PR #630, preserved verbatim in [Original design note](#original-design-note-pr-630). + +## Context + +PR #630 (contributor Baylem) shipped this feature with a single design note at `specs/023-standalone-panel/spec.md`. That number clashed with `specs/023-change-based-test-selection/`, so the folder was renamed to `specs/025-standalone-panel/`. The spec was then rewritten into the spec-kit template, and the plan and tasks were written after the fact. They describe what PR #630 built and do not add work. Requirements the merged code does not fully meet are listed under [Known gaps](#known-gaps) and have no tasks. + +## Clarifications + +### Session 2026-10-14 (valgul, on PR #630 review) + +- Q: Should the panel API support overlapping processes (rolling updates, several replicas)? → A: No. Keep one API process with the `Recreate` strategy, because the audit hash chain and user-management safeguards use process-local locks. Multi-replica HA would be a separate future feature. +- Q: Must standalone installs send a telemetry marker before merge? → A: No. A telemetry marker for standalone installs is a follow-up, not part of this feature. +- Q: Must the chart enforce `--skip-crds` for the panel-only profile? → A: Not in this feature. The requirement is documented; valgul owes a follow-up issue. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Run a panel with no local cluster (Priority: P1) + +An operator runs the dashboard and API on a host that is not a Kubernetes cluster, such as a VM with Docker Compose. The panel starts with no kubeconfig, no Kubernetes service account, no operator and no agent. The operator bootstraps an admin, logs in, manages panel settings, and sees empty cluster and fleet lists. Nothing survives as a fake `local` cluster, and everything persists across restarts. + +**Why this priority**: This is the feature's purpose. Without it, every panel needs a local cluster. + +**Independent Test**: Start the Compose profile on a host with no Kubernetes, bootstrap an admin, log in, list clusters and fleet, restart, and log in again. + +**Acceptance Scenarios**: + +1. **Given** a host with no Kubernetes credentials, **When** the API starts in standalone mode, **Then** it starts without consulting a kubeconfig or in-cluster credentials and registers no `local` cluster. +2. **Given** a fresh standalone panel, **When** an authenticated user lists clusters or the fleet, **Then** both lists succeed and are empty. +3. **Given** a standalone panel with an admin and settings, **When** the containers restart, **Then** the admin can log in and settings are unchanged. +4. **Given** standalone mode, **When** cluster-ops (local credential minting) is also requested, **Then** startup fails with a clear error. + +--- + +### User Story 2 - Register, rotate and remove remote clusters (Priority: P1) + +An administrator registers a remote Kubernetes cluster (which runs its own operator and agents) by pasting a self-contained kubeconfig. The panel stores the credential encrypted, checks the cluster's health on a schedule, and lets the administrator replace the kubeconfig or the agent-gateway credentials, or remove the registration. + +**Why this priority**: A standalone panel is useless until it can manage remote clusters. + +**Independent Test**: Register a cluster, confirm its credential is ciphertext in the database, rotate its kubeconfig, restart, and remove it. + +**Acceptance Scenarios**: + +1. **Given** a valid kubeconfig with a verified HTTPS server, **When** it is registered, **Then** the registration and its credential are committed together, or neither is. +2. **Given** a kubeconfig that disables TLS verification, sets a proxy, or points at loopback, metadata or another unsafe address, **When** it is registered, **Then** the request is rejected. +3. **Given** a registered cluster, **When** its kubeconfig is replaced, **Then** the registration keeps its gateway settings and grants, and the old credential is removed. +4. **Given** any registration or gateway request, **When** a response body, error or log line is produced, **Then** it contains no credential material. + +--- + +### User Story 3 - Work on remote clusters from the dashboard (Priority: P2) + +A user picks a registered cluster in the dashboard and manages workloads, modules, module sources, backup destinations and node inventory there. Every request names the selected cluster explicitly and is checked against the user's grants for that cluster. Agent operations (console, files, logs) go through the remote cluster's agent gateway. + +**Why this priority**: Day-to-day use after registration. It depends on User Story 2. + +**Independent Test**: With two registered clusters and a user granted on one, confirm module and inventory requests reach only the selected cluster and are refused on the other. + +**Acceptance Scenarios**: + +1. **Given** zero registered clusters, **When** the dashboard loads, **Then** it shows a "register a cluster" or "select a cluster" state and sends no inventory request to the panel host. +2. **Given** standalone mode, **When** a module request omits the cluster or names `local`, **Then** the API refuses it. +3. **Given** a user without a cluster-wide modules or templates grant on the selected cluster, **When** they open Modules, **Then** management controls are disabled and writes are refused. +4. **Given** a logged-out visitor, **When** they load the login page, **Then** nothing reveals whether the panel is standalone. + +--- + +### User Story 4 - Keep the panel key safe and rotate it (Priority: P2) + +An operator keeps the encryption key on storage separate from the database, or provisions it read-only from a secret store. The panel refuses an unsafe, missing or wrong key rather than starting with data it cannot read. The operator can rotate the master key offline. + +**Why this priority**: Credential safety for every registered cluster depends on it. + +**Independent Test**: Start with a provisioned read-only key, then with a wrong key (startup fails), then rotate to a new key and confirm every credential still decrypts. + +**Acceptance Scenarios**: + +1. **Given** a key file with unsafe ownership, permissions, type, size, links or ACLs, **When** the API starts, **Then** it refuses to use it. +2. **Given** a database holding credentials and a missing or wrong key, **When** the API starts, **Then** it fails closed and never generates a replacement. +3. **Given** `--panel-key-provisioned`, **When** the key file is missing, **Then** startup fails and no key is generated. +4. **Given** an offline rotation, **When** it completes, **Then** every credential is re-encrypted under the new key in one transaction, the old key file is untouched, and a process still holding the old key cannot write. + +--- + +### User Story 5 - Install a panel-only Helm release (Priority: P3) + +An operator installs a central panel on a Kubernetes cluster that will not host games, using a documented panel-only Helm profile. The default combined install and the remote operator and gateway profiles behave as before. + +**Why this priority**: An alternative to Compose for operators who already run Kubernetes. + +**Independent Test**: Render the chart for combined, remote-only and standalone values and check which resources each profile contains. + +**Acceptance Scenarios**: + +1. **Given** `api.standalone=true` and `operator.enabled=false`, **When** the chart renders, **Then** it contains no operator, agent or local-credential resources and mounts the panel key from separate storage. +2. **Given** an invalid standalone combination (operator on, gateway on, more than one API replica, conflicting key sources), **When** the chart renders, **Then** rendering fails with a clear message. +3. **Given** default values, **When** the chart renders, **Then** the combined install is unchanged apart from the documented RBAC and service-account changes. + +--- + +### Edge Cases + +- Removing the cluster the user has selected clears the selection; no other cluster is picked automatically. +- A health check against an unreachable or misbehaving cluster records a fixed status message, never the transport error text. +- A compromised remote API that sends huge responses, huge watch frames or huge object lists cannot grow the panel's memory without bound. +- A remote API or gateway that answers with a redirect does not receive or forward credentials elsewhere. +- Concurrent key publication by two processes leaves exactly one key. +- A rotation interrupted after commit can be retried safely. +- Existing Kubernetes-managed registrations are not imported into standalone storage; switching an existing deployment needs a deliberate transfer and backups. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The API MUST offer a standalone mode (`--standalone`, `GAMEPLANE_STANDALONE=true`) that stores management records in the panel database and never discovers Kubernetes credentials. +- **FR-002**: In standalone mode the API MUST NOT register a synthetic `local` cluster; cluster and fleet lists MUST succeed when empty. +- **FR-003**: Cluster registrations and labelled management credentials MUST go through one narrow storage interface; the combined install MUST keep delegating it to Kubernetes. +- **FR-004**: Standalone credentials MUST be authenticated-encrypted with a key stored outside the database, selected by `--panel-key-file` (`GAMEPLANE_PANEL_KEY_FILE`, legacy default `/data/panel.key`; deployment profiles use `/keys/panel.key`). +- **FR-005**: `--panel-key-provisioned` MUST require an existing key and MUST NOT generate one. +- **FR-006**: The key file's ownership, permissions, type and size MUST be checked before use, and a generated key MUST be published crash-durably. +- **FR-007**: A missing, replaced or wrong key MUST fail closed when credentials exist. +- **FR-008**: Master-key rotation MUST be an offline, single-transaction re-encryption with versioned authenticated ciphertext and a singleton key-state lock shared by credential writers; old and new key files stay separate, and a stale process MUST NOT write under a retired key. +- **FR-009**: Initial standalone registration MUST commit the registration and its credential atomically. +- **FR-010**: Kubeconfig rotation MUST replace the credential reference with UID and concurrency checks and preserve gateway settings and grants. +- **FR-011**: Registration and gateway credentials MUST NOT appear in response bodies, errors or logs. +- **FR-012**: Remote clients MUST require verified HTTPS, ignore forward proxies, refuse redirects and validate the destination at dial time; optional operator CIDRs narrow access, and loopback, metadata and other unsafe address classes stay blocked. +- **FR-013**: Remote responses and watch frames MUST be bounded after decompression and before decoding, and notification snapshots MUST be bounded. +- **FR-014**: The API MUST maintain remote cluster health without a central operator. +- **FR-015**: Agent operations MUST use each remote cluster's existing private gateway protocol. +- **FR-016**: In standalone mode, module requests MUST target one explicitly selected registered cluster; workload, inventory and agent requests MUST target only selected registered clusters; RBAC MUST be preserved. Authentication, users, roles and installation settings stay central. +- **FR-017**: An authenticated endpoint MUST tell the dashboard which mode it runs in, without leaking infrastructure details before login. +- **FR-018**: The dashboard MUST derive cluster choices from the API, support zero clusters, and never fabricate a local entry or send background inventory requests to the panel host. +- **FR-019**: Helm MUST expose operator enablement and standalone API configuration independently, with a documented panel-only profile that suppresses workload resources and local credentials, and MUST keep combined and remote operator/gateway profiles working. +- **FR-020**: Docker Compose MUST contain only the API and web services, with persistent database and key storage. +- **FR-021**: The panel API MUST run as a single process; Helm MUST use the `Recreate` strategy and one replica in standalone mode (Clarification 2026-10-14). + +### Key Entities + +- **Management object**: a stored cluster registration or labelled credential, identified by kind, namespace and name, with a UID and version for optimistic concurrency. +- **Panel key**: the 32-byte master encryption key, kept outside the database. +- **Key state**: the singleton record of the active key identity and the previous one. +- **Cluster registration**: a remote cluster's name, display name, credential references, agent-gateway settings and health status. +- **Installation capabilities**: the authenticated answer to "is this panel standalone, does it have a local cluster, which version is it". + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: A standalone panel starts, bootstraps an admin, logs in, and returns empty cluster and fleet lists on a host with no Kubernetes (CI `standalone-compose` job). +- **SC-002**: After a restart, the admin, settings and every registration remain, and stored credentials are ciphertext in the database. +- **SC-003**: With a wrong or missing key and stored credentials, startup fails every time. +- **SC-004**: After rotation, 100% of stored credentials decrypt with the new key and none with the old one. +- **SC-005**: The chart renders the combined, remote-only and standalone profiles, and every invalid standalone combination fails to render (CI chart-template job). +- **SC-006**: No response body, error or log line produced by registration or gateway handlers contains credential material (handler tests). + +## Assumptions + +- Remote clusters run their own Gameplane operator and agents, installed with the remote operator and gateway profiles. +- Standalone mode is for fresh central panels; migration of an existing combined install is out of scope. +- Read-only mounting of a provisioned key is the deployment's job (Compose secret, Helm Secret volume); the API checks the file, not the mount. + +## Known gaps + +Found while documenting the merged code (2026-10-14). They are reported, not planned; no task exists for them here. + +- **No `test/e2e/` coverage for standalone mode** (Constitution I). Standalone is exercised only by the Compose smoke test (`deploy/standalone/smoke.py`, CI job `standalone-compose`) and render-only chart checks (`charts/tests/check_standalone.py`). No bucket in `test/e2e/buckets.sh` runs a standalone panel, and no CI job installs the Helm panel-only profile in a cluster. +- **Design came after the first UI code** (Constitution II). The first dashboard commit `0182e16` (2026-10-07) predates the design commit `6eb7ac3` (2026-10-08). The design export is complete in the merged tree. +- **`--skip-crds` is documented, not enforced.** A panel-only Helm install without `--skip-crds` still creates the Gameplane CRDs (`charts/gameplane/examples/standalone-panel-values.yaml`, `templates/NOTES.txt`). Follow-up issue owed by valgul. +- **Single API container in Compose is documented, not enforced.** Helm enforces `api.replicas=1` (`templates/validate.yaml`); `deploy/standalone/compose.yaml` does not. +- **Gateway redirects are not followed but not rejected.** The agent-gateway client returns a 3xx to the caller (`api/internal/ws/gateway_client.go`, `CheckRedirect` returns `http.ErrUseLastResponse`) instead of failing like the Kubernetes clients; no test covers it. Credentials are not sent to the redirect target. +- **Combined installs changed in three places** that FR-019 does not mention: module and template permissions now need a cluster-wide grant on the target in every install (`api/internal/auth/perms.go`); the agent-gateway destination guard now also applies to combined installs (`api/internal/ws/gateway_client.go`); `gameplane-api-read` gains `update` on clusters and web pods stop mounting a service-account token (`charts/gameplane/templates/api.yaml`, `web.yaml`). +- **Untested paths**: the 501 from Kubernetes system logs in standalone (`api/internal/handlers/systemlogs.go`), the standalone telemetry cluster count (`api/internal/telemetry/collect.go`), and a 401 from `/admin/installation` for anonymous callers (the handler test mounts it without the session group). +- **`deploy/standalone/compose.provisioned-key.yaml` is not exercised by CI.** `smoke.py` tests provisioned-key mode with its own generated override. +- **Telemetry marker for standalone installs**: follow-up (Clarification 2026-10-14). + +## Original design note (PR #630) + +The contributor's note. The text is unchanged; its title line was dropped and its headings were demoted one level to sit under this heading. + +### Intent Run the panel and API independently of the operator and agents, including with Docker Compose on a host that is not a Kubernetes cluster. Start with no registered clusters and add remote clusters explicitly. Preserve the default combined Kubernetes installation. -## Design +### Design `--standalone` (`GAMEPLANE_STANDALONE=true`) selects database-backed management storage and skips Kubernetes credential discovery. The API does not register a synthetic `local` cluster. Cluster registration metadata and labelled management credentials use a narrow storage interface; the existing installation delegates that interface to Kubernetes. Standalone secrets are authenticated-encrypted with a persistent key outside the database, selected by `--panel-key-file` (`GAMEPLANE_PANEL_KEY_FILE`, legacy default `/data/panel.key`; deployment profiles use separate storage at `/keys/panel.key`). Back up the database and key separately under different access policies. `--panel-key-provisioned` requires a protected existing key mounted read-only and never generates a missing key. File ownership, permissions, type and size are checked before use; generated key publication is crash durable. Losing or replacing a key must fail closed. @@ -18,7 +203,7 @@ The dashboard derives cluster choices from the API and supports zero registered Helm exposes independent operator enablement and standalone API configuration. A documented panel-only profile suppresses workload resources and local credentials. Docker Compose contains only API and web services with persistent database and key storage. Existing combined and remote operator/gateway profiles retain their behavior. -## Acceptance and validation +### Acceptance and validation - Start without kubeconfig, Kubernetes service credentials, operator, or agent. - Login, manage panel settings, register/remove remote clusters and survive restart. diff --git a/specs/025-standalone-panel/tasks.md b/specs/025-standalone-panel/tasks.md new file mode 100644 index 000000000..5fc1bef6d --- /dev/null +++ b/specs/025-standalone-panel/tasks.md @@ -0,0 +1,171 @@ +--- +description: "Task list for Feature 025: Standalone panel and API (retroactive, PR #630)" +--- + +# Tasks: Standalone Panel and API + +**Input**: `specs/025-standalone-panel/` (spec.md, plan.md, research.md, data-model.md, contracts/, quickstart.md, OPEN-DECISIONS.md) + +**Retroactive**: Tasks T001 to T049 were done by PR #630 (contributor Baylem, merge commit `6ed25c1`, 2026-10-14). Each names the files and the PR commits that did it (`git log 6ed25c1^1..6ed25c1^2`). T050 and T051 are bookkeeping: T050 records the merge, T051 is the follow-up documentation change on branch `claude/project-thread-gzh0dk`. No new work is listed; requirements the code does not fully meet are in [spec.md Known gaps](spec.md#known-gaps). + +**Tests**: PR #630 added tests with each change; they are listed in the task that introduced them. + +**Format**: `[ID] [P?] [Story] Description` + +## Phase 1: Setup + +- [X] T001 Add migration `api/internal/db/migrations/common/016_standalone_management.sql` (table `management_objects`) — `0182e16` +- [X] T002 Add migration `api/internal/db/migrations/common/017_standalone_key_state.sql` (table `management_key_state`) — `c01a963` + +## Phase 2: Foundational + +- [X] T003 Define the management storage interface (`SecretStore`, `ClusterStore`, `ClusterRegistrar`, `Client.Secrets`, `Client.Clusters`, `Client.IsStandalone`) in `api/internal/kube/management.go` and `api/internal/kube/client.go`; route handlers, `api/internal/auth/registry.go` and `api/internal/registry/keys.go` through it — `0182e16`, `c01e3bb`, `3d8a62e` +- [X] T004 Implement the SQL management store with compare-and-swap, label selectors, continue tokens and dry-run refusal in `api/internal/controlplane/store.go` and `api/internal/controlplane/resources.go`; tests `store_test.go`, `store_postgres_test.go` — `0182e16`, `9f698da`, `3035757` +- [X] T005 Separate management clients from workload clients and keep a management backend apart from the default cluster in `api/cmd/management.go` and `api/internal/kube/registry.go` (`Registry.SetManagement`) — `0182e16`, `3d8a62e` +- [X] T006 Run `./internal/controlplane/...` tests against PostgreSQL in the `api-postgres` job of `.github/workflows/ci.yaml` — `9f698da`, `b73bbbc` + +## Phase 3: User Story 1 - Run a panel with no local cluster (P1) + +**Goal**: The API and dashboard start and work with no Kubernetes on the host. + +**Independent Test**: CI `standalone-compose` job (`deploy/standalone/smoke.py`): bootstrap, login, empty clusters and fleet, restart. + +- [X] T007 [US1] Add `--standalone` / `GAMEPLANE_STANDALONE` and refuse cluster-ops and local credential minting with it in `api/cmd/main.go` and `api/cmd/management.go`; test `api/cmd/management_test.go` — `0182e16`, `3d8a62e`, `6564774` +- [X] T008 [P] [US1] Omit the synthetic `local` cluster from `/clusters`, fleet candidates and `/cluster` inventory in `api/internal/handlers/clusters.go`, `fleet.go`, `cluster.go`; test `standalone_test.go` — `0182e16`, `28dd876` +- [X] T009 [P] [US1] Return `501` with container-runtime guidance from Kubernetes system logs in standalone in `api/internal/handlers/systemlogs.go` — `0182e16` +- [X] T010 [P] [US1] Exclude the synthetic local cluster from the telemetry cluster count in `api/internal/telemetry/collect.go` — `0182e16` +- [X] T011 [US1] Add the Docker Compose profile with only `gameplane-api` and `web`, separate `panel-data` and `panel-keys` volumes and a hardened API container in `deploy/standalone/compose.yaml`; pre-create `/data` and `/keys` for the non-root user in `api/Dockerfile` — `0182e16`, `3d8a62e`, `7e6d247` +- [X] T012 [US1] Add the Compose smoke test `deploy/standalone/smoke.py` and the `standalone-compose` CI job with its `standalone` path filter in `.github/workflows/ci.yaml` — `0182e16`, `9f698da`, `a4cb5fe` + +**Checkpoint**: A Compose panel starts, logs in and persists with zero clusters. + +## Phase 4: User Story 2 - Register, rotate and remove remote clusters (P1) + +**Goal**: Remote clusters are registered with encrypted credentials, checked for health, rotated and removed safely. + +**Independent Test**: `smoke.py` registration, ciphertext snapshot, kubeconfig replacement, delete and re-register; handler and kube unit tests. + +- [X] T013 [US2] Encrypt credentials with AES-256-GCM, the `gpk1..` header and record-identity associated data in `api/internal/controlplane/key_ciphertext.go` and `store.go`; tests `store_test.go` — `0182e16`, `9f698da`, `c01a963` +- [X] T014 [US2] Commit standalone registration and its credential in one transaction, adopting only an exact API-owned orphan, in `api/internal/controlplane/registrations.go` and `api/internal/handlers/clusters.go`; tests `registrations_test.go`, `clusters_registration_test.go`, `standalone_storage_test.go` — `0182e16`, `c01e3bb`, `c01a963` +- [X] T015 [US2] Validate standalone kubeconfigs (HTTPS, verified TLS, no proxy, no userinfo, safe destination) and refuse redirects in `api/internal/kube/remote_access.go` and `loader.go`; tests `remote_access_test.go`, `clusters_remote_policy_test.go` — `3d8a62e`, `cc21cfa` +- [X] T016 [US2] Add dial-time destination checks with an optional `--remote-allowed-cidrs` allowlist in `api/internal/kube/remote_access.go`, `api/cmd/main.go`, `api/cmd/management.go` — `3d8a62e`, `cc21cfa` +- [X] T017 [US2] Bound decompressed responses (8 MiB, 64 KiB for `/version`) and watch frames (1 MiB) and reject unsolicited upgrades in `api/internal/kube/remote_response.go`; test `remote_response_test.go` — `8d94492`, `ca65c90`, `a42796f`, `cc21cfa` +- [X] T018 [US2] Sequence registry updates per cluster and reload credentials by UID in `api/internal/kube/registry.go`, `watch.go`, `loader.go`; test `watch_rotation_test.go` — `55f1bcf`, `cc21cfa`, `a0dc7c4` +- [X] T019 [US2] Poll remote health every 30 s and persist a fixed status projection in `api/internal/kube/watch_standalone.go`; tests `watch_standalone_test.go`, `api/internal/controlplane/remote_health_fixture_test.go` — `0182e16`, `8d94492`, `0828690`, `cc21cfa` +- [X] T020 [US2] Replace a registered kubeconfig in place (`PUT /clusters/{name}/kubeconfig`) preserving gateway and grants in `api/internal/handlers/clusters_kubeconfig.go`; tests `clusters_kubeconfig_test.go`, `credential_review_test.go` — `55f1bcf`, `ca5cbac`, `6564774` +- [X] T021 [US2] Configure and remove agent-gateway credentials (`PUT`/`DELETE /clusters/{name}/gateway`) in `api/internal/handlers/clusters_gateway.go`; test `clusters_gateway_test.go` — `28dd876`, `67fd660`, `6564774` +- [X] T022 [US2] Remove registrations and API-owned credentials with UID and version preconditions, keeping unrelated Secret keys, in `api/internal/handlers/clusters.go` and `secrets_managed.go`; test `secrets_managed_test.go` — `0182e16`, `ca5cbac`, `ca0f49b` +- [X] T023 [US2] Add the dashboard register and remove dialogs and empty states in `web/src/components/RegisterClusterDialog.tsx`, `web/src/routes/Clusters.tsx`, `web/src/lib/endpoints.ts`; test `web/src/routes/Standalone.test.tsx` — `0182e16`, `3741ae5`, `be6f0e6` + +**Checkpoint**: Registration, rotation and removal work with encrypted storage. + +## Phase 5: User Story 3 - Work on remote clusters from the dashboard (P2) + +**Goal**: Every module, inventory and agent request names an explicit registered cluster and respects grants on it. + +**Independent Test**: `web/src/routes/Standalone.test.tsx`, `api/internal/handlers/modules_cluster_test.go`, `api/internal/auth/perms_test.go`. + +- [X] T024 [US3] Add the authenticated `GET /admin/installation` in `api/internal/handlers/installation.go`, `api/cmd/main.go`, `api/internal/rbac/rbac.go`; test `installation_test.go` — `0182e16`, `9f698da`, `6564774` +- [X] T025 [US3] Route standalone `/modules` to one explicit registered cluster in `api/internal/handlers/modules_cluster.go`; test `modules_cluster_test.go` — `0182e16` +- [X] T026 [US3] Check modules and templates permissions as cluster-wide grants on the target, and allow them for remote bindings, in `api/internal/auth/perms.go`, `api/internal/rbac/rbac.go`, `api/internal/handlers/binding_permissions.go`, `fleet.go`; tests `perms_test.go`, `fleet_test.go` — `0182e16` +- [X] T027 [US3] Guard pod exec, attach and stdin with a policy-aware SPDY executor in `api/internal/kube/remote_spdy.go`, `stdin.go`, `api/internal/ws/attach.go`; test `remote_spdy_test.go` — `a42796f`, `cda9658`, `068eb4c`, `cc21cfa` +- [X] T028 [US3] Apply the destination guard to agent-gateway connections in `api/internal/ws/gateway_client.go`; tests `gateway_client_test.go`, `gateway_private_fixture_test.go` — `0182e16`, `3d8a62e`, `6564774` +- [X] T029 [US3] Take bounded per-cluster notification snapshots and tag events with a cluster in `api/internal/notify/poll_standalone.go`, `registry.go`, `watch.go`, `notify.go`, `format.go`; tests `poll_standalone_test.go`, `registry_test.go`, `format_test.go` — `0182e16`, `28dd876`, `8d94492`, `cc21cfa` +- [X] T030 [P] [US3] Read installation capabilities and drop the implicit `local` in `web/src/lib/useInstallation.ts`, `web/src/lib/cluster.ts`, `web/src/components/ClusterSelector.tsx`, `web/src/components/AppLayout.tsx`, `web/src/routes/Cluster.tsx`; tests `AppLayout.test.tsx`, `ClusterSelector.test.tsx`, `cluster.test.ts` — `0182e16`, `30a194f`, `3741ae5` +- [X] T031 [P] [US3] Target modules, sources, builder and uploads at an explicit cluster in `web/src/components/ModuleTarget.tsx`, `web/src/routes/Modules.tsx`, `web/src/components/modules/*`, `web/src/lib/endpoints.ts`, `web/src/lib/api.ts`; tests `moduleTargets.test.ts`, `ModuleCard.test.tsx`, `Modules.test.tsx` — `0182e16`, `30a194f`, `2e5e46a`, `3741ae5` +- [X] T032 [P] [US3] Adapt admin screens (backup destinations, About version, updates, system logs, notifications bell, user grants) in `web/src/routes/AdminSettings.tsx`, `AdminLogs.tsx`, `Users.tsx`, `web/src/components/ui/NotificationsPanel.tsx`; tests `AdminLogs.test.tsx`, `Users.test.tsx`, `NotificationsPanel.test.tsx` — `0182e16`, `30a194f`, `3741ae5` +- [X] T033 [US3] Design and export the 19 standalone frames in `design.pen` and `assets/design-export/` (MANIFEST section "Standalone panel (PR #630, 2026-10-08)") — `6eb7ac3`, `b73bbbc` + +**Checkpoint**: The dashboard works with zero, one or many registered clusters. + +## Phase 6: User Story 4 - Keep the panel key safe and rotate it (P2) + +**Goal**: The key lives apart from the database, is checked before use, fails closed and can be rotated offline. + +**Independent Test**: `api/internal/controlplane/key_*_test.go`, `api/cmd/panel_key_test.go`, `smoke.py` rotation and read-only provisioned-key run. + +- [X] T034 [US4] Add `--panel-key-file` and `--panel-key-provisioned` in `api/cmd/main.go` and `api/internal/controlplane/key_options.go`; tests `key_options_test.go`, `api/cmd/management_test.go` — `0182e16`, `7e6d247` +- [X] T035 [US4] Validate key ownership, mode, type, size, links and ACLs on the opened inode in `api/internal/controlplane/key_file_unix.go`, `key_acl_linux.go`, `key_acl_unsupported.go`, `key_file_unsupported.go`; tests `key_file_unix_test.go`, `key_acl_linux_test.go`, `key_review_test.go` — `2414e1a`, `7bca550`, `3035757` +- [X] T036 [US4] Publish generated keys crash-durably under a lock file in `api/internal/controlplane/store.go`, `key_directory.go`, `key_publication_unix.go`; test `key_durability_test.go` — `6dffc99`, `076fa85`, `e0e3595`, `3035757` +- [X] T037 [US4] Fail closed on a missing or wrong key and lock key state on every credential write in `api/internal/controlplane/key_state.go`; tests `store_test.go`, `key_rotation_test.go` — `9f698da`, `c01a963` +- [X] T038 [US4] Add offline `api rotate-panel-key` with environment-only DSN in `api/cmd/panel_key.go`, `api/cmd/main.go`, `api/internal/controlplane/key_rotation.go`; tests `panel_key_test.go`, `key_rotation_test.go`, `key_rotation_postgres_test.go` — `c01a963`, `3035757`, `6564774` +- [X] T039 [US4] Return typed, payload-free credential errors in `api/internal/controlplane/credential_error.go`; test `key_review_test.go` — `3035757` +- [X] T040 [US4] Add the provisioned-key Compose overlay `deploy/standalone/compose.provisioned-key.yaml` — `7e6d247` + +**Checkpoint**: Key custody and rotation work on SQLite and PostgreSQL. + +## Phase 7: User Story 5 - Install a panel-only Helm release (P3) + +**Goal**: A panel-only Helm profile next to unchanged combined and remote profiles. + +**Independent Test**: `charts/tests/check_standalone.py` in the chart-template CI job. + +- [X] T041 [US5] Add `operator.enabled` and guard every operator resource with `gameplane.operatorEnabled` in `charts/gameplane/values.yaml`, `templates/_helpers.tpl` and the operator templates — `0182e16` +- [X] T042 [US5] Add `api.standalone`, `api.panelKey.*` and `api.remoteAllowedCIDRs`, the key PVC and Secret mount, and standalone pod settings (`Recreate`, no service-account token) in `charts/gameplane/values.yaml`, `templates/api.yaml`, `templates/panel-key.yaml`; drop the service-account token from web pods in every profile in `templates/web.yaml` (see spec Known gaps) — `0182e16`, `7e6d247`, `391ca03`, `0ece9ae`, `3d8a62e` +- [X] T043 [US5] Fail invalid standalone renders in `charts/gameplane/templates/validate.yaml` — `0182e16`, `a4cb5fe` +- [X] T044 [US5] Add the panel-only example `charts/gameplane/examples/standalone-panel-values.yaml` and standalone `templates/NOTES.txt` — `0182e16`, `a4cb5fe` +- [X] T045 [US5] Add the chart matrix check `charts/tests/check_standalone.py` to the chart-template job in `.github/workflows/ci.yaml` — `0182e16`, `a4cb5fe`, `b73bbbc` + +**Checkpoint**: Combined, remote-only and standalone profiles render; invalid combinations fail. + +## Phase 8: Polish & Cross-Cutting Concerns + +- [X] T046 [P] Write the operator guide `docs/standalone-panel.md` and update `docs/install.md`, `docs/security.md`, `docs/architecture.md`, `docs/gateway-install.md`, `docs/multicluster-*.md`, `README.md`, `CHANGELOG.md`, `deploy/standalone/README.md` — `a60c4d3`, `6952098`, `b73bbbc` +- [X] T047 [P] Update module specs `api/specs.md` and `web/specs.md` (Constitution IV) — `0182e16`, `b73bbbc` +- [X] T048 [P] Keep one-time share-link tokens across Settings refreshes in `web/src/routes/tabs/Settings.tsx`; test `Settings.test.tsx` (fix bundled in PR #630, not standalone-specific) — `834c3ca` +- [X] T049 Verify remote template writes need a remote grant in `test/e2e/multicluster_e2e_test.go` (combined-mode RBAC, not a standalone test) — `28dd876` +- [X] T050 CI green on PR #630 head `117caf35` (97 checks), human approval, merge into `master` as `6ed25c1` (2026-10-14) +- [X] T051 Renumber this folder from `specs/023-standalone-panel/` to `specs/025-standalone-panel/`, update `api/specs.md`, and complete the spec-kit artifacts (this change) + +## Requirement Coverage + +| Requirement | Tasks | +|---|---| +| FR-001 | T007, T034 | +| FR-002 | T008 | +| FR-003 | T003, T004, T005 | +| FR-004 | T013, T034, T042 | +| FR-005 | T034, T040 | +| FR-006 | T035, T036 | +| FR-007 | T037 | +| FR-008 | T002, T037, T038 | +| FR-009 | T014 | +| FR-010 | T018, T020 | +| FR-011 | T020, T021, T022, T039 | +| FR-012 | T015, T016, T027, T028 | +| FR-013 | T017, T029 | +| FR-014 | T019 | +| FR-015 | T028 | +| FR-016 | T025, T026, T027, T029, T030, T031, T032 | +| FR-017 | T024, T030 | +| FR-018 | T023, T030, T031 | +| FR-019 | T041 to T045 | +| FR-020 | T011, T040 | +| FR-021 | T042, T043 (Helm only; Compose does not enforce one container, see spec Known gaps) | +| SC-001 | T011, T012 | +| SC-002 | T012, T013, T018 | +| SC-003 | T037 | +| SC-004 | T038 | +| SC-005 | T043, T045 | +| SC-006 | T020, T021, T022, T039 | + +## Dependencies & Execution Order + +- Phase 1 and Phase 2 came first: every story needs the tables (T001, T002) and the storage interface (T003 to T005). +- US1 and US2 are both P1. US2 needs T013 (encryption) and US4's key loading (T034); in PR #630 they landed together in `0182e16`, then were hardened. +- US3 depends on US2 (it needs registered clusters). US4 hardens storage from US2. US5 is independent of the API work except for the flags it passes. + +## Parallel Opportunities + +- T008, T009, T010 touch separate handlers. +- T030, T031, T032 touch separate dashboard areas. +- T046, T047, T048 touch separate files. + +## Implementation Strategy + +As delivered: one large PR. The first commit `0182e16` built the MVP across US1, US2, US3 and US5; later commits hardened key custody (US4), remote access (US2), and the dashboard after review. + +## Notes + +- Every task is ticked and PR #630 is merged, so Constitution IV and CLAUDE.md rule 16 call for renaming this folder to `done_025-standalone-panel`. The rename is pending OD-5, because Constitution I also requires a `test/e2e/` test and there is none (spec Known gaps). +- Follow-ups outside this feature: telemetry marker for standalone installs (OD-2); `--skip-crds` issue (OD-3). From a4ff2157d4cdb819fa68ba7696ca01600d18e554 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 21:44:33 +0000 Subject: [PATCH 3/4] docs: correct spec 025 requirements checklist Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claude --- specs/025-standalone-panel/checklists/requirements.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/specs/025-standalone-panel/checklists/requirements.md b/specs/025-standalone-panel/checklists/requirements.md index b6adb62be..8f738b2ae 100644 --- a/specs/025-standalone-panel/checklists/requirements.md +++ b/specs/025-standalone-panel/checklists/requirements.md @@ -16,7 +16,7 @@ - [x] No [NEEDS CLARIFICATION] markers remain - [x] Requirements are testable and unambiguous - [x] Success criteria are measurable -- [x] Success criteria are technology-agnostic (no implementation details) +- [ ] Success criteria are technology-agnostic (no implementation details) - [x] All acceptance scenarios are defined - [x] Edge cases are identified - [x] Scope is clearly bounded @@ -26,11 +26,12 @@ - [x] All functional requirements have clear acceptance criteria - [x] User scenarios cover primary flows -- [ ] Feature meets measurable outcomes defined in Success Criteria +- [x] Feature meets measurable outcomes defined in Success Criteria - [x] No implementation details leak into specification ## Notes - Written retroactively for merged PR #630. Flags, paths and file names appear because the contributor's design note named them and operators configure them; they are the user-facing contract, not implementation choices. -- "Feature meets measurable outcomes" stays unchecked: SC-001 to SC-006 are met by CI jobs and unit tests, but Constitution I requires a `test/e2e/` test, which does not exist (spec Known gaps). +- "Technology-agnostic" stays unchecked: SC-001, SC-005 and SC-006 name the CI jobs and tests that measure them, because this folder documents shipped behaviour. +- SC-001 to SC-006 are met by CI jobs and unit tests. Separately, Constitution I requires a `test/e2e/` test, which does not exist (spec Known gaps, OD-5). - OD-4 (legacy key formats) and OD-5 (`done_` rename) are open; neither changes a requirement of the merged feature. From 5b018ccbdf010ddebd481455215051356621f6a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 22:43:36 +0000 Subject: [PATCH 4/4] docs: record spec 025 OD-4 and OD-5 rulings valgul kept the legacy panel-key formats and kept the 025- folder name until a standalone E2E test exists. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claude --- specs/025-standalone-panel/OPEN-DECISIONS.md | 8 ++++---- specs/025-standalone-panel/checklists/requirements.md | 2 +- specs/025-standalone-panel/plan.md | 2 +- specs/025-standalone-panel/tasks.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/specs/025-standalone-panel/OPEN-DECISIONS.md b/specs/025-standalone-panel/OPEN-DECISIONS.md index 5b0addfad..61d03f1ea 100644 --- a/specs/025-standalone-panel/OPEN-DECISIONS.md +++ b/specs/025-standalone-panel/OPEN-DECISIONS.md @@ -14,23 +14,23 @@ A follow-up, not part of this feature. No task here. Documented requirement only in this feature; valgul agreed to open a follow-up issue. No task here. -## OD-4: Legacy panel-key formats — **Open** +## OD-4: Legacy panel-key formats — **Settled (valgul, 2026-10-14): keep** PR #630 merged with two legacy paths still supported: headerless ciphertext (decrypted and re-encrypted with the `gpk1.` header by rotation) and the legacy default key path `/data/panel.key` (on the database volume). This was still an open review thread on PR #630 when it merged. - **Keep** (as merged): existing early standalone installs keep working; no change. - **Drop in a follow-up**: remove headerless decryption and make the key path required, after a release note. -Recommendation: keep. Asked in the spec 025 thread on 2026-10-14. +Ruling: keep both legacy paths as merged. No follow-up. -## OD-5: Archive this folder as `done_025-standalone-panel` — **Open** +## OD-5: Archive this folder as `done_025-standalone-panel` — **Settled (valgul, 2026-10-14): keep `025-`** Every task is ticked and PR #630 is merged, so Constitution IV and CLAUDE.md rule 16 call for `git mv specs/025-standalone-panel specs/done_025-standalone-panel` in its own `docs:` commit. Constitution I also says a feature is incomplete without a `test/e2e/` test, and standalone mode has none (spec Known gaps). - **Keep `025-`**: leave it in flight until a standalone E2E test exists, which would be new work under a new decision. - **Archive now**: rename to `done_025-`; the missing E2E test stays recorded as a known gap. -Recommendation: keep `025-`. Asked in the spec 025 thread on 2026-10-14. +Ruling: the folder keeps the bare `025-` name until a standalone E2E test exists. Adding that test is new work and needs its own decision. ## Moot diff --git a/specs/025-standalone-panel/checklists/requirements.md b/specs/025-standalone-panel/checklists/requirements.md index 8f738b2ae..5e83ce59e 100644 --- a/specs/025-standalone-panel/checklists/requirements.md +++ b/specs/025-standalone-panel/checklists/requirements.md @@ -34,4 +34,4 @@ - Written retroactively for merged PR #630. Flags, paths and file names appear because the contributor's design note named them and operators configure them; they are the user-facing contract, not implementation choices. - "Technology-agnostic" stays unchecked: SC-001, SC-005 and SC-006 name the CI jobs and tests that measure them, because this folder documents shipped behaviour. - SC-001 to SC-006 are met by CI jobs and unit tests. Separately, Constitution I requires a `test/e2e/` test, which does not exist (spec Known gaps, OD-5). -- OD-4 (legacy key formats) and OD-5 (`done_` rename) are open; neither changes a requirement of the merged feature. +- OD-4 (keep legacy key formats) and OD-5 (keep the `025-` name) are settled; neither changes a requirement of the merged feature. diff --git a/specs/025-standalone-panel/plan.md b/specs/025-standalone-panel/plan.md index ab4c2dfd4..d73d1b23c 100644 --- a/specs/025-standalone-panel/plan.md +++ b/specs/025-standalone-panel/plan.md @@ -43,7 +43,7 @@ The API gains a standalone mode that runs without any local Kubernetes cluster. | V. Delegation | Not applicable | Contributor PR. | | VI. CI bears the load | Met | All 97 checks green on head `117caf35` before merge. | -**Gate decision**: The feature is merged, so the gate cannot block it. Every task is ticked, so rule 16 calls for the `done_025-` rename; it is pending OD-5 because of the Principle I gap (see [OPEN-DECISIONS.md](OPEN-DECISIONS.md)). +**Gate decision**: The feature is merged, so the gate cannot block it. Every task is ticked, so rule 16 calls for the `done_025-` rename; valgul decided to keep the `025-` name until a standalone E2E test exists (OD-5) (see [OPEN-DECISIONS.md](OPEN-DECISIONS.md)). ## Project Structure diff --git a/specs/025-standalone-panel/tasks.md b/specs/025-standalone-panel/tasks.md index 5fc1bef6d..172697680 100644 --- a/specs/025-standalone-panel/tasks.md +++ b/specs/025-standalone-panel/tasks.md @@ -167,5 +167,5 @@ As delivered: one large PR. The first commit `0182e16` built the MVP across US1, ## Notes -- Every task is ticked and PR #630 is merged, so Constitution IV and CLAUDE.md rule 16 call for renaming this folder to `done_025-standalone-panel`. The rename is pending OD-5, because Constitution I also requires a `test/e2e/` test and there is none (spec Known gaps). +- Every task is ticked and PR #630 is merged, so Constitution IV and CLAUDE.md rule 16 call for renaming this folder to `done_025-standalone-panel`. valgul decided to keep the `025-` name until a standalone E2E test exists, because Constitution I also requires one and there is none (OD-5, spec Known gaps). - Follow-ups outside this feature: telemetry marker for standalone installs (OD-2); `--skip-crds` issue (OD-3).