Files
CIAgent 85dce28ca3 docs(PDLC): lock M3 spec v1.2 Final — Chat, Orchestration, Hardening
15 REQs (028-037, 041-045) with REQ-035 amendment (auto-promote at step 21).
3 personas (Devon/Sam/Casey), 5 journeys + J-infra, no orphan REQs.
Decisions #1-#10 locked; OQ1-6 resolved, OQ7 open (control_state write-through).
Migrations 0004-0006 (chat/workflow/posture), per-table for rollback isolation.
STATE.md updated: M3 spec locked, implementation pending.

---ci---
phase: 3
stage: spec-lock
milestone: v0.2-M3
spec: .ciagent/steer-m3-spec.md
spec_version: v1.2
status: ready-for-handoff
reqs: [028,029,030,031,032,033,034,035,036,037,041,042,043,044,045]
req035_amended: true
invariants: [INV-1,INV-2,INV-3,INV-4,INV-5,INV-6,INV-7,INV-8]
release: none
2026-08-25 12:41:16 +00:00

270 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CoreCI Chat — M3 Specification (Chat, Orchestration, Hardening)
**Owner:** Sarah Chen (Sr. PM)
**Status:** Final (v1.2)
**Type:** Milestone
**Target Milestone:** v0.2 — M3 (Chat, Orchestration, Hardening)
**Locked:** 2026-08-25
> **Operating Principles for this Spec:**
> 1. **Incremental Delivery:** This spec defines net-new work only. Pre-existing systems and locked architectures (M1, M2) are referenced, not restated.
> 2. **Zero Ambiguity:** If a requirement cannot be translated into a pass/fail test by QA, it is incomplete and will be rejected by Engineering.
> 3. **Invariants preserved:** INV-1..INV-8 from `.ciagent/ARCHITECTURE.md` hold as written. M3 adds no invariant amendments (Q5 session-batched Merkle is additive, not a semantic change to INV-4).
---
## 1. Objective
M3 delivers a diagnostic copilot that lets operators ask natural-language questions about tenant infrastructure and receive cited, evidence-backed answers within the M3 acceptance gate. It ships chat UI with streamed tool execution (≤20 steps inline, auto-promoted to durable Trigger.dev workflows beyond), persistent conversation history, per-inference usage metering, a SOC2 posture page with live control probes, and Vanta evidence sync. Three personas benefit: Devon (Operator) gets the copilot; Sam (Admin) gets posture + usage visibility; Casey (Compliance Reader) gets read-only attestation surfaces. The milestone closes v0.1 with 15 REQs total and refines (does not rewrite) the acceptance gate.
*Acceptance Gate:* A developer reading this can state: "M3 ships a multi-turn diagnostic copilot that streams tool execution, persists durable workflows, meters usage, and instruments SOC2 controls."
---
## 2. Scope & Target Milestones
### 2.1 In Scope (Explicit Additions)
15 REQs in three clusters:
**Chat & Orchestration (REQ-028..037):** chat UI surface, natural-language input, streamed response with inline citations, streamed tool traces, conversation history (first-class data model), LLM tool reasoning, multi-step workflows, ≤20-step inline limit (amended: auto-promote at 21), durable execution via Trigger.dev, workflow rejoin. New packages: `packages/orchestrator` (step counter, promotion logic), `packages/chat` (stream manager, citation builder). New schemas: `chat_sessions`, `chat_turns`, `workflow_events` (additive per `steer-m2-spec.md:162`). API contracts: `/api/chat/stream` (inline + durable rejoin via `Last-Event-ID`), `workflow.promoted` event (inline→durable handoff).
**Hardening (REQ-041..045):** REQ-041 SOC2 posture page with live control probes (5-min default interval, configurable per tenant) + `control_state` cache table (write-through with probe transaction; Decision #8); REQ-042 Vanta evidence sync (1-h default, no signed attestations); REQ-043 usage metering (per-inference `usage` block from REQ-030 feeds the pipeline; aggregation 1-min default); REQ-044 usage dashboard (tenant-facing rendering); REQ-045 Redis-backed rate aggregation (cross-process coordination between API gateway and Trigger.dev workers, replacing M2's in-memory token bucket; fail-closed 503 if Redis down).
**Architectural amendments:** REQ-035 amendment text baked in (§4). New audit event types: `chat.session.created`, `chat.turn.*`, `llm.inference.{requested,succeeded,failed}`, `tool.plan.{emitted,selected,rejected}`, `workflow.{started,step.*,promoted,completed,timeout,promotion_failed,rejoined}`, `control.probe.{succeeded,failed}`, `vanta.sync.{succeeded,failed}`, `rate.redis.unavailable`, `evidence.exported`, `authz.denied`.
### 2.2 Out of Scope (Explicit Exclusions)
Two categories. Anti-goals (vision spec §2.2 — never / post-MVP / v1.2+) vs. M4+ deferrals (no REQ slot in M3 set):
**Anti-goals (vision §2.2, not M3 deferrals):** write actions on infrastructure (v1.1+, INV-7 holds); hosted LLM inference (never, INV-5 holds, planner split rejected per Phase 1 Q3); RAG / vector retrieval (v1.1+); SOC 2 final certification (post-MVP); custom RBAC roles (v1.2+); BYOK / customer-managed keys (v1.2+); multi-region deployment (MVP single-region); Windows support (not planned v1.x); fine-tuning (not planned); Kubernetes / ArgoCD / Helm; Slack / Teams / CLI / mobile clients (v1.1+); approval-gated remediation (v1.1+).
**M4+ deferrals (no REQ slot exists in M3 — each item carries REQ-gap reasoning):**
- **Secret rotation** — `SecretProvider` interface exists (REQ-039, M1); rotation is an ops process. No REQ slot in 028-037/041-045.
- **Production deployment of M1+M2+M3** — ops track parallel to PDLC. M3 REQs satisfied by code shipping to `main` + Gitea releases. No REQ slot.
- **Adapter SSRF/CSRF guardrails** — tenant-supplied hostnames are trusted per M2 spec (`steer-m2-spec.md:172`); hardening requires a new REQ. No REQ slot.
- **OTel / RED metrics / distributed tracing** — observability stack. No REQ slot. REQ-041 is posture page, not instrumentation.
- **SBOM / supply-chain / signed releases** — No REQ slot.
- **Gitea Actions runner enablement** — M2 CI prereq, ops responsibility; blocks full M2 verification, not M3 REQ satisfaction. Listed as ship-blocker prereq in §6.
- **Track B GitHub smoke (`GITHUB_SMOKE_PAT`)** — M2 CI prereq, ops responsibility. Listed as ship-blocker prereq in §6.
- **LLM-as-executor over user-authored plans** — Phase 1 Q2 marked OUT; no REQ slot.
- **Async audit (decoupled via Trigger.dev)** — Phase 1 Q5 rejected; would amend INV-4 without justification.
- **Planner / executor BYOM split** — Phase 1 Q3 rejected without D-008; `byom_endpoints` is single-row-per-tenant.
- **Per-inference cost attribution as a billing signal** — REQ-043 meters; billing is not in scope (no billing REQ).
### 2.3 Milestone Breakdown
M3 is a single milestone. Sub-phases for delivery sequencing (not separate milestones):
- **M3.a — Chat inline + citation + history (REQ-028..033):** chat UI, NL input, streaming + citations, tool traces, conversation history + lifecycle, planner.
- **M3.b — Orchestration + durability (REQ-034..037):** multi-step workflows, ≤20-step inline limit (amended), durable Trigger.dev execution, rejoin protocol.
- **M3.c — Hardening + metering (REQ-041..045):** SOC2 posture page + live probes + `control_state`, Vanta sync, usage metering + dashboard, Redis rate aggregation.
Each sub-phase is independently shippable behind a feature flag; M3 final ship is the union of all three.
*Acceptance Gate (M3 ship):* All 15 REQs PASS, ≥80% coverage on new packages, INV-1..INV-8 preserved as written, M1+M2 non-regression suite green, M3 acceptance gate demonstrably satisfied on dev environment (with durable-path SLO refinement from §6 anchored).
---
## 3. Personas & User Journeys
### 3.1 Personas
- **Devon (Operator)** — Tenant user who invokes the diagnostic copilot. Technical sophistication: SRE / DevOps practitioner. Goal: ask a natural-language diagnostic question, see streamed tool execution, get a cited evidence-backed answer without hand-crafting curl commands or navigating the Test-Call UI.
- **Sam (Admin)** — Tenant administrator who configures adapters, BYOM endpoints, team membership, and reads posture + usage. Technical sophistication: platform admin. Goal: ensure controls are green, usage is within budget, evidence flows to Vanta.
- **Casey (Compliance Reader)** — Read-only stakeholder (auditor, GRC reviewer). Technical sophistication: compliance professional. Goal: view posture page + usage + audit log without write or admin powers. RBAC granularity: Casey sees all controls (posture, usage, audit) read-only; no probe-trigger, no config edit, no Vanta credential view. (New in M2; first explicit M3 journey in J3.)
### 3.2 Happy Paths
**Journey 1 (J1) — Devon asks an inline diagnostic question (≤20 steps).**
1. Devon opens `/chat`, types "Why is the staging nginx fleet degraded?", submits. → Chat UI creates a `chat_session` row, emits `chat.session.created` audit event. *(Maps to REQ-028, REQ-029, REQ-032)*
2. Server begins streaming SSE on `/api/chat/stream`. First event: `turn.user` with the question. → Client renders user message. *(Maps to REQ-030)*
3. Orchestrator invokes planner via tenant BYOM. → Emits `llm.inference.requested` with `{turn_id, byom_endpoint_id, prompt_tokens_estimated, workflow_id=null}`. *(Maps to REQ-033, REQ-029)*
4. Planner returns tool-call plan. → Emits `tool.plan.{emitted,selected}`. Intermediate planner message stored `visible=false` in `chat_turns`. Server forwards tool trace to client. *(Maps to REQ-031, REQ-033)*
5. For each tool call: orchestrator invokes broker adapter router (one row = one step). → Emits `adapter.capability_invoked` (already in M2 audit). Increments step counter. *(Maps to REQ-031, REQ-034)*
6. Tool result returns; planner reasons again; loop continues until planner emits final answer or step 20. *(Maps to REQ-033, REQ-034)*
7. Server emits final assistant message with citations array per REQ-030 shape. `usage` block attached: `{prompt_tokens, completion_tokens, byom_endpoint_id, latency_ms, workflow_id, correlation_id}`. *(Maps to REQ-030, REQ-043)*
8. Server emits `llm.inference.succeeded` per BYOM call. Client closes SSE on `turn.assistant.final`. *(Maps to REQ-030, REQ-043)*
*Testable Acceptance (BDD):*
- **Given** Devon is authenticated, tenant BYOM is configured, ≥1 adapter is configured, **when** Devon submits a question that triggers ≤20 tool calls, **then** the response streams with citations, all `adapter.capability_invoked` rows have `session_id` FK set, all `llm.inference.*` rows are present, and the final SSE event is `turn.assistant.final` within 5 min p95.
- **Given** J1 has just completed, **when** Devon reloads `/chat`, **then** the conversation appears in the history list with all turns and citations renderable.
- **Given** Devon submits a question, **when** the planner emits a tool call, **then** the SSE stream emits a `tool.trace` event before the corresponding `adapter.capability_invoked` row is written to `audit_log`.
**Journey 1-durable (J1-durable) — Devon triggers a workflow that exceeds 20 steps or must survive restart.**
1. Devon submits a question whose planner-execution loop is projected to exceed 20 steps (e.g., "Audit all 50 Proxmox nodes for CVE-2024-xxxx"). → Orchestrator detects projection ≥21 at step 20. *(Maps to REQ-034, REQ-035)*
2. At step 21, orchestrator auto-promotes: assigns `workflow_id` (ULID), creates Trigger.dev task, emits `workflow.promoted` event on the SSE stream, bridges handoff. *(Maps to REQ-035 amended, REQ-036)*
3. Client UI updates state to "Background workflow — running asynchronously" indicator upon receipt of `workflow.promoted`. *(Phase 2 Item 5 addition)*
4. Trigger.dev worker continues execution; each step emits `workflow.step.{started,completed,failed}` to `workflow_events` table; `audit_log` continues to receive per-row hash-chained events with `session_id` FK and new `workflow_id` column. *(Maps to REQ-036)*
5. On completion, worker emits `workflow.completed` audit event and closes the Trigger.dev task. Server emits `workflow.completed` SSE event to any connected client. *(Maps to REQ-036, REQ-037)*
6. Devon returns to dashboard → "Background workflows" card shows the completed workflow with `workflow_id`. *(Maps to REQ-037 fallback)*
7. Devon clicks the card → client opens new SSE connection to `/api/chat/stream?workflow_id=...&last_event_id=...` → server replays from `workflow_events` starting after `last_event_id` + tails live events. *(Maps to REQ-037)*
*Testable Acceptance (BDD):*
- **Given** Devon submits a question projected to exceed 20 steps, **when** step 21 begins, **then** `workflow.promoted` is emitted on the SSE stream, `workflow_id` is assigned, and the Trigger.dev task is created within 1s.
- **Given** Trigger.dev is unreachable at step 21, **when** promotion is attempted, **then** the orchestrator halts and preserves the inline state, emits `workflow.promotion_failed` audit event, and returns an error to the client with retry guidance (legacy REQ-035 contract).
- **Given** a durable workflow completed 5 minutes ago, **when** Devon reopens it via dashboard card with `Last-Event-ID` set to event N, **then** the SSE stream replays events N+1 onward from `workflow_events` and the first replayed event arrives within 30s p95 of `workflow.completed` audit emission.
- **Given** a durable workflow is still running at TTL (24h), **when** the TTL elapses, **then** the workflow is marked timed-out, `workflow.timeout` audit event is emitted, partial state is preserved, and the workflow is not resumable.
**Journey 2 (J2) — Sam runs onboarding + smoke.** *Existing M1 journey with M3 extensions — refer to `steer-v0.1-spec.md` for full text. M3 additions: smoke test exercises the J1 inline path and emits `llm.inference.*` audit events feeding REQ-043 metering. Maps to REQ-033, REQ-034, REQ-035, REQ-043, REQ-044.*
**Journey 2-hardening (J2-hardening) — Sam configures and reviews posture + usage + Vanta.**
1. Sam opens `/settings/posture`. → Posture page renders `{control_name, status, last_checked_at, evidence_ref}` matrix for {RLS, audit hash-chain, WorkOS SSO, BYOM reachability}. *(Maps to REQ-041)*
2. Server initiates live probes on 5-min interval (configurable per tenant). Each probe emits `control.probe.{succeeded,failed}`. *(Maps to REQ-041)*
3. Sam opens `/settings/usage`. → Dashboard renders per-tenant aggregates (tokens in/out, BYOM calls, latency percentiles) sliced by conversation/workflow. *(Maps to REQ-043, REQ-044)*
4. Sam configures Vanta integration: API key, sync interval (1h default). → Server schedules sync task; Vanta API key resolved via `SecretProvider` (INV-3). *(Maps to REQ-042)*
5. Vanta sync runs on schedule, pushes evidence bundle: probe results + config snapshots + control attestation records. *(Maps to REQ-042)*
*Testable Acceptance (BDD):*
- **Given** Sam opens `/settings/posture`, **when** the page renders, **then** every row in the matrix has `last_checked_at` within the past 5 minutes and an `evidence_ref` pointing to a `control.probe.*` audit row.
- **Given** a probe fails (e.g., WorkOS API returns 5xx), **when** the failure is detected, **then** the posture page updates within 1 probe interval and the failure row links to the `control.probe.failed` audit event.
- **Given** Sam configures Vanta with a valid API key, **when** the scheduled sync runs, **then** an evidence bundle is pushed to Vanta and a `vanta.sync.{succeeded,failed}` audit event is emitted.
- **Given** Sam opens `/settings/usage`, **when** the dashboard renders, **then** per-tenant aggregates show `{prompt_tokens, completion_tokens, byom_call_count, latency_p50/p95/p99}` for the selected time window.
**Journey 3 (J3) — Casey (compliance reader) reviews posture + usage + audit.**
1. Casey logs in via SSO. RBAC resolves to `compliance-reader` role. → Casey has read access to `/settings/posture`, `/settings/usage`, `/audit` but no write or admin routes. *(Maps to REQ-041, REQ-042, REQ-043, REQ-044)*
2. Casey browses posture, usage, audit. All three pages render read-only. No edit, no config, no probe-trigger controls.
3. Casey exports evidence bundle (audit log slice + posture snapshot + usage slice) for offline review. *(Maps to REQ-042 evidence lineage)*
*Testable Acceptance (BDD):*
- **Given** Casey is authenticated as `compliance-reader`, **when** Casey requests any write or admin endpoint, **then** the request is rejected with 403 and `authz.denied` audit event is emitted.
- **Given** Casey exports an evidence bundle, **when** the bundle is generated, **then** every included row has a verifiable provenance chain (audit hash-chain + Merkle session root) and the bundle itself is emitted as `evidence.exported` audit event.
**Journey infra (J-infra) — Cross-process rate aggregation (REQ-045, no user persona).**
1. API gateway receives a chat request. → Consults Redis-backed token bucket for tenant + global keys. *(Maps to REQ-045)*
2. Trigger.dev worker emits a step-completion event. → Worker also consults/updates Redis bucket to enforce cross-process rate limits on durable workflows. *(Maps to REQ-045)*
3. If bucket exhausted → request rejected with 429 + `Retry-After`; workflow step retried per Trigger.dev backoff or rejected if global ceiling hit. *(Maps to REQ-045)*
*Testable Acceptance (BDD):*
- **Given** API gateway instance A and Trigger.dev worker instance W both serve tenant T, **when** T's per-tenant rate bucket is exhausted on A, **then** a subsequent request to W is rejected with 429 within 1s.
- **Given** Redis is unreachable, **when** rate lookup fails, **then** requests are rejected with 503 (fail-closed) and `rate.redis.unavailable` audit event is emitted.
### 3.3 Failure & Edge Paths
- **Edge 1 — BYOM unreachable mid-turn:** REQ-009 reject path. Server emits `llm.inference.failed` with `reason=byom_unreachable`, surfaces error to client, conversation state preserved.
- **Edge 2 — Hash-chain trigger fails on insert (INV-4):** INSERT rolls back, enclosing transaction rolls back, request returns 500; current operation has no audit row because the chain broke (preserved semantics).
- **Edge 3 — Planner emits malformed tool call:** Orchestrator rejects, emits `tool.plan.rejected`, requests planner retry. If 3 consecutive malformed plans → surface error to user, conversation state preserved.
- **Edge 4 — Tool call exceeds adapter capability (INV-7 write blocklist):** Broker rejects with `adapter.write_rejected` (already in M2). Conversation continues without that step.
- **Edge 5 — Durable workflow TTL timeout:** `workflow.timeout` audit event, workflow marked not resumable, partial state preserved.
- **Edge 6 — Vanta API failure during sync:** `vanta.sync.failed` audit event, retry with exponential backoff per Trigger.dev task config, posture page surfaces Vanta integration status.
- **Edge 7 — Probe fails (e.g., WorkOS 5xx):** `control.probe.failed` audit event, posture page status = `degraded`, `evidence_ref` points to failed probe.
- **Edge 8 — Redis unreachable:** Fail-closed 503, `rate.redis.unavailable` audit event.
- **Edge 9 — Client reconnect during inline SSE with no `Last-Event-ID`:** Server replays entire conversation state from `chat_turns` (full re-replay acceptable for inline streams).
- **Edge 10 — `workflow_id` lost client-side:** Dashboard "Background workflows" card surfaces all workflows for tenant; any persona with read access can rejoin by clicking the card.
---
## 4. Functional Requirements
| ID | Title | Journeys | Priority | Acceptance Criteria (Given/When/Then or explicit rules) |
| :--- | :--- | :--- | :--- | :--- |
| **REQ-028** | Chat UI surface | J1 | High | **Given** Devon is authenticated, **when** Devon navigates to `/chat`, **then** the UI renders an input surface + empty conversation state + streaming-capable message list. |
| **REQ-029** | Natural-language input | J1 | High | **Given** chat UI is open, **when** Devon submits a natural-language question, **then** server validates input length (≤4000 chars), creates `chat_session` row, emits `chat.session.created` audit event with `{session_id, tenant_id, user_id}`, returns first SSE event (`turn.user`) within 500ms p95. |
| **REQ-030** | Streamed response with citations | J1 | High | **1.** Every assistant message contains a `citations` array per the shape below. **2.** Each citation's `tool_invocation_id` is a valid FK to `audit_log.adapter.capability_invoked`. **3.** `usage` block on every assistant message (every BYOM call) with `workflow_id` (nullable) + `correlation_id`. **4.** Inline path: SSE completes within 5 min p95. **Citation message shape:** ```json { "message_id":"ulid","turn_id":"ulid","role":"assistant", "content":"nginx is down [c:1]...", "citations":[{"citation_id":"c:1","claim_text":"nginx is down", "tool_invocation_id":"ulid (FK audit_log adapter.capability_invoked)", "adapter_id":"ssh:web-server-01","capability":"ssh.run_whitelisted_command", "source_locator":{"type":"ssh_command_output","command":"systemctl status nginx", "output_snippet":"Active: inactive (dead)","output_full_ref":"audit_log.row_id"}}], "usage":{"prompt_tokens":1820,"completion_tokens":340, "byom_endpoint_id":"ulid","latency_ms":4200, "workflow_id":"ulid\|null","correlation_id":"ulid"}} ``` |
| **REQ-031** | Streamed tool traces | J1, J1-durable | High | **Given** orchestrator invokes a tool, **when** the broker adapter router begins execution, **then** SSE emits a `tool.trace` event with `{tool_invocation_id, adapter_id, capability, started_at}` before the `adapter.capability_invoked` audit row is written. |
| **REQ-032** | Conversation history (first-class data model) | J1 | High | **1.** `chat_sessions` and `chat_turns` tables exist (additive migration per `steer-m2-spec.md:162`). **2.** Retention: indefinite for `chat_sessions` and `chat_turns`. **3.** Devon can list all sessions for the tenant; clicking a session loads full turn history with citations and tool traces. **4.** Session lifecycle: soft-close on 30-min idle; hard-close on explicit user action or session age >7 days. Soft-close = read-only + audit log complete; not deleted. |
| **REQ-033** | LLM tool reasoning (planner) | J1, J2 | High | **1.** Every planner invocation goes through tenant BYOM endpoint (INV-5). **2.** Every planner invocation emits `llm.inference.{requested,succeeded,failed}` audit events. **3.** Intermediate planner messages (reasoning traces) stored in `chat_turns` with `visible=false`. **4.** Planner rejects malformed tool calls per Edge 3. |
| **REQ-034** | Multi-step workflows | J1, J1-durable | High | **Given** orchestrator is executing a workflow, **when** the planner emits a multi-step plan, **then** the orchestrator executes steps sequentially through the broker adapter router, each step increments the per-`workflow_id` counter, and each step emits `adapter.capability_invoked` audit event. |
| **REQ-035** | ≤20-step inline limit (AMENDED) | J1, J1-durable | High | **Given** step counter reaches 20 in an inline workflow, **when** step 21 is initiated, **then** the orchestrator auto-promotes the workflow to durable: assigns `workflow_id` (ULID), creates Trigger.dev task, emits `workflow.promoted` SSE event, bridges handoff. **Degraded mode:** if Trigger.dev unreachable at promotion time, halt and preserve per legacy contract, emit `workflow.promotion_failed` audit event, return error to client with retry guidance. |
| **REQ-036** | Durable execution | J1-durable | High | **1.** Durable workflows execute in Trigger.dev tasks with `workflow_id` correlation. **2.** Each step emits `workflow.step.{started,completed,failed}` to `workflow_events` table. **3.** `audit_log` continues per-row hash-chaining; new `workflow_id` column on audit rows; `session_id` FK still set. **4.** On completion: `workflow.completed` audit event + Trigger.dev task close. **5.** Hard TTL 24h → `workflow.timeout` + partial state preserved + workflow not resumable. **6.** `workflow_events` retention aligned with `audit_log` (90d min, 1y target); S3 WORM archive covers long-term. **7.** Trigger.dev task schema: input=`{session_id, user_turn_id, planner_plan, byom_endpoint_id}`; output=`{workflow.completed event payload}`; retry=exponential backoff ×3; idempotency key=`workflow_id`. |
| **REQ-037** | Workflow rejoin | J1-durable | High | **1.** Client reconnects via `/api/chat/stream?workflow_id=...&last_event_id=...`. **2.** Server replays from `workflow_events` starting after `last_event_id` + tails live events. **3.** Rejoin p95 ≤30s, anchored: `workflow.completed` audit emission → first replayed event receipt. **4.** Fallback: dashboard "Background workflows" card lists all workflows for tenant; clicking opens rejoin stream. |
| **REQ-041** | SOC2 posture page | J2-hardening, J3 | High | **1.** Page renders `{control_name, status, last_checked_at, evidence_ref}` matrix for {RLS, audit hash-chain, WorkOS SSO, BYOM reachability}. **2.** Live probes run on 5-min interval (configurable per tenant). **3.** Each probe emits `control.probe.{succeeded,failed}` audit event. **4.** Posture page never displays a status without an `evidence_ref` to a probe audit row. **5.** Live probes themselves are auditable (INV-4). **6.** `control_state` cache table (write-through with probe transaction, Decision #8) backs the page render for <500ms p95 render time. **7.** Casey RBAC: read-only; no probe-trigger, no config edit, no Vanta credential view. |
| **REQ-042** | Vanta evidence sync | J2-hardening, J3 | High | **1.** Scheduled sync (1-h default, configurable). **2.** Pushes evidence bundle: probe results + config snapshots + control attestation records. **3.** No signed attestations in M3 (avoids SOC2-cert anti-goal). **4.** Emits `vanta.sync.{succeeded,failed}` audit event per run. **5.** Retry with exponential backoff on failure. **6.** Vanta API key resolved via `SecretProvider` (INV-3). |
| **REQ-043** | Usage metering | J1, J2-hardening, J3 | High | **1.** Every BYOM call emits `llm.inference.{requested,succeeded,failed}` with `usage` block (REQ-030 schema). **2.** Metering pipeline aggregates per-tenant: `{prompt_tokens, completion_tokens, byom_call_count, latency_p50/p95/p99}` sliced by conversation/workflow. **3.** Aggregation interval: 1-min default (configurable). **4.** Failed inferences (no completion) are counted at zero tokens + recorded as failures. |
| **REQ-044** | Usage dashboard | J2-hardening, J3 | High | **Given** Sam or Casey opens `/settings/usage`, **when** the dashboard renders, **then** it displays metered data (REQ-043) for the selected time window with per-tenant aggregates + drill-down to conversation/workflow level. Casey sees all controls read-only; no edit, no config. |
| **REQ-045** | Redis-backed rate aggregation | J-infra | High | **1.** Rate limiter state in Redis (Valkey-compatible). **2.** API gateway + Trigger.dev workers share the same bucket state per tenant. **3.** If bucket exhausted: 429 + `Retry-After` for gateway requests; Trigger.dev backoff/reject for workflow steps. **4.** If Redis unreachable: fail-closed 503 + `rate.redis.unavailable` audit event. **5.** Bucket config: per-tenant + global keys; limits configurable per tenant. |
---
## 5. Technical Constraints & NFRs
- **INV-1 (API gateway ordering):** Every HTTP request hits API gateway first: auth → tenant resolve → RBAC → audit. M3 `/api/chat/stream` and `/api/chat/rejoin` MUST conform.
- **INV-2 (Tenant scoping):** Every DB query runs under `SET app.tenant_id` via `withTenant` transaction; RLS enforces scoping. M3 queries against `chat_sessions`, `chat_turns`, `workflow_events`, `control_state` MUST use `withTenant`. RLS policies for new tables ship in the M3 migration.
- **INV-3 (Secret resolution):** Every credential resolved via `SecretProvider.get`; never env/config/DB for tenant secrets. M3 scheduler tasks (Trigger.dev) for Vanta sync + posture probes MUST resolve tenant Vanta API key via SecretProvider, not env.
- **INV-4 (Audit hash-chain):** Every auditable event appended to `audit_log` with hash-chain; `UPDATE/DELETE` REVOKE'd; write failure halts enclosing operation. **M3 preserves INV-4 as written.** Per-row hash-chaining continues. Session-scoped Merkle root per `chat_session` row is additive and does not weaken INV-4.
- **INV-5 (BYOM routing):** Every LLM inference call routed to tenant's BYOM endpoint; unconfigured/unreachable → reject (REQ-009). **M3 enforces this for all roles (planner, executor, synthesis) — no D-008 split.** All N inference calls per turn hit the same `byom_endpoints` row.
- **INV-6 (Relay Agent outbound-only):** Unchanged from M2. M3 chat does not directly invoke Relay Agent; chat → broker → adapter → (optionally) Relay Agent.
- **INV-7 (Read-only by default):** Closed 9-tool registry is the primary boundary; write-method blocklist is the backstop. M3 orchestrator MUST refuse any tool call that resolves to a write capability, even if planner requests it.
- **INV-8 (PGlite RLS gap):** PGlite 0.5.7 doesn't enforce RLS on SELECT. App-layer `withTenant` + explicit `WHERE tenant_id` is primary in dev/test; RLS + FORCE RLS is prod backstop. M3 new tables (`chat_sessions`, `chat_turns`, `workflow_events`, `control_state`) MUST have explicit `tenant_id` columns + RLS policies + `withTenant` usage.
**Performance NFRs:**
- Inline path: SSE completes within 5 min p95 (acceptance gate).
- Durable rejoin: p95 ≤30s from `workflow.completed` audit emission to first replayed event receipt (Phase 2 Item 6 anchor).
- Posture page render: <500ms p95 (backed by `control_state` write-through cache).
- Probe cadence: 5-min default interval (REQ-041).
- Vanta sync: 1-h default interval (REQ-042).
- Metering aggregation: 1-min default interval (REQ-043).
**Schema additivity (per `steer-m2-spec.md:162` — "new tables" parenthetical explicitly permits new tables, not just columns):**
- New tables: `chat_sessions`, `chat_turns`, `workflow_events`, `control_state`.
- New `audit_log` columns: `session_id` (FK `chat_sessions`, nullable), `workflow_id` (nullable).
- New audit event types: see §2.1.
**Migration shape (Decision #7 — per-table for rollback isolation, one PR):**
- `0004_chat_schema.sql` — `chat_sessions`, `chat_turns` + RLS; `audit_log.session_id` column.
- `0005_workflow_schema.sql` — `workflow_events` + RLS; `audit_log.workflow_id` column.
- `0006_posture_schema.sql` — `control_state` + RLS.
**Anti-goal enforcement:**
- No write actions on infrastructure (closed 9-tool registry + write-method blocklist + INV-7).
- No hosted LLM inference (single BYOM endpoint, all roles, INV-5).
- No SOC2 final cert (no signed attestations in M3; REQ-042 pushes evidence only).
**`control_state` cache table (Decision #8 — write-through):**
- `control_state` row updated in the same Postgres transaction as the `control.probe.{succeeded,failed}` audit event emission. Cache is always consistent with audit log at probe completion.
- **Implementation question (§7 Q1):** Engineering to confirm write-through is achievable in the chosen probe pipeline (likely Trigger.dev task per REQ-042 architecture). If a single transaction is not achievable, surface divergence handling in a v1.3 spec amendment.
---
## 6. Milestone Plan & Release Gates
**Test evidence required for Production Release (M3):**
- [ ] Code coverage ≥80% on new packages (`packages/orchestrator`, `packages/chat`); existing M2 floor (92.3% on `packages/mcp`) maintained.
- [ ] All 15 REQs PASS (REQ-028..037, REQ-041..045) including REQ-035 amendment acceptance criteria.
- [ ] CI/CD pipeline GREEN (Gitea Actions, both `test-pglite` and `test-postgres` jobs).
- [ ] M1+M2 non-regression: full 656-test suite green.
- [ ] INV-1..INV-8 explicitly tested (new tests for INV-4 audit hash-chain preservation across M3 event types; new tests for INV-5 BYOM routing across planner/executor/synthesis; new tests for INV-2 RLS on `chat_sessions`/`chat_turns`/`workflow_events`/`control_state`).
- [ ] MCP 2025-06-18 conformance verified (M2 gate, non-regression).
- [ ] LLM smoke Track A passes (mock-path, P0 gate, no external deps).
- [ ] QA sign-off: 100% of J1, J1-durable, J2-hardening, J3 integration tests pass; J-infra (REQ-045) has unit + cross-process integration test demonstrating gateway ↔ Trigger.dev worker rate coordination.
- [ ] Security/compliance review: posture page + Vanta sync demonstrate evidence lineage (every rendered status has an audit row); RBAC denies Casey on all write/admin endpoints; audit log hash-chain integrity verified end-to-end.
- [ ] Production deployment is an ops track (not in this gate list); deploy to AWS us-east-1 follows the ops track's own gate.
**Durable-path SLO refinement (gate clarification — does not rewrite `REQUIREMENTS.md:40`):**
> Inline path: p95 ≤5min. Durable path: rejoin p95 ≤30s from `workflow.completed` audit emission to first replayed event receipt; hard TTL 24h. `REQUIREMENTS.md:40` raw gate language stays untouched; this refinement documents the durable-path clarification locally in the M3 spec.
**Operational prereqs (ship-blockers, NOT M3 REQ deliverables — ops track, parallel to PDLC):**
- Gitea Actions runner enablement on forge (blocks full M2 + M3 CI verification on `test-postgres` job).
- `GITHUB_SMOKE_PAT` provisioning as Gitea Actions secret (blocks Track B GitHub LLM smoke; Track A mock-path is the P0 gate and does not need this).
---
## 7. Open Questions & Assumptions
1. **Trigger.dev task schema for durable workflows** — RESOLVED. Workflow input=`{session_id, user_turn_id, planner_plan, byom_endpoint_id}`; output=`{workflow.completed event payload}`; retry policy=exponential backoff ×3; idempotency key=`workflow_id`. Baked into REQ-036 §4.7.
2. **M3 acceptance gate refinement (durable path SLO)** — RESOLVED. "Inline path: p95 ≤5min. Durable path: rejoin p95 ≤30s post-`workflow.completed` audit emission; hard TTL 24h." Baked into §6. `REQUIREMENTS.md:40` stays untouched; refinement documented locally in M3 spec §6.
3. **Chat session lifecycle** — RESOLVED. Soft-close 30-min idle; hard-close 7d or explicit. Read-only post-close, not deleted. Baked into REQ-032 §4.
4. **Workflow event retention vs audit log retention** — RESOLVED. Align `workflow_events` with `audit_log` (90d min, 1y target). S3 WORM archive covers long-term. Baked into REQ-036 §4.6.
5. **Citation rendering UX** — DEFERRED. Exact UI for inline `[c:1]` markers + citation popover. Defer to design review. Non-blocking for spec ratification. Proposed: inline superscript numbers; click expands to source adapter + tool invocation link + output snippet.
6. **Posture page RBAC granularity** — RESOLVED. Casey sees all controls (posture, usage, audit) read-only; no probe-trigger, no config edit, no Vanta credential view. Baked into §3.1 + REQ-041 §4.7 + REQ-044 §4.
7. **`control_state` cache invalidation strategy** — OPEN (engineering confirm during implementation). Spec default: write-through (same transaction as `control.probe.*` audit emission). Engineering to confirm feasibility in chosen probe pipeline; if not achievable, v1.3 amendment with divergence handling.
---
## 8. Changelog
| Version | Date | Author | What Changed | REQs Affected |
| :--- | :--- | :--- | :--- | :--- |
| v1.0 | 2026-08-25 | Sarah Chen (PM) | Initial M3 spec draft from Phase 3 generation | REQ-028..037, REQ-041..045 (with REQ-035 amendment) |
| v1.1 | 2026-08-25 | Sarah Chen (PM) | Open Questions 1-6 resolved and baked in (Trigger.dev task schema, durable SLO, session lifecycle, workflow retention, Casey RBAC); OQ5 deferred to design review | REQ-032, REQ-036, REQ-041, REQ-044 |
| v1.2 | 2026-08-25 | Sarah Chen (PM) | Engineering handoff: Decision #7 (per-table migrations `0004`/`0005`/`0006`), Decision #8 (`control_state` write-through cache), Decision #9 (Redis fail-closed 503), Decision #10 (usage on all BYOM calls); OQ7 opened (`control_state` write-through feasibility); ops prereqs documented in §6 | REQ-036, REQ-041, REQ-043, REQ-045 |
---
**M3 spec v1.2 Final. Ready for engineering handoff.**