Moving a service onto its contract
ADR 0027 and 0028; the design is superpowers/specs/2026-10-21-api-contracts-design.md. status is the
reference (status/apiv1, status/internal/api/api.go handlerWith, contract_test.go): copy its shape.
1. The contract module <svc>/apiv1
- Its own module:
apiv1/go.mod is module go.xvii.network/<svc>/apiv1, requiring only kit. The service's
go.mod requires go.xvii.network/<svc>/apiv1 v0.0.0 with replace go.xvii.network/<svc>/apiv1 => ./apiv1.
types.go: the wire types, field for field what the service answers today (the views, not the store
rows: can_edit, url and the like included), with doc comments on types and fields people need to
read (units, allowed values). Inputs are XFields structs of pointers for what a person sets.
ops.go: one xapi.Op[In, Out] per route, in the route table's order, with ID (snake_case; keep the
names of existing MCP tools), Pattern, Summary (≤ 80 characters, no full stop), SummaryRu for anything
low risk and up, Scope, Risk (none for reads, low for reversible, medium for what others use, high for
access and moves between teams, critical for irreversible or secrets), Resource, Audit (the action it
journals, or "-"), Status (201, 204), Access (Public, Service with Services, Person), Superuser.
In carries path:"…" and query:"…" fields and a Body of the JSON it reads; xapi.None and
xapi.Empty for nothing.
- A route other services call with their client credentials lists them in
Services; one only they
call is Access: xapi.Service.
- Uploads, downloads and streams are
Internal: true until kit/api has their helpers; what isn't an API
operation (RSS, mail links, /healthz, ingest) stays on the mux.
CLI: &xapi.CLI{Name, Args, Columns, Examples} on the operations people run from a terminal: the
lists (with Columns), get, create, change, delete and the actions. Names are <things> <verb> with
the verbs ls, get, add, set, rm and the action's own (monitors check, issues triage,
plans run); a service with one kind of thing may use bare verbs. A hand-written command of the same
name wins (xcli.Merge); one named like a namespace is what the namespace runs by itself. MCP: xapi.MCP{Pin: true} on the
two or three daily ones; Expose: xapi.ExposeOff on public operations that aren't for agents.
contract_test.go: func TestContract(t *testing.T) { schema.Test(t, Contract) }; generate the document
with go test -run TestContract . -update and commit openapi.json (embedded as Contract.Document).
2. The router
handlerWith builds xapi.NewRouter(mux, apiv1.Contract, xapi.Config[*caller]{Guard, Caller}) in place of
the route loop and binds every operation. Handlers keep their shape through xapi.HandleRaw (status'
raw(op).bind); turning one into a typed xapi.Handle that maps store rows to wire types is later work,
operation by operation, with no change on the wire.
- Checks the loop made that aren't access and scope (a token narrowed to a project, say) are a middleware
keyed by operation id (
rt.Use).
POST /mcp behind the guard serves rt.MCP(version); rt.Platform(xapi.Meta{Version, Capabilities: []string{"mcp"}}) is last. Hand-written mcp.go tools go: the contract's tools replace them.
2b. Limits
Every path that creates or grows something a limit counts checks it (ADR 0013): the create operation and
also imports, templates, previews, transfers in, links redeemed later, things other services or background
loops make, and bytes that grow content. Count and add in one transaction after
limits.LockTeam(ctx, tx, team, key), so creations at once can't pass together; a service that keeps the
thing in memory holds a mutex across count and add instead. A test proves each limit refuses.
3. Tests
- The service's tests run as they did (
rt.Check panics in tests when what is bound and the contract
differ).
internal/api/contract_test.go: strict[apiv1.T] decodes the answers of the main operations with
unknown fields refused, so the document can't drift from what is served; and a /mcp test lists tools
and calls one.
4. The CLI
cli/cmd/xvii/contracts.go adds the service's namespace from xcli.FromContract(<svc>v1.Contract), next
to its hand-written commands (deploy --dir, logs -f, listen stay hand-written). xvii api <svc>
reaches every operation from the instance's document with no CLI release.
5. Release
- The service: a minor tag. The contract:
apiv1/vX.Y.Z tags (v0.x until a native app depends on it);
a change to an operation or a wire type is a new contract version, regenerated and tagged.