singularity-forge/docs/dev/ADR-020-internal-wire-architecture.md
Mikael Hugo 064dff2f0f feat: SF strengthening + ADR-020 wire architecture (Phases 1-2)
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>
2026-05-02 00:03:34 +02:00

7.6 KiB

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 — Workspace VM Convergence (this ADR replaces ADR-019's "MCP scope" section)
  • ADR-014 — singularity-memory Go migration
  • ADR-013 — tailnet + remote execution substrate