Phase 1 — Software Architecture Diagrams

Merlin Architecture: Current State & Proposed Direction

Two diagrams, drawn from direct repository inspection this session — no new architecture introduced while drawing. Current-state claims are cited against real files; proposed-state components are marked Planned and do not exist in code today.

Repo ~/Developer/cdacs, branch control-plane-v1 Source direct file inspection this session, not external docs Date 2026-08-04

§0 Legend

One shared encoding for both diagrams below.

CurrentBuilt and live in the repo today
PlannedDecided this investigation, not built
FutureNamed in docs, not scoped or decided
External / 3rd partyOutside KEEP's own trust boundary
Trust boundaryA zone crossing — network, org, or vendor
Customer telemetry leaving the HubThe one flow Phase 1 was asked to trace
Audit writeWhere (if anywhere) an action is recorded
Documented gapChannel exists client-side; receiver doesn't

§1 Current KEEP Architecture

Hub, Spoke, Control Plane, both databases, Merlin's three built surfaces, the live Anthropic path, trust boundaries, and audit destinations — as they exist in the repo right now.

Director / Tech Browser — HTTPS + NextAuth session CUSTOMER NETWORK — TRUST BOUNDARY Client Devices switches, servers, UPS, workstations, printers SNMP / WinRM / SSH / LDAP / ICMP — read-only SPOKE discover.js · poll.js wmi.js · ssh.js · snmp same codebase as Hub CURRENT TUNNEL HTTPS ingest, tunneled via WireGuard (Tailscale/ Headscale) or direct HTTPS, untunneled HUB — TRUST BOUNDARY (MSP-operated, on-prem or cloud VM) Hub App (Next.js) — "cdacs" Rule Boundary + Standing Policy Evaluation merlin-rule-boundary.ts · merlin-standing-policy.ts — 100% deterministic, no LLM call CURRENT Device Diagnosis /api/merlin/ device-diagnosis capability: analyze CURRENT Onboarding Analysis /api/merlin/ onboarding-analysis capability: correlate CURRENT Incident Coordinator /api/incidents/ [id]/merlin capability: recommend CURRENT each route: deterministic SQL context-gather → prompt built → provider call incident_log real audit path — Incident Coordinator success + refusals only merlin_standing_policy_ ambiguity_events append-only, migration 068 all 3 routes — ambiguity only console.warn server logs — NOT durable or queryable. Device Diag + Onboarding refusals go here. Device Diagnosis + Onboarding Analysis successful responses: returned to browser only — correct: informational output, not required to be durably audited (§8). Other Hub Services (self-contained containers) Uptime Kuma · Guacamole + guacd · ntopng · Greenbone/OpenVAS · Headscale (self-hosted tunnel, port 47822) CURRENT Undercroft (Postgres) SYSTEM OF RECORD — Hub bind-mounted, outside container all KEEP operational data External Integrations — fire-and-forget, outbound only Slack webhook · PSA (ConnectWise / Autotask / HaloPSA) · AD/LDAP or OIDC directory EXTERNAL — OUTSIDE TRUST BOUNDARY Anthropic API — 3rd party, external api.anthropic.com/v1/messages model: claude-sonnet-4-6 (hardcoded) single provider, no fallback CURRENT Customer telemetry leaves the Hub here: device names, IPs, usernames (event logs), incident/work-log text. Only sent when a route above runs — never continuous or automatic. Never credentials — structurally enforced (MerlinProviderRequest has no credential field). No reviewed BAA/DPA confirmed. KEEP-OPERATED — NOT A 3RD PARTY KEEPMSP.io Control Plane separate Next.js app — Railway Neon Postgres — SYSTEM OF RECORD license_grants · billing_standing · account_operational_states heartbeat — receiver live enforces entitlement for Evaluation + commercial accounts license verification (Ed25519, packages/license-verifier) CURRENT (separate app, separate DB) Spoke's own direct heartbeat — never via Hub Hub's own direct heartbeat — HTTPS
Figure 1. Device Diagnosis and Onboarding Analysis don't durably persist their output — correct by design under §8, since neither can ever change the environment. Refusals from all three Merlin surfaces reach only console.warn, not a durable record.

§1a Current Architecture — Notes

Two systems of record, never merged

