Files
coreci-chat/.ciagent/PLAN.md
T
CIAgent 61221222eb docs(P00): apply grill fixes + MVP/UX check
Apply 10 binding grill fixes (G-001..G-010) to PLAN.md:
- G-001: /api/byom/test-inference flagged as M3-deprecation proxy for REQ-008
- G-002: Trigger.dev health writes to runtime_health, NOT audit_log
- G-003: Trigger.dev bootstrap + SSH whitelist hook annotated as M2/M3
  pre-investments with expected payoff
- G-004: CheckCommand signature + whitelist JSON locked as M2 contract
- G-005: relay token contract (signed JWT, 24h) defined in Wave A so
  B and D parallelize without blocking
- G-006: per-tenant audit hash-chain concurrent-write serialization
  documented as known M1 limit; M3 mitigation noted
- G-007: shadow exec.Cmd integration test added to whitelist hook
- G-008: explicit Wave D scope statement - M1 ships REQ-026 partially
- G-009: unsupported-OS test matrix expanded to >=2 cases
- G-010: two-tier credential taxonomy (infra env vars vs tenant
  SecretProvider-only) documented

MVP/UX check: all 3 sections present in PLAN.md (User-Facing Surface,
Happy Path, UX Acceptance Criteria). Gate PASS, EXECUTE unblocked.

Checkpoint: stage=mvp_ux_check.

---ci---
phase: 0
milestone: v0.1
status: mvp_ux_check
---/ci---
2026-08-24 22:41:17 +00:00

