61221222eb
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---
279 lines
33 KiB
Markdown
279 lines
33 KiB
Markdown
# 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 A–E). 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 1–4 + 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. |