Phase 1 — close SF-side polish gaps: - codebase-generator: distinguish uv/poetry/pdm in Python stack-signals; surface configured tooling (ruff/mypy/pyright) when config files exist - doctor-environment: new checkPythonEnvironment — detects uv/poetry/pdm via lockfile, verifies binary on PATH, warns with install hint when missing - doctor-environment: new checkSiftAvailable — recommends sift install for repos > 5000 source files when not on PATH - tech-debt-tracker: documented future memory-as-sub-extension extraction (defer until real backend-swap requirement) Phase 2 — internal wire architecture: - ADR-020: singularity-grpc as shared schema repo; gRPC + typed clients for first-party services; MCP façade only at external-tool boundary - ADR-019: trimmed MCP scope section to a 3-line summary linking to ADR-020 to avoid the wire-format table living in two places - design-docs/index.md: ADR-020 added to ADR table These changes make SF stronger for autonomous work on Python repos (particularly ace-coder) and capture the internal wire architecture decision as a durable ADR before any singularity-grpc code lands. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
166 lines
7.6 KiB
Markdown
166 lines
7.6 KiB
Markdown
# ADR-020: Internal Wire Architecture — singularity-grpc and MCP Scope
|
|
|
|
**Status:** Proposed
|
|
**Date:** 2026-05-02
|
|
**Deciders:** Mikael Hugo
|
|
**Context repos:** `singularity-grpc` (new), `singularity-forge` (SF), `ace-coder` (ACE), `singularity-memory`
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
The first-party services that make up the singularity stack — SF
|
|
(`singularity-forge`), ACE (`ace-coder`), `singularity-memory`, and the
|
|
future `singularity-*` services — need a wire format for talking to each
|
|
other. Until now this has been an open question, with MCP (Model Context
|
|
Protocol) sometimes treated as the default because it was already in use
|
|
for external coding tools.
|
|
|
|
**MCP causes misunderstandings.** Concretely:
|
|
|
|
- **String-typed tools.** Every cross-service call is funnelled through
|
|
a string tool name and a JSON blob. Refactoring a callsite means
|
|
grepping for strings; type checkers cannot follow the call graph.
|
|
- **JSON-RPC framing tax.** Every request pays the cost of JSON
|
|
marshalling, schema-less envelope handling, and per-message protocol
|
|
overhead, even for hot internal paths.
|
|
- **Namespace pollution in agent context.** Internal service tools
|
|
exposed via MCP show up alongside user-facing tools in the agent's
|
|
tool list, crowding the LLM's context with infrastructure plumbing
|
|
that should never have been a "tool" in the first place.
|
|
- **Lost typing across language boundaries.** SF (TypeScript), ACE
|
|
(Python), and `singularity-memory` (Go) all share a build system and
|
|
can generate native clients from a single schema source. MCP throws
|
|
that away on every call.
|
|
|
|
Internal services share our build system. External LLM-driven coding
|
|
tools (Claude Code, Cursor, third-party agents) do not. Conflating those
|
|
two audiences into one wire format is the root cause of the confusion.
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
1. **Create `singularity-grpc`** as the shared schema-and-runtime repo
|
|
for first-party services. It owns common proto types, the codegen
|
|
toolchain, and per-language runtime helpers (auth, tracing, retry).
|
|
2. **First-party services talk to each other via gRPC** with typed
|
|
clients generated from each service's protos. SF, ACE, and
|
|
`singularity-memory` all consume `singularity-grpc` for shared types
|
|
and runtime.
|
|
3. **MCP is reserved for the external-tool boundary only.** It exists
|
|
as a façade for LLM-driven coding tools that don't share our build
|
|
system. It is a temporary scaffold for the period when external
|
|
coders help build the platform; the surface shrinks as the system
|
|
becomes self-hosting.
|
|
|
|
### `singularity-grpc` repo structure
|
|
|
|
```
|
|
singularity-grpc/
|
|
├── proto/
|
|
│ └── common/ # shared types: notification, error, identity,
|
|
│ # workspace base, pagination, timestamps
|
|
├── tools/
|
|
│ └── codegen/ # proto → Python / TypeScript / Go / Rust
|
|
├── runtime/
|
|
│ ├── python/ # auth interceptors, tracing, retry helpers
|
|
│ ├── typescript/ # same, in TS
|
|
│ └── go/ # same, in Go
|
|
└── docs/
|
|
├── conventions.md # naming, versioning, error model
|
|
├── auth.md # how services authenticate to each other
|
|
└── why-not-mcp.md # the rationale captured here, expanded
|
|
```
|
|
|
|
### Per-service ownership
|
|
|
|
Each service owns its own protos and server implementation. SF owns
|
|
nothing service-specific because SF *consumes* clients published by
|
|
other services — it does not expose a gRPC API of its own.
|
|
|
|
| Repo | Owns | Imports from `singularity-grpc` |
|
|
|------|------|---------------------------------|
|
|
| `ace-coder` | `api/proto/workspace.proto`, `api/proto/htdag.proto`, server impl, generated clients, the SF extension that registers the ACE engine | common types, runtime helpers, codegen |
|
|
| `singularity-memory` | `api/proto/memory.proto`, server impl, generated clients in 3 languages (Python, TS, Go) | common types, runtime helpers, codegen |
|
|
| `singularity-forge` | nothing service-specific — SF is a *consumer* of generated clients published by `ace-coder` and `singularity-memory` | nothing direct |
|
|
|
|
### The wire-format table (canonical)
|
|
|
|
This is the canonical reference for which wire goes where.
|
|
|
|
| Caller | Callee | Wire | Why |
|
|
|--------|--------|------|-----|
|
|
| ACE host → ACE tools | in-process Python imports | function call | type-safe, zero overhead |
|
|
| ACE host → singularity-memory | typed Python client (gen from Go API) | HTTP/gRPC | typed, fast, refactorable |
|
|
| SF → singularity-memory | typed TS client (gen from Go API) | HTTP/gRPC | same, in TS |
|
|
| SF → ACE worker | existing JSON-RPC stdio (`rpc-client`) | stdio JSON-RPC | already in production, language-agnostic |
|
|
| ACE worker VM → host | direct gRPC over tailnet | gRPC | typed, low-latency |
|
|
| Claude Code / Cursor → singularity-memory | MCP façade | MCP | external tool, no shared types |
|
|
| Claude Code → ACE | MCP façade (temporary) | MCP | external coder helping build, until self-hosting |
|
|
|
|
---
|
|
|
|
## Consequences
|
|
|
|
- **Removes string-typed tool naming from cross-service calls.**
|
|
Refactoring a service API now changes generated client code, which
|
|
the type checker enforces at every callsite.
|
|
- **Removes JSON-RPC framing tax for typed services.** Hot paths
|
|
between SF/ACE/memory pay only the gRPC binary protocol cost.
|
|
- **Existing `packages/rpc-client/` stdio JSON-RPC stays.** It is the
|
|
Codex-compatible RPC mode for driving SF as a child process. That is
|
|
a different concern from service-to-service typed wires and is not
|
|
affected by this ADR.
|
|
- **MCP is scoped narrowly.** Only external LLM-driven tools that don't
|
|
share our build system get an MCP façade. As the platform self-hosts,
|
|
this surface shrinks.
|
|
- **`singularity-grpc` becomes a versioning chokepoint** for shared
|
|
types. This is intentional — it forces a single source of truth for
|
|
the cross-service vocabulary (notifications, errors, identity).
|
|
|
|
---
|
|
|
|
## Naming
|
|
|
|
The repo is named **`singularity-grpc`** as a forcing function. The
|
|
name commits the project to gRPC as the implementation, which prevents
|
|
the kind of "let's just use HTTP/JSON for now" drift that produced the
|
|
MCP-everywhere situation in the first place.
|
|
|
|
Alternative names considered:
|
|
|
|
- **`singularity-wire`** — transport-agnostic. Rejected because the
|
|
ambiguity is exactly what we want to avoid; the name has to commit.
|
|
- **`singularity-rpc`** — more general. Rejected for the same reason;
|
|
too easy to backslide into JSON-RPC.
|
|
|
|
If gRPC ever needs to be replaced (e.g. with Cap'n Proto or a future
|
|
standard), the rename will be a deliberate, visible decision rather
|
|
than a silent transport swap inside a generic-named repo.
|
|
|
|
---
|
|
|
|
## Alternatives considered
|
|
|
|
- **Stay on MCP for everything.** Rejected. The string-typing,
|
|
framing tax, and namespace pollution problems compound as more
|
|
services are added. Refactor pain grows non-linearly.
|
|
- **HTTP/JSON without typed clients.** Rejected. Same string-typing
|
|
problem at smaller scale; no schema enforcement across services;
|
|
every service reinvents auth, retry, and error handling.
|
|
- **Cap'n Proto / FlatBuffers.** Rejected for now. gRPC has better
|
|
tooling, broader language support, and a mature ecosystem. The
|
|
schema layer (proto3) is portable enough that we can swap the
|
|
runtime later if a specific workload demands it.
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [ADR-019](./ADR-019-workspace-vm-convergence.md) — Workspace VM
|
|
Convergence (this ADR replaces ADR-019's "MCP scope" section)
|
|
- [ADR-014](./ADR-014-singularity-knowledge-and-agent-platform.md) —
|
|
singularity-memory Go migration
|
|
- [ADR-013](./ADR-013-network-and-remote-execution.md) — tailnet +
|
|
remote execution substrate
|