279 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan — Milestone 1 (v0.1)
Source spec: `.ciagent/steer-v0.1-spec.md` (v1.1, locked 2026-08-24).
M1 scope: REQ-001..014, REQ-038, REQ-039, REQ-040 (17 REQs). M1 acceptance gate: spec §2.3.
Architecture: `.ciagent/ARCHITECTURE.md`. Decisions: `.ciagent/CLARIFY.md`. Research: `.ciagent/RESEARCH.md`. Personas: `.ciagent/PERSONAS.md`.
M1 is decomposed into **5 execution phases** (Waves AE). Each wave is a vertical slice: scaffolds + implements + tests + ships a patch on the v0.0.x line. Each wave maps to explicit REQ IDs and has must-have verification items. Waves are ordered by dependency; A → B → C may overlap (B starts once A's `withTenant` + `audit` are green); D is independent of B/C and can run in parallel; E depends on B + D.
## Phase mapping (CIAgent phase model)
| CIAgent phase | Wave | Branch | Patch tag | REQs |
|---------------|------|--------|-----------|------|
| Phase 0 | pre-execution | `phase/00-pre-execution` | v0.0.1 | (this plan) |
| Phase 1 | Wave A — Foundations | `phase/01-foundations` | v0.0.2 | REQ-038, REQ-039, REQ-040 |
| Phase 2 | Wave B — Identity & RBAC | `phase/02-identity-rbac` | v0.0.3 | REQ-001, REQ-002, REQ-003, REQ-004, REQ-005 |
| Phase 3 | Wave C — BYOM | `phase/03-byom` | v0.0.4 | REQ-006, REQ-007, REQ-008, REQ-009 |
| Phase 4 | Wave D — Relay Agent | `phase/04-relay-agent` | v0.0.5 | REQ-010, REQ-011, REQ-012, REQ-013, REQ-026 (whitelist hook) |
| Phase 5 | Wave E — Dashboard surfacing | `phase/05-dashboard` | v0.0.6 | REQ-014 |
| Phase 6 | Final — Review + Ship | `phase/06-final-review-ship` | v0.0.7 ← milestone release | all M1 |
Tags run on the v0.0.x patch line (no prior minor). The final phase's patch (v0.0.7) IS the v0.1 milestone release. The milestone merge to `main` happens at the final phase.
### Grill fixes applied (G-001..G-010)
This plan was grilled (`.ciagent/GRILL.md`, verdict PASS-WITH-FIXES). The 10 binding fixes are integrated below and flagged inline as `[G-NNN]`. Summary:
- **G-001** (Wave C): `/api/byom/test-inference` is a plan-time proxy for REQ-008 (no M3 orchestration to drive inference); marked for M3 deprecation; PO-acknowledged.
- **G-002** (Wave A): Trigger.dev health-check ticks write to a `runtime_health` table, NOT `audit_log`. REQ-038's auditable events are business events only.
- **G-003** (Wave A + D): Trigger.dev bootstrap + SSH whitelist hook are pre-investments for M2/M3, with one-line expected payoff.
- **G-004** (Wave D): `CheckCommand(cmd) error` signature + whitelist JSON schema are the M2 SSH adapter contract; changes require a documented migration.
- **G-005** (Wave A + B + D): `POST /api/relay/issue-token` owned by backend-engineer; token contract (format/scope/lifetime) defined in Wave A secrets package so B and D parallelize without blocking.
- **G-006** (Wave A): per-tenant hash-chain serializes concurrent audit writes via constraint-trigger rollback — known M1-acceptable limit; M3 mitigation documented.
- **G-007** (Wave D): shadow `exec.Cmd` integration test proves `CheckCommand` composes with `os/exec` without a live SSH server.
- **G-008** (Wave D): M1 ships REQ-026 *partially* (whitelist file + hook + tests); spec §4 SSH-key-auth + tool-call execution are M2.
- **G-009** (Wave D): unsupported-OS test matrix is ≥2 cases (Fedora + Alpine), not a single container.
- **G-010** (Wave A): two-tier credential taxonomy — infra/bootstrap creds (env vars, listed) vs tenant creds (SecretProvider only). PO's "no env vars" applies to tenant creds.
### Credential taxonomy [G-010]
The PO's "no env vars, no config files, no DB columns. Ever." (REQ-040) applies to **tenant credentials**. The platform has a two-tier model:
- **Tier (a) — Infra/bootstrap credentials** (platform-level, NOT tenant-scoped): loaded via `packages/config` from environment variables. These are: `DATABASE_URL`, `WORKOS_API_KEY`, `TRIGGER_API_KEY`, `TRIGGER_API_URL`, `AWS_REGION`, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` (or IAM role), `SECRET_MASTER_KEY_DEV` (dev/test only). They are never tenant secrets.
- **Tier (b) — Tenant credentials** (BYOM key, Proxmox token, SSH key, Git token, tenant registration token): via `SecretProvider` ONLY. Never env vars, never config files, never DB columns. The DB stores only a `SecretRef`.
This two-tier model is the documented exception to the PO's verbatim "no env vars" claim and will be cited in the M1 security review.
---
## User-Facing Surface (MVP/UX §1)
M1 ships ONE user-facing surface: the **Platform Lead admin dashboard** (browser, Next.js, at `/dashboard`). The chat UI is M3 — explicitly not in M1.
M1 dashboard surfaces (each is a pass/fail QA surface):
1. **SSO entry** (`/login`): "Sign in with SSO" button → WorkOS redirect → session → `/dashboard` redirect. (REQ-001)
2. **Onboarding checklist** (`/dashboard`): first-tenant view shows the 5 onboarding steps with status badges (grey/green): Configure BYOM, Install Relay Agent, Register Target, Verify Green Status, Invite Team. (REQ-002)
3. **BYOM config form** (`/dashboard/byom`): URL input + API key input + "Validate & Save" button. On save: green "Validated" badge with the test inference result, or red error panel with details. (REQ-006, REQ-007)
4. **Relay Agent install instructions** (`/dashboard/relay`): per-tenant `curl|bash` install command with the tenant registration token embedded; supported OS list shown; "unsupported OS" callout. (REQ-010)
5. **Targets list** (`/dashboard/targets`): one row per registered Relay Agent — hostname, OS, IP, agent version, health (green/yellow/red), last-seen, "View logs" link. (REQ-012, REQ-014)
6. **Target detail** (`/dashboard/targets/<id>`): health badge, last 100 log lines (streamed via WebSocket fan-out), registration metadata. (REQ-014)
7. **Team / RBAC** (`/dashboard/team`): list members + role; "Invite" form (email → single-use link); role change dropdown (Admin/Operator/Viewer). (REQ-003, REQ-004)
8. **Audit export** (`/dashboard/audit`): admin-only "Download audit log (CSV)" button — no query UI in MVP (spec §2.2). (REQ-038)
Non-UI surfaces: the Relay Agent install script (`curl|bash`), the Go binary, the systemd unit. These are operator-facing artifacts, documented in the install guide.
## Happy Path (MVP/UX §2)
End-to-end M1 scenario (maps to spec Journey 2 steps 14 + 7 + 9), written before EXECUTE:
> **Given** a fresh CoreCI Chat deployment with Postgres + WorkOS + AWS Secrets Manager configured,
> **when** a Platform Lead completes the M1 onboarding,
> **then** the M1 acceptance gate (spec §2.3) passes: SSO signup → BYOM green validation → Relay Agent install on a target Linux host → target registered → green status in the admin dashboard. Audit logging, RLS, and secret manager are operational.
BDD steps:
1. **Given** an unauthenticated visitor, **when** they hit `/login` and complete WorkOS SSO, **then** a tenant is provisioned (first signup), they are assigned Admin, and `/dashboard` loads with the 5-step onboarding checklist (all grey). (REQ-001, REQ-002)
2. **Given** the Admin on `/dashboard/byom`, **when** they submit a valid BYOM URL + API key and click "Validate & Save", **then** the API key is stored in the secret manager (not the DB), a test inference call to `/v1/chat/completions` returns 200, the endpoint row is inserted under RLS, the checklist step turns green, and an audit entry is appended. (REQ-006, REQ-007, REQ-038, REQ-039, REQ-040)
3. **Given** the Admin on `/dashboard/relay`, **when** they copy the `curl|bash` command and run it on an Ubuntu 24.04 host, **then** the install script detects Ubuntu, downloads the Go binary, verifies the checksum, writes the systemd unit, writes the tenant token to `/etc/coreci/relay.env`, enables + starts the service, and exits 0. (REQ-010)
4. **Given** the systemd service is running, **when** the Relay Agent starts, **then** it opens an outbound WebSocket to the SaaS within 60s, registers (tenant/target/hostname/Ubuntu 24.04/IP/version), the control plane inserts a `targets` row under RLS + appends an audit entry, and the dashboard `/targets` list shows the new row. (REQ-011, REQ-012, REQ-038, REQ-039)
5. **Given** the Relay Agent is registered, **when** it sends a heartbeat every 30s, **then** `last_seen` updates and the dashboard health badge is **green**. (REQ-013, REQ-014)
6. **Given** the Admin on `/dashboard/team`, **when** they invite `ops@example.com` as Operator, **then** WorkOS sends a single-use acceptance link; when the invitee accepts and hits the API, RBAC enforces Operator permissions (e.g., cannot edit BYOM). (REQ-003, REQ-004, REQ-005)
7. **Given** a second tenant T2 exists, **when** T1's user issues any DB query, **then** RLS returns zero T2 rows (cross-tenant pen test passes). (REQ-039)
8. **Given** the unsupported-OS case, **when** the install script runs on Fedora, **then** it exits non-zero with a message listing Ubuntu 24.04 LTS and Debian 12+. (REQ-010, Edge 16)
This Happy Path is the M1 demo recording required by the M1 review.
## UX Acceptance Criteria (MVP/UX §3)
1. **SSO works in <3 clicks** from `/login` to `/dashboard` for a returning user (REQ-001).
2. **BYOM validation feedback is synchronous** — the "Validate & Save" button shows a spinner and resolves in <10s with a green/red result (REQ-007).
3. **Dashboard health badge turns green within 90s** of the Relay Agent's first heartbeat (REQ-013, REQ-014).
4. **Install command is copy-pasteable** — the `/dashboard/relay` page shows one `curl -fsSL <url> | sh` line with the tenant token already embedded; no manual editing required (REQ-010).
5. **RBAC is enforced on the very next API call** after a role change — the dashboard re-fetches and the new permissions apply immediately (REQ-004, REQ-005).
6. **Cross-tenant isolation is verifiable** — the M1 pen-test script demonstrates zero leakage across two tenants (REQ-039).
7. **Audit log is append-only** — a test that attempts `UPDATE`/`DELETE` on `audit_log` as the app role fails with a permission error (REQ-038).
8. **Secrets are never in the DB** — a test that scans `byom_endpoints` and all tenant-scoped tables for plaintext credentials passes (returns zero matches); secrets are resolvable only via `SecretProvider.get` (REQ-040).
9. **Unsupported OS aborts cleanly** — running the install script on an unsupported OS exits non-zero with an actionable message (REQ-010, Edge 16).
---
## Wave A — Foundations (Phase 1)
**Goal:** Monorepo scaffold + Postgres schema with RLS + append-only hash-chain audit + SecretProvider interface + Trigger.dev bootstrap. No HTTP routes yet.
**Depends on:** Phase 0.
**REQs covered:** REQ-038, REQ-039, REQ-040.
**Personas:** backend-engineer, data-engineer, security-engineer (sign-off).
### Tasks
1. **Scaffold monorepo** (backend-engineer): pnpm workspace; `apps/control-plane` (Next.js App Router + TS), `apps/relay-agent` (Go module, empty for now), `apps/dashboard` (part of control-plane for M1), `packages/{db,auth,audit,secrets,config,runtime}`. `tsconfig.json` base + per-package extends. `package.json` scripts: `lint`, `typecheck`, `test` (vitest), `migrate`. `turbo.json` or pnpm `--filter` orchestration. `.gitignore` additions (`.secrets/`, `node_modules`, `dist`, `.next`).
2. **Postgres schema + migrations** (data-engineer): `packages/db/migrations/0001_init.sql``tenants`, `users`, `tenant_memberships (user_id, tenant_id, role)`, `targets`, `byom_endpoints (tenant_id, url, secret_ref, validated)`, `invitations`, `audit_log (id BIGSERIAL, tenant_id, prev_hash, curr_hash, payload JSONB, created_at, user_id, target_id, correlation_id, event_type)`, `runtime_health (id BIGSERIAL, component, status, payload JSONB, created_at)` (NOT tenant-scoped; NOT an audit table — see G-002). All tenant-scoped tables carry `tenant_id UUID NOT NULL`.
3. **RLS policies** (data-engineer + security-engineer): per-table policy `USING (tenant_id = current_setting('app.tenant_id')::uuid)`. `packages/db/rls.sql` run by the migrator. App role `coreci_app` with INSERT/SELECT only; `REVOKE UPDATE, DELETE ON audit_log FROM coreci_app`. `migrator` role with BYPASSRLS for migrations only.
4. **`withTenant` helper** (data-engineer): `packages/db/withTenant.ts` — opens a transaction, `SET LOCAL app.tenant_id = $1`, runs the callback, commits. Throws if called outside a transaction. Unit test: a query outside `withTenant` returns zero tenant-scoped rows.
5. **Audit writer** (backend-engineer + security-engineer): `packages/audit/writer.ts``append(event)` computes `curr_hash = sha256(prev_hash || canonical(payload))`, INSERTs inside the caller's transaction. Constraint trigger rejects a forged `prev_hash`. `AuditWriteHaltError` thrown on failure → caller's transaction rolls back. Unit test: append 3 entries, verify the chain; attempt UPDATE/DELETE → permission denied. **[G-006] Known M1-acceptable limit:** the per-tenant hash-chain serializes concurrent audit writes within one tenant (two simultaneous appends read the same `prev_hash`; the second INSERT fails the constraint and rolls back). Acceptable for M1 volume (onboarding + dashboard). **M3 mitigation:** `pg_advisory_xact_lock(hashtext(tenantId))` before the INSERT, or a per-tenant sequence for `prev_hash` ordering. Documented here so M3 is not a surprise.
6. **`SecretProvider` interface + impls** (backend-engineer + security-engineer): `packages/secrets/provider.ts` (interface), `packages/secrets/aws-sm.ts` (`@aws-sdk/client-secrets-manager`), `packages/secrets/local-encrypted.ts` (AES-256-GCM, master key from `SECRET_MASTER_KEY_DEV`). `SecretValue` type with `[REDACTED]` toString. Unit tests for both impls.
7. **Trigger.dev bootstrap** (backend-engineer): `packages/runtime/index.ts` — initializes the Trigger.dev client from config; registers a `runtimeHealthCheck` task that runs every 5 min and writes a row to `runtime_health` (NOT `audit_log` — see G-002; REQ-038's audit store is for business events only: prompts, tool calls, SSH commands, responses). No chat tasks. **[G-003] Pre-investment for M3:** shipping the runtime now means M3 chat orchestration plugs in without a runtime bootstrap rewrite; the 5-min health tick proves the runtime is wired without polluting the audit store.
8. **Cross-tenant pen-test scaffold** (security-engineer): `tests/pen/cross-tenant.test.ts` — two tenants, attempt to read T2 as T1, assert zero rows. (Full pen test runs at M1 review.)
9. **Tenant registration token contract** (backend-engineer + security-engineer): `packages/secrets/relay-token.ts` — defines the token issued by `POST /api/relay/issue-token` (Wave D Task 1) and consumed by the Go Relay Agent (Wave D Task 3). **[G-005] Contract (locked here so Wave B's auth middleware and Wave D's WS server + agent parallelize without blocking):** token is a signed JWT (HS256, key from `SECRET_MASTER_KEY_DEV` in dev / KMS-derived in prod — NOT a tenant secret, it's a platform bootstrap signing key in tier (a) of the credential taxonomy), claims `{tenantId, scope: "relay.register", iat, exp}`, lifetime 24h, refreshable. Stored as a `SecretRef` for re-issuance. The endpoint itself is built in Wave D; the contract + signing helper live here so neither wave blocks.
### Must-haves (verify before ship)
- [ ] `pnpm typecheck` + `pnpm lint` + `pnpm test` green.
- [ ] Migrations run clean against a fresh Postgres 16.
- [ ] `withTenant` test: query outside wrapper returns zero tenant rows.
- [ ] Audit chain test: 3 appends verify; UPDATE/DELETE rejected.
- [ ] `SecretProvider` test: put/get round-trip for both impls; `toString()` returns `[REDACTED]`.
- [ ] Cross-tenant pen-test scaffold compiles + runs (T1 sees zero T2 rows).
- [ ] Trigger.dev health task writes a `runtime_health` row on a 5-min tick (NOT `audit_log`). [G-002]
- [ ] `runtime_health` table is NOT tenant-scoped (no RLS); `audit_log` IS tenant-scoped.
- [ ] Relay token contract: JWT signed, claims `{tenantId, scope: "relay.register"}`, 24h lifetime; signing helper + verify helper unit-tested. [G-005]
- [ ] Audit concurrent-write limit documented in code comments (per-tenant serialization; M3 mitigation noted). [G-006]
- [ ] Code coverage ≥ 80% on `packages/db`, `packages/audit`, `packages/secrets`, `packages/runtime`.
## Wave B — Identity & RBAC (Phase 2)
**Goal:** WorkOS SSO + session + tenant resolution + RBAC at the API gateway from the first endpoint. First HTTP routes.
**Depends on:** Wave A (withTenant, audit). Wave B does NOT own the relay token-issuance endpoint (that's Wave D Task 1); the token contract is defined in Wave A Task 9 so B and D parallelize. [G-005]
**REQs covered:** REQ-001, REQ-002, REQ-003, REQ-004, REQ-005.
**Personas:** backend-engineer, frontend-engineer, security-engineer (sign-off).
### Tasks
1. **WorkOS SSO route** (backend-engineer): `/api/auth/login` → WorkOS hosted SSO redirect; `/api/auth/callback` → code exchange → session. Session stored httpOnly cookie + `sessions` table row (tenant_id, user_id, role).
2. **Tenant provisioning** (backend-engineer): on first signup, if the WorkOS `organizationId` has no tenant, INSERT tenant + tenant_membership(role=Admin) inside `withTenant`. Audit append (provision event).
3. **Tenant resolution middleware** (backend-engineer): `packages/auth/middleware.ts` — reads cookie, loads session, `SET app.tenant_id` via `withTenant`, attaches `req.user = {id, tenantId, role}`.
4. **RBAC enforcement** (backend-engineer + security-engineer): `packages/auth/rbac.ts` — role → route permission map (Admin: all; Operator: read + chat; Viewer: read). Applied at the API gateway. First protected endpoint: `GET /api/me` (returns user + tenant). Test: Viewer calling `POST /api/byom` → 403.
5. **Invitations** (backend-engineer): `POST /api/invitations` (Admin only) → WorkOS invitation API → single-use link emailed. `POST /api/invitations/accept` → creates tenant_membership. Edge 15: bounce webhook → mark invalid.
6. **Role assignment** (backend-engineer): `PATCH /api/team/<userId>` (Admin only) → updates `tenant_memberships.role`. Enforced on next API call.
7. **Login + dashboard shell** (frontend-engineer): `/login` page (SSO button), `/dashboard` shell with the 5-step onboarding checklist (all grey — steps light up as later waves ship). `/dashboard/team` page (invite + role UI).
### Must-haves
- [ ] SSO round-trip works (test with WorkOS sandbox).
- [ ] First signup provisions tenant + Admin role; audit entry written.
- [ ] RBAC: Viewer → 403 on Admin-only route; Operator → 200 on read.
- [ ] Role change enforced on the next API call (test).
- [ ] Invitation email sent (WorkOS); acceptance creates membership; bounce marks invalid.
- [ ] Audit entry appended for every auth event (login, provision, invite, role change).
- [ ] Coverage ≥ 80% on `packages/auth`.
## Wave C — BYOM (Phase 3)
**Goal:** BYOM endpoint registry + validate-on-save + routing shim (OpenAI-compatible). No chat/orchestration in M1 — the routing shim is a proxy contract + REQ-009 reject path.
**Depends on:** Wave A (secrets, audit, withTenant), Wave B (RBAC).
**REQs covered:** REQ-006, REQ-007, REQ-008, REQ-009.
**Personas:** backend-engineer, frontend-engineer, security-engineer (sign-off).
### Tasks
1. **BYOM endpoints table + routes** (backend-engineer): `POST /api/byom` (Admin only) — takes `{url, apiKey}`; `secrets.put(tenantId, "byom", apiKey)``secret_ref`; INSERT `byom_endpoints (url, secret_ref, validated=false)` under `withTenant`; audit append (config event).
2. **Validate-on-save** (backend-engineer): after INSERT, call the BYOM validator: `POST <url>/v1/chat/completions` with a trivial test prompt; on 200 → `UPDATE ... validated=true`, audit append (validation ok), return green; on failure → DELETE the row (or mark invalid), audit append (validation fail), return error details (Edge 11). All inside one transaction per the audit-halt rule.
3. **Routing shim** (backend-engineer): `packages/byom/router.ts``routeInference(tenantId, payload)` resolves the tenant's validated endpoint via `withTenant`, fetches the key via `secrets.get`, POSTs to `/v1/chat/completions`. M1 exposes a test endpoint `POST /api/byom/test-inference` (Admin only) that calls the shim and returns the raw response. **[G-001] Scope note:** this endpoint is a plan-time proxy to satisfy REQ-008 ("100% of LLM inference calls routed to BYOM, verified via outbound traffic log") in the absence of M3 chat orchestration — there is no other driver of inference in M1. It is marked for M3 deprecation once the chat orchestrator (REQ-033) drives real inference. PO-acknowledged (non-blocking). The outbound traffic log test for REQ-008 runs against this endpoint's egress.
4. **REQ-009 reject path** (backend-engineer + security-engineer): if no validated BYOM endpoint exists or the endpoint is unreachable, `routeInference` throws `ByomUnconfiguredError` / `ByomUnreachableError` → API returns a clear actionable error. Test: with no endpoint configured, calling `/api/byom/test-inference` → 400 with actionable message; with an unreachable URL → 503 with actionable message.
5. **BYOM dashboard page** (frontend-engineer): `/dashboard/byom` — URL + API key form, "Validate & Save" button, green/red result panel, current endpoint status. `/dashboard` checklist step 1 turns green on validated save.
### Must-haves
- [ ] Save stores key in secret manager; DB holds only `secret_ref` (test: scan tables for plaintext keys → zero).
- [ ] Validation POST hits the configured endpoint; green/red result accurate.
- [ ] Routing shim sends 100% of test-inference calls to the configured endpoint (outbound traffic log test).
- [ ] REQ-009: unconfigured → 400 actionable; unreachable → 503 actionable. No inference attempted.
- [ ] Audit entries for config + validation events.
- [ ] Coverage ≥ 80% on `packages/byom` + BYOM routes.
## Wave D — Relay Agent (Phase 4)
**Goal:** Modular install script + Go binary + WebSocket registration + heartbeat + auto-reconnect + SSH whitelist hook (no SSH adapter yet).
**Depends on:** Wave A (audit, withTenant, relay-token contract from Wave A Task 9 [G-005]), Wave B (auth middleware).
**REQs covered:** REQ-010, REQ-011, REQ-012, REQ-013, REQ-026 (partial — whitelist file + hook only; see G-008).
**Personas:** go-engineer (phase-specific), backend-engineer (control-plane WS server), security-engineer (sign-off on whitelist hook).
### Scope note — REQ-026 partial coverage in M1 [G-008]
M1 ships REQ-026 **partially**: the whitelist file format + the `CheckCommand` enforcement hook + unit + shadow-exec tests. The spec §4 REQ-026 acceptance criteria (customer generates an SSH keypair; the Relay Agent receives a tool call; only whitelisted commands execute; non-whitelisted rejected + audited) describe **end-to-end SSH-key-auth + tool-call-driven execution**, which is **M2** work (the SSH adapter plugs into the hook shipped here). M1's must-have is the hook + whitelist + tests, NOT end-to-end SSH execution. This matches the REQUIREMENTS.md traceability (REQ-026 deferred to M2; whitelist format + hook ship M1 Wave D).
### Tasks
1. **Tenant registration token endpoint** (backend-engineer): `POST /api/relay/issue-token` (Admin only) — issues the per-tenant registration token defined in Wave A Task 9 [G-005] (signed JWT, `scope: "relay.register"`, 24h). Stores a re-issuance ref via `secrets.put`. The dashboard `/dashboard/relay` page shows the `curl|bash` command with the token embedded.
2. **WS server** (backend-engineer): `apps/control-plane/api/relay/ws` — accepts outbound WebSocket, authenticates the tenant token (verify JWT from Wave A Task 9), on `register` message INSERTs `targets (tenant_id, hostname, os, os_version, ip, agent_version, last_seen)` under `withTenant`, audit append. Responds `{registered, targetId}`. Handles `ping` → updates `last_seen``pong`.
3. **Go binary — WebSocket client** (go-engineer): `apps/relay-agent/main.go` — reads `CORECI_TENANT_TOKEN` + `CORECI_SAAS_URL` from `/etc/coreci/relay.env`, connects `wss://<saas>/api/relay/ws`, sends `register`, then `ping` every 30s. On disconnect: exponential backoff (1/2/4/8/16s), max 5 attempts → alert + keep trying every 60s. systemd `Restart=on-failure` for hard crashes.
4. **Install script (modular)** (go-engineer): `scripts/install.sh` with separate functions `detect_os`, `install_binary`, `write_systemd_unit`, `register_target`, `main`. `detect_os` parses `/etc/os-release` (Ubuntu ≥ 24.04, Debian ≥ 12); else exit non-zero with the supported-OS list (Edge 16). `install_binary` downloads the Go binary for the detected arch, verifies SHA256, installs to `/usr/local/bin/coreci-relay-agent`. `write_systemd_unit` writes the unit + `daemon-reload` + `enable --now`. `register_target` writes `/etc/coreci/relay.env` with the tenant token. Idempotent (re-run upgrades).
5. **apt fallback** (go-engineer): documented apt package path (same script, `--method=apt` flag). The apt package ships the same binary + unit. (Documented; primary path is curl|bash.)
6. **SSH whitelist file + enforcement hook** (go-engineer + security-engineer): `/etc/coreci/ssh-whitelist.json` (versioned schema `{"version": 1, "commands": [...], "arguments": {"deny": [...]}}`; fixed list: cat, ls, systemctl status, journalctl, df, du, ps, top, ss, netstat, ip, uptime, uname, free, who, w, last, dmesg, lscpu, lspci, lsblk, mount, findmnt, hostname, "ip addr", "ip route", "ss -tlnp"; argument deny list: -exec, -execdir, --exec, |, >, >>, &, ;, &&, ||). `apps/relay-agent/whitelist/check.go``CheckCommand(cmd string) error` parses the command, checks base + args, returns error if rejected. Unit tests: every whitelist command passes; `rm -rf`, `find -exec`, `cat /etc/shadow | nc` all rejected. **[G-004] Contract lock:** the `CheckCommand(cmd string) error` signature + the whitelist JSON schema are the **M2 SSH adapter contract**. M2 must consume them as-shipped; any signature change requires a documented migration with a compatibility shim. **[G-003] Pre-investment for M2:** shipping the hook now means M2's SSH adapter plugs in without reworking the enforcement boundary; the cost is justified by avoiding the "retrofit = rewrite" risk the PO flagged. **[G-007] Shadow `exec.Cmd` integration test:** a Go test that constructs `exec.Command("systemctl", "status", "nginx")` from a parsed whitelist command and asserts `CheckCommand` accepts it (positive), plus a negative test that `exec.Command("rm", "-rf", "/")` is rejected by `CheckCommand` *before* the Cmd would be started — proving the hook composes with `os/exec` without a live SSH server. No SSH execution path in M1 — M2 plugs the adapter into `CheckCommand`.
### Must-haves
- [ ] Install script on Ubuntu 24.04 succeeds (exit 0, systemd service running, agent connected within 60s).
- [ ] Install script on Debian 12+ succeeds (same).
- [ ] **[G-009] Install script on ≥2 unsupported OSes aborts cleanly** with the supported-OS list (Edge 16): at minimum one non-Debian-family (e.g., Fedora or Alpine) AND one wrong-version Debian-family (e.g., Ubuntu 22.04 or Debian 11). Single-OS "unsupported" is not sufficient evidence.
- [ ] Re-running the script upgrades, does not fail.
- [ ] Relay Agent registers with full metadata (tenant/target/hostname/OS/IP/version); audit entry written.
- [ ] Heartbeat updates `last_seen`; dashboard can read it (Wave E surfaces this).
- [ ] Auto-reconnect: kill the WS server, agent retries with backoff, max 5 → alert; restart server → agent reconnects.
- [ ] `CheckCommand`: every whitelist command passes; every deny-list case rejected. Coverage 100% on the whitelist module.
- [ ] **[G-007] Shadow `exec.Cmd` test:** positive (`systemctl status nginx` accepted, composes to `exec.Command`) + negative (`rm -rf /` rejected pre-start) both pass.
- [ ] Whitelist file format is versioned (JSON `{"version": 1, ...}`). [G-004]
- [ ] `CheckCommand(cmd string) error` signature + whitelist JSON schema documented as the M2 contract. [G-004]
## Wave E — Dashboard surfacing (Phase 5)
**Goal:** Dashboard shows Relay Agent health (green/yellow/red), target hostname, last 100 log lines, per-tenant view under RLS. M1 gate demo path complete.
**Depends on:** Wave B (auth, dashboard shell), Wave D (Relay Agent + WS server).
**REQs covered:** REQ-014.
**Personas:** frontend-engineer, backend-engineer.
### Tasks
1. **Status fan-out WebSocket** (backend-engineer): `apps/control-plane/api/relay/status` — Admin-authenticated WS that pushes target status changes (health, last_seen, log lines) to connected dashboard clients. Status computed from `targets.last_seen` (green < 60s, yellow < 5min, red > 5min).
2. **Targets list page** (frontend-engineer): `/dashboard/targets` — server component fetches targets via the API (under RLS), renders rows (hostname, OS, IP, version, health badge, last-seen, "View logs"). Client component subscribes to the status WS for live updates.
3. **Target detail page** (frontend-engineer): `/dashboard/targets/<id>` — health badge, registration metadata, last 100 log lines (streamed). Under RLS (T1 admin cannot view T2's target — pen-test asserts).
4. **Onboarding checklist completion** (frontend-engineer): `/dashboard` checklist step "Verify Green Status" turns green when at least one target is green. The M1 demo path (SSO → BYOM green → install → register → green dashboard) is end-to-end walkable.
5. **Audit export** (backend-engineer + frontend-engineer): `/dashboard/audit` — admin-only "Download CSV" button. No query UI (spec §2.2). CSV scoped to the tenant under RLS.
### Must-haves
- [ ] Dashboard shows a registered target as green within 90s of first heartbeat.
- [ ] Killing the agent → badge turns yellow then red as `last_seen` ages.
- [ ] Restarting the agent → badge turns green again.
- [ ] T1 admin cannot view T2's target (RLS pen-test asserts).
- [ ] Last 100 log lines render on the target detail page.
- [ ] Audit CSV export is tenant-scoped (no cross-tenant rows).
- [ ] The full M1 Happy Path (UX §2) is walkable end-to-end.
---
## Final Phase — Review + Ship (Phase 6)
**Goal:** Multi-persona code review + project health audit + milestone ship (v0.1.0 release, merge to main).
**REQs covered:** all M1 (sign-off).
### Tasks
1. **Code review** (lead-developer + all personas): review all changes in `milestone/v0.1-bootstrap` since `main`. Auto-apply P0 fixes; flag P1+ for post-hoc.
2. **Audit** (lead-developer): reconstruction test (git log matches `.ciagent/` files); file/branch/commit discipline; cross-tenant pen test runs green; install script runs on the 3 OS cases (Ubuntu 24.04 pass, Debian 12+ pass, unsupported abort).
3. **M1 acceptance gate verification** (lead-developer): spec §2.3 — Platform Lead can SSO → BYOM green → install → register → green dashboard. Audit/RLS/secrets operational.
4. **Milestone ship**: tag `v0.0.7` (final phase patch = v0.1 milestone release); merge `phase/06``milestone/v0.1-bootstrap``main`; create Gitea release with full milestone summary; build + upload Relay Agent binaries (linux amd64/arm64) + install script + apt package.
5. **Complete**: mark all M1 REQs complete in REQUIREMENTS.md; mark milestone complete in ROADMAP.md; clear checkpoint.
### M1 review deliverables (per Sarah's kickoff)
1. Per-REQ pass/fail test report with evidence (17 REQs).
2. Demo recording: fresh tenant → SSO → BYOM green → install → register → green dashboard.
3. Cross-tenant isolation pen test result (zero leakage).
4. Install script logs: Ubuntu 24.04 (pass), Debian 12+ (pass), unsupported OS (clean abort).
---
## Wave ordering & parallelism
```
Phase 0 (this plan) ──▶ Wave A (foundations)
├──▶ Wave B (identity/RBAC) ──▶ Wave E (dashboard) ──▶ Final
│ ▲
└──▶ Wave C (BYOM) ─────────────────┤
Wave D (relay agent) ─────────────┘
```
- A must complete first (B, C, D all depend on `withTenant` + `audit` + `secrets`).
- B and C can run in parallel after A (different territories; both depend on A only).
- D can run in parallel after A (independent of B/C; the WS server in D needs B's auth token-issuance endpoint — coordinate the contract in the plan, then D's WS server + B's token endpoint can land in the same wave window).
- E depends on B (dashboard shell + auth) and D (agent + WS server).
- Final depends on all.
## Test strategy
- Unit: vitest in `packages/*` + `apps/control-plane`; Go `testing` in `apps/relay-agent`.
- Integration: a Postgres 16 container in CI; `withTenant` + audit + RLS + secrets tests against it.
- Pen test: `tests/pen/cross-tenant.test.ts` runs at Wave A (scaffold) and Final (full).
- Install test: CI matrix runs `scripts/install.sh` on Ubuntu 24.04, Debian 12, Fedora (expects abort) containers.
- Coverage gate: ≥ 80% on new modules (spec §6).
- Lint + typecheck: `pnpm lint` + `pnpm typecheck` must be green before any wave ships.