ADR 0032 — the service base API: a uniform …z + identity surface every member serves
- Status: accepted
- Date: 2026-06-21
- Builds on: 0011 (collect-never-invent — the runtime probe that reads this surface and grades the gaps as findings), 0026 (the runtime registry that says which
(system, environment)to probe; pharos attaches the environment a report must not self-claim), 0027 (/versionzis its SUBSTRATE_MIGRATION §4 self-report + the Gate-C observed-epoch source) - Relates: 0028 (
/identityadvertises the service public key the single-root model grants authority TO), 0030 (a provider authenticates the runtime —/identityis the discovery seam for the keys involved), 0016 (/identityis lifted from arche-facts, which stele's node mirrors)
Every constellation member that serves HTTP serves a uniform base API. Two endpoints are universal —
/healthz(liveness: the process answers) and/versionz(the 0027 self-report, schemaatlas-versionz.schema.json). Two more are required of every substrate participant (a node or a fact-writing client) —/readyz(readiness: admitted/serving, distinct from liveness) and/identity({"identity":"<base64 Ed25519 public key>"}, the discovery seam a peer or the root hits to learn the key the service signs facts with). The control plane (pharos) serves/healthz+/versionz+/readyzbut not/identity— it holds no substrate signing identity (it verifies others', never asserts as one). Atlas owns the schemas + the conformance probe; each service owns the report it serves; pharos/atlas consume. This is one surface, not four ad-hoc ones — so discovery, readiness gating, version grounding, and key lookup are the same call everywhere.
Context
The pieces already existed, unevenly. /versionz is fully specified — atlas-versionz.schema.json
- the
atlas substrate versionzvalidator + pharos's collector probing it fleet-wide and enforcing that a service cannot claim to be another (ADR 0027 §4)./healthzand/readyzare probed by the atlas runtime collector (ADR 0026/0011:runtime/unreachable,runtime/endpoint-not-admitted)./identityis the stele node's discovery endpoint, itself lifted from arche-facts:GET /identity → {"identity":"<base64 node pubkey>"}.
But adoption was a patchwork. A survey of the running fleet:
| service | archetype | /healthz |
/versionz |
/readyz |
/identity |
|---|---|---|---|---|---|
| stele | substrate node | ✓ | ✓ | ✓ | ✓ (reference) |
| corpus | substrate client (provider) | ✓ | ✓ | — | — |
| kosmos | substrate runtime (adstrate) | ✓ | — | — | ✓ |
| pharos | control plane | ✓ | ✓ | — | — (n/a) |
The gap that surfaced this: to repoint corpus/production at a new stele node, the root must
stele grant corpus's identity — but corpus exposes no way to learn its own public key (it is
derived from a sealed seed). The one-off fix would be "add /identity to corpus." The right fix is
to notice this is the same discovery seam every substrate participant needs, and that the whole base
surface should be a named, conformance-checked standard rather than whatever each repo happened to
ship. It spans every member and has no single-repo owner — so its seat is atlas (the membership test).
Decision
1. The universal pair (every HTTP-serving member).
GET /healthz— liveness only: the process is up and answering. Unauthenticated, never gated by any lock/readiness. Bodyok. It says nothing about whether the service can do useful work.GET /versionz— the service self-report, conforming toatlas-versionz.schema.json(required:service,revision; the service reports neither its environment nor a claim to be another member — pharos attaches the(system, environment)it probed). This is the 0027 lifecycle inventory / format-capability surface; it is what makes Gate C observed rather than derived.
2. The substrate-participant pair (a node, or a client that writes facts under an identity).
GET /readyz— readiness, distinct from liveness:200bodyreadywhen the service can serve its purpose;503while it cannot yet. For a node, "ready" = its endpoint self-assert was admitted (the root granted the node identity); a service with no pending-admission state is ready as soon as it serves (the nil-readiness path). Unauthenticated; reports503rather than erroring while not-ready, so a prober can distinguish "down" from "up-but-not-ready."GET /identity—{"identity":"<base64-std Ed25519 public key>"}, the service's substrate public key: the key it signs facts/bindings with, the thing the root grants authority to and a peer authenticates. Public — safe to serve unauthenticated (it is the published half). Mounted only when the service has an identity: a service running without one (a dev/in-memory mode) does not mount the route (404), exactly as stele gates it on having a key. Never serves the private half.
3. The control plane (pharos) is exempt from /identity. pharos actuates and verifies other
services' identities; it holds no substrate signing identity of its own, so it has nothing to
advertise. It serves /healthz + /versionz + /readyz (readiness = its data plane is up: the
encrypted vault is unlocked and the store is attached), and /readyz is exempt from the locked gate
so it can truthfully report 503 while locked.
4. Ownership & conformance. Atlas owns the schemas (atlas-versionz.schema.json,
atlas-identity.schema.json) and the conformance probe: the runtime collector reads this surface
for every registered (system, environment) and grades a missing/malformed endpoint as a graded
finding (ADR 0011 collect-never-invent; a live-ops lapse is a warning, never a committed-baseline
lie). Each service owns the report it serves; pharos and atlas consume it. Archetype is read
from the member's place in the model (a runtime env carrying a rootPubkey/identity is a substrate
participant; the control plane is named).
Consequences
- One surface, four guarantees, everywhere. Discovery (
/identity), readiness gating (/readyz), version grounding (/versionz), and liveness (/healthz) are the same call against any member. Tooling (pharos's collector, atlas's probe, a human withcurl) stops special-casing. - It unblocks cross-service grants by construction. corpus gaining
/identityis not a one-off — it is corpus conforming. The root learns any participant's grantee key from one endpoint, sostele grant <pubkey>no longer needs the sealed seed extracted; the discovery seam is the standard. - Conformance becomes a doctor gate. A member that ships without
/versionzor (as a participant)/identity//readyzshows up as a finding, the same way a stale figure or an unreachable node does. - Cost. Each service maintains four endpoints and keeps its
/versionzschema-valid; the probe is one more thing the collector does per env. The archetype rule is a heuristic over the model, not a per-service declaration — if a member ever needs a non-obvious archetype, that becomes an explicit field (recorded open, below).
Open questions (recorded, not decided)
- Archetype as data. Today archetype (participant vs control-plane) is inferred from the model (identity-bearing runtime env vs the named control plane). If a third archetype appears, or the inference misfires, promote it to an explicit manifest field rather than widening the heuristic.
/readyzdepth. "Ready" is currently shallow (admitted / data-plane-up). Whether it should also assert downstream dependency health (a node's DB, a provider's substrate link) is deferred — start shallow, deepen per service if a real outage would have been caught by it.- One more endpoint? A
/metricsz(or aligning on OpenMetrics) is plausible but out of scope here; this ADR fixes the identity/lifecycle/health quartet only.