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---
35 KiB
Engineering Specification — CoreCI Chat v0.1, M2: MCP Layer & Day 1 Adapters
Version: 1.0 Date: 2026-08-25 Owner: Sarah Chen (Product Owner) Status: Locked — Ready for Dev Type: Integration Target Milestone(s): v0.1 — M2 (MCP Layer & Day 1 Adapters) Predecessor: M1 (v0.1 — Read-Only Diagnostic MVP, shipped v0.0.1..v0.0.7)
Operating Principles for this Spec:
- Incremental Delivery: This spec defines net-new work only. M1 systems (auth, BYOM, Relay, audit, RLS, secrets) are referenced, not restated.
- Zero Ambiguity: Every requirement below is translatable into a pass/fail test by QA. Incomplete REQs are rejected.
1. Objective
M2 delivers CoreCI Chat's read-only Model Context Protocol (MCP) gateway and four Day-1 infrastructure adapters (Proxmox, SSH/Linux, GitHub, Gitea), enabling safe, capability-brokered queries of customer infrastructure through a closed tool set. Building on M1's SSO, BYOM, Relay Agent, audit, and RLS foundation, this milestone introduces the structured capability broker that enforces INV-7 (read-only by default, 100% write rejection at the gateway), routes tenant-scoped tool calls to the correct adapter, streams execution output via SSE, and rate-limits per user and per tenant. The M2 acceptance gate requires real GitHub smoke + mock validation for the other three adapters + an LLM-driven tool-calling smoke proving broker callability. M2's customer-facing surface is the Settings → Adapters configuration UI and a Test-Call UI in the existing M1 dashboard; M3 (next milestone) consumes M2's gateway to deliver the chat orchestration surface.
2. Scope & Target Milestones
2.1 In Scope (Explicit Additions)
- MCP capability broker gateway — closed read-only tool registry, tenant-scoped routing, write-rejection enforcement, token-bucket rate limiting (per user + per tenant), multi-target scope disambiguation, SSE streaming endpoint.
- Read-only Proxmox VE adapter —
PVEAuditorrole; PVE API client; capability→API translation. - Read-only SSH/Linux adapter via existing M1 Relay Agent — fixed whitelist command set per spec §7 Q3.
- Read-only GitHub adapter — fine-grained PAT (
metadata:read+actions:readper D-006); REST API client. - Read-only Gitea adapter — read-only token; version-aware scope validation; mirror of GitHub adapter for Gitea REST API.
- Adapter configuration UI in existing M1 dashboard — per-adapter forms, SecretProvider-backed credential entry, role/scope validation on submit.
- Test-Call UI in existing M1 dashboard — capability picker, argument forms, staleness indicator for inventory calls, consumes gateway SSE endpoint.
- Audit integration for adapter events — reuses M1
audit_log; new event types:adapter.configured,adapter.test_connection.{succeeded,failed},adapter.capability_invoked,adapter.write_rejected. - Wave 0 prerequisites — CI Postgres 16 container with RLS verification; real GitHub PAT available in CI (ephemeral or test-org-scoped).
- Verification gate — LLM-driven tool-calling smoke using CI-only mock provider (
packages/llm-mock) with OpenAI-compatible + tool-calling contract.
2.2 Out of Scope (Explicit Exclusions)
- LLM chat UI / orchestration (M3).
- Trigger.dev task execution (M3; M1 bootstrap only).
- Any write capability (INV-7 hard; broker rejects 100% of write attempts).
- S3 Object Lock WORM audit (M3 per D-004); M2 uses M1's Postgres hash-chain only.
- Vanta evidence collection (M3).
- New Postgres tables for MCP caching (Q4 decision: in-memory only).
- 5+ Day-1 adapters (only Proxmox, SSH, GitHub, Gitea).
- Persistence of MCP results beyond audit events (no snapshot tables, no time-series tables).
- Cross-tenant adapter sharing (adapters are per-tenant; broker scopes via
withTenant+ RLS). - Custom MCP server authoring tools for customers (v1.2+, spec §2.2).
- Custom RBAC roles beyond M1 (v1.2+).
- Custom capability tool set per tenant (broker exposes fixed tool registry; per-tenant policy may disable tools but not add new ones).
- Anti-goals per spec §2.2: hosted LLM, K8s/ArgoCD/Helm, Slack/Teams/CLI/mobile, approval-gated remediation, RAG, SOC 2 final cert, BYOK, multi-region, Windows, fine-tuning.
2.3 Milestone Breakdown
Wave 0 — Prerequisites (must complete before any M2 adapter work; not spec REQs):
- CI Postgres 16 container provisioned; RLS policies exercised by tests (replaces PGlite-only verification).
- Real GitHub PAT available in CI (ephemeral or test-org-scoped).
Milestone 2 — M2 (MCP Layer & Day 1 Adapters): Ships REQ-015 through REQ-027.
- Acceptance Gate: See Section 6. All 13 REQs pass, mocks + real GitHub smoke + LLM smoke + INV-7 verified, CI Postgres RLS verified, M1 non-regression.
3. Personas & User Journeys
3.1 Personas
- Persona A — Sam (Tenant Admin): Platform owner. Configures CoreCI for their org: BYOM endpoint (M1 REQ-008), team/RBAC (M1), adapters. Primary M2 user; configures adapters via Settings → Adapters and validates connections via Test-Call. Sophistication: high (platform engineering background).
- Persona B — Devon (Platform Engineer): Hands-on-keyboard SRE/DevOps. Will be the LLM chat power-user in M3. In M2, surfaces only through the Test-Call UI to verify adapter behavior pre-M3.
- Persona C — Casey (Compliance Reader): Reads audit log for evidence collection. Passive consumer in M2 (no M2 journey; consumes M1 audit surface which now includes adapter events).
3.2 Happy Paths
Journey 1 — Sam configures and validates an adapter
- Step 1: Sam signs in via WorkOS SSO (M1 REQ-001..003, INV-1) → dashboard loads (references M1).
- Step 2: Sam navigates to Settings → Adapters and clicks "Add adapter" → adapter type picker shown (Proxmox / GitHub / Gitea / SSH) (Maps to REQ-020..023).
- Step 3: Sam selects adapter type and enters config → SecretProvider.set invoked (Proxmox: host +
PVEAuditortoken; GitHub/Gitea: host + read-only PAT; SSH: hostname + port + Relay registration token) (Maps to REQ-025, REQ-026, REQ-027; INV-3). - Step 4: Sam submits → broker validates role/scope at submit time → adapter config persisted to Postgres under
withTenant+ RLS (Maps to REQ-024 if multi-target; INV-2). - Step 5:
adapter.configuredaudit event appended to M1audit_log(INV-4) → UI confirms save. - Step 6: Sam clicks "Test connection" → broker invokes
test_connectioncapability (REQ-016) through the closed tool registry (REQ-015); read-only enforcement verified (REQ-018, INV-7). - Step 7: Broker returns structured pass/fail within 5s; UI renders result;
adapter.test_connection.{succeeded,failed}audit event appended (INV-4).
Testable Acceptance (BDD Format):
- Given Sam is signed in as
adminwith a valid Proxmox adapter config, when Sam submits the config, then the adapter is persisted,adapter.configuredis appended, and the UI confirms save. - Given a Proxmox adapter is configured, when Sam clicks "Test connection", then the broker returns a structured pass/fail within 5 seconds and
adapter.test_connection.succeeded(or.failed) is appended. - Given Sam submits a Proxmox token without
PVEAuditorrole, when the broker validates, then submission returns HTTP 422 with role-violation error and no config is persisted. - Given Sam submits a GitHub PAT lacking
metadata:readoractions:read, when the broker validates scope, then submission returns HTTP 422 with scope-violation error and no config is persisted (D-006).
Journey 2 — Sam / Devon reads infrastructure via Test-Call UI
- Step 1: Sam or Devon opens the Test-Call panel for a configured adapter (Maps to REQ-020..023, REQ-016).
- Step 2: They select a capability from the closed read-only tool set (e.g.,
proxmox.list_vms,github.get_recent_ci_runs,ssh.run_whitelisted_commandwithuptime) (Maps to REQ-015). - Step 3: They optionally enter required arguments (e.g.,
vmidforproxmox.get_vm_status) validated against the tool's JSON Schema inputSchema (Maps to REQ-015). - Step 4: They submit → broker resolves capability (REQ-016), enforces multi-target scope (REQ-024), checks rate limit (REQ-019), invokes adapter.
- Step 5: Adapter calls upstream → response normalized → SSE stream initiated on
GET /api/mcp/stream/:correlationId(REQ-017). - Step 6: UI displays:
- Live capabilities (logs, metrics, events, CI runs): fresh result, no staleness indicator.
- Inventory capabilities (
list_*): result + "cached Xs ago" if served from 60s in-memory TTL cache.
- Step 7:
adapter.capability_invokedaudit event appended with adapter identity, capability name, params hash, result status (INV-4).
Testable Acceptance (BDD Format):
- Given an adapter is configured and rate limits are not exceeded, when Sam invokes
proxmox.list_vmsvia the Test-Call UI, then the broker returns the live VM list as an SSE stream andadapter.capability_invokedis appended. - Given
proxmox.list_vmswas invoked within the last 60 seconds, when Sam invokes it again, then the broker returns the cached result with a "cached Xs ago" staleness indicator. - Given Sam invokes a non-
list_*capability (e.g.,proxmox.get_node_metrics), when the call completes, then the UI shows the result with no staleness indicator (live path). - Given Sam has exceeded 60 req/min, when Sam invokes any capability, then the broker returns HTTP 429 with
Retry-Afterheader and no adapter call is made (REQ-019).
3.3 Failure & Edge Paths
- Edge 1 — Write attempt at broker: Sam (or a malicious payload) attempts
proxmox.shutdown_vmvia Test-Call → broker rejects at gateway before adapter invocation (REQ-018, INV-7) → returns HTTP 403 with structured error →adapter.write_rejectedaudit event appended. - Edge 2 — Adapter upstream timeout: Adapter call to upstream (e.g., Proxmox API) times out after 10s → broker returns HTTP 504 →
adapter.capability_invokedaudit event withresult=failureappended. - Edge 3 — Multi-target disambiguation: Tenant has 2 Proxmox hosts configured; Sam invokes
proxmox.list_vmswithout selectingtarget_id→ broker returns HTTP 400 with "target required" error (REQ-024). - Edge 4 — Invalid capability argument: Sam invokes
proxmox.get_vm_statuswithoutvmid→ broker returns HTTP 400 with JSON Schema validation error (REQ-015). - Edge 5 — Rate limit exceeded (per-tenant): Tenant exceeds 300 req/min aggregate → all subsequent requests for that tenant return HTTP 429 (REQ-019).
- Edge 6 — Secret resolution failure: Adapter config references a secret SecretProvider cannot resolve (e.g., revoked AWS SM secret) → adapter invocation fails → broker returns HTTP 503 "credential unavailable" → audit event appended (INV-3).
- Edge 7 — SSH whitelist violation: Sam attempts
ssh.run_whitelisted_commandwith a non-whitelisted command (e.g.,rm -rf /) → broker rejects at gateway (defense-in-depth layer 1) AND Relay Agent rejects at whitelist hook (layer 2, REQ-026) → broker returns HTTP 403 → audit event appended. - Edge 8 — SSE stream lifecycle: Client disconnects mid-stream → broker cleans up correlation context; no orphan adapter calls; no audit event for client-side cancellation.
4. Functional Requirements
All REQs inherit M1 invariants: INV-1 (auth gateway ordering), INV-2 (withTenant + RLS), INV-3 (SecretProvider only), INV-4 (audit completeness), INV-7 (read-only). Where a REQ column says "M1" it is a cross-reference only, not new work.
| ID | Title | Journeys | Priority | Acceptance Criteria |
|---|---|---|---|---|
| REQ-015 | Define abstract MCP tool schema | J2 | High | Given the broker exposes the closed read-only tool set, when a tool is registered, then it has name, description, inputSchema (JSON Schema) per MCP standard, the schema is in the broker's tool registry before any adapter invocation, any tool call with arguments not matching inputSchema returns HTTP 400 with a schema-validation error, and the tool registry is closed and enumerated with per-tenant policy able to disable individual tools but never add new ones. M2 starter set is locked at: proxmox.list_vms (inventory), proxmox.get_vm_status (live), proxmox.get_node_metrics (live), ssh.run_whitelisted_command (live), github.list_repos (inventory), github.get_recent_ci_runs (live), github.get_workflow_run (live), gitea.list_repos (inventory), gitea.get_recent_ci_runs (live). |
| REQ-016 | Route abstract MCP calls to tenant-specific adapter | J1, J2 | High | Given an MCP tool call request with a tenant-scoped adapter binding (tenant_id, adapter_type, target_id), when the broker receives the call, then the call is routed to the adapter resolved by that tuple, the response is returned as an SSE stream, and routing errors return HTTP 404 with a structured error. |
| REQ-017 | Stream tool execution output to chat UI via SSE | J2 | High | Given the broker invokes an adapter capability, when the adapter returns partial or complete output, then the broker emits an SSE stream on GET /api/mcp/stream/:correlationId with Content-Type: text/event-stream and each event has id, event, data fields per the SSE specification; the stream terminates with a terminal event (done or error) on completion or error. Per-call lifecycle: one stream per capability invocation; correlation ID = ULID minted at POST /api/mcp/invoke. Client disconnect (Edge 8) cancels in-flight adapter call; no audit event for client-side cancellation. |
| REQ-018 | Enforce read-only at MCP gateway proxy layer | Edge 1, Edge 7 | High | Given a write-capable method per the adapter's known write surface (Proxmox: POST/PUT/DELETE; SSH: non-whitelist commands; GitHub: scopes outside metadata:read+actions:read; Gitea: POST/PUT/DELETE/PATCH on all endpoints), when the request reaches the broker, then the broker rejects with HTTP 403, appends adapter.write_rejected audit event, and never invokes the adapter; verified by a test per adapter at the M2 gate. The broker is the load-bearing safety boundary (INV-7 enforcement at the gateway, not at the adapter). |
| REQ-019 | Apply token-bucket rate limit per user and per tenant | Edge 5 | High | Given a user has exceeded 60 req/min OR a tenant has exceeded 300 req/min, when any subsequent capability invocation is attempted, then the broker returns HTTP 429 with Retry-After header and no adapter call is made; rate limit state is process-local in M2 (in-memory token-bucket, capacity = rate, refill 1/sec user / 5/sec tenant). |
| REQ-020 | Implement read-only Proxmox MCP adapter | J1, J2 | High | Given a Proxmox adapter is configured with a PVEAuditor-scoped API token, when an MCP tool call routes to it, then the adapter calls only Proxmox GET endpoints (e.g., /api2/json/nodes, /api2/json/qemu, /api2/json/nodes/{node}/qemu/{vmid}/status/current) and never mutates state; supported capabilities include proxmox.list_vms (inventory), proxmox.get_vm_status, proxmox.get_node_metrics. |
| REQ-021 | Implement read-only SSH/Linux Server MCP adapter | J1, J2 | High | Given an SSH adapter is configured via M1 Relay Agent (REQ-026), when an MCP tool call routes to it, then the adapter invokes only commands from the fixed whitelist subset (uptime, df -h, free -m, systemctl status <svc>, journalctl -n <N>, systemctl list-units --type=service) via the M1 Relay whitelist hook; the broker validates command against this subset BEFORE dispatch to the Relay Agent (defense-in-depth layer 1); the Relay Agent CheckCommand is the second enforcement layer (layer 2); non-whitelist commands return HTTP 403 (REQ-026). |
| REQ-022 | Implement read-only GitHub MCP adapter | J1, J2 | High | Given a GitHub adapter is configured with a fine-grained PAT (metadata:read + actions:read minimum per D-006), when an MCP tool call routes to it, then the adapter calls only GitHub REST GET endpoints and rejects any token lacking required scopes; supported capabilities include github.list_repos (inventory), github.get_recent_ci_runs, github.get_workflow_run. |
| REQ-023 | Implement read-only Gitea MCP adapter | J1, J2 | High | Given a Gitea adapter is configured with a read-only token, when an MCP tool call routes to it, then the adapter calls only Gitea REST GET endpoints and rejects any token lacking required read scopes; version-aware validation: Gitea ≥1.22 requires read:repository scope; Gitea <1.22 accepts any token with broker-side write-method blocklist (POST/PUT/DELETE/PATCH) as security backstop; supported capabilities mirror the GitHub adapter (gitea.list_repos, gitea.get_recent_ci_runs). |
| REQ-024 | Scope MCP queries to explicitly selected target in multi-target tenants | Edge 3 | High | Given a tenant has multiple adapters of the same type configured (e.g., 2 Proxmox hosts), when a capability invocation is received without an explicit target_id for that adapter type, then the broker returns HTTP 400 "target required" with a list of available targets; the UI surfaces a target picker. |
| REQ-025 | Authenticate to Proxmox via scoped API token + PVEAuditor | J1 | High | Given a Proxmox adapter config submission, when the token is submitted, then the broker verifies the token's role on the target is PVEAuditor before persisting; tokens without PVEAuditor return HTTP 422 with role-violation error and no config is persisted; the token is stored via SecretProvider.set (INV-3). |
| REQ-026 | Authenticate to Linux servers via SSH key + whitelist execution | J1, Edge 7 | High | Given an SSH adapter config submission, when the Relay registration token is stored via SecretProvider.set (INV-3), then all subsequent SSH commands are validated against the fixed whitelist subset at two layers: (1) broker validates command before dispatch, (2) M1 Relay Agent CheckCommand validates at execution; non-whitelisted commands return HTTP 403 with a structured error and adapter.write_rejected audit event is appended. |
| REQ-027 | Authenticate to GitHub and Gitea via scoped API tokens | J1 | High | Given a GitHub or Gitea adapter config submission, when the token is submitted, then the broker validates token scopes before persisting (GitHub: fine-grained PAT with metadata:read + actions:read minimum per D-006; Gitea ≥1.22: read:repository minimum; Gitea <1.22: any token accepted with broker-side write-method blocklist as security backstop); insufficient scopes return HTTP 422 with scope-violation error and no config is persisted; the token is stored via SecretProvider.set (INV-3). |
Cross-cutting (inherited from M1, not new REQs): All M2 REQs run under withTenant + RLS (INV-2), resolve credentials via SecretProvider (INV-3), append audit events to M1's audit_log (INV-4), and enforce the auth → tenant resolve → RBAC → audit ordering (INV-1).
5. Technical Constraints & NFRs
- Closed tool registry (REQ-015): Broker exposes a fixed, enumerated set of read-only tools per adapter. Per-tenant policy may disable tools but not add new ones. No "custom tool" endpoint in M2. Full enumeration: see Section 4 REQ-015 acceptance criterion.
- Read-only enforcement (REQ-018, INV-7): Every adapter has a known write surface (Proxmox: POST/PUT/DELETE; SSH: non-whitelist commands; GitHub: scopes outside
metadata:read+actions:read; Gitea: POST/PUT/DELETE/PATCH on all endpoints). Broker maintains an adapter-specific write-method blocklist and rejects 100% of write attempts before adapter invocation. The broker is the load-bearing safety boundary. Verified by a test per adapter at the M2 gate. - Multi-tenancy (INV-2): Every MCP capability invocation runs under
withTenanttransaction + RLS. Adapters are per-tenant; no cross-tenant adapter sharing. Adapter rows in Postgres are tenant-scoped (REQ-024 handles same-type multi-target). - Credential resolution (INV-3): All adapter credentials resolved via
SecretProvider.get. No env/config/DB fallback. Credentials never logged; secret identifiers hashed in audit events. - Audit completeness (INV-4): Every MCP capability invocation, every adapter config change, every write rejection, every test connection appends an audit event to M1's
audit_log(hash-chained, append-only, UPDATE/DELETE REVOKE'd). New event types:adapter.configured,adapter.test_connection.{succeeded,failed},adapter.capability_invoked,adapter.write_rejected. - Rate limiting (REQ-019): Token-bucket per user (60 req/min) and per tenant (300 req/min). In-memory implementation in M2 (process-local, capacity = rate, refill 1/sec user / 5/sec tenant); cross-instance aggregation deferred to M3 if Trigger.dev tasks or horizontal scaling require it (Redis migration path documented in code comments).
- Data freshness: Live query for time-sensitive capabilities (logs, metrics, events, CI runs — i.e., non-
list_*). In-memory TTL cache (60s, LRU-evicting) for inventory capabilities only (list_*). No Postgres tables for cache. Staleness surfaced in M2 Test-Call UI for cached calls; chat surface staleness is M3. - MCP protocol conformance: Broker implements MCP spec version
2025-06-18(latest stable with complete published documentation). Conformance verified against modelcontextprotocol.io before architecture locks — verification artifact required at M2 gate. Gateway = MCP client; adapters = MCP servers (in-process custom transport for Proxmox/GitHub/Gitea; downstream WebSocket to M1 Relay Agent for SSH). JSON-RPC 2.0 ↔ OpenAItool_callstranslation contract documented as a typed translator module. - MCP transport architecture (D-007): Three layers: (1) broker ↔ Proxmox/GitHub/Gitea adapters use in-process custom MCP transport (JSON-RPC messages in-process, no subprocess spawning); (2) broker ↔ SSH adapter MCP layer is in-process, with downstream WebSocket transport to M1 Relay Agent (Go binary) — the MCP
tools/callJSON-RPC sits between broker and TS SSH adapter module, the TS module then calls the Relay Agent over M1's WebSocket; (3) broker ↔ CI/LLM smoke uses stdio transport. Broker ↔ UI uses REST facade + SSE (not MCP Streamable HTTP — browser-friendly facade with MCP-compliant tool schemas/results inside). - OpenAI ↔ MCP translation contract:
tool_calls[].function.{name, arguments}→params.{name, arguments}(parsed JSON object);result.content[].text+isError→ OpenAI tool message{role:"tool", tool_call_id, content}. Documented as a typed translator module inpackages/mcp/translator.ts. - SSE event format:
id: <ulid>-<sequence>; event: tool_result; data: {"content":[...],"isError":false}; terminal eventsevent: done(completion) orevent: error(failure); correlation ID (ULID) surfaced in audit eventcorrelation_idfield and Test-Call UI. - Token-bucket parameters: capacity = rate (60 user / 300 tenant); refill 1/sec (user) / 5/sec (tenant); process-local in M2; Redis migration path documented in code.
- Per-adapter write-method blocklist: Proxmox POST/PUT/DELETE; GitHub scopes outside
metadata:read+actions:read; Gitea POST/PUT/DELETE/PATCH on all endpoints; SSH non-whitelist commands (subset per REQ-021). - RLS verification environment: All M2 schema additions and queries verified against real Postgres 16 in CI (not PGlite). PGlite permitted only for unit tests where RLS gap is documented (per intake §5 tensions) and app-layer withTenant + explicit WHERE is primary enforcement.
- 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).
Performance NFRs:
- MCP capability invocation P95 latency < 2s for live capabilities (excluding upstream call time).
- SSE stream chunk delivery < 100ms between events.
- Rate limiter check < 5ms.
- Adapter upstream timeout: 10s (broker returns HTTP 504 on timeout).
- SecretProvider.get timeout: 5s (broker returns HTTP 503 on timeout).
Adapter upstream protocol constraints:
- Proxmox: PVE API over HTTPS; cookie-based auth; respect PVE rate limits.
- SSH: via M1 Relay Agent WebSocket; outbound-only from customer hosts (INV-6).
- GitHub: REST API;
X-RateLimit-Remainingheader observed; back off on 429. - Gitea: REST API; mirror GitHub adapter patterns; verify against customer's Gitea version.
6. Milestone Plan & Release Gates
Test evidence required for Production Release (M2 gate):
- M1 acceptance gate still passing — no regression to M1 REQs (001-014, 038-040).
- All 13 M2 REQs (015-027) have passing tests with Given/When/Then coverage.
- Code coverage ≥ 80% on new M2 modules (gate per spec §6).
- DB coverage ≥ 80% maintained on
packages/db(M1 floor). - CI/CD pipeline builds successfully (GREEN).
- Wave 0 prerequisites met: CI Postgres 16 container running; RLS policies verified against real Postgres (not PGlite); real GitHub PAT available in CI.
- Adapter validation: Proxmox, SSH, Gitea validated via mocks; GitHub validated via real-target smoke in CI (live API call).
- LLM smoke (P0 — not deferrable): A chat-completion request with tools parameter invokes
github.list_reposvia the broker, receives adapter response, and returns a synthesized LLM response grounded in adapter data. Uses CI-only mock provider with OpenAI-compatible + tool-calling contract (packages/llm-mock). Ifpackages/llm-mockcannot reliably drive the full OpenAI→MCP→adapter→result→synthesis path against a real GitHub target in CI, that's a P0 issue for the M2 cycle, not a deferral to M3. - INV-7 verified by tests: 100% of known write-capable upstream methods per adapter are rejected at the broker with audit event (REQ-018 acceptance criterion validated per adapter). The broker is the load-bearing safety boundary.
- Multi-target scope verified by tests: Tenant with 2 same-type adapters cannot invoke without
target_id(REQ-024). - Rate limit verified by tests: Per-user (60/min) and per-tenant (300/min) limits enforced (REQ-019).
- SSE streaming verified by tests: Stream emits correctly formed events; client disconnect handled cleanly (REQ-017).
- Adapter audit events visible in M1's audit export.
- Security/Compliance review approved — audit completeness, secret handling, RLS enforcement, write-rejection defense-in-depth.
- MCP conformance verification artifact — recorded evidence that the broker's MCP implementation conforms to spec version
2025-06-18(lowest-confidence area — verify before locking).
7. Open Questions & Assumptions
All 9 open questions resolved and approved by Product Owner on 2026-08-25. Confidence range: 0.75–0.85 (all above 0.6 threshold).
Q1 — MCP protocol version + transport + conformance
Decision: MCP spec version 2025-06-18 (latest stable with complete published documentation). Three transport layers: (1) in-process custom transport for broker ↔ Proxmox/GitHub/Gitea adapters; (2) REST facade + SSE for broker ↔ UI; (3) stdio for broker ↔ CI/LLM smoke. OpenAI ↔ MCP translation contract: tool_calls[].function.{name, arguments} → params.{name, arguments}; result.content[].text + isError → OpenAI tool message. Documented as Phase 0 deliverable.
Confidence: 0.80
Impact if wrong: Broker may need rework if MCP standard diverges from assumptions; LLM smoke contract mismatch.
D-007: Record in CLARIFY (transport architecture for SSH — in-process MCP layer with downstream WebSocket to M1 Relay Agent).
Q2 — Closed tool set enumeration per adapter (REQ-015 freeze)
Decision: Lock the 9-tool starter set across 4 adapters: proxmox.list_vms (inventory), proxmox.get_vm_status (live), proxmox.get_node_metrics (live), ssh.run_whitelisted_command (live), github.list_repos (inventory), github.get_recent_ci_runs (live), github.get_workflow_run (live), gitea.list_repos (inventory), gitea.get_recent_ci_runs (live). No gitea.get_workflow_run in M2 (deferred to v1.2+). Additions require spec amendment (v1.2+).
Confidence: 0.85
Impact if wrong: UX gaps in Test-Call UI; broker expansion post-M2.
Q3 — SSH whitelist command set (REQ-021 + REQ-026 freeze)
Decision: Conservative 6-command subset of M1's whitelist: uptime, df -h, free -m, systemctl status <svc>, journalctl -n <N> (1-500), systemctl list-units --type=service. Broker validates command against this subset BEFORE dispatch to Relay Agent (defense-in-depth layer 1); Relay Agent CheckCommand is layer 2. Expansion requires spec amendment.
Confidence: 0.80
Impact if wrong: SSH adapter may ship with commands operators don't need, or miss commonly-needed ones.
Q4 — Rate limit token-bucket storage
Decision: In-memory (process-local) for M2. Token-bucket per user (60 req/min) and per tenant (300 req/min). capacity = rate, refill 1/sec (user) / 5/sec (tenant). Redis migration path documented in code comments for M3 if horizontal scaling or Trigger.dev tasks require cross-instance aggregation. Confidence: 0.85 Impact if wrong: Multi-instance deployments may allow rate limit bypass until M3.
Q5 — GitHub "read-only" scope granularity
Decision: Fine-grained PATs with metadata:read + actions:read minimum (no contents:read — M2 GitHub tools do not read repo contents). Per-tool additional scopes validated at invocation time. D-006 deviation from spec recommendation (contents:read + metadata:read): M2 tools don't need repo contents access; contents:read adds no value and broadens attack surface.
Confidence: 0.80
Impact if wrong: Some tools may fail at runtime due to insufficient scope; UX friction.
D-006: Record in CLARIFY. REQ-027 acceptance criterion updated to match.
Q6 — Gitea "read-only" scope mapping
Decision: Version-aware token validation. Gitea ≥1.22: require read:repository scope (fine-grained OAuth2 scopes added in 1.22). Gitea <1.22: accept any token (no read-only scope available) with broker-side write-method blocklist (POST/PUT/DELETE/PATCH) as security backstop. Version detected via GET /api/v1/version and recorded in adapter config row. Submit-time validation via GET /api/v1/repos/search?limit=1.
Confidence: 0.75
Impact if wrong: Gitea adapter may reject valid tokens or accept over-scoped tokens.
Q7 — SSE stream lifecycle and correlation ID
Decision: Per-call streams (one stream per capability invocation) for M2. Correlation ID = ULID (26-char, lexicographically sortable) minted at POST /api/mcp/invoke. Surfaces in SSE event id field, audit event correlation_id, and Test-Call UI. Client disconnect (Edge 8) cancels in-flight adapter call; no audit event for client-side cancellation. Session-based streams considered for M3.
Confidence: 0.85
Impact if wrong: UX complexity, correlation issues, or orphaned streams.
Q8 — LLM smoke mock implementation location
Decision: New packages/llm-mock as a devDependency (not production dependency). CI-only; import-guarded against prod bundle via build-time check/eslint rule. Implements OpenAI-compatible /v1/chat/completions that accepts tools parameter, returns tool_calls, accepts follow-up tool messages, and synthesizes grounded responses.
Confidence: 0.85
Impact if wrong: Mock leaks into prod builds or runtime bundle bloat.
Q9 — M3 interface contract from M2
Decision: Yes — M2's gateway (REST + SSE) is a stable contract from M2's acceptance gate onward. 5-endpoint contract frozen (see Section 9). M3 treats these as a stable API; additive changes (new tools, adapters, SSE event types) permitted; breaking changes require M3 spec amendment + deprecation period. M3 token-streaming SSE endpoint (for LLM output tokens) is a separate design — not in M2 scope, but documented as the M3 chat orchestration requirement (known M2→M3 boundary). Confidence: 0.80 Impact if wrong: M3 chat UI may need gateway rework if M2 API changes post-gate.
8. Changelog
| Version | Date | Author | What Changed | REQs Affected |
|---|---|---|---|---|
| v1.0 | 2026-08-25 | Sarah Chen | Initial M2 spec — MCP Layer & Day 1 Adapters. All 9 open questions resolved. D-006 (GitHub scopes) and D-007 (MCP transport) recorded as deviations. | REQ-015 through REQ-027 |
| v1.0-locked | 2026-08-25 | Sarah Chen + ciagent | Spec delta applied: REQ-015 closed-tool-set enumeration, REQ-021 defense-in-depth, REQ-027 D-006/D-007 scope updates, Section 5 MCP conformance/transport/SSE/token-bucket constraints, Section 9 M2→M3 contract freeze. | REQ-015, REQ-021, REQ-027, Section 5, Section 9 |
9. M2→M3 Contract Freeze
This section freezes the M2 gateway API as a stable contract from M2's acceptance gate onward. M3 treats these endpoints as a stable API.
| 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 (J1 Step 3) | 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:
- From the M2 acceptance gate onward, these endpoints' request/response shapes are frozen. M3 treats them as a stable API.
- Additive changes (new tools, new adapters, new event types in SSE) are allowed and do not break the contract.
- Breaking changes (renaming endpoints, changing response shapes) require an M3 spec amendment and a deprecation period.
- The OpenAI ↔ MCP translation contract (Q1) is part of this freeze — M3's chat orchestration relies on the broker accepting OpenAI
tool_callsand returning OpenAI tool messages.
Open boundary (M3 design, not M2 build):
M3 likely needs a separate SSE endpoint for LLM token streaming (distinct from MCP tool output streaming). M2's /api/mcp/stream/:correlationId streams tool execution output, not LLM completion tokens. M3's chat orchestration will need a /api/chat/stream (or similar) endpoint for streaming LLM output tokens to the chat UI. This is a known M2→M3 boundary, documented here as a future endpoint, not built in M2.
End of CoreCI Chat v0.1 M2 Engineering Specification v1.0 (Locked)
Product Owner: Sarah Chen — Locked 2026-08-25 — Ready for ciagent Milestone 2 implementation.