diff --git a/tasks/0.1-gitops-repo.md b/tasks/0.1-gitops-repo.md new file mode 100644 index 0000000..1d5f4a8 --- /dev/null +++ b/tasks/0.1-gitops-repo.md @@ -0,0 +1,15 @@ +# 0.1 — Resolve GitOps repo ownership + +Phase: 0 — Unblock + +Depends on: [00-decisions.md](00-decisions.md) decision #1 + +- [ ] Resolve which GitOps repo owns the portfolio; delete or correct the losing manifest + +Cluster evidence (2026-08-18, `kubectl get applications -n argocd`): `portfolio` and `auth-infra` (from this repo's `infra/argocd-apps.yaml`) do not exist in-cluster. `homarr` — also defined in this repo's manifest — does exist, but its live source is 100% `homelab-root` (`github.com/Riotpiaole/riotpiao.homelab.com`, `project: homelab`), not this repo. Points toward homelab-root as authoritative, but decision is still open — needs explicit sign-off, not inferred. + +## Verify + +```bash +kubectl get application portfolio -n argocd # shows one, correct, source +``` diff --git a/tasks/0.2-fix-nxdomain.md b/tasks/0.2-fix-nxdomain.md new file mode 100644 index 0000000..7967fd9 --- /dev/null +++ b/tasks/0.2-fix-nxdomain.md @@ -0,0 +1,15 @@ +# 0.2 — Fix NXDOMAIN repoURL/image reference + +Phase: 0 — Unblock + +Depends on: [0.1](0.1-gitops-repo.md) + +- [ ] Point `infra/argocd-apps.yaml` `repoURL` and image reference at real hostnames (fix NXDOMAIN) + +Root cause confirmed (2026-08-18): `*.riotpiao.homelab.com` is fully retired, replaced by `*.riotpiao.com`. `infra/argocd-apps.yaml` still points at `forgejo.riotpiao.homelab.com` (dead domain); `forgejo.riotpiao.com` is live and returns 200. Fix is a straight domain swap in `infra/argocd-apps.yaml` (`portfolio` and `auth-infra` Application `spec.source.repoURL`) — not open diagnosis anymore. `portfolio` namespace still does not exist in-cluster until this + [0.1](0.1-gitops-repo.md) land. + +## Verify + +```bash +kubectl get pods -n portfolio # 2/2 Running +``` diff --git a/tasks/0.3-immutable-tags.md b/tasks/0.3-immutable-tags.md new file mode 100644 index 0000000..1eb7645 --- /dev/null +++ b/tasks/0.3-immutable-tags.md @@ -0,0 +1,11 @@ +# 0.3 — Replace :latest with commit-SHA tag + +Phase: 0 — Unblock + +- [ ] Replace `:latest` tag with commit-SHA tag in `infra/portfolio/base/deployment.yaml`; keep `imagePullPolicy: IfNotPresent` (correct once tags are immutable) + +## Verify + +```bash +# new commit -> new tag -> Argo rolls out automatically, no manual `kubectl set image` +``` diff --git a/tasks/0.4-forgejo-init-stuck.md b/tasks/0.4-forgejo-init-stuck.md new file mode 100644 index 0000000..0e2f3be --- /dev/null +++ b/tasks/0.4-forgejo-init-stuck.md @@ -0,0 +1,19 @@ +# 0.4 — Diagnose forgejo-gitea stuck Init:0/3 + +Phase: 0 — Unblock + +- [x] Diagnose `forgejo-gitea` stuck `Init:0/3` (3h+) — read init container logs before changing anything + +Resolved — `forgejo-gitea` pod Running 1/1 (confirmed via `kubectl get pods -n cicd`, 2026-08-18). + +## Command + +```bash +kubectl logs -n cicd -c +``` + +## Verify + +```bash +kubectl get pods -n cicd # forgejo-gitea 1/1 Running +``` diff --git a/tasks/0.5-apex-403.md b/tasks/0.5-apex-403.md new file mode 100644 index 0000000..8dff763 --- /dev/null +++ b/tasks/0.5-apex-403.md @@ -0,0 +1,15 @@ +# 0.5 — Diagnose apex 403 + +Phase: 0 — Unblock + +Depends on: [00-decisions.md](00-decisions.md) decision #2 + +- [ ] Diagnose apex 403 — check Cloudflare tunnel Public Hostnames list and WAF event log + +Partial evidence (2026-08-18): `kubectl get ingress -A` has zero rules for bare host `riotpiao.com` — every ingress is a subdomain (22 hosts, all `*.riotpiao.com` per the `*.riotpiao.homelab.com` retirement). No in-cluster origin exists for the apex today. Leans toward "no origin configured" as the cause, but Cloudflare tunnel Public Hostnames list + WAF event log still needed to confirm the tunnel-side config (not visible from `kubectl`). + +## Verify + +```bash +curl -sS -o /dev/null -w '%{http_code}\n' https://riotpiao.com # 200 +``` diff --git a/tasks/0.6-vitest-setup.md b/tasks/0.6-vitest-setup.md new file mode 100644 index 0000000..6cf36f6 --- /dev/null +++ b/tasks/0.6-vitest-setup.md @@ -0,0 +1,11 @@ +# 0.6 — Add Vitest + Testing Library + msw + +Phase: 0 — Unblock + +- [ ] Add Vitest + Testing Library + `msw`; add `test` and `test:watch` scripts to `package.json` + +## Verify + +```bash +pnpm test # runner executes, 0 tests, exit 0 +``` diff --git a/tasks/0.6a-playwright-setup.md b/tasks/0.6a-playwright-setup.md new file mode 100644 index 0000000..021915d --- /dev/null +++ b/tasks/0.6a-playwright-setup.md @@ -0,0 +1,12 @@ +# 0.6a — Add Playwright (hard requirement) + +Phase: 0 — Unblock + +- [ ] Add Playwright (`pnpm create playwright`); add `test:e2e` script to `package.json`; wire into CI as a hard gate (build fails if `test:e2e` fails) +- [ ] Playwright is a hard requirement, not optional, for every phase that ships a UI surface (Phase 2, 3, 4, 5) — no surface merges without a passing e2e spec + +## Verify + +```bash +pnpm test:e2e # runner executes, 0 tests, exit 0 +``` diff --git a/tasks/0.7-triage-unrelated.md b/tasks/0.7-triage-unrelated.md new file mode 100644 index 0000000..17119f6 --- /dev/null +++ b/tasks/0.7-triage-unrelated.md @@ -0,0 +1,10 @@ +# 0.7 — Triage unrelated cluster issues + +Phase: 0 — Unblock + +- [ ] Triage unrelated cluster issues: `sms` Application Degraded (`macos-bluebubbles` Pending 3h), `longhorn-config` OutOfSync + - Unrelated to atlas, but delivery tree (Phase 3) will render both red on day one — fix or explicitly accept as known-red + +Status (2026-08-18, `kubectl get application -n argocd`): +- `longhorn-config` — Synced / Healthy. Resolved. +- `sms` — Synced / Degraded. Still open. diff --git a/tasks/00-decisions.md b/tasks/00-decisions.md index da06d2c..a46b0d3 100644 --- a/tasks/00-decisions.md +++ b/tasks/00-decisions.md @@ -6,7 +6,9 @@ All five must resolve before Phase 1 starts. Source: ADR-0001 review notes. Two roots exist: - `homelab-root` → `git@github.com:Riotpiaole/riotpiao.homelab.com.git`, path `k8s/argocd/apps` (31 child Applications, this is the live one) -- This repo's `infra/argocd-apps.yaml` → `forgejo.riotpiao.homelab.com` (NXDOMAIN, `portfolio`/`auth-infra` Applications don't exist in cluster) +- This repo's `infra/argocd-apps.yaml` → `forgejo.riotpiao.homelab.com` (`portfolio`/`auth-infra` Applications don't exist in cluster) + +**Update (2026-08-18):** `*.riotpiao.homelab.com` is fully retired — confirmed NXDOMAIN across the board (`argocd.riotpiao.homelab.com`, `forgejo.riotpiao.homelab.com`, etc.), not just the forgejo subdomain. Live ingress hosts are all `*.riotpiao.com` now (`forgejo.riotpiao.com` → 200, `argocd.riotpiao.com`, `vault.riotpiao.com`, etc. — 22 hosts total). `infra/argocd-apps.yaml`'s `repoURL: https://forgejo.riotpiao.homelab.com/...` in this repo is pointed at the dead domain; the working equivalent is `forgejo.riotpiao.com`. This doesn't answer which repo is authoritative, but it means 0.2's NXDOMAIN fix is now a known, mechanical domain swap rather than an open diagnosis. **Decision needed:** GitHub or Forgejo. Blocks Phase 0.1–0.3. @@ -14,6 +16,8 @@ Two roots exist: `riotpiao.com` resolves via Cloudflare (172.67.196.33 / 104.21.60.115) but returns HTTP 403 at the edge, no origin headers. +**Update (2026-08-18):** `kubectl get ingress -A` shows zero ingress objects with host exactly `riotpiao.com` (bare apex) — every rule is a subdomain (`argocd.`, `forgejo.`, `grafana.`, ...). No in-cluster origin is configured for the apex at all, which is consistent with (though not proof of) "no origin configured" as the 403 cause. Still need the Cloudflare tunnel Public Hostnames list / WAF event log to confirm whether the tunnel even has an apex entry, since that's Cloudflare-side config not visible from `kubectl`. + **Decision needed:** tunnel route missing, WAF rule, or no origin configured at all. Check Cloudflare tunnel Public Hostnames list + WAF event log (event log names the blocking rule). Blocks Phase 0.5. ## 3. Is homarr still wanted? @@ -34,7 +38,7 @@ This repo (`riotpiao`) or the homelab repo. Follows from decision 1 — whicheve --- -Once answered, update this file with the decisions taken (date + rationale) before starting [01-phase0-unblock.md](01-phase0-unblock.md). +Once answered, update this file with the decisions taken (date + rationale) before starting Phase 0 tasks in [INDEX.md](INDEX.md). ## Hard requirement: Playwright diff --git a/tasks/01-phase0-unblock.md b/tasks/01-phase0-unblock.md deleted file mode 100644 index ba34d2b..0000000 --- a/tasks/01-phase0-unblock.md +++ /dev/null @@ -1,44 +0,0 @@ -# Phase 0 — Unblock - -Blocking. Nothing ships until this lands. Requires [00-decisions.md](00-decisions.md) #1 and #2 answered first. - -## Tasks - -- [ ] **0.1** Resolve which GitOps repo owns the portfolio; delete or correct the losing manifest - - Depends on: decision #1 - - Verify: `kubectl get application portfolio -n argocd` shows one, correct, source - -- [ ] **0.2** Point `infra/argocd-apps.yaml` `repoURL` and image reference at real hostnames (fix NXDOMAIN) - - Verify: `kubectl get pods -n portfolio` → `2/2 Running` - -- [ ] **0.3** Replace `:latest` tag with commit-SHA tag in `infra/portfolio/base/deployment.yaml`; keep `imagePullPolicy: IfNotPresent` (correct once tags are immutable) - - Verify: new commit → new tag → Argo rolls out automatically, no manual `kubectl set image` - -- [ ] **0.4** Diagnose `forgejo-gitea` stuck `Init:0/3` (3h+) — read init container logs before changing anything - - Command: `kubectl logs -n cicd -c ` - - Verify: pod reaches `Running`, image builds succeed - -- [ ] **0.5** Diagnose apex 403 — check Cloudflare tunnel Public Hostnames list and WAF event log - - Depends on: decision #2 - - Verify: `curl -sS -o /dev/null -w '%{http_code}\n' https://riotpiao.com` → `200` - -- [ ] **0.6** Add Vitest + Testing Library + `msw`; add `test` and `test:watch` scripts to `package.json` - - Verify: `pnpm test` → runner executes, 0 tests, exit 0 - -- [ ] **0.6a** Add Playwright (`pnpm create playwright`); add `test:e2e` script to `package.json`; wire into CI as a hard gate (build fails if `test:e2e` fails) - - Playwright is a hard requirement, not optional, for every phase that ships a UI surface (Phase 2, 3, 4, 5) — no surface merges without a passing e2e spec - - Verify: `pnpm test:e2e` → runner executes, 0 tests, exit 0 - -- [ ] **0.7** Triage unrelated cluster issues: `sms` Application Degraded (`macos-bluebubbles` Pending 3h), `longhorn-config` OutOfSync - - Unrelated to atlas, but delivery tree (Phase 3) will render both red on day one — fix or explicitly accept as known-red - -## Phase verify - -```bash -kubectl get pods -n portfolio # 2/2 Running -curl -sS -o /dev/null -w '%{http_code}\n' https://riotpiao.com # 200 -pnpm test # runner executes, 0 tests, exit 0 -pnpm test:e2e # runner executes, 0 tests, exit 0 -``` - -Next: [02-phase1-atlas-core.md](02-phase1-atlas-core.md) diff --git a/tasks/02-phase1-atlas-core.md b/tasks/02-phase1-atlas-core.md deleted file mode 100644 index 7949e06..0000000 --- a/tasks/02-phase1-atlas-core.md +++ /dev/null @@ -1,35 +0,0 @@ -# Phase 1 — atlas core - -~4 days. Every surface depends on this. RED → GREEN → REFACTOR. - -## RED (write tests first, confirm they fail) - -- [ ] `redact_test.go` — golden test: serialized snapshot contains none of the denied fields, run against a fixture captured from the real cluster -- [ ] `rbac_test.go` — atlas ServiceAccount receives 403 on `get secrets` in every namespace -- [ ] `snapshot_test.go` — an informer event produces the expected delta - -## GREEN - -- [ ] ClusterRole: verbs `get,list,watch` only, explicit resource list — no `secrets`, no `*`, no wildcard apiGroups -- [ ] client-go informers: kube API (nodes, namespaces, workloads), Argo CD `Application` CRs -- [ ] Reducer: informer events → in-memory snapshot, redacted **at write time** -- [ ] DTO construction — allowlist only. Emitted: name, namespace, kind, phase, ready counts, restart count, age, node name, health status, sync status, sync wave, explicit label subset. Never emitted: container env/args, image digests/tags, `spec.source.repoURL`, `spec.source.path`, annotations, pod IPs, cluster IPs, Secret names, `status.conditions[].message`, node internal IPs -- [ ] Redis publish (snapshot deltas → `kmsvc-redis-master.sqs:6379`) -- [ ] NetworkPolicy on atlas: egress restricted to kube API, `prometheus-operated.monitoring`, `reasoning-predictor.llm-serving`, `kmsvc-redis-master.sqs`; ingress from `ingress-nginx` only -- [ ] Container hardening: `runAsNonRoot`, read-only root filesystem, all capabilities dropped, `seccompProfile: RuntimeDefault` - -## REFACTOR - -- [ ] Run `simplify` skill pass on reducer/DTO code -- [ ] Confirm no `_ =` on errors, no naked returns, every upstream call carries a `context.Context` (go-error-handling, go-context skills) - -## Verify - -```bash -kubectl auth can-i get secrets --as=system:serviceaccount:portfolio:atlas # no -go test ./... -run TestRedact -v -go test ./... -run TestRBAC -v -go test ./... -run TestSnapshot -v -``` - -Next: [03-phase2-topology.md](03-phase2-topology.md) diff --git a/tasks/03-phase2-topology.md b/tasks/03-phase2-topology.md deleted file mode 100644 index ff77334..0000000 --- a/tasks/03-phase2-topology.md +++ /dev/null @@ -1,31 +0,0 @@ -# Phase 2 — Surface B: cluster topology - -~3 days. Proves the snapshot + SSE pipeline end to end. - -## RED - -- [ ] `stream_test.go` — SSE emits a delta within 5s of a pod state change -- [ ] `topology.test.tsx` — graph re-renders on delta without a full reload -- [ ] `topology.e2e.ts` (Playwright, hard requirement) — load `/topology`, assert graph nodes render, delete a pod, assert node count updates without reload - -## GREEN - -- [ ] `GET /api/topology` — nodes, namespaces, workload summaries; envelope `{"data": {}, "meta": {"snapshotAge", "generation"}}`; capped 256 KB, `meta.truncated: true` on overflow, never a silent drop -- [ ] `GET /api/stream` — SSE, session cookie, 2 concurrent/IP, `topology` event type; keepalive comment frame every 30s; `Last-Event-ID` supported for resumable deltas -- [ ] Frontend: React Flow, force layout, node → namespace → workload -- [ ] Security headers on all responses: CSP (no `unsafe-inline`), `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, HSTS; CORS same-origin only -- [ ] Rate limiting Tier 1 (Cloudflare edge — WAF, Bot Fight Mode, per-IP rules) + Tier 2 (Kong `rate-limiting`, `policy: redis`, generous profile for topology) - -## REFACTOR - -- [ ] `simplify` pass on SSE handler + React Flow wiring - -## Verify - -```bash -# delete a pod, observe graph update in <5s without reloading -kubectl delete pod -n -pnpm test:e2e topology.e2e.ts # required, must pass before merge -``` - -Next: [04-phase3-delivery.md](04-phase3-delivery.md) diff --git a/tasks/04-phase3-delivery.md b/tasks/04-phase3-delivery.md deleted file mode 100644 index f8352d5..0000000 --- a/tasks/04-phase3-delivery.md +++ /dev/null @@ -1,33 +0,0 @@ -# Phase 3 — Surface E: delivery tree - -~3 days. Zero new data sources, zero new attack surface, highest signal — reads as platform engineering, not hobby. - -## RED - -- [ ] `delivery_test.go` — apps group correctly by `sync-wave`; Secret names absent from output (kind+count only); `repoURL` absent from output -- [ ] `delivery.test.tsx` — 550-node tree renders under frame budget with virtualization on -- [ ] `delivery.e2e.ts` (Playwright, hard requirement) — load `/delivery`, click app node, assert side panel opens with virtualized resource tree, assert no Secret names appear in DOM - -## GREEN - -- [ ] `GET /api/delivery` — Argo apps, wave-grouped (0→8), resource children lazy; capped 256 KB -- [ ] `GET /api/delivery/{app}/resources` — cursor-paginated at 100 items (prometheus alone has 68 resources today) -- [ ] Frontend: React Flow, wave columns left→right from `sync-wave` annotations. Click app → side panel with `react-arborist` virtualized resource tree, lazy-loaded children -- [ ] Live sync animation `OutOfSync → Syncing → Synced` driven by Application watch (reuse Phase 2 SSE `/api/stream`, add `delivery` event type) -- [ ] Redaction check specific to this surface: 21 `Secret` resources appear in Argo trees today — render kind+count only, never names (includes `sops-secrets`); `status.conditions[].message` echoes raw errors with internal hostnames — emit condition **type** only - -## REFACTOR - -- [ ] `simplify` pass on wave-grouping + virtualized tree code - -## Verify - -```bash -# trigger an Argo sync, observe wave-ordered animation -argocd app sync homelab-root -pnpm test:e2e delivery.e2e.ts # required, must pass before merge -``` - -Also in scope for this phase: delete fabricated stats in `app/page.tsx` ("40% CPU reduction", "99.2% uptime", "Mission-critical", "60% latency cut") — wire each card to a real number from `/api/topology` or `/api/delivery`, or remove the claim. Five of six landing cards link to routes that don't exist (`/infrastructure`, `/systems`, `/llm`, `/kafka`, `/opensource`) — fix or remove. - -Next: [05-phase4-terminal.md](05-phase4-terminal.md) diff --git a/tasks/05-phase4-terminal.md b/tasks/05-phase4-terminal.md deleted file mode 100644 index 4b329f3..0000000 --- a/tasks/05-phase4-terminal.md +++ /dev/null @@ -1,32 +0,0 @@ -# Phase 4 — Surface C: terminal - -~2 days. First surface accepting user input — injection (A03) is the primary risk here. - -## RED - -- [ ] `exec_parse_test.go` — fuzz corpus: every non-allowlisted input rejects and performs zero upstream calls -- [ ] `exec_test.go` — unknown namespace rejects on snapshot membership, not regex -- [ ] `terminal.e2e.ts` (Playwright, hard requirement) — load terminal UI, submit `get nodes`, assert rendered output; submit an injection payload, assert rejection surfaces in UI, zero upstream call - -## GREEN - -- [ ] `POST /api/exec` — session cookie, 20/min/session; input parses to a closed command enum, anything unmatched rejected before any lookup -- [ ] Command set: `get nodes`, `get pods `, `get apps`, `top nodes`, `describe pod `, `help` -- [ ] Namespace and resource-name arguments validated by **set membership against current snapshot**, not regex/escaping -- [ ] No shell, no `exec`, no `kubectl` binary in the container image -- [ ] Frontend: reuse [components/InteractiveTerminal.tsx](../components/InteractiveTerminal.tsx), wire to `/api/exec` -- [ ] Structured logging: every rejected `/api/exec` input logged - -## REFACTOR - -- [ ] `simplify` pass on the enum parser - -## Verify - -```bash -# attempt injection payloads against /api/exec; all rejected, all logged -curl -X POST https://riotpiao.com/api/exec -d '{"cmd":"get pods; rm -rf /"}' -pnpm test:e2e terminal.e2e.ts # required, must pass before merge -``` - -Next: [06-phase5-chat.md](06-phase5-chat.md) diff --git a/tasks/06-phase5-chat.md b/tasks/06-phase5-chat.md deleted file mode 100644 index 4a95f2b..0000000 --- a/tasks/06-phase5-chat.md +++ /dev/null @@ -1,48 +0,0 @@ -# Phase 5 — Surface D: chat + rate limiter - -~5 days, one PR. Highest risk, highest cost — ships last, ships with its limiter, never after. - -Depends on [00-decisions.md](00-decisions.md) #4 (is chat in scope for v1). - -## RED - -- [ ] `ratelimit_test.go` — 7th concurrent chat queues rather than reaching vLLM -- [ ] `disconnect_test.go` — client abort cancels the upstream request -- [ ] `budget_test.go` — 13th message in 24h returns `429` with `Retry-After` -- [ ] `injection_test.go` — snapshot content cannot alter system-prompt behaviour -- [ ] `context_test.go` — history truncation keeps total tokens under 16384 -- [ ] `chat.e2e.ts` (Playwright, hard requirement) — load chat UI, send message, assert streamed tokens render, assert queue position renders under load, assert reasoning block is collapsible - -## GREEN - -- [ ] `POST /api/chat` — session + Turnstile, 12/day/session, 6 global concurrent, SSE token stream -- [ ] Rate limiting Tier 3 (atlas): global chat semaphore = 6 (2 of 8 GPU slots kept as operator headroom); queue depth 20 then reject `429`; per-session budget 12 msg/24h; per-request timeout 120s hard server-side; disconnect cancels upstream immediately (`req.Context()` threaded to vLLM request) -- [ ] Queue position streamed as SSE `{"type":"queue","position":N}` -- [ ] Prompt injection defenses: system prompt is compile-time constant, unreachable by user input; cluster snapshot digest injected in a delimited block explicitly labelled untrusted data; user message always last; **no tool-calling** — model reads pre-built digest, cannot query anything; `max_tokens: 1500` cap -- [ ] Context budget (16384 total): system prompt ~300, snapshot digest capped at 2000, `max_tokens` 1500, ~12500 for history, truncated oldest-first -- [ ] Chat SSE events: `{"type":"reasoning"|"content"|"queue"|"done"|"error"}` — render `reasoning_content` in collapsible block (this is the demo) -- [ ] Frontend: new chat component — collapsible reasoning block, queue position, streaming tokens -- [ ] Prometheus metrics: `atlas_chat_concurrent`, `atlas_chat_queue_depth`, `atlas_ratelimit_rejections_total`, `atlas_snapshot_age_seconds` -- [ ] Alertmanager rule: chat queue saturated > 5 min, snapshot age > 60s -- [ ] `pnpm audit` + `govulncheck` in CI, fail build on high severity - -## REFACTOR - -- [ ] `simplify` pass on rate limiter + SSE chat handler - -## Verify - -```bash -# load test at 20 concurrent clients -# GPU sequence usage never exceeds 6, no upstream 5xx, queue drains -pnpm test:e2e chat.e2e.ts # required, must pass before merge -``` - -## Done — atlas v1 shipped - -Success criteria (from PLAN-atlas.md): -- [ ] Protected services redirect unauthenticated to Authentik (if still applicable post decision #3) -- [ ] Fabricated stats gone, real numbers or removed -- [ ] `riotpiao.com` is the only public hostname (I1 held) -- [ ] Golden redaction test passes against live-cluster fixture -- [ ] Load test: 20 concurrent clients, GPU usage ≤ 6, no 5xx, queue drains diff --git a/tasks/1.1-redact-test.md b/tasks/1.1-redact-test.md new file mode 100644 index 0000000..8d09b64 --- /dev/null +++ b/tasks/1.1-redact-test.md @@ -0,0 +1,12 @@ +# 1.1 — redact_test.go (RED) + +Phase: 1 — Atlas Core +Stage: RED + +- [ ] Golden test: serialized snapshot contains none of the denied fields, run against a fixture captured from the real cluster + +## Verify + +```bash +go test ./... -run TestRedact -v +``` diff --git a/tasks/1.10-container-hardening.md b/tasks/1.10-container-hardening.md new file mode 100644 index 0000000..22df0fd --- /dev/null +++ b/tasks/1.10-container-hardening.md @@ -0,0 +1,6 @@ +# 1.10 — Container hardening (GREEN) + +Phase: 1 — Atlas Core +Stage: GREEN + +- [ ] Container hardening: `runAsNonRoot`, read-only root filesystem, all capabilities dropped, `seccompProfile: RuntimeDefault` diff --git a/tasks/1.11-simplify-pass.md b/tasks/1.11-simplify-pass.md new file mode 100644 index 0000000..bd8c6f3 --- /dev/null +++ b/tasks/1.11-simplify-pass.md @@ -0,0 +1,8 @@ +# 1.11 — simplify skill pass (REFACTOR) + +Phase: 1 — Atlas Core +Stage: REFACTOR + +Depends on: [1.4](1.4-clusterrole.md), [1.5](1.5-informers.md), [1.6](1.6-reducer.md), [1.7](1.7-dto-allowlist.md), [1.8](1.8-redis-publish.md) + +- [ ] Run `simplify` skill pass on reducer/DTO code diff --git a/tasks/1.12-go-hygiene.md b/tasks/1.12-go-hygiene.md new file mode 100644 index 0000000..326ba5e --- /dev/null +++ b/tasks/1.12-go-hygiene.md @@ -0,0 +1,16 @@ +# 1.12 — Go error-handling / context hygiene (REFACTOR) + +Phase: 1 — Atlas Core +Stage: REFACTOR + +Depends on: [1.11](1.11-simplify-pass.md) + +- [ ] Confirm no `_ =` on errors, no naked returns, every upstream call carries a `context.Context` (go-error-handling, go-context skills) + +## Verify + +```bash +go test ./... -run TestRedact -v +go test ./... -run TestRBAC -v +go test ./... -run TestSnapshot -v +``` diff --git a/tasks/1.2-rbac-test.md b/tasks/1.2-rbac-test.md new file mode 100644 index 0000000..f96ac4d --- /dev/null +++ b/tasks/1.2-rbac-test.md @@ -0,0 +1,13 @@ +# 1.2 — rbac_test.go (RED) + +Phase: 1 — Atlas Core +Stage: RED + +- [ ] atlas ServiceAccount receives 403 on `get secrets` in every namespace + +## Verify + +```bash +kubectl auth can-i get secrets --as=system:serviceaccount:portfolio:atlas # no +go test ./... -run TestRBAC -v +``` diff --git a/tasks/1.3-snapshot-test.md b/tasks/1.3-snapshot-test.md new file mode 100644 index 0000000..d73924d --- /dev/null +++ b/tasks/1.3-snapshot-test.md @@ -0,0 +1,12 @@ +# 1.3 — snapshot_test.go (RED) + +Phase: 1 — Atlas Core +Stage: RED + +- [ ] An informer event produces the expected delta + +## Verify + +```bash +go test ./... -run TestSnapshot -v +``` diff --git a/tasks/1.4-clusterrole.md b/tasks/1.4-clusterrole.md new file mode 100644 index 0000000..884f3b0 --- /dev/null +++ b/tasks/1.4-clusterrole.md @@ -0,0 +1,14 @@ +# 1.4 — ClusterRole (GREEN) + +Phase: 1 — Atlas Core +Stage: GREEN + +Depends on: [1.2](1.2-rbac-test.md) + +- [ ] ClusterRole: verbs `get,list,watch` only, explicit resource list — no `secrets`, no `*`, no wildcard apiGroups + +## Verify + +```bash +kubectl auth can-i get secrets --as=system:serviceaccount:portfolio:atlas # no +``` diff --git a/tasks/1.5-informers.md b/tasks/1.5-informers.md new file mode 100644 index 0000000..9568b20 --- /dev/null +++ b/tasks/1.5-informers.md @@ -0,0 +1,14 @@ +# 1.5 — client-go informers (GREEN) + +Phase: 1 — Atlas Core +Stage: GREEN + +Depends on: [1.3](1.3-snapshot-test.md) + +- [ ] client-go informers: kube API (nodes, namespaces, workloads), Argo CD `Application` CRs + +## Verify + +```bash +go test ./... -run TestSnapshot -v +``` diff --git a/tasks/1.6-reducer.md b/tasks/1.6-reducer.md new file mode 100644 index 0000000..23fd5b0 --- /dev/null +++ b/tasks/1.6-reducer.md @@ -0,0 +1,14 @@ +# 1.6 — Reducer: informer events -> snapshot (GREEN) + +Phase: 1 — Atlas Core +Stage: GREEN + +Depends on: [1.5](1.5-informers.md) + +- [ ] Reducer: informer events -> in-memory snapshot, redacted **at write time** + +## Verify + +```bash +go test ./... -run TestSnapshot -v +``` diff --git a/tasks/1.7-dto-allowlist.md b/tasks/1.7-dto-allowlist.md new file mode 100644 index 0000000..d070963 --- /dev/null +++ b/tasks/1.7-dto-allowlist.md @@ -0,0 +1,16 @@ +# 1.7 — DTO construction, allowlist only (GREEN) + +Phase: 1 — Atlas Core +Stage: GREEN + +Depends on: [1.1](1.1-redact-test.md), [1.6](1.6-reducer.md) + +- [ ] DTO construction — allowlist only. + - Emitted: name, namespace, kind, phase, ready counts, restart count, age, node name, health status, sync status, sync wave, explicit label subset + - Never emitted: container env/args, image digests/tags, `spec.source.repoURL`, `spec.source.path`, annotations, pod IPs, cluster IPs, Secret names, `status.conditions[].message`, node internal IPs + +## Verify + +```bash +go test ./... -run TestRedact -v +``` diff --git a/tasks/1.8-redis-publish.md b/tasks/1.8-redis-publish.md new file mode 100644 index 0000000..54fd431 --- /dev/null +++ b/tasks/1.8-redis-publish.md @@ -0,0 +1,8 @@ +# 1.8 — Redis publish (GREEN) + +Phase: 1 — Atlas Core +Stage: GREEN + +Depends on: [1.6](1.6-reducer.md) + +- [ ] Redis publish (snapshot deltas -> `kmsvc-redis-master.sqs:6379`) diff --git a/tasks/1.9-networkpolicy.md b/tasks/1.9-networkpolicy.md new file mode 100644 index 0000000..1b1c54d --- /dev/null +++ b/tasks/1.9-networkpolicy.md @@ -0,0 +1,6 @@ +# 1.9 — NetworkPolicy on atlas (GREEN) + +Phase: 1 — Atlas Core +Stage: GREEN + +- [ ] NetworkPolicy on atlas: egress restricted to kube API, `prometheus-operated.monitoring`, `reasoning-predictor.llm-serving`, `kmsvc-redis-master.sqs`; ingress from `ingress-nginx` only diff --git a/tasks/2.1-stream-test.md b/tasks/2.1-stream-test.md new file mode 100644 index 0000000..b0b69a0 --- /dev/null +++ b/tasks/2.1-stream-test.md @@ -0,0 +1,12 @@ +# 2.1 — stream_test.go (RED) + +Phase: 2 — Topology (Surface B) +Stage: RED + +- [ ] SSE emits a delta within 5s of a pod state change + +## Verify + +```bash +go test ./... -run TestStream -v +``` diff --git a/tasks/2.2-topology-component-test.md b/tasks/2.2-topology-component-test.md new file mode 100644 index 0000000..79beedf --- /dev/null +++ b/tasks/2.2-topology-component-test.md @@ -0,0 +1,12 @@ +# 2.2 — topology.test.tsx (RED) + +Phase: 2 — Topology (Surface B) +Stage: RED + +- [ ] Graph re-renders on delta without a full reload + +## Verify + +```bash +pnpm test topology.test.tsx +``` diff --git a/tasks/2.3-topology-e2e.md b/tasks/2.3-topology-e2e.md new file mode 100644 index 0000000..481209c --- /dev/null +++ b/tasks/2.3-topology-e2e.md @@ -0,0 +1,12 @@ +# 2.3 — topology.e2e.ts (RED, Playwright — hard requirement) + +Phase: 2 — Topology (Surface B) +Stage: RED + +- [ ] Load `/topology`, assert graph nodes render, delete a pod, assert node count updates without reload + +## Verify + +```bash +pnpm test:e2e topology.e2e.ts # required, must pass before merge +``` diff --git a/tasks/2.4-api-topology.md b/tasks/2.4-api-topology.md new file mode 100644 index 0000000..de50c42 --- /dev/null +++ b/tasks/2.4-api-topology.md @@ -0,0 +1,8 @@ +# 2.4 — GET /api/topology (GREEN) + +Phase: 2 — Topology (Surface B) +Stage: GREEN + +Depends on: [1.7](1.7-dto-allowlist.md) + +- [ ] `GET /api/topology` — nodes, namespaces, workload summaries; envelope `{"data": {}, "meta": {"snapshotAge", "generation"}}`; capped 256 KB, `meta.truncated: true` on overflow, never a silent drop diff --git a/tasks/2.5-api-stream.md b/tasks/2.5-api-stream.md new file mode 100644 index 0000000..f474a7a --- /dev/null +++ b/tasks/2.5-api-stream.md @@ -0,0 +1,14 @@ +# 2.5 — GET /api/stream (GREEN) + +Phase: 2 — Topology (Surface B) +Stage: GREEN + +Depends on: [2.1](2.1-stream-test.md), [1.8](1.8-redis-publish.md) + +- [ ] `GET /api/stream` — SSE, session cookie, 2 concurrent/IP, `topology` event type; keepalive comment frame every 30s; `Last-Event-ID` supported for resumable deltas + +## Verify + +```bash +go test ./... -run TestStream -v +``` diff --git a/tasks/2.6-frontend-react-flow.md b/tasks/2.6-frontend-react-flow.md new file mode 100644 index 0000000..dd28ec3 --- /dev/null +++ b/tasks/2.6-frontend-react-flow.md @@ -0,0 +1,15 @@ +# 2.6 — Frontend: React Flow topology graph (GREEN) + +Phase: 2 — Topology (Surface B) +Stage: GREEN + +Depends on: [2.2](2.2-topology-component-test.md), [2.4](2.4-api-topology.md), [2.5](2.5-api-stream.md) + +- [ ] Frontend: React Flow, force layout, node -> namespace -> workload + +## Verify + +```bash +# delete a pod, observe graph update in <5s without reloading +kubectl delete pod -n +``` diff --git a/tasks/2.7-security-headers.md b/tasks/2.7-security-headers.md new file mode 100644 index 0000000..0dff971 --- /dev/null +++ b/tasks/2.7-security-headers.md @@ -0,0 +1,6 @@ +# 2.7 — Security headers (GREEN) + +Phase: 2 — Topology (Surface B) +Stage: GREEN + +- [ ] Security headers on all responses: CSP (no `unsafe-inline`), `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, HSTS; CORS same-origin only diff --git a/tasks/2.8-rate-limiting-tier1-2.md b/tasks/2.8-rate-limiting-tier1-2.md new file mode 100644 index 0000000..4ad5ca7 --- /dev/null +++ b/tasks/2.8-rate-limiting-tier1-2.md @@ -0,0 +1,6 @@ +# 2.8 — Rate limiting Tier 1 + Tier 2 (GREEN) + +Phase: 2 — Topology (Surface B) +Stage: GREEN + +- [ ] Rate limiting Tier 1 (Cloudflare edge — WAF, Bot Fight Mode, per-IP rules) + Tier 2 (Kong `rate-limiting`, `policy: redis`, generous profile for topology) diff --git a/tasks/2.9-simplify-pass.md b/tasks/2.9-simplify-pass.md new file mode 100644 index 0000000..bd46a18 --- /dev/null +++ b/tasks/2.9-simplify-pass.md @@ -0,0 +1,15 @@ +# 2.9 — simplify skill pass (REFACTOR) + +Phase: 2 — Topology (Surface B) +Stage: REFACTOR + +Depends on: [2.5](2.5-api-stream.md), [2.6](2.6-frontend-react-flow.md) + +- [ ] `simplify` pass on SSE handler + React Flow wiring + +## Verify + +```bash +kubectl delete pod -n # graph updates in <5s without reload +pnpm test:e2e topology.e2e.ts # required, must pass before merge +``` diff --git a/tasks/3.1-delivery-test.md b/tasks/3.1-delivery-test.md new file mode 100644 index 0000000..334eba2 --- /dev/null +++ b/tasks/3.1-delivery-test.md @@ -0,0 +1,12 @@ +# 3.1 — delivery_test.go (RED) + +Phase: 3 — Delivery Tree (Surface E) +Stage: RED + +- [ ] Apps group correctly by `sync-wave`; Secret names absent from output (kind+count only); `repoURL` absent from output + +## Verify + +```bash +go test ./... -run TestDelivery -v +``` diff --git a/tasks/3.10-landing-page-cleanup.md b/tasks/3.10-landing-page-cleanup.md new file mode 100644 index 0000000..1d02296 --- /dev/null +++ b/tasks/3.10-landing-page-cleanup.md @@ -0,0 +1,7 @@ +# 3.10 — Landing page: remove fabricated stats and dead links + +Phase: 3 — Delivery Tree (Surface E) +Stage: GREEN + +- [ ] Delete fabricated stats in `app/page.tsx` ("40% CPU reduction", "99.2% uptime", "Mission-critical", "60% latency cut") — wire each card to a real number from `/api/topology` or `/api/delivery`, or remove the claim +- [ ] Five of six landing cards link to routes that don't exist (`/infrastructure`, `/systems`, `/llm`, `/kafka`, `/opensource`) — fix or remove diff --git a/tasks/3.2-delivery-component-test.md b/tasks/3.2-delivery-component-test.md new file mode 100644 index 0000000..1e1f3c0 --- /dev/null +++ b/tasks/3.2-delivery-component-test.md @@ -0,0 +1,12 @@ +# 3.2 — delivery.test.tsx (RED) + +Phase: 3 — Delivery Tree (Surface E) +Stage: RED + +- [ ] 550-node tree renders under frame budget with virtualization on + +## Verify + +```bash +pnpm test delivery.test.tsx +``` diff --git a/tasks/3.3-delivery-e2e.md b/tasks/3.3-delivery-e2e.md new file mode 100644 index 0000000..05ae34f --- /dev/null +++ b/tasks/3.3-delivery-e2e.md @@ -0,0 +1,12 @@ +# 3.3 — delivery.e2e.ts (RED, Playwright — hard requirement) + +Phase: 3 — Delivery Tree (Surface E) +Stage: RED + +- [ ] Load `/delivery`, click app node, assert side panel opens with virtualized resource tree, assert no Secret names appear in DOM + +## Verify + +```bash +pnpm test:e2e delivery.e2e.ts # required, must pass before merge +``` diff --git a/tasks/3.4-api-delivery.md b/tasks/3.4-api-delivery.md new file mode 100644 index 0000000..6a4e305 --- /dev/null +++ b/tasks/3.4-api-delivery.md @@ -0,0 +1,8 @@ +# 3.4 — GET /api/delivery (GREEN) + +Phase: 3 — Delivery Tree (Surface E) +Stage: GREEN + +Depends on: [3.1](3.1-delivery-test.md) + +- [ ] `GET /api/delivery` — Argo apps, wave-grouped (0->8), resource children lazy; capped 256 KB diff --git a/tasks/3.5-api-delivery-resources.md b/tasks/3.5-api-delivery-resources.md new file mode 100644 index 0000000..f2512f2 --- /dev/null +++ b/tasks/3.5-api-delivery-resources.md @@ -0,0 +1,8 @@ +# 3.5 — GET /api/delivery/{app}/resources (GREEN) + +Phase: 3 — Delivery Tree (Surface E) +Stage: GREEN + +Depends on: [3.4](3.4-api-delivery.md) + +- [ ] `GET /api/delivery/{app}/resources` — cursor-paginated at 100 items (prometheus alone has 68 resources today) diff --git a/tasks/3.6-frontend-wave-columns.md b/tasks/3.6-frontend-wave-columns.md new file mode 100644 index 0000000..08ad0c1 --- /dev/null +++ b/tasks/3.6-frontend-wave-columns.md @@ -0,0 +1,8 @@ +# 3.6 — Frontend: wave columns + resource tree (GREEN) + +Phase: 3 — Delivery Tree (Surface E) +Stage: GREEN + +Depends on: [3.2](3.2-delivery-component-test.md), [3.4](3.4-api-delivery.md), [3.5](3.5-api-delivery-resources.md) + +- [ ] Frontend: React Flow, wave columns left->right from `sync-wave` annotations. Click app -> side panel with `react-arborist` virtualized resource tree, lazy-loaded children diff --git a/tasks/3.7-live-sync-animation.md b/tasks/3.7-live-sync-animation.md new file mode 100644 index 0000000..02aa7c2 --- /dev/null +++ b/tasks/3.7-live-sync-animation.md @@ -0,0 +1,15 @@ +# 3.7 — Live sync animation (GREEN) + +Phase: 3 — Delivery Tree (Surface E) +Stage: GREEN + +Depends on: [2.5](2.5-api-stream.md), [3.6](3.6-frontend-wave-columns.md) + +- [ ] Live sync animation `OutOfSync -> Syncing -> Synced` driven by Application watch (reuse Phase 2 SSE `/api/stream`, add `delivery` event type) + +## Verify + +```bash +# trigger an Argo sync, observe wave-ordered animation +argocd app sync homelab-root +``` diff --git a/tasks/3.8-redaction-check.md b/tasks/3.8-redaction-check.md new file mode 100644 index 0000000..0dd41c0 --- /dev/null +++ b/tasks/3.8-redaction-check.md @@ -0,0 +1,14 @@ +# 3.8 — Redaction check specific to delivery surface (GREEN) + +Phase: 3 — Delivery Tree (Surface E) +Stage: GREEN + +Depends on: [3.4](3.4-api-delivery.md) + +- [ ] 21 `Secret` resources appear in Argo trees today — render kind+count only, never names (includes `sops-secrets`); `status.conditions[].message` echoes raw errors with internal hostnames — emit condition **type** only + +## Verify + +```bash +go test ./... -run TestDelivery -v +``` diff --git a/tasks/3.9-simplify-pass.md b/tasks/3.9-simplify-pass.md new file mode 100644 index 0000000..f6b0ea0 --- /dev/null +++ b/tasks/3.9-simplify-pass.md @@ -0,0 +1,15 @@ +# 3.9 — simplify skill pass (REFACTOR) + +Phase: 3 — Delivery Tree (Surface E) +Stage: REFACTOR + +Depends on: [3.6](3.6-frontend-wave-columns.md), [3.7](3.7-live-sync-animation.md) + +- [ ] `simplify` pass on wave-grouping + virtualized tree code + +## Verify + +```bash +argocd app sync homelab-root # wave-ordered animation +pnpm test:e2e delivery.e2e.ts # required, must pass before merge +``` diff --git a/tasks/4.1-exec-parse-test.md b/tasks/4.1-exec-parse-test.md new file mode 100644 index 0000000..ec36f37 --- /dev/null +++ b/tasks/4.1-exec-parse-test.md @@ -0,0 +1,12 @@ +# 4.1 — exec_parse_test.go (RED) + +Phase: 4 — Terminal (Surface C) +Stage: RED + +- [ ] Fuzz corpus: every non-allowlisted input rejects and performs zero upstream calls + +## Verify + +```bash +go test ./... -run TestExecParse -v +``` diff --git a/tasks/4.10-simplify-pass.md b/tasks/4.10-simplify-pass.md new file mode 100644 index 0000000..4f59dc8 --- /dev/null +++ b/tasks/4.10-simplify-pass.md @@ -0,0 +1,15 @@ +# 4.10 — simplify skill pass (REFACTOR) + +Phase: 4 — Terminal (Surface C) +Stage: REFACTOR + +Depends on: [4.5](4.5-command-set.md), [4.6](4.6-namespace-validation.md) + +- [ ] `simplify` pass on the enum parser + +## Verify + +```bash +curl -X POST https://riotpiao.com/api/exec -d '{"cmd":"get pods; rm -rf /"}' +pnpm test:e2e terminal.e2e.ts # required, must pass before merge +``` diff --git a/tasks/4.2-exec-test.md b/tasks/4.2-exec-test.md new file mode 100644 index 0000000..66e27df --- /dev/null +++ b/tasks/4.2-exec-test.md @@ -0,0 +1,12 @@ +# 4.2 — exec_test.go (RED) + +Phase: 4 — Terminal (Surface C) +Stage: RED + +- [ ] Unknown namespace rejects on snapshot membership, not regex + +## Verify + +```bash +go test ./... -run TestExec -v +``` diff --git a/tasks/4.3-terminal-e2e.md b/tasks/4.3-terminal-e2e.md new file mode 100644 index 0000000..05ea25e --- /dev/null +++ b/tasks/4.3-terminal-e2e.md @@ -0,0 +1,12 @@ +# 4.3 — terminal.e2e.ts (RED, Playwright — hard requirement) + +Phase: 4 — Terminal (Surface C) +Stage: RED + +- [ ] Load terminal UI, submit `get nodes`, assert rendered output; submit an injection payload, assert rejection surfaces in UI, zero upstream call + +## Verify + +```bash +pnpm test:e2e terminal.e2e.ts # required, must pass before merge +``` diff --git a/tasks/4.4-api-exec.md b/tasks/4.4-api-exec.md new file mode 100644 index 0000000..c6e7361 --- /dev/null +++ b/tasks/4.4-api-exec.md @@ -0,0 +1,14 @@ +# 4.4 — POST /api/exec (GREEN) + +Phase: 4 — Terminal (Surface C) +Stage: GREEN + +Depends on: [4.1](4.1-exec-parse-test.md), [4.2](4.2-exec-test.md) + +- [ ] `POST /api/exec` — session cookie, 20/min/session; input parses to a closed command enum, anything unmatched rejected before any lookup + +## Verify + +```bash +curl -X POST https://riotpiao.com/api/exec -d '{"cmd":"get pods; rm -rf /"}' +``` diff --git a/tasks/4.5-command-set.md b/tasks/4.5-command-set.md new file mode 100644 index 0000000..deb42d2 --- /dev/null +++ b/tasks/4.5-command-set.md @@ -0,0 +1,8 @@ +# 4.5 — Command set (GREEN) + +Phase: 4 — Terminal (Surface C) +Stage: GREEN + +Depends on: [4.4](4.4-api-exec.md) + +- [ ] Command set: `get nodes`, `get pods `, `get apps`, `top nodes`, `describe pod `, `help` diff --git a/tasks/4.6-namespace-validation.md b/tasks/4.6-namespace-validation.md new file mode 100644 index 0000000..868fb25 --- /dev/null +++ b/tasks/4.6-namespace-validation.md @@ -0,0 +1,14 @@ +# 4.6 — Namespace/resource-name validation by snapshot membership (GREEN) + +Phase: 4 — Terminal (Surface C) +Stage: GREEN + +Depends on: [4.2](4.2-exec-test.md), [1.7](1.7-dto-allowlist.md) + +- [ ] Namespace and resource-name arguments validated by **set membership against current snapshot**, not regex/escaping + +## Verify + +```bash +go test ./... -run TestExec -v +``` diff --git a/tasks/4.7-no-shell-in-image.md b/tasks/4.7-no-shell-in-image.md new file mode 100644 index 0000000..a6545ab --- /dev/null +++ b/tasks/4.7-no-shell-in-image.md @@ -0,0 +1,6 @@ +# 4.7 — No shell, no exec, no kubectl binary in image (GREEN) + +Phase: 4 — Terminal (Surface C) +Stage: GREEN + +- [ ] No shell, no `exec`, no `kubectl` binary in the container image diff --git a/tasks/4.8-frontend-terminal-wiring.md b/tasks/4.8-frontend-terminal-wiring.md new file mode 100644 index 0000000..98e8284 --- /dev/null +++ b/tasks/4.8-frontend-terminal-wiring.md @@ -0,0 +1,8 @@ +# 4.8 — Frontend: wire InteractiveTerminal to /api/exec (GREEN) + +Phase: 4 — Terminal (Surface C) +Stage: GREEN + +Depends on: [4.4](4.4-api-exec.md) + +- [ ] Frontend: reuse [components/InteractiveTerminal.tsx](../components/InteractiveTerminal.tsx), wire to `/api/exec` diff --git a/tasks/4.9-structured-logging.md b/tasks/4.9-structured-logging.md new file mode 100644 index 0000000..9ac4b58 --- /dev/null +++ b/tasks/4.9-structured-logging.md @@ -0,0 +1,15 @@ +# 4.9 — Structured logging of rejections (GREEN) + +Phase: 4 — Terminal (Surface C) +Stage: GREEN + +Depends on: [4.4](4.4-api-exec.md) + +- [ ] Structured logging: every rejected `/api/exec` input logged + +## Verify + +```bash +# attempt injection payloads against /api/exec; all rejected, all logged +curl -X POST https://riotpiao.com/api/exec -d '{"cmd":"get pods; rm -rf /"}' +``` diff --git a/tasks/5.1-ratelimit-test.md b/tasks/5.1-ratelimit-test.md new file mode 100644 index 0000000..b3f8ed0 --- /dev/null +++ b/tasks/5.1-ratelimit-test.md @@ -0,0 +1,14 @@ +# 5.1 — ratelimit_test.go (RED) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: RED + +Depends on: [00-decisions.md](00-decisions.md) decision #4 + +- [ ] 7th concurrent chat queues rather than reaching vLLM + +## Verify + +```bash +go test ./... -run TestRateLimit -v +``` diff --git a/tasks/5.10-prompt-injection-defenses.md b/tasks/5.10-prompt-injection-defenses.md new file mode 100644 index 0000000..ce014c2 --- /dev/null +++ b/tasks/5.10-prompt-injection-defenses.md @@ -0,0 +1,14 @@ +# 5.10 — Prompt injection defenses (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.4](5.4-injection-test.md) + +- [ ] System prompt is compile-time constant, unreachable by user input; cluster snapshot digest injected in a delimited block explicitly labelled untrusted data; user message always last; **no tool-calling** — model reads pre-built digest, cannot query anything; `max_tokens: 1500` cap + +## Verify + +```bash +go test ./... -run TestInjection -v +``` diff --git a/tasks/5.11-context-budget.md b/tasks/5.11-context-budget.md new file mode 100644 index 0000000..984e8ad --- /dev/null +++ b/tasks/5.11-context-budget.md @@ -0,0 +1,14 @@ +# 5.11 — Context budget (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.5](5.5-context-test.md) + +- [ ] Context budget (16384 total): system prompt ~300, snapshot digest capped at 2000, `max_tokens` 1500, ~12500 for history, truncated oldest-first + +## Verify + +```bash +go test ./... -run TestContext -v +``` diff --git a/tasks/5.12-chat-sse-events.md b/tasks/5.12-chat-sse-events.md new file mode 100644 index 0000000..1861e79 --- /dev/null +++ b/tasks/5.12-chat-sse-events.md @@ -0,0 +1,8 @@ +# 5.12 — Chat SSE event types (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.7](5.7-api-chat.md) + +- [ ] Chat SSE events: `{"type":"reasoning"|"content"|"queue"|"done"|"error"}` — render `reasoning_content` in collapsible block (this is the demo) diff --git a/tasks/5.13-frontend-chat-component.md b/tasks/5.13-frontend-chat-component.md new file mode 100644 index 0000000..d03c8ad --- /dev/null +++ b/tasks/5.13-frontend-chat-component.md @@ -0,0 +1,8 @@ +# 5.13 — Frontend: chat component (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.6](5.6-chat-e2e.md), [5.12](5.12-chat-sse-events.md) + +- [ ] Frontend: new chat component — collapsible reasoning block, queue position, streaming tokens diff --git a/tasks/5.14-prometheus-metrics.md b/tasks/5.14-prometheus-metrics.md new file mode 100644 index 0000000..ae9c5da --- /dev/null +++ b/tasks/5.14-prometheus-metrics.md @@ -0,0 +1,8 @@ +# 5.14 — Prometheus metrics (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.8](5.8-rate-limiting-tier3.md) + +- [ ] Prometheus metrics: `atlas_chat_concurrent`, `atlas_chat_queue_depth`, `atlas_ratelimit_rejections_total`, `atlas_snapshot_age_seconds` diff --git a/tasks/5.15-alertmanager-rule.md b/tasks/5.15-alertmanager-rule.md new file mode 100644 index 0000000..e21dc40 --- /dev/null +++ b/tasks/5.15-alertmanager-rule.md @@ -0,0 +1,8 @@ +# 5.15 — Alertmanager rule (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.14](5.14-prometheus-metrics.md) + +- [ ] Alertmanager rule: chat queue saturated > 5 min, snapshot age > 60s diff --git a/tasks/5.16-audit-ci-gate.md b/tasks/5.16-audit-ci-gate.md new file mode 100644 index 0000000..b174846 --- /dev/null +++ b/tasks/5.16-audit-ci-gate.md @@ -0,0 +1,6 @@ +# 5.16 — pnpm audit + govulncheck CI gate (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +- [ ] `pnpm audit` + `govulncheck` in CI, fail build on high severity diff --git a/tasks/5.17-simplify-pass.md b/tasks/5.17-simplify-pass.md new file mode 100644 index 0000000..f198b39 --- /dev/null +++ b/tasks/5.17-simplify-pass.md @@ -0,0 +1,25 @@ +# 5.17 — simplify skill pass (REFACTOR) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: REFACTOR + +Depends on: [5.8](5.8-rate-limiting-tier3.md), [5.13](5.13-frontend-chat-component.md) + +- [ ] `simplify` pass on rate limiter + SSE chat handler + +## Verify + +```bash +# load test at 20 concurrent clients +# GPU sequence usage never exceeds 6, no upstream 5xx, queue drains +pnpm test:e2e chat.e2e.ts # required, must pass before merge +``` + +## Done — atlas v1 shipped + +Success criteria (from PLAN-atlas.md): +- [ ] Protected services redirect unauthenticated to Authentik (if still applicable post decision #3) +- [ ] Fabricated stats gone, real numbers or removed +- [ ] `riotpiao.com` is the only public hostname (I1 held) +- [ ] Golden redaction test passes against live-cluster fixture +- [ ] Load test: 20 concurrent clients, GPU usage <= 6, no 5xx, queue drains diff --git a/tasks/5.2-disconnect-test.md b/tasks/5.2-disconnect-test.md new file mode 100644 index 0000000..65e8061 --- /dev/null +++ b/tasks/5.2-disconnect-test.md @@ -0,0 +1,12 @@ +# 5.2 — disconnect_test.go (RED) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: RED + +- [ ] Client abort cancels the upstream request + +## Verify + +```bash +go test ./... -run TestDisconnect -v +``` diff --git a/tasks/5.3-budget-test.md b/tasks/5.3-budget-test.md new file mode 100644 index 0000000..a56af2a --- /dev/null +++ b/tasks/5.3-budget-test.md @@ -0,0 +1,12 @@ +# 5.3 — budget_test.go (RED) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: RED + +- [ ] 13th message in 24h returns `429` with `Retry-After` + +## Verify + +```bash +go test ./... -run TestBudget -v +``` diff --git a/tasks/5.4-injection-test.md b/tasks/5.4-injection-test.md new file mode 100644 index 0000000..ec4ef45 --- /dev/null +++ b/tasks/5.4-injection-test.md @@ -0,0 +1,12 @@ +# 5.4 — injection_test.go (RED) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: RED + +- [ ] Snapshot content cannot alter system-prompt behaviour + +## Verify + +```bash +go test ./... -run TestInjection -v +``` diff --git a/tasks/5.5-context-test.md b/tasks/5.5-context-test.md new file mode 100644 index 0000000..59c8261 --- /dev/null +++ b/tasks/5.5-context-test.md @@ -0,0 +1,12 @@ +# 5.5 — context_test.go (RED) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: RED + +- [ ] History truncation keeps total tokens under 16384 + +## Verify + +```bash +go test ./... -run TestContext -v +``` diff --git a/tasks/5.6-chat-e2e.md b/tasks/5.6-chat-e2e.md new file mode 100644 index 0000000..d1bf927 --- /dev/null +++ b/tasks/5.6-chat-e2e.md @@ -0,0 +1,12 @@ +# 5.6 — chat.e2e.ts (RED, Playwright — hard requirement) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: RED + +- [ ] Load chat UI, send message, assert streamed tokens render, assert queue position renders under load, assert reasoning block is collapsible + +## Verify + +```bash +pnpm test:e2e chat.e2e.ts # required, must pass before merge +``` diff --git a/tasks/5.7-api-chat.md b/tasks/5.7-api-chat.md new file mode 100644 index 0000000..8977ec6 --- /dev/null +++ b/tasks/5.7-api-chat.md @@ -0,0 +1,8 @@ +# 5.7 — POST /api/chat (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.1](5.1-ratelimit-test.md), [5.2](5.2-disconnect-test.md), [5.3](5.3-budget-test.md) + +- [ ] `POST /api/chat` — session + Turnstile, 12/day/session, 6 global concurrent, SSE token stream diff --git a/tasks/5.8-rate-limiting-tier3.md b/tasks/5.8-rate-limiting-tier3.md new file mode 100644 index 0000000..1d98ec6 --- /dev/null +++ b/tasks/5.8-rate-limiting-tier3.md @@ -0,0 +1,15 @@ +# 5.8 — Rate limiting Tier 3 (atlas) (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.7](5.7-api-chat.md) + +- [ ] Global chat semaphore = 6 (2 of 8 GPU slots kept as operator headroom); queue depth 20 then reject `429`; per-session budget 12 msg/24h; per-request timeout 120s hard server-side; disconnect cancels upstream immediately (`req.Context()` threaded to vLLM request) + +## Verify + +```bash +# load test at 20 concurrent clients +# GPU sequence usage never exceeds 6, no upstream 5xx, queue drains +``` diff --git a/tasks/5.9-queue-position-sse.md b/tasks/5.9-queue-position-sse.md new file mode 100644 index 0000000..60b22a7 --- /dev/null +++ b/tasks/5.9-queue-position-sse.md @@ -0,0 +1,8 @@ +# 5.9 — Queue position streamed as SSE (GREEN) + +Phase: 5 — Chat + Rate Limiter (Surface D) +Stage: GREEN + +Depends on: [5.8](5.8-rate-limiting-tier3.md) + +- [ ] Queue position streamed as SSE `{"type":"queue","position":N}` diff --git a/tasks/INDEX.md b/tasks/INDEX.md new file mode 100644 index 0000000..4761344 --- /dev/null +++ b/tasks/INDEX.md @@ -0,0 +1,121 @@ +# atlas — Task Board + +Source of truth: [docs/PLAN-atlas.md](../docs/PLAN-atlas.md), [docs/adr/ADR-0001-atlas-cluster-visualization.md](../docs/adr/ADR-0001-atlas-cluster-visualization.md) + +Blocking decisions (must answer before Phase 1): [00-decisions.md](00-decisions.md) + +Rules carried from the ADR: +- I1: `riotpiao.com` is the only public hostname, ever +- I2: browser never talks to an internal API directly — atlas is the only origin +- I3: redaction is allowlist-only, enforced by DTO construction +- I4: no free-form string reaches an internal system (closed enum, snapshot-membership validation) +- I5: GPU chat concurrency capped at 6 of 8 sequence slots, disconnect cancels upstream immediately +- TDD: RED (tests named before code) -> GREEN (minimal code) -> REFACTOR, per phase +- Playwright e2e is a hard requirement for every UI-shipping phase (2, 3, 4, 5) — no surface merges without a passing spec + +## 0 — Unblock + +| Task | Description | +|---|---| +| [0.1](0.1-gitops-repo.md) | Resolve GitOps repo ownership | +| [0.2](0.2-fix-nxdomain.md) | Fix NXDOMAIN repoURL/image reference | +| [0.3](0.3-immutable-tags.md) | Replace :latest with commit-SHA tag | +| [0.4](0.4-forgejo-init-stuck.md) | Diagnose forgejo-gitea stuck Init:0/3 — done | +| [0.5](0.5-apex-403.md) | Diagnose apex 403 | +| [0.6](0.6-vitest-setup.md) | Add Vitest + Testing Library + msw | +| [0.6a](0.6a-playwright-setup.md) | Add Playwright (hard requirement) | +| [0.7](0.7-triage-unrelated.md) | Triage unrelated cluster issues | + +## 1 — Atlas Core + +| Task | Description | +|---|---| +| [1.1](1.1-redact-test.md) | redact_test.go (RED) | +| [1.2](1.2-rbac-test.md) | rbac_test.go (RED) | +| [1.3](1.3-snapshot-test.md) | snapshot_test.go (RED) | +| [1.4](1.4-clusterrole.md) | ClusterRole (GREEN) | +| [1.5](1.5-informers.md) | client-go informers (GREEN) | +| [1.6](1.6-reducer.md) | Reducer: informer events -> snapshot (GREEN) | +| [1.7](1.7-dto-allowlist.md) | DTO construction, allowlist only (GREEN) | +| [1.8](1.8-redis-publish.md) | Redis publish (GREEN) | +| [1.9](1.9-networkpolicy.md) | NetworkPolicy on atlas (GREEN) | +| [1.10](1.10-container-hardening.md) | Container hardening (GREEN) | +| [1.11](1.11-simplify-pass.md) | simplify skill pass (REFACTOR) | +| [1.12](1.12-go-hygiene.md) | Go error-handling / context hygiene (REFACTOR) | + +## 2 — Topology (Surface B) + +| Task | Description | +|---|---| +| [2.1](2.1-stream-test.md) | stream_test.go (RED) | +| [2.2](2.2-topology-component-test.md) | topology.test.tsx (RED) | +| [2.3](2.3-topology-e2e.md) | topology.e2e.ts (RED, Playwright — hard requirement) | +| [2.4](2.4-api-topology.md) | GET /api/topology (GREEN) | +| [2.5](2.5-api-stream.md) | GET /api/stream (GREEN) | +| [2.6](2.6-frontend-react-flow.md) | Frontend: React Flow topology graph (GREEN) | +| [2.7](2.7-security-headers.md) | Security headers (GREEN) | +| [2.8](2.8-rate-limiting-tier1-2.md) | Rate limiting Tier 1 + Tier 2 (GREEN) | +| [2.9](2.9-simplify-pass.md) | simplify skill pass (REFACTOR) | + +## 3 — Delivery Tree (Surface E) + +| Task | Description | +|---|---| +| [3.1](3.1-delivery-test.md) | delivery_test.go (RED) | +| [3.2](3.2-delivery-component-test.md) | delivery.test.tsx (RED) | +| [3.3](3.3-delivery-e2e.md) | delivery.e2e.ts (RED, Playwright — hard requirement) | +| [3.4](3.4-api-delivery.md) | GET /api/delivery (GREEN) | +| [3.5](3.5-api-delivery-resources.md) | GET /api/delivery/{app}/resources (GREEN) | +| [3.6](3.6-frontend-wave-columns.md) | Frontend: wave columns + resource tree (GREEN) | +| [3.7](3.7-live-sync-animation.md) | Live sync animation (GREEN) | +| [3.8](3.8-redaction-check.md) | Redaction check specific to delivery surface (GREEN) | +| [3.9](3.9-simplify-pass.md) | simplify skill pass (REFACTOR) | +| [3.10](3.10-landing-page-cleanup.md) | Landing page: remove fabricated stats and dead links (GREEN) | + +## 4 — Terminal (Surface C) + +| Task | Description | +|---|---| +| [4.1](4.1-exec-parse-test.md) | exec_parse_test.go (RED) | +| [4.2](4.2-exec-test.md) | exec_test.go (RED) | +| [4.3](4.3-terminal-e2e.md) | terminal.e2e.ts (RED, Playwright — hard requirement) | +| [4.4](4.4-api-exec.md) | POST /api/exec (GREEN) | +| [4.5](4.5-command-set.md) | Command set (GREEN) | +| [4.6](4.6-namespace-validation.md) | Namespace/resource-name validation by snapshot membership (GREEN) | +| [4.7](4.7-no-shell-in-image.md) | No shell, no exec, no kubectl binary in image (GREEN) | +| [4.8](4.8-frontend-terminal-wiring.md) | Frontend: wire InteractiveTerminal to /api/exec (GREEN) | +| [4.9](4.9-structured-logging.md) | Structured logging of rejections (GREEN) | +| [4.10](4.10-simplify-pass.md) | simplify skill pass (REFACTOR) | + +## 5 — Chat + Rate Limiter (Surface D) + +| Task | Description | +|---|---| +| [5.1](5.1-ratelimit-test.md) | ratelimit_test.go (RED) | +| [5.2](5.2-disconnect-test.md) | disconnect_test.go (RED) | +| [5.3](5.3-budget-test.md) | budget_test.go (RED) | +| [5.4](5.4-injection-test.md) | injection_test.go (RED) | +| [5.5](5.5-context-test.md) | context_test.go (RED) | +| [5.6](5.6-chat-e2e.md) | chat.e2e.ts (RED, Playwright — hard requirement) | +| [5.7](5.7-api-chat.md) | POST /api/chat (GREEN) | +| [5.8](5.8-rate-limiting-tier3.md) | Rate limiting Tier 3 (atlas) (GREEN) | +| [5.9](5.9-queue-position-sse.md) | Queue position streamed as SSE (GREEN) | +| [5.10](5.10-prompt-injection-defenses.md) | Prompt injection defenses (GREEN) | +| [5.11](5.11-context-budget.md) | Context budget (GREEN) | +| [5.12](5.12-chat-sse-events.md) | Chat SSE event types (GREEN) | +| [5.13](5.13-frontend-chat-component.md) | Frontend: chat component (GREEN) | +| [5.14](5.14-prometheus-metrics.md) | Prometheus metrics (GREEN) | +| [5.15](5.15-alertmanager-rule.md) | Alertmanager rule (GREEN) | +| [5.16](5.16-audit-ci-gate.md) | pnpm audit + govulncheck CI gate (GREEN) | +| [5.17](5.17-simplify-pass.md) | simplify skill pass (REFACTOR) | + +## Progress + +Not started, Phase 0 partially unblocked. 5 decisions still block Phase 1 — see [00-decisions.md](00-decisions.md). + +Status as of 2026-08-18: +- 0.4 (forgejo-gitea Init stuck) — resolved, pod Running 1/1 +- 0.7 (`longhorn-config` OutOfSync) — resolved, now Synced/Healthy; `sms` Degraded still open +- `*.riotpiao.homelab.com` fully retired, replaced by `*.riotpiao.com` — confirmed root cause for 0.2 (dead-domain `repoURL`, not a broken forgejo pod) +- 0.1/0.2 (portfolio GitOps repo, dead-domain repoURL) — still open, `application portfolio` not found in cluster, fix is now a known domain swap +- 0.5 (apex 403) — still open, `curl riotpiao.com` returns 403; no in-cluster ingress exists for bare apex, still need Cloudflare-side confirmation diff --git a/tasks/README.md b/tasks/README.md index 0cb57f6..34709d2 100644 --- a/tasks/README.md +++ b/tasks/README.md @@ -6,19 +6,20 @@ Supersedes: [PLAN.md](../PLAN.md), [IMPLEMENTATION.md](../IMPLEMENTATION.md) (Ho ## Status -Not started. **5 decisions block Phase 1** — see [00-decisions.md](00-decisions.md). +Not started, Phase 0 partially unblocked. **5 decisions block Phase 1** — see [00-decisions.md](00-decisions.md). -## Phases +## Task board -| File | Phase | Est. | -|---|---|---| -| [00-decisions.md](00-decisions.md) | Blocking decisions (must answer before Phase 1) | — | -| [01-phase0-unblock.md](01-phase0-unblock.md) | Phase 0 — unblock deployment | — | -| [02-phase1-atlas-core.md](02-phase1-atlas-core.md) | Phase 1 — atlas core (RBAC, informers, redaction) | ~4d | -| [03-phase2-topology.md](03-phase2-topology.md) | Phase 2 — Surface B: cluster topology | ~3d | -| [04-phase3-delivery.md](04-phase3-delivery.md) | Phase 3 — Surface E: delivery tree | ~3d | -| [05-phase4-terminal.md](05-phase4-terminal.md) | Phase 4 — Surface C: terminal | ~2d | -| [06-phase5-chat.md](06-phase5-chat.md) | Phase 5 — Surface D: chat + rate limiter | ~5d | +Task board lives in [INDEX.md](INDEX.md) — one row per task, one file per task (`-.md`), agent-harness format. That file is the source of truth for execution order; this README is a human overview only. + +| Phase | Est. | +|---|---| +| 0 — unblock deployment | — | +| 1 — atlas core (RBAC, informers, redaction) | ~4d | +| 2 — Surface B: cluster topology | ~3d | +| 3 — Surface E: delivery tree | ~3d | +| 4 — Surface C: terminal | ~2d | +| 5 — Surface D: chat + rate limiter | ~5d | **Total: ~17 working days.**