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---
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "plan",
|
||||
"stage": "mvp_ux_check",
|
||||
"milestone": "v0.1",
|
||||
"phase_role": "pre_execution",
|
||||
"attempts": 1,
|
||||
"updated_at": "2026-08-24T23:05:00Z"
|
||||
"updated_at": "2026-08-24T23:15:00Z"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# MVP/UX Check (Phase 0 gate)
|
||||
|
||||
Per the run workflow, the MVP/UX checkpoint (REQ-MVP-UX-001) runs between GRILL and EXECUTE. At `full` autonomy, the orchestrator verifies the 3 sections are present in PLAN.md and auto-generates any missing sections.
|
||||
|
||||
## Verification
|
||||
|
||||
PLAN.md (`.ciagent/PLAN.md`) is checked for the 3 mandatory sections:
|
||||
|
||||
| Section | Required | Present | Location | Content |
|
||||
|---------|----------|---------|----------|---------|
|
||||
| `## User-Facing Surface` | yes | ✓ | PLAN.md line 49 | Names the M1 user-facing surface: Platform Lead admin dashboard (browser, Next.js, at `/dashboard`). Lists 8 specific dashboard surfaces (SSO entry, onboarding checklist, BYOM config form, relay install instructions, targets list, target detail, team/RBAC, audit export) + 3 non-UI operator surfaces (install script, Go binary, systemd unit). Explicitly states chat UI is M3, not M1. |
|
||||
| `## Happy Path` | yes | ✓ | PLAN.md line 66 | End-to-end M1 scenario written BEFORE execute. Maps to spec Journey 2 (steps 1–4 + 7 + 9). 8 BDD steps (Given/When/Then) covering: SSO signup + tenant provision, BYOM validate-and-save, install script on Ubuntu 24.04, Relay Agent registration + heartbeat, dashboard green, team invite + RBAC enforcement, cross-tenant isolation, unsupported-OS abort. States this Happy Path IS the M1 demo recording required by the M1 review. |
|
||||
| `## UX Acceptance Criteria` | yes | ✓ | PLAN.md line 86 | 9 explicit pass/fail criteria: SSO <3 clicks, BYOM feedback synchronous <10s, dashboard green within 90s of first heartbeat, install command copy-pasteable, RBAC enforced on next call, cross-tenant isolation verifiable, audit append-only (UPDATE/DELETE fails), secrets never in DB (scan returns zero), unsupported OS aborts cleanly. Each maps to a REQ. |
|
||||
|
||||
All 3 sections present and substantive. No auto-generation needed.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS** — PLAN.md satisfies REQ-MVP-UX-001. EXECUTE is unblocked.
|
||||
|
||||
## Post-check actions
|
||||
|
||||
- Update CHECKPOINT.json: `stage: "mvp_ux_check"` → next: PHASE 0 SHIP.
|
||||
- Proceed to Phase 0 ship (tag `v0.0.1`, merge `phase/00-pre-execution` → `milestone/v0.1-bootstrap`).
|
||||
+47
-14
@@ -20,6 +20,30 @@ M1 is decomposed into **5 execution phases** (Waves A–E). Each wave is a verti
|
||||
|
||||
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)
|
||||
@@ -82,13 +106,14 @@ This Happy Path is the M1 demo recording required by the M1 review.
|
||||
|
||||
### 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)`. All tenant-scoped tables carry `tenant_id UUID NOT NULL`.
|
||||
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.
|
||||
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 appends an audit entry. No chat tasks.
|
||||
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.
|
||||
@@ -97,13 +122,16 @@ This Happy Path is the M1 demo recording required by the M1 review.
|
||||
- [ ] 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 appends an audit entry on a 5-min tick.
|
||||
- [ ] Code coverage ≥ 80% on `packages/db`, `packages/audit`, `packages/secrets`.
|
||||
- [ ] 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).
|
||||
**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).
|
||||
|
||||
@@ -135,7 +163,7 @@ This Happy Path is the M1 demo recording required by the M1 review.
|
||||
### 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 — proves REQ-008 routing without needing chat.
|
||||
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.
|
||||
|
||||
@@ -150,28 +178,33 @@ This Happy Path is the M1 demo recording required by the M1 review.
|
||||
## 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), Wave B (auth token issuance).
|
||||
**REQs covered:** REQ-010, REQ-011, REQ-012, REQ-013, REQ-026 (whitelist hook only).
|
||||
**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** (backend-engineer): `POST /api/relay/issue-token` (Admin only) — issues a per-tenant registration token, stored as a secret-manager ref. 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, 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`.
|
||||
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` (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) 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. **No SSH execution path in M1** — M2 plugs the adapter into `CheckCommand`.
|
||||
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).
|
||||
- [ ] Install script on an unsupported OS (e.g., Fedora) aborts cleanly with the supported-OS list (Edge 16).
|
||||
- [ ] **[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.
|
||||
- [ ] Whitelist file format is versioned (JSON `{"version": 1, "commands": [...], "arguments": {"deny": [...]}}`).
|
||||
- [ ] **[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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user