Decision 058: Re-layer the CLI as core → HTTP API → clients; the service is the CLI's only execution path
- Status: proposed
- Date: 2026-08-05
- Deciders: @MadaraUchiha-314 (owner, via PR #162 phase-1 review), harness
- Work item: issue-161
Context
Issue #161 asks for a control plane: every capability the CLI carries (poller, webhook receiver, sessions, graph, events, repo-scoped queries) invocable by any client — CLI, an agent over MCP, and a control-plane UI — through durable APIs. The owner resolved the five architecture forks on the phase-1 review of PR #162: a framework such as FastAPI is sanctioned; the CLI uses the service as its default and only mode; the UI lives under ui/ on Vite-class tooling, TypeScript only; MCP is HTTP-only, no stdio; and delivery is a single PR.
Decision
- Three layers. A transport-agnostic
the_loop.corefacade (one module per capability, delegating to the existing modules) is the single implementation;the_loop.api(FastAPI) exposes it at/api/v1per an authored OpenAPI contract in this repo'sapiSpecs.rest.dir(docs/api-specs/openapi/, an override of the shippedspecs/openapidefault — owner decision on PR #162); the CLI and the MCP endpoint are thin clients of that surface. (A UI client was part of this decision as drafted; it was descoped from issue-161 on owner review and deferred to a follow-up work item.) Superseded on owner review (PR #162):"No extras pls. It creates a nightmare when installing. All deps get installed when one installs the-loopy-one."[service]extra.fastapi/uvicornare an optional extra; the base install keeps exactlypyyaml.fastapi,uvicorn,mcpandslack-sdkare required dependencies; the published extra names survive as empty no-ops so pinned install lines keep resolving. The MCP SDK's floor moves the package to Python 3.10+.- Service-only CLI with auto-start. Core-capability commands have no in-process path. The CLI auto-starts a local service (pidfile + flock, the issue-159 lifecycle discipline) when none is reachable, keeping the one-command UX. What stays local is local by nature: the bootstrap commands that manage the installation or the service process itself (
install,upgrade,migrate-config,service *,--version),sessions attach(it execs tmux onto the caller's terminal),sessions reset(recovery must work when nothing is running), andpoll start/gh-webhook start(foreground daemons that cron and systemd units depend on — the detached path is/api/v1/daemons). MCP as a ~150-line JSON-RPC endpoint whose tool registry is generated from the core surface; theSuperseded on owner review (PR #162): "I hope we are using the official python SDK for MCP… Don't want to maintain custom implementation. Follow official SDKs."mcpSDK rejected on the minimalism ladder./mcpis served by the officialmcpSDK, mounted on the same app; this repo keeps only the binding from core toadd_tool. HTTP transport only (no stdio) is unchanged, as is the exclusion of destructive/attribution-forging operations (sessions reset,graph force).- UI: Vite + vanilla TypeScript under
ui/, static-hostable build with a configurable API base; no component framework until the view count warrants one.
Consequences
- Every capability becomes reachable programmatically, and new core capabilities are exposable over CLI/REST/MCP without duplicating logic.
- The install grows four required dependencies and a Python floor of 3.10, and in exchange there is nothing to remember: one
pip install the-loopy-onecan host the service, serve MCP and use every transport. Executing a core command now needs a reachable service (auto-started by default) — a real behaviour change, gated fail-closed with an explicitthe-loop service startmessage. check's purity contract moves to the core function; the CLI↔service hop is transport.- The CLI stops being an implementation: it renders the
messagesandexitCodethe core facade returns, so an operator's command, an HTTP call and an MCP tool call produce the same words from the same code. - The API is an RCE-equivalent surface and is treated as such: loopback-only by default, no CORS headers. (The per-boot bearer-token auth this decision originally specified was superseded by decision-059: the service carries no in-app auth — a gateway owns it — and its own boundary is network scoping via the exposure guard.)
Alternatives considered
- stdlib
http.serverservice (zero new deps) — rejected by the owner's framework sanction and by the cost of re-implementing validation/OpenAPI/ASGI lifecycle by hand. - In-process fallback when no service runs — rejected by the owner: the service is the only mode; a fallback would fork behaviour across two paths.
- MCP via stdio transport — rejected by the owner; HTTP only. (The accompanying rejection of the official SDK was itself later reversed by the owner — see decision 4 above.)
- React (or similar) UI — deferred; three views need no framework, and the TS-only rule is independent of framework choice.