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---
33 KiB
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-inferenceis 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_healthtable, NOTaudit_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) errorsignature + whitelist JSON schema are the M2 SSH adapter contract; changes require a documented migration. - G-005 (Wave A + B + D):
POST /api/relay/issue-tokenowned 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.Cmdintegration test provesCheckCommandcomposes withos/execwithout 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/configfrom 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
SecretProviderONLY. Never env vars, never config files, never DB columns. The DB stores only aSecretRef.
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):
- SSO entry (
/login): "Sign in with SSO" button → WorkOS redirect → session →/dashboardredirect. (REQ-001) - 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) - 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) - Relay Agent install instructions (
/dashboard/relay): per-tenantcurl|bashinstall command with the tenant registration token embedded; supported OS list shown; "unsupported OS" callout. (REQ-010) - 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) - Target detail (
/dashboard/targets/<id>): health badge, last 100 log lines (streamed via WebSocket fan-out), registration metadata. (REQ-014) - Team / RBAC (
/dashboard/team): list members + role; "Invite" form (email → single-use link); role change dropdown (Admin/Operator/Viewer). (REQ-003, REQ-004) - 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:
- Given an unauthenticated visitor, when they hit
/loginand complete WorkOS SSO, then a tenant is provisioned (first signup), they are assigned Admin, and/dashboardloads with the 5-step onboarding checklist (all grey). (REQ-001, REQ-002) - 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/completionsreturns 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) - Given the Admin on
/dashboard/relay, when they copy thecurl|bashcommand 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) - 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
targetsrow under RLS + appends an audit entry, and the dashboard/targetslist shows the new row. (REQ-011, REQ-012, REQ-038, REQ-039) - Given the Relay Agent is registered, when it sends a heartbeat every 30s, then
last_seenupdates and the dashboard health badge is green. (REQ-013, REQ-014) - Given the Admin on
/dashboard/team, when they inviteops@example.comas 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) - 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)
- 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)
- SSO works in <3 clicks from
/loginto/dashboardfor a returning user (REQ-001). - BYOM validation feedback is synchronous — the "Validate & Save" button shows a spinner and resolves in <10s with a green/red result (REQ-007).
- Dashboard health badge turns green within 90s of the Relay Agent's first heartbeat (REQ-013, REQ-014).
- Install command is copy-pasteable — the
/dashboard/relaypage shows onecurl -fsSL <url> | shline with the tenant token already embedded; no manual editing required (REQ-010). - 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).
- Cross-tenant isolation is verifiable — the M1 pen-test script demonstrates zero leakage across two tenants (REQ-039).
- Audit log is append-only — a test that attempts
UPDATE/DELETEonaudit_logas the app role fails with a permission error (REQ-038). - Secrets are never in the DB — a test that scans
byom_endpointsand all tenant-scoped tables for plaintext credentials passes (returns zero matches); secrets are resolvable only viaSecretProvider.get(REQ-040). - 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
- 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.jsonbase + per-package extends.package.jsonscripts:lint,typecheck,test(vitest),migrate.turbo.jsonor pnpm--filterorchestration..gitignoreadditions (.secrets/,node_modules,dist,.next). - 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 carrytenant_id UUID NOT NULL. - RLS policies (data-engineer + security-engineer): per-table policy
USING (tenant_id = current_setting('app.tenant_id')::uuid).packages/db/rls.sqlrun by the migrator. App rolecoreci_appwith INSERT/SELECT only;REVOKE UPDATE, DELETE ON audit_log FROM coreci_app.migratorrole with BYPASSRLS for migrations only. withTenanthelper (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 outsidewithTenantreturns zero tenant-scoped rows.- Audit writer (backend-engineer + security-engineer):
packages/audit/writer.ts—append(event)computescurr_hash = sha256(prev_hash || canonical(payload)), INSERTs inside the caller's transaction. Constraint trigger rejects a forgedprev_hash.AuditWriteHaltErrorthrown 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 sameprev_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 forprev_hashordering. Documented here so M3 is not a surprise. SecretProviderinterface + 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 fromSECRET_MASTER_KEY_DEV).SecretValuetype with[REDACTED]toString. Unit tests for both impls.- Trigger.dev bootstrap (backend-engineer):
packages/runtime/index.ts— initializes the Trigger.dev client from config; registers aruntimeHealthChecktask that runs every 5 min and writes a row toruntime_health(NOTaudit_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. - 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.) - Tenant registration token contract (backend-engineer + security-engineer):
packages/secrets/relay-token.ts— defines the token issued byPOST /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 fromSECRET_MASTER_KEY_DEVin 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 aSecretReffor 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 testgreen.- Migrations run clean against a fresh Postgres 16.
withTenanttest: query outside wrapper returns zero tenant rows.- Audit chain test: 3 appends verify; UPDATE/DELETE rejected.
SecretProvidertest: 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_healthrow on a 5-min tick (NOTaudit_log). [G-002] runtime_healthtable is NOT tenant-scoped (no RLS);audit_logIS 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
- WorkOS SSO route (backend-engineer):
/api/auth/login→ WorkOS hosted SSO redirect;/api/auth/callback→ code exchange → session. Session stored httpOnly cookie +sessionstable row (tenant_id, user_id, role). - Tenant provisioning (backend-engineer): on first signup, if the WorkOS
organizationIdhas no tenant, INSERT tenant + tenant_membership(role=Admin) insidewithTenant. Audit append (provision event). - Tenant resolution middleware (backend-engineer):
packages/auth/middleware.ts— reads cookie, loads session,SET app.tenant_idviawithTenant, attachesreq.user = {id, tenantId, role}. - 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 callingPOST /api/byom→ 403. - 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. - Role assignment (backend-engineer):
PATCH /api/team/<userId>(Admin only) → updatestenant_memberships.role. Enforced on next API call. - Login + dashboard shell (frontend-engineer):
/loginpage (SSO button),/dashboardshell with the 5-step onboarding checklist (all grey — steps light up as later waves ship)./dashboard/teampage (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
- BYOM endpoints table + routes (backend-engineer):
POST /api/byom(Admin only) — takes{url, apiKey};secrets.put(tenantId, "byom", apiKey)→secret_ref; INSERTbyom_endpoints (url, secret_ref, validated=false)underwithTenant; audit append (config event). - Validate-on-save (backend-engineer): after INSERT, call the BYOM validator:
POST <url>/v1/chat/completionswith 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. - Routing shim (backend-engineer):
packages/byom/router.ts—routeInference(tenantId, payload)resolves the tenant's validated endpoint viawithTenant, fetches the key viasecrets.get, POSTs to/v1/chat/completions. M1 exposes a test endpointPOST /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. - REQ-009 reject path (backend-engineer + security-engineer): if no validated BYOM endpoint exists or the endpoint is unreachable,
routeInferencethrowsByomUnconfiguredError/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. - BYOM dashboard page (frontend-engineer):
/dashboard/byom— URL + API key form, "Validate & Save" button, green/red result panel, current endpoint status./dashboardchecklist 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
- 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 viasecrets.put. The dashboard/dashboard/relaypage shows thecurl|bashcommand with the token embedded. - WS server (backend-engineer):
apps/control-plane/api/relay/ws— accepts outbound WebSocket, authenticates the tenant token (verify JWT from Wave A Task 9), onregistermessage INSERTstargets (tenant_id, hostname, os, os_version, ip, agent_version, last_seen)underwithTenant, audit append. Responds{registered, targetId}. Handlesping→ updateslast_seen→pong. - Go binary — WebSocket client (go-engineer):
apps/relay-agent/main.go— readsCORECI_TENANT_TOKEN+CORECI_SAAS_URLfrom/etc/coreci/relay.env, connectswss://<saas>/api/relay/ws, sendsregister, thenpingevery 30s. On disconnect: exponential backoff (1/2/4/8/16s), max 5 attempts → alert + keep trying every 60s. systemdRestart=on-failurefor hard crashes. - Install script (modular) (go-engineer):
scripts/install.shwith separate functionsdetect_os,install_binary,write_systemd_unit,register_target,main.detect_osparses/etc/os-release(Ubuntu ≥ 24.04, Debian ≥ 12); else exit non-zero with the supported-OS list (Edge 16).install_binarydownloads the Go binary for the detected arch, verifies SHA256, installs to/usr/local/bin/coreci-relay-agent.write_systemd_unitwrites the unit +daemon-reload+enable --now.register_targetwrites/etc/coreci/relay.envwith the tenant token. Idempotent (re-run upgrades). - apt fallback (go-engineer): documented apt package path (same script,
--method=aptflag). The apt package ships the same binary + unit. (Documented; primary path is curl|bash.) - 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) errorparses the command, checks base + args, returns error if rejected. Unit tests: every whitelist command passes;rm -rf,find -exec,cat /etc/shadow | ncall rejected. [G-004] Contract lock: theCheckCommand(cmd string) errorsignature + 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] Shadowexec.Cmdintegration test: a Go test that constructsexec.Command("systemctl", "status", "nginx")from a parsed whitelist command and assertsCheckCommandaccepts it (positive), plus a negative test thatexec.Command("rm", "-rf", "/")is rejected byCheckCommandbefore the Cmd would be started — proving the hook composes withos/execwithout a live SSH server. No SSH execution path in M1 — M2 plugs the adapter intoCheckCommand.
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.Cmdtest: positive (systemctl status nginxaccepted, composes toexec.Command) + negative (rm -rf /rejected pre-start) both pass. - Whitelist file format is versioned (JSON
{"version": 1, ...}). [G-004] CheckCommand(cmd string) errorsignature + 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
- 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 fromtargets.last_seen(green < 60s, yellow < 5min, red > 5min). - 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. - 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). - Onboarding checklist completion (frontend-engineer):
/dashboardchecklist 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. - 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_seenages. - 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
- Code review (lead-developer + all personas): review all changes in
milestone/v0.1-bootstrapsincemain. Auto-apply P0 fixes; flag P1+ for post-hoc. - 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). - M1 acceptance gate verification (lead-developer): spec §2.3 — Platform Lead can SSO → BYOM green → install → register → green dashboard. Audit/RLS/secrets operational.
- Milestone ship: tag
v0.0.7(final phase patch = v0.1 milestone release); mergephase/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. - Complete: mark all M1 REQs complete in REQUIREMENTS.md; mark milestone complete in ROADMAP.md; clear checkpoint.
M1 review deliverables (per Sarah's kickoff)
- Per-REQ pass/fail test report with evidence (17 REQs).
- Demo recording: fresh tenant → SSO → BYOM green → install → register → green dashboard.
- Cross-tenant isolation pen test result (zero leakage).
- 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; Gotestinginapps/relay-agent. - Integration: a Postgres 16 container in CI;
withTenant+ audit + RLS + secrets tests against it. - Pen test:
tests/pen/cross-tenant.test.tsruns at Wave A (scaffold) and Final (full). - Install test: CI matrix runs
scripts/install.shon Ubuntu 24.04, Debian 12, Fedora (expects abort) containers. - Coverage gate: ≥ 80% on new modules (spec §6).
- Lint + typecheck:
pnpm lint+pnpm typecheckmust be green before any wave ships.