M2 delivers the read-only MCP capability broker gateway and four Day-1 infrastructure adapters (Proxmox, SSH/Linux, GitHub, Gitea). 13 REQs (015-027) all pass. 656 tests green. M1 non-regression verified. MCP spec 2025-06-18 conformance verified (PROTOCOL.md + 7 tests). Defense-in-depth SSH (broker layer 1 + Relay Agent layer 2 + no-shell exec). Two-track LLM smoke (Track A mock-path P0 gate passes). CI: Gitea Actions (.gitea/workflows/ci.yml) with Postgres 16 + RLS verification. Phases shipped: P0 pre-execution v0.1.0 P1 Wave F — MCP gateway v0.1.1 P2 Wave G — Proxmox v0.1.2 P3 Wave H — SSH/Linux v0.1.3 P4 Wave I — Git adapters v0.1.4 P5 Wave J — SSE+smoke+UI v0.1.5 P6 Final — review+ship v0.1.6 ← milestone release ---ci--- phase: 6 milestone: v0.2 status: complete phase_role: final milestone_complete: true requirements: covered: [REQ-015, REQ-016, REQ-017, REQ-018, REQ-019, REQ-020, REQ-021, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026, REQ-027] partial: [] ---/ci---
28 KiB
Architecture
Overview
CoreCI Chat v0.1 is a multi-tenant SaaS with a TypeScript control plane, a Go Relay Agent distributed to customer Linux hosts, an OpenAI-compatible BYOM routing layer, and (M2) an MCP capability broker gateway with four Day-1 infrastructure adapters. The control plane runs in a single AWS region (us-east-1) and enforces tenant isolation via Postgres Row-Level Security. Every request flows through an API gateway that authenticates the session, resolves the tenant, enforces RBAC, and writes an immutable audit entry. The Relay Agent is an outbound-only WebSocket client that registers as a target and heartbeats to the control plane; in M2 it receives MCP tool calls (SSH) and enforces a fixed command whitelist (defense-in-depth layer 2; the broker is layer 1). All credentials live in AWS Secrets Manager (prod) or a local-encrypted fallback (dev) behind a SecretProvider interface — never env vars, config files, or DB columns.
The v0.1 wedge is read-only diagnostic: no write actions, no hosted inference, no remediation. Async durable execution is provided by Trigger.dev (instrumented in M3 for chat workflows; the runtime bootstrap lands in M1 Wave A so M3 plugs in cleanly).
M2 addition — MCP capability broker
M2 introduces the Model Context Protocol (MCP) capability broker as the structured layer between the LLM/UI and customer infrastructure. The broker enforces INV-7 (read-only by default) as the load-bearing safety boundary: it maintains a closed, enumerated tool registry (9 tools across 4 adapters), a per-adapter write-method blocklist, token-bucket rate limiting, multi-target scope disambiguation, and SSE streaming. The broker implements MCP spec version 2025-06-18. Conformance is verified against modelcontextprotocol.io (artifact required at M2 gate). M3 consumes the broker's REST+SSE gateway as a stable contract (M2 spec §9).
MCP transport architecture (D-007): three layers:
- Broker ↔ Proxmox/GitHub/Gitea adapters: in-process custom MCP transport. JSON-RPC 2.0 messages (
tools/list,tools/call) passed in-process between the broker and the TS adapter modules. MCP2025-06-18allows custom transports provided they preserve the JSON-RPC message format and lifecycle requirements. No subprocess spawning for same-process TS modules. - Broker ↔ SSH adapter: the MCP
tools/callJSON-RPC layer sits between the broker and the TS SSH adapter module (in-process). The TS SSH adapter module then calls the M1 Relay Agent (Go binary) over WebSocket — this downstream WebSocket transport is inherited from M1 infrastructure and is downstream of the JSON-RPC layer. It does not affect MCP conformance. - Broker ↔ CI/LLM smoke: stdio transport (JSON-RPC over stdin/stdout) for the CI
packages/llm-mocksmoke test. The mock LLM acts as an MCP host.
Broker ↔ UI: REST facade + SSE (NOT MCP Streamable HTTP — a browser-friendly facade with MCP-compliant tool schemas and results inside). POST /api/mcp/invoke returns {correlationId, streamUrl}; GET /api/mcp/stream/:correlationId is the SSE stream.
OpenAI ↔ MCP translation contract: tool_calls[].function.{name, arguments} (OpenAI) → params.{name, arguments} (MCP, parsed JSON object); result.content[].text + isError (MCP) → OpenAI tool message {role:"tool", tool_call_id, content}. Documented as a typed translator module in packages/mcp/translator.ts.
Init-time invariants (preserved by the architecture-drift guard)
- Single git repository at
~/coreci-chat, branch hierarchymain → milestone/v0.1-bootstrap → phase/NN-*. .ciagent/reference files at repo root (single-project mode).- Release target: Gitea
coreci/coreci-chatathttps://git.cloudinit.dev. - Autonomy:
full(no HITL after clarify; decision threshold 0.6).
Architecture invariants (M1 → v0.1)
- Every HTTP request hits the API gateway first: auth → tenant resolve → RBAC → audit.
- Every DB query runs under
SET app.tenant_id = ?via a session-scoped transaction; RLS policies enforce scoping. Cross-tenant queries return empty. - Every credential is resolved via
SecretProvider.get(tenantId, key); the provider abstracts AWS Secrets Manager (prod) and local-encrypted (dev/test). - Every auditable event (prompt, tool call, SSH command, response) is appended to
audit_logwith aprev_hash → curr_hashchain;UPDATE/DELETEare REVOKE'd from the app role; a write failure halts the enclosing operation (REQ-038 Edge 7). - Every LLM inference call is routed to the tenant's configured BYOM endpoint via an OpenAI-compatible
/v1/chat/completionscontract; unconfigured/unreachable → reject with actionable error (REQ-009). - The Relay Agent makes only outbound connections (WebSocket to SaaS). No inbound firewall rules on customer hosts.
Architecture invariants (M2 addition)
- INV-7 at the broker (load-bearing): 100% of write-action requests rejected at the MCP broker (HTTP 403) before adapter invocation. The broker is the load-bearing safety boundary, NOT the adapter. Per-adapter write-method blocklist: Proxmox POST/PUT/DELETE; SSH non-whitelist commands; GitHub scopes outside
metadata:read+actions:read; Gitea POST/PUT/DELETE/PATCH on all endpoints. Verified by a test per adapter at the M2 gate. - Closed tool registry (REQ-015): 9-tool starter set locked. Per-tenant policy may disable individual tools but never add new ones. Additions require spec amendment (v1.2+).
- Defense-in-depth for SSH (REQ-021, REQ-026): broker validates
commandagainst the 6-command whitelist subset BEFORE dispatch to Relay Agent (layer 1); M1 Relay AgentCheckCommandvalidates at execution (layer 2). Both layers must pass; either rejecting → HTTP 403 +adapter.write_rejectedaudit event. - MCP capability invocations run under
withTenant+ RLS (INV-2): every capability invocation runs in a tenant-scoped transaction. Adapters are per-tenant; no cross-tenant adapter sharing. - Adapter credentials via SecretProvider (INV-3): all adapter credentials (Proxmox token, GitHub/Gitea PAT, SSH registration token) resolved via
SecretProvider.get. Credentials never logged; secret identifiers hashed in audit events. - Audit completeness for adapter events (INV-4): new event types appended to M1's
audit_log:adapter.configured,adapter.test_connection.{succeeded,failed},adapter.capability_invoked,adapter.write_rejected. All hash-chained, append-only, UPDATE/DELETE REVOKE'd. - Rate limiting (REQ-019): token-bucket per user (60 req/min) and per tenant (300 req/min). In-memory, process-local in M2. Capacity = rate; refill 1/sec (user) / 5/sec (tenant).
- SSE streaming (REQ-017): per-call streams with ULID correlation IDs. Client disconnect cancels in-flight adapter call; no audit event for client-side cancellation.
- M1 non-regression: all M1 REQs (001-014, 038-040) remain passing. No schema, invariant, or behavioral changes to M1 systems except additive (new tables, new audit event types).
Components
apps/control-plane (TypeScript, Next.js App Router API routes + standalone services)
- Description: API gateway, tenant resolution, RBAC enforcement, BYOM registry/validation/routing, Relay Agent WebSocket server, audit writer, secret provider bootstrap, dashboard (Next.js). Hosts the admin dashboard in M1; the chat UI lands here in M3.
- Boundaries: All inbound HTTP/WebSocket enters here. Owns the Postgres connection and the
SET app.tenant_iddiscipline. Owns the Trigger.dev runtime bootstrap. - Depends on:
packages/db,packages/auth,packages/audit,packages/secrets,packages/config, Trigger.dev, WorkOS, AWS Secrets Manager.
apps/relay-agent (Go)
- Description: Single static binary. Outbound WebSocket client to the control plane; registration (tenant/target/hostname/OS/IP/version); heartbeat + exponential-backoff reconnect (max 5 → alert); systemd service; SSH whitelist file format + enforcement hook (the SSH adapter plugs into the hook in M2 — no SSH execution in M1).
- Boundaries: No inbound ports. Reads its tenant registration token from a local config written by the install script (token resolves to a secret-manager reference, never the raw secret). All state is in-memory or on the control plane.
- Depends on: control-plane WebSocket endpoint; systemd; the fixed SSH whitelist file shipped alongside the binary.
apps/dashboard (Next.js, part of apps/control-plane in M1)
- Description: Tenant admin dashboard surfaced in the same Next.js app. M1 surfaces Relay Agent status (green/yellow/red), target hostname, last 100 log lines, BYOM config + validation, RBAC user/role management. Per-tenant view under RLS.
- Boundaries: Server components read via the same API gateway (no DB access bypassing RLS). Client components subscribe to a status WebSocket fan-out.
- Depends on:
apps/control-planeAPI,packages/auth.
packages/db (TypeScript)
- Description: Postgres schema, migrations, RLS policies, the
withTenant(tenantId, fn)helper that setsapp.tenant_idand runsfnin a transaction. Tables:tenants,users,tenant_memberships(user × tenant × role),targets,byom_endpoints(URL in DB, key ref to secret manager),audit_log(append-only, hash-chain),invitations. All tenant-scoped tables carrytenant_idand an RLS policy. - Boundaries: The ONLY module that opens a Postgres connection. App role has INSERT/SELECT on
audit_logonly; UPDATE/DELETE REVOKE'd. Schema migrations run via a gated migrator. - Depends on: Postgres 16.
packages/auth (TypeScript)
- Description: WorkOS SSO/SAML integration, session management, tenant resolution middleware, RBAC role → route permission map (Admin/Operator/Viewer). Enforces RBAC at the API gateway from the first endpoint (REQ-005 pattern).
- Boundaries: Sessions are httpOnly cookies; tokens never logged. WorkOS is the only IdP in v0.1.
- Depends on: WorkOS SDK,
packages/db.
packages/audit (TypeScript)
- Description: Append-only audit writer. Computes
curr_hash = sha256(prev_hash || canonical_payload). Payload includes tenant_id, user_id, target_id (for SSH), timestamp, correlation_id, event_type, event_body. On write failure: throwsAuditWriteHaltError→ enclosing operation halts + admin alert (REQ-038 Edge 7). - Boundaries: Never exposes UPDATE/DELETE. Read path is admin-export only (no query UI in MVP — spec §2.2).
- Depends on:
packages/db.
packages/secrets (TypeScript)
- Description:
SecretProviderinterface with two impls:AwsSecretsManagerProvider(prod, us-east-1, KMS-backed) andLocalEncryptedProvider(dev/test, AES-256-GCM with a key from an env var only for the master key — the master key is the ONE allowed env var, everything else is provider-resolved). Every tenant credential (BYOM key, Proxmox token, SSH key, Git token) is stored/retrieved viaprovider.put(tenantId, name, value)/provider.get(tenantId, name). - Boundaries: Secrets never logged, never written to DB columns, never in config files. The DB stores only a reference (e.g.
aws-sm:coreci/<tenantId>/byom). - Depends on: AWS SDK (prod), Node crypto (dev).
packages/config (TypeScript)
- Description: Typed config loading (env-aware: prod/staging/dev/test). Loads non-secret config only. The only env vars consumed here are infra-level (DATABASE_URL, WORKOS_API_KEY, AWS_REGION, SECRET_MASTER_KEY_DEV, TRIGGER_API_KEY). All tenant/customer secrets go through
packages/secrets. - Boundaries: Never reads tenant secrets directly.
- Depends on: nothing.
Components (M2 addition)
packages/mcp (TypeScript — NEW in M2)
- Description: The MCP capability broker gateway. Implements MCP spec version
2025-06-18. Contains: (1) the closed tool registry (9 tools, REQ-015); (2) the adapter router (REQ-016) resolving(tenant_id, adapter_type, target_id)tuples to adapter instances; (3) the write-method blocklist enforcer (REQ-018, INV-7 — the load-bearing safety boundary); (4) the token-bucket rate limiter (REQ-019, in-memory, capacity = rate); (5) the SSE stream manager (REQ-017, per-call streams, ULID correlation IDs); (6) the OpenAI ↔ MCP translator module (translator.ts); (7) the in-process custom MCP transport for broker ↔ adapter JSON-RPC. - Boundaries: The broker NEVER invokes an adapter for a write-capable method. The broker validates SSH
commandagainst the whitelist subset BEFORE dispatch (defense-in-depth layer 1). All capability invocations run underwithTenant+ RLS. - Depends on:
packages/db(withTenant, adapter config rows),packages/audit(adapter event types),packages/secrets(adapter credentials),packages/auth(RBAC),packages/config.
packages/mcp/adapters/proxmox (TypeScript — NEW in M2)
- Description: Read-only Proxmox VE adapter. PVE API client over HTTPS (cookie-based auth). Capabilities:
proxmox.list_vms(inventory,GET /api2/json/nodes/{node}/qemu),proxmox.get_vm_status(live,GET /api2/json/nodes/{node}/qemu/{vmid}/status/current),proxmox.get_node_metrics(live,GET /api2/json/nodes/{node}/status). ValidatesPVEAuditorrole at config submit time (REQ-025). Write-method blocklist: POST/PUT/DELETE. - Boundaries: Calls only GET endpoints. Token stored via
SecretProvider.set(INV-3). - Depends on:
packages/mcp(broker),packages/secrets,packages/audit.
packages/mcp/adapters/ssh (TypeScript — NEW in M2)
- Description: Read-only SSH/Linux adapter via M1 Relay Agent. The MCP
tools/callJSON-RPC layer is in-process (broker → TS SSH module); the TS module then calls the M1 Relay Agent (Go binary) over WebSocket (M1 infrastructure, downstream of JSON-RPC). Capability:ssh.run_whitelisted_command(live). Broker validatescommandagainst the 6-command subset (uptime,df -h,free -m,systemctl status <svc>,journalctl -n <N>,systemctl list-units --type=service) BEFORE dispatch (layer 1); Relay AgentCheckCommandvalidates at execution (layer 2, M1 G-004 contract). - Boundaries: Never invokes non-whitelisted commands. Two-layer enforcement. Relay registration token stored via
SecretProvider.set(INV-3). - Depends on:
packages/mcp(broker),packages/secrets,packages/audit, M1 Relay Agent (Go binary, WebSocket).
packages/mcp/adapters/github (TypeScript — NEW in M2)
- Description: Read-only GitHub adapter. REST API client. Capabilities:
github.list_repos(inventory,GET /user/repos?per_page=100),github.get_recent_ci_runs(live,GET /repos/{owner}/{repo}/actions/runs),github.get_workflow_run(live,GET /repos/{owner}/{repo}/actions/runs/{run_id}). Validates fine-grained PAT scopes at submit time:metadata:read+actions:readminimum (D-006). ObservesX-RateLimit-Remainingheader; backs off on 429. - Boundaries: Calls only REST GET endpoints. Fine-grained PAT only (no classic PAT — coarse scopes grant write). Token stored via
SecretProvider.set(INV-3). - Depends on:
packages/mcp(broker),packages/secrets,packages/audit.
packages/mcp/adapters/gitea (TypeScript — NEW in M2)
- Description: Read-only Gitea adapter. REST API client. Capabilities:
gitea.list_repos(inventory,GET /user/repos?limit=50),gitea.get_recent_ci_runs(live,GET /repos/{owner}/{repo}/actions/runs). Version-aware scope validation: Gitea ≥1.22 requiresread:repository; Gitea <1.22 accepts any token with broker-side write-method blocklist (POST/PUT/DELETE/PATCH) as security backstop. Version detected viaGET /api/v1/versionand recorded in adapter config row. - Boundaries: Calls only REST GET endpoints. Write-method blocklist enforced at broker for all versions. Token stored via
SecretProvider.set(INV-3). - Depends on:
packages/mcp(broker),packages/secrets,packages/audit.
packages/llm-mock (TypeScript — NEW in M2, devDependency, CI-only)
- Description: CI-only mock LLM provider for the M2 gate LLM smoke test. Implements OpenAI-compatible
/v1/chat/completionsthat accepts atoolsparameter (OpenAI tool definitions from the broker'sGET /api/mcp/tools), returnstool_callsreferencing one of the provided tools, accepts follow-uptoolrole messages (broker's translated adapter result), and synthesizes a grounded text response. - Boundaries:
devDependencyonly (not a production dependency). Import-guarded against prod bundle via build-time check/eslint rule. Consumed only by CI. - Depends on: nothing (mock provider; the broker calls it as a BYOM endpoint).
apps/control-plane (M2 additions)
- M2 routes:
GET /api/mcp/tools(list tools, MCPtools/listfacade),POST /api/mcp/invoke(invoke capability, returns{correlationId, streamUrl}),GET /api/mcp/stream/:correlationId(SSE stream),POST /api/mcp/adapter(configure adapter),PATCH/DELETE /api/mcp/adapter/:id(update/remove adapter). Settings → Adapters UI + Test-Call UI in the dashboard. - M2 schema:
mcp_adapterstable (tenant_id, adapter_type, target_id, config JSON, secret_ref, validated, created_at) — tenant-scoped with RLS. No cache tables (in-memory only). - M2 audit events:
adapter.configured,adapter.test_connection.{succeeded,failed},adapter.capability_invoked,adapter.write_rejected— all appended to M1'saudit_logwith hash-chain.
M3 consumer contract (M2 spec §9 — frozen at M2 acceptance gate)
| Endpoint | Method | Purpose | M3 consumer |
|---|---|---|---|
/api/mcp/tools |
GET | List available tools (MCP tools/list facade) |
M3 chat UI populates the LLM's tools parameter |
/api/mcp/invoke |
POST | Invoke a capability; returns {correlationId, streamUrl} |
M3 chat orchestration calls when the LLM emits tool_calls |
/api/mcp/stream/:correlationId |
GET (SSE) | Stream tool execution output | M3 chat UI streams tool traces to the trace panel |
/api/mcp/adapter |
POST | Configure an adapter | M3 does not call (M2 Settings UI only) |
/api/mcp/adapter/:id |
PATCH/DELETE | Update/remove adapter config | M3 does not call (M2 Settings UI only) |
Stability rules: additive changes (new tools, adapters, SSE event types) permitted; breaking changes require M3 spec amendment + deprecation period.
Open boundary (M3 design, not M2 build): M3 needs a separate SSE endpoint for LLM token streaming (/api/chat/stream or similar) — distinct from MCP tool output streaming. M2's /api/mcp/stream/:correlationId streams tool execution output, not LLM completion tokens.
Data Flow
M1 happy path — Relay Agent registration (J2 steps 3–4)
Platform Lead ──install script──▶ customer Linux host
detect-OS → install-binary → write-systemd-unit → register-target (writes tenant reg token to local config)
systemd ──start──▶ relay-agent (Go binary)
relay-agent ──outbound WebSocket (wss, tenant reg token)──▶ control-plane WS server
control-plane: auth token → resolve tenant (RLS) → INSERT target (hostname/OS/IP/version) → audit append
control-plane ──ack + target_id──▶ relay-agent
relay-agent ──heartbeat every Ns──▶ control-plane (updates last_seen → green status)
control-plane ──status fan-out──▶ dashboard (server-sent events / WS)
M1 happy path — BYOM configure + validate (J2 step 2)
Admin ──POST /api/byom (url, key)──▶ API gateway
auth → tenant resolve (RLS) → RBAC (Admin only) → audit append (config event)
secrets.put(tenantId, "byom", key) → returns ref
INSERT byom_endpoints (tenant_id, url, secret_ref) — under RLS
BYOM validator: fetch OpenAI-compatible /v1/chat/completions with a trivial test prompt
on success → mark validated=true, audit append (validation ok), return green
on failure → rollback insert, audit append (validation fail), return error details (Edge 11)
M3 (forward reference) — chat diagnostic (J1)
Operator ──prompt──▶ API gateway → auth → tenant → RBAC (Operator) → audit append (prompt)
orchestrator (Trigger.dev task) → BYOM routing shim → POST tenant's BYOM /v1/chat/completions
if unconfigured/unreachable → reject (REQ-009, Edge 1), audit append, halt
LLM returns tool selection → MCP gateway → route to adapter (M2) → Relay Agent (SSH, M2)
read-only enforced at gateway (403 on write) and Relay Agent (whitelist)
tool output streams via SSE → UI trace panel → audit append (tool call + SSH command + output)
LLM synthesizes cited answer → stream to UI → audit append (response)
conversation persisted (tenant-scoped) → audit append
M2 happy path — adapter configure + test connection (J1)
Sam ──POST /api/mcp/adapter (type=proxmox, host, token)──▶ API gateway
auth → tenant resolve (RLS) → RBAC (Admin only) → audit append (adapter.configured)
broker validates PVEAuditor role on target (REQ-025)
if role missing → HTTP 422, no persist, audit append (validation fail)
secrets.set(tenantId, "proxmox:<targetId>", token) → returns ref
INSERT mcp_adapters (tenant_id, type, target_id, config, secret_ref, validated) — under RLS
UI confirms save
Sam ──clicks "Test connection"──▶ broker invokes test_connection (REQ-016)
broker: write-method blocklist check (GET only for Proxmox) → rate limit check → route to adapter
adapter: GET /api2/json/nodes (PVE API, cookie auth) → 200 OK
broker: audit append (adapter.test_connection.succeeded) → UI green
M2 happy path — capability invocation via Test-Call UI (J2)
Sam/Devon ──POST /api/mcp/invoke (tool=github.list_repos, target_id, args={})──▶ API gateway
auth → tenant resolve (RLS) → RBAC → audit append (adapter.capability_invoked, correlation_id=ULID)
broker: write-method blocklist check (GET only) → rate limit check (60/min user, 300/min tenant)
if rate exceeded → HTTP 429 + Retry-After, no adapter call
broker: resolve adapter (tenant_id, github, target_id) → route (REQ-016)
broker → returns {correlationId, streamUrl} to UI
UI ──GET /api/mcp/stream/:correlationId (SSE)──▶ broker
broker: invoke adapter (in-process custom MCP transport, tools/call JSON-RPC)
adapter: secrets.get(tenantId, "github:<targetId>") → PAT
adapter: GET /user/repos?per_page=100 (GitHub REST, fine-grained PAT, metadata:read+actions:read)
if 429 → back off (X-RateLimit-Remaining observed)
if 403 → scope violation, HTTP 403 + audit append (adapter.write_rejected)
adapter: normalize response → return to broker
broker: emit SSE events (id=<ulid>-<seq>, event=tool_result, data={content:[...],isError:false})
broker: terminal event (event=done) → close stream
broker: audit append (adapter.capability_invoked, result=success)
UI: render result (inventory call → "cached Xs ago" if served from 60s TTL cache)
M2 edge — SSH whitelist violation (Edge 7, defense-in-depth)
Sam ──POST /api/mcp/invoke (tool=ssh.run_whitelisted_command, args={command:"rm -rf /"})──▶ broker
broker: validate command against 6-command subset (layer 1)
"rm" not in {uptime, df, free, systemctl, journalctl} → REJECT at broker
broker: HTTP 403 + audit append (adapter.write_rejected) → never invokes adapter
(if broker somehow passed it: Relay Agent CheckCommand (layer 2) would also reject — defense-in-depth)
M2 edge — SSE client disconnect (Edge 8)
Client ──GET /api/mcp/stream/:correlationId──▶ broker (SSE stream open)
Client disconnects mid-stream
broker: detect EventSource close → cancel in-flight adapter call
broker: clean up correlation context → no orphan adapter calls
broker: NO audit event for client-side cancellation (per spec Edge 8)
Build Order (M1 waves — each a vertical slice, testable + shippable)
- Wave A — Foundations. Monorepo (pnpm):
apps/control-plane,apps/relay-agent(Go module),apps/dashboard,packages/{db,auth,audit,secrets,config}. Postgres schema + RLS +withTenant.audit_loghash-chain + REVOKE UPDATE/DELETE.SecretProviderinterface + AWS SM impl + local-encrypted impl. Trigger.dev bootstrap (runtime wired, no tasks yet). Covers REQ-038, REQ-039, REQ-040. - Wave B — Identity & RBAC. WorkOS SSO; session; tenant resolution middleware (
SET app.tenant_id); RBAC at API gateway (Admin/Operator/Viewer → route permission map). First endpoint protected on day one. Covers REQ-001, REQ-002, REQ-003, REQ-004, REQ-005. - Wave C — BYOM. Tenant-scoped BYOM endpoint registry (URL in DB, key in secret manager); validate-on-save test inference call (OpenAI-compatible); routing shim (no inference in M1 — proxy contract + REQ-009 reject path). Covers REQ-006, REQ-007, REQ-008, REQ-009.
- Wave D — Relay Agent. Modular install script (detect-OS/install-binary/write-systemd-unit/register-target; clean abort on unsupported OS). Go binary: outbound wss + tenant reg token, registration (tenant/target/hostname/OS/IP/version), heartbeat + exp-backoff (max 5 → alert), auto-reconnect, systemd unit w/ auto-restart. SSH whitelist file format + enforcement hook (no adapter yet — M2 plugs in). Covers REQ-010, REQ-011, REQ-012, REQ-013, (REQ-026 whitelist hook).
- Wave E — Dashboard surfacing. WebSocket fan-out of agent status to dashboard; green/yellow/red; target hostname; last 100 log lines; per-tenant view under RLS. M1 gate demo: SSO → BYOM green → install → register → green dashboard. Covers REQ-014.
Build Order (M2 waves — each a vertical slice, testable + shippable)
M2 ships on the v0.1.x patch line (M1's previous minor). Phase 0 seeds v0.1.0; each execution phase ships a progressive patch; the final phase's patch IS the M2 milestone release.
Wave 0 — Prerequisites (not a spec REQ; must complete before any M2 adapter work): CI Postgres 16 container with RLS verification (replaces PGlite-only verification); real GitHub PAT available in CI (ephemeral or test-org-scoped). Retroactively validates M1's RLS claims.
- Phase 0 — pre-execution. SPECIFY → CLARIFY → RESEARCH → PLAN → GRILL → MVP/UX. Ships
v0.1.0. - Wave F — MCP Gateway core.
packages/mcpbroker: closed tool registry (9 tools), adapter router, write-method blocklist enforcer (INV-7 at broker), token-bucket rate limiter, SSE stream manager, OpenAI ↔ MCP translator, in-process custom transport.mcp_adapterstable with RLS. Covers REQ-015, REQ-016, REQ-017, REQ-018, REQ-019, REQ-024. - Wave G — Proxmox adapter.
packages/mcp/adapters/proxmox: PVE API client,PVEAuditorrole validation (REQ-025), 3 capabilities (list_vms,get_vm_status,get_node_metrics). Covers REQ-020, REQ-025. - Wave H — SSH/Linux adapter (Relay Agent).
packages/mcp/adapters/ssh: broker SSH whitelist validation (layer 1), downstream WebSocket to M1 Relay Agent,ssh.run_whitelisted_commandcapability. Reactivates go-engineer persona for Relay Agent integration. Covers REQ-021, REQ-026 (full — M1 shipped the hook; M2 plugs the adapter in). - Wave I — Git adapters.
packages/mcp/adapters/github+packages/mcp/adapters/gitea: REST API clients, fine-grained PAT scope validation (D-006 for GitHub, version-aware for Gitea), 5 capabilities. Covers REQ-022, REQ-023, REQ-027. - Wave J — SSE integration + LLM smoke + adapter UI.
packages/llm-mock(CI-only devDependency), LLM smoke test (OpenAI→MCP→adapter→result→synthesis against real GitHub), Settings → Adapters UI + Test-Call UI. Covers REQ-017 integration, M2 gate item 8 (LLM smoke). - Final — Review + Audit + Ship. Multi-persona review, project health audit, milestone ship v0.1.(N+1) = milestone release, merge to main. Covers all M2 (sign-off).
Wave ordering & parallelism: F must complete first (all adapters depend on the broker). G, H, I can run in parallel after F (different adapter territories; no cross-dependencies). J depends on F + at least one adapter (for the LLM smoke against GitHub). Final depends on all.