2026-09-23·by Sijie Wang#standmeet#architecture#design

convergence-inbound-and-outbound

Two convergence points — outbound and inbound

Parent: structure

The deepest structural idea in the backend, and the one that had no note. Everything a capability can do, and everything it can ask back for, passes through exactly one place each — and both are enforced by gates, not by review.

Outbound: the dispatcher

Every domain declares what it can do as data (fp.Op — id, description, input schema, kind, reach). The dispatcher aggregates those declarations into one registry; every outward face — admin HTTP, owner MCP, the API-key facade, the visitor agent's tools — is a projection of that one registry, never a hand-written gateway beside it.

check-routes-via-dispatcher holds the line: a face reaching a domain facade directly is red. It runs as a ratchet — a baseline of files still to migrate that may only shrink.

Why it matters: four hand-written gateways is how a product ends up with an op that exists on the admin panel and not on MCP. Parity stops being something you audit and becomes something you cannot violate (facade-parity, ui-mcp-parity).

Inbound: the desk

A sandboxed capability can reach back into the host (read corpus, write its own store, check quota). That reach-back converges on internal/routes/hostdesk, and two structural rules keep it there:

  1. Only the desk opens a capability socket. capsocket.ListenWith may appear in hostdesk (prod) and in agentcore (the eval mini-host that serves the same domain-declared ops over stdio). Anywhere else — above all the composition root — means a capability got a socket the desk's list does not describe, and "what can a sandbox ask the host for?" stops having an answer.
  2. Only a domain declares a host op. hostop.Op{ literals live in internal/<domain>/{ops,usecase}/ or in the two axes' own route packages. The composition root wires dependencies; it does not mint verbs.

The two are mirrors: the dispatcher answers "what can the host do for the outside", the desk answers "what can a sandbox ask the host for". Symmetry is the point — an inbound reach that skips the desk is exactly as invisible as an outbound face that skips the dispatcher.

The kernel holds no capability

check-core-agnostic is a string ratchet, and it exists because go-arch-lint has a blind spot: it tracks import arrows between packages, so it cannot see a kernel file that wrote booking logic in its own package — every arrow stays green.

The baseline is drained; the guard runs in pure-red mode, nothing grandfathered. The last entry is worth keeping as the canonical example: agent_instruction.go carried the word calendar, because the always-on datetime context told every visitor that "the owner's calendar runs in this timezone" and to confirm their zone before proposing times — a scheduling instruction delivered to visitors who had no booking tool at all. The kernel now states only facts (the time, its zone, the visitor's zone when known); how to convert, when to ask back, whether to show both zones is the booking capability's business and ships in its own MCP instructions. That is the rule in one line: the kernel states facts; capabilities give instructions.

The inward side seals and cannot unseal

A credential reaches half-way out: on the inward side the owner fills a form, it is sealed and stored, and that side has no way to open it at all. Opening happens on the outbound side, which hands plaintext to whoever spends it. Seal/open is not a kind of connector — it is the base mechanism both sit on.

check-core-seals-only guards it with two scopes, because the invariant has two escapes and only one is visible. Import-level checking catches an inward package importing the opener. It does not catch a struct field of type func([]byte) ([]byte, error) handed in from outside — that imports nothing illegal, and go-arch-lint sees an all-green graph. So the kernel is denied the vocabulary of unsealing, not merely the import.

One scheduler, and axes that declare in data

  • check-periodic-via-scheduler — anything that runs on a timer runs through the one scheduler. The hand-written version of the corpus reconcile loop was never registered anywhere: it ran continuously and did not appear on the Monitor's background-jobs panel. A timer nobody can enumerate is a process nobody can be told about.
  • check-axes-declare-in-data — both plugin axes declare their built-ins as data in their own top-level directory (backend/capabilities/<id>/manifest.yaml, backend/connectors/<id>/manifest.yaml); the composition root only assembles. They used to be Go literals inside cmd/server: a capability's identity, which host ops it orders, which field it occupies on an invite code, its config defaults — all written where the program is wired rather than where the capability is described. Adding one meant editing the assembly root, and the root grew a copy of every capability's shape.

The shared shape

All five rules are the same move: one place holds the fact, everything else is a projection of it, and a mechanical guard makes the second copy impossible rather than discouraged (mechanical-guardrails). The failure they prevent is not a bug — it is drift between two descriptions of the same thing, which review reliably fails to catch because each copy looks correct on its own.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

convergence-inbound-and-outbound