Undercroft (Hub-local Postgres) holds all operational KEEP data. KEEPMSP.io's Neon Postgres holds licensing/billing/account data. CLAUDE.md's own rule forbids merging them.

Four trust-boundary crossings carry real payload

Spoke→Hub (device telemetry, tunneled), Hub→Anthropic (descriptive customer telemetry, plaintext HTTPS to a third party), Hub→Control Plane (license state, heartbeat), Spoke→Control Plane (its own independent license state and heartbeat, never relayed through the Hub). Each is drawn as a distinct crossing rather than folded into one generic "external calls" arrow, since each carries a different kind of data and a different trust question.

The credential boundary holds up under inspection

MerlinProviderRequest (merlin-provider.ts) has no field a caller could put a credential in — this is structural, not caller discipline. True for all three Merlin routes.

§2 Proposed Merlin Architecture

The pipeline agreed this investigation. Governance/Rule Boundary is drawn where it actually executes today — as a gate immediately after a request is made, before any context-gathering — not at the end of the chain, since a refused request never reaches the Correlation or Knowledge Engine. Nothing in the Planned tier exists in code.

KEEP Event Detection incidents · CONDITION_POLICY · severity.ts CURRENT — deterministic incident / condition raised KEEP Governance & Rule Boundary merlin-rule-boundary.ts Identity → Authority → Policy → Rule Boundary → Approval CURRENT — already gates all 3 built surfaces refused → recorded, stops here allowed Deterministic Correlation Engine Pattern-matches known signal combinations (e.g. UPS-on-battery + other devices down) PLANNED — not built candidate hypotheses / evidence pointers KEEP Knowledge Engine KEEP-native — not gbrain (investigated, not adopted; see gbrain findings in project memory) Storage: Hub Postgres vs. embedded engine — undecided (Phase 2) PLANNED — not built bounded evidence assembled fork: is an LLM configured and reachable? LLM Provider Layer merlin-provider.ts Anthropic today; provider-neutral interface is the §17 target, not yet built CURRENT — abstraction built, 1 provider wired Offline / Degraded Path Structured findings only, no narrative synthesis Today: routes fail closed instead (502/503) — that fail-closed behavior is itself CURRENT PLANNED — graceful output, not built Presentation & Notification Layer UI display exists today; formal layer + Slack/PSA push of Merlin's own output does not PARTIALLY CURRENT KEEP Audit & Reporting Standing-policy ambiguity audit is real (migration 068); ordinary refusal + successful-response audit is inconsistent across routes today — see §3 Contradictions PARTIALLY CURRENT
Figure 2. Solid-border boxes exist in code today; dashed boxes are decided but unbuilt. The refused-request path (right side) bypasses the entire Correlation/Knowledge/LLM chain and goes straight to audit — that ordering matches what the three built routes already do, not a new invention.

§2a Proposed Architecture — Notes

Governance runs before any context-gathering

Rule Boundary evaluation runs first, in all three live routes — a request is either allowed through or refused before any data is even gathered.

Fail-closed-on-LLM-loss is already true today

Every route checks merlinProvider.isConfigured() before touching the database. The Planned "Offline / Degraded Path" upgrades that from a hard failure (502/503) to a still-useful structured answer — it does not invent offline behavior from nothing.

§2b What Keeps Working Without the LLM or Internet

Confirmed by code inspection, not the proposed diagram — this is true today.

Unaffected by LLM / internet loss

  • Monitor (Compliance + Network)
  • Incident detection, SLA tracking, compound risk
  • Device Inventory, Assets, Environment, Temperature/UPS
  • Identity Conflicts, Decommissions
  • Reports (all non-AI report types)
  • Governance & Rule Boundary evaluation — 100% deterministic
  • Standing Policy matching — deterministic by design (§6/§16)
  • Spoke↔Hub ingest (local network only, unless Hub itself is cloud-hosted)

Requires LLM + internet reachability

  • Device Diagnosis (/api/merlin/device-diagnosis)
  • Onboarding Scan Analysis (/api/merlin/onboarding-analysis)
  • Incident Coordinator (/api/incidents/[id]/merlin)
  • Nothing else in KEEP touches the Anthropic provider — confirmed by grep, not assumed