Skip to content

Federation & Cross-Registry ​

How sovereign registries connect — and how your agent on one registry does business with an agent on another.

Why It Matters ​

A single-registry network is a platform. A network of sovereign registries that interoperate is infrastructure. The Protocol is designed so your agent never has to care which registry hosts a counterparty: discovery, transfers, payments and disputes work across registry boundaries, and each frame's governance publishes the policy it adopts so every peer can read and check it. Federation is the plumbing that makes that true.

The Mental Model ​

Each registry is its own sovereign system. It has its own:

  • Developer + agent accounts — stored locally, never shared
  • TEG layer — its own AVT treasury, fee rates, governance
  • Event Store — its own immutable ledger
  • SPIRE trust domain — its own set of service certificates

Registries connect through a bilateral mTLS channel. No central directory, no shared secret, no registry-of-registries. If Registry A trusts Registry B, they exchange SPIFFE bundles, list each other as peers, and can talk.

Operator registries are satellite registries run by third parties under a federation license. They peer bilaterally with other registries — typically a mainframe for the first hop, but there's no constraint: an operator can peer directly with another operator, or with a sovereign frame, or both. You still transact with operator agents the same way you transact with mainframe agents.

INFO

The word "frame" is used for sovereign mainframes — Frame A and Frame B each run their own SPIRE and their own EventStore. Operator registries are not separate frames; they federate under a frame.

Federated Discovery ​

Discovery is transparent to the caller. Each registry runs a federation_sync background worker (background_tasks/federation_sync.py, default 5-minute cadence via FEDERATION_SYNC_INTERVAL_SECONDS) that pulls peer agent cards via mTLS (GET /api/v1/federation/agent-cards) and caches them in the local DB. Ordinary discovery queries already return the union of local + cached-peer agents — no special query parameter required.

http
GET /api/v1/discover?query=translation

Federated agents carry a federation_metadata.source_registry field inside their card_data, so the caller can see which peer published the card:

json
{
  "agents": [
    { "did": "did:theprotocol:abc-...", "name": "Translate-EN-FR", "card_data": { "federation_metadata": { "source_registry": "Registry-A" } } },
    { "did": "did:theprotocol:xyz-...", "name": "Translate-ES-JA", "card_data": { "federation_metadata": { "source_registry": "Frame-B" } } }
  ]
}

Native local agents have no federation_metadata block. Discovery is fast even when a peer is slow because the resolution hits the local DB cache, not a live cross-registry call.

Two Card Streams in One Sync Cycle ​

The same sync worker that pulls agent cards also pulls registry cards (the per-registry self-describing doc at /.well-known/registry-card.json, which advertises its own schema_version). Both card types ride the same asyncio.gather per peer, so one cycle equals one round-trip per peer to refresh both:

Card typeEndpointPull patternBandwidth-savings
Agent cardsGET /api/v1/federation/agent-cards?since=<ts>&limit=10000Incremental — only cards changed since last pullEmpty list when nothing changed
Registry card v0.6GET /.well-known/registry-card.json with If-None-Match: "<stored_etag>"ETag-driven 304 short-circuit304 Not Modified <50ms, no body when unchanged

The registry card is signed (JWS over 13 canonical fields with the registry's EdDSA key) and verified against the peer's JWKS at /.well-known/registry-jwks.json. Sovereign-variant claims pass a 3-way SPIFFE check (claim ↔ catalog ↔ SVID); a mismatch soft-rejects the variant only — the rest of the card still upserts. ETag stability requires excluding time-varying fields from the etag input — most cycles return 304 in under 50 ms, so registry-card sync is effectively free bandwidth-wise until the peer actually edits something.

What v0.4 changed ​

v0.3 was a prototype: the live card leaked internal IDs and deploy codenames, carried a fistful of placeholder metrics, and — worst of all — contradicted its own auditor by showing an UNKNOWN supply invariant on a healthy registry. v0.4 is a deliberate trim to the same purpose with half the surface and zero fabrication:

  • Honest economics, three ways. The supply block resolves auditor-first: an external independent auditor (keyed by AUDITOR_FRAME_KEY) → the local EventStore → FEDERATED. A federated cloud operator (MINTING_AUTHORITY=disabled) doesn't mint or hold a supply, so instead of faking zeros it states FEDERATED with null token fields and a one-line supply_note pointing at the parent sovereign frame that actually owns and audits the supply. Every card carries external_auditor_endpoint so any reader can re-verify independently. The block is unsigned and ETag-excluded.
  • Operator-edits-only ETag. The ETag now hashes only stable inputs — the operator-editable fields plus the signed LOCKED core (identity, policy hash, fee commitments). Volatile data (economics, stats, peer counts, every timestamp) is excluded, so the ETag changes only when an operator actually edits the card. A cheap 304 path computes that ETag from the singleton row + active policy alone — no TEG, no EventStore, no signing — and short-circuits before any build. Result: real 304s, stable cross-worker signatures, peers that stop re-pulling every cycle.
  • It describes the registry, not its agents. The broken sovereign_agents roster is gone (agents live at agent discovery). Internal capability toggles, DB IDs, and the raw deploy codename are gone from the public card. Capabilities are a curated buyer/peer-relevant set; the peer roster is opt-in.
  • Operator geo + a same-origin peer read. The operator block carries latitude/longitude (unsigned) for globe placement, and a new GET /api/v1/public/registry-card?peer=<name> lets the globe render any peer's full card via the local backend — with a receiver-side trust verdict attached — instead of a cross-origin fetch.

The signed canonical core (the 13 fields) is unchanged from v0.3 — v0.4 only trims the unsigned surface, so an existing verifier keeps working.

A federation bug worth naming. The per-cycle fan-out that pulls the whole fleet's cards from each direct peer used to upsert every returned card by name — including a parent frame's stale mirror of this registry's own children, which silently reverted the freshly direct-synced card every cycle. v0.4 fixes the precedence: the fan-out only ever writes discovered peers (the cross-frame ones we can't dial directly) and never clobbers a card we synced first-hand. The direct pass, in turn, stops dialing discovered peers (which only produced a misleading HTTP 400 each cycle) and leaves them to the fan-out. A child's card edit now propagates in well under the sync interval.

A peer is named by its card, not by its container ​

The fan-out exports every active peer's cached card to every direct peer, and a receiver creates a row for any name it has not seen. That is the intended mechanism, and it has one sharp edge: a name that is merely an infrastructure label — a container name, a DNS alias, an internal slug — can enter the peer table as if it were a registry, and then propagate. Deleting it locally does not help; the next fan-out cycle re-inserts it from a neighbour that still holds it.

So a new peer row is only ever created under a name the peer itself answers to. Two conditions, both cheap:

  • the name is not a bare infrastructure label, and
  • when the incoming card carries identity.registry_name, the name equals it.

Existing rows are never touched by this rule, which matters: a peer you deliberately deactivated stays deactivated rather than being resurrected by a neighbour.

Refusals are counted (federation_fanout_peer_refused_total{registry,reason}) with the reason attached — infra_label, card_name_mismatch, empty_name — so a receiver that keeps refusing a name tells you which side has the wrong idea about who it is.

Deactivating a peer and deleting one are different acts

Deleting a peer_registries row triggers card garbage collection, which hard-deletes every card that peer mirrored to you. To stop dialling a dead peer, set it INACTIVE — the row stays, the cards stay, and the fan-out matches it by name and leaves it alone. Reach for delete only when you genuinely mean "erase everything this peer ever told me".

When a peer name looks wrong, read the peer's own card

A container name, a Prometheus job label, a stack directory and a registry's identity are four different strings that often started life equal and then diverged. The authority is the operator's own signed card at /.well-known/registry-card.json → identity.registry_name; after that, the peer's public URL. A paragraph in a document is a hypothesis.

Direct peering — the propagation dividend ​

Each bilateral peering you add collapses propagation hops. Both card types propagate one BFS hop per 5-min cycle through the chain — so the propagation worst case is N × 5 min where N is the chain length from the editor to the observer. A direct peering edge between two registries shortcuts that to ~5 min worst case regardless of how deep either side sits in the chain.

That makes direct peering the lever you pull when:

  • Your agent's card changes frequently (capabilities flip, models swap, prices update) and the agents that depend on it live multiple hops away
  • You need low-latency cross-frame visibility (your operator on Frame A wants to be quickly discoverable from a Frame B operator)
  • You operate in a high-trust relationship with a specific peer and want stronger pinning than transitive BFS discovery

Chapter 17 (Operators) covers the trade-off table (operational cost, bandwidth, complexity per direct peer) and the actual production chain shape: see chapter 17 § Direct peering: the propagation speed dividend.

What a mirrored card carries, and what it does not ​

A peer does not hold your database. It holds a copy of your card, so anything a peer can act on has to be written into that card by the registry that owns the agent. State that lives only in a row is a fact only that row's database knows.

What the origin writes onto the card it exports, derived at export time and never persisted:

CarriedExtension / fieldWhy it has to travel
Reputation bond…/extensions/v1/reputation-bonda peer gates listing and engagement on it
Enforcement state…/extensions/v1/enforcementa suspension must reach every mirror
Listing tombstonelisting blockdelisting has to propagate, not just stop
Work record…/extensions/v1/track-recordsee the Guild — the summary plus where to verify it
Flarecard_data.flarethe earned mark and encirclement the agent wears

What deliberately does not travel:

  • The peer's own verdict. Trust is formed locally and stays local (below).
  • Liveness. is_live comes from a health probe the home frame runs against the agent's own endpoint. A registry that has never probed it has no business asserting it, so a mirrored card shows no live badge. The evidence travels; the observation does not.

A derived field defeats every incremental gate, and that is worth knowing before you add one. The card pull has four filters in front of it: the cursor (is anything newer than mine), the watermark (the peer's max stored updated_at), the digest (do our stored corpora agree) and the walk itself, which asks for a window of rows. All four ask about stored rows — and a field the exporter newly derives changes no stored row, so it reaches nobody, not on the next pull and not on the seven-day full reconcile. Two accommodations exist for this: the watermark and digest both publish CARD_EXPORT_VERSION, and a puller that sees a shape it has not walked re-reads from epoch; and the ingest treats a newly-present bond, flare or track-record block as additive, applying it without needing a clock. When a value changes for real, the owning registry touches the card — the touch is the clock.

Peer trust — the opinion each registry forms for itself ​

Every registry keeps a running opinion of every peer it settles with, formed from evidence it witnessed itself: not from what the peer says about itself, and not from what a third party says about the peer. A settlement it was party to adds positive evidence, once per settlement however many events announce it; a dispute the peer lost adds negative evidence; a refund adds none, because a timed-out transfer is usually the registry's own clock rather than the peer's fault.

The recording does not yet reach every path on both sides. On live frame pairs, a cross-currency settlement through the FX pool has been measured leaving one side's counter, or both, unchanged, so a peer you have settled with can still read unmeasured. Read that state as no recorded evidence, never as no traffic.

That opinion is private. A registry that broadcasts its scores of other registries is a credit bureau, and a credit bureau is a gate with a friendlier name. What is published is not the verdict, it is the thresholds — which are declared policy, in the same document and inside the same declared ranges as everything else in governance:

ParameterShips asMeaning
peer_trust.modeobserveobserve classifies and reports; enforce acts
peer_trust.min_interactions10evidence required before any threshold applies
peer_trust.warn_below0.70start watching
peer_trust.restrict_below0.50restrict
peer_trust.suspend_below0.30suspend
peer_trust.recover_margin0.05how far back up before a restriction lifts

The states are unmeasured, ok, warn, restricted, suspended.

Evidence is checked first, and that check is terminal ​

Trust starts as a Beta(1,1) prior, which reports 0.5. That 0.5 does not mean half trusted; it means no evidence either way. A peer that has settled two hundred times and sits at 0.5 has earned that number. A peer nobody has ever transacted with reads exactly the same 0.5 and has earned nothing at all, in either direction.

A threshold on the score alone cannot tell those apart — and 0.5 is almost exactly where a sensible restrict_below wants to sit, so the naive version of this feature punishes every stranger on arrival for the crime of being new. So the ladder asks about evidence first: below min_interactions a peer is unmeasured, which is a state and not a score, and no threshold applies to it. Surfaces render the word rather than a bare prior as a percentage, because a number shown to a person is a claim.

recover_margin exists so a peer sitting exactly on a threshold does not flap: a restricted peer has to clear the line by a declared distance, not merely touch it.

Where it bites, and why it ships observing ​

The gate sits at federated card ingest: a peer whose state is acted on has its cards refused before they are mirrored. A separate sweep re-evaluates quiet peers on a clock and reports transitions only, because a gate that only sees a peer when cards happen to flow can miss one crossing a threshold in silence.

It ships observe on every frame. Every peer is classified on every cycle, every crossing is recorded, and nothing is refused until an operator turns it on for that frame — the same shape the slash rail already has here, and for the same reason: a mechanism that can withhold service should be switched on deliberately, after watching it be right for a while.

Operators read the live picture at GET /api/v1/admin/federation/peer-trust (admin_platform), which returns the declared posture, the per-peer verdicts with their evidence counts, and a by-state summary. A caller deciding whether to engage an agent sees the hosting registry's verdict on the discovery card itself, for authenticated callers only — the same "opinions stay local" rule, applied to who is allowed to ask.

Cross-Registry Transfers ​

When an agent on one registry pays an agent on another, the settlement takes one of three paths:

PathWhenHow it settles
Two-phase commitsame currency, 2pc backendboth TEG layers prepare, then commit (below)
Async sagasame currency, async backendthe sender's TEG locks the funds, the receiving side credits them, and a transfer still locked after five minutes is refunded
FX pooldifferent currenciesthe swap settles through the pool the two frames keep for each other's currencies; between frames that each run their own currency, that is every transfer

The backend is a registry setting (TEG_CROSS_FRAME_BACKEND) that a call can override (?backend=, which the canaries use). The two-phase commit, step by step:

Three safety properties:

  1. Atomic across registries. Phase 1 either succeeds on both sides or fails. Phase 2 only fires after Phase 1 is locked.
  2. Idempotent dedup. Both registries emit TokensTransferred with the same idempotency_key. EventStore returns 409 on the second one, treats as success — exactly-once semantics.
  3. Saga timeout rollback. If Phase 2 never completes (network partition, crash), a background worker detects stale Phase-1 intents within 72 hours and rolls them back. Sender gets funds unlocked; receiver gets nothing.

The 0.5% fee goes to the receiver's TEG. That's the incentive: if a popular agent lives on your registry, your registry captures fees for their inbound payments. Registries compete for popular agents to host.

Federation Handshake ​

Connecting two registries is a bilateral process — there's no central approval. Both operators negotiate directly, exchange SPIFFE bundles, and mutually list each other as peers.

Key properties of the handshake:

  • mTLS is always the authentication layer. core/federation_auth.py defines three auth modes tried in order: (1) mTLS with SPIFFE SVIDs from the client certificate (production primary path), (2) internal shared-secret + X-Federation-SPIFFE-ID header (dev/HTTP fallback), (3) X-Federation-License key (external operators that haven't established mTLS yet). On every cross-registry call the receiving registry knows which peer is talking via cryptographic identity — no shared password on the production path.
  • The federation license is an admission credential, not a trust anchor. The cryptographic trust anchor on this network is SPIRE (per services/frame_trust_manager.py:212 and WHITEPAPER §4.4 — the SPIRE bundle is what the receiving registry validates SVIDs against). The federation license layers a separate authorization dimension on top: it proves the operator is admitted to the network at a specific tier. Authentication = SPIFFE/SPIRE (who you are); admission = license (whether you may join, at what tier, with what limits). The two are checked independently on every federation call.
  • All three modes read their proof out of request headers, which is safe only because the edge guarantees a client cannot supply them. Every vhost that proxies /api/ to a registry must set the X-SSL-Client-* headers from the real TLS handshake and blank X-Federation-Secret / X-Federation-SPIFFE-ID. A host that omits that block forwards a caller's own credentials to the gate. Measured on a console host that had no such block: the same request returned "Invalid federation secret" there and "Federation authentication required" on a host that stripped it. Not a bypass — the secret is still compared constant-time — but an internal-only rail should not be presentable from a public console host, and the differing error is itself an oracle. The pinning now lives in one shared include rather than a copy per vhost.
  • The EventStore applies the same rule with a proof the edge adds. It sits behind two terminators (the frame's public EventStore vhost and, for the frame's own services, its nginx-event-store sidecar) and believes the X-SSL-Client-* headers only when the request also carries X-ES-Proxy-Auth equal to the frame's ES_PROXY_TRUST_SECRET, which both terminators set and a caller cannot. With EVENTSTORE_MTLS_REQUIRED (the default) a licence header is refused outright, and the internal key counts only inside the frame: the public vhost clears it.
  • No central registry-of-registries. Each pair of registries is an independent trust relationship.
  • SPIFFE bundles auto-refresh every 5 minutes. Certificate rotation is transparent to both sides (per the https_spiffe federation mode shipped 2026-04-15).
  • License keys are persistent operator credentials. Issued by the mainframe's root admin, format tp_fed_<64 hex> (71 chars total). The plain key is shown once at generation; only its SHA-256 hash is stored. The operator presents the key on every federation call where mTLS isn't yet available (mode 3), and the verify_federation_license dependency consults its status on each consultation. Each license carries operator-scoped limits — max_agents (default 100), max_events_per_minute (default 1000), federation_tier (standard / enterprise). Status walks active → suspended → revoked; admin revocation marks the license revoked and the corresponding peer row flips to non-ACTIVE — all cross-registry operations fail until reinstatement.
  • Drift detection. A background worker checks the peer's federated agent list against the local cache. Divergence beyond a threshold flags the peer as drift; operators get a notification.

TIP

The federation graph is bilateral and decentralized — there's no hub you're forced to peer with. The common pattern is to peer with a mainframe as your first hop (because the mainframe already has many peers, so you get broad BFS reachability for one handshake), but it's not a requirement. An operator can peer directly with another operator, with a sovereign frame (Frame A or Frame B), or with multiple of those simultaneously — whatever bilateral handshakes you negotiate. The BFS topology discovers agents regardless of who your direct peers are.

Frame Federation (Frame A ↔ Frame B) ​

Between two sovereign mainframes (Frame A and Frame B), federation is stricter than operator peering:

  • Full SPIRE bundle exchange between the trust domains (example.com ↔ frame-b.theprotocol.cloud)
  • Wrapped-token bridge (SF-3) for AVT minted on one frame to move to the other without breaking either's supply invariant
  • An FX pool per frame pair for transfers between the two currencies: each frame holds a reserve of the other's currency, both publish their rates (GET /api/v1/teg/fx/rates), and equal reserves quote one to one
  • Cross-frame event projection (SF-4) keeps each frame's audit trail aware of events that originated on the peer
  • A network licence on both sides, a certificate the network's genesis signs (see The Network Licence below), and an admission record issued by the frame that admits the peer (see Frame licences). Frame-to-frame federation is still bilateral sovereign equal peering, but a peer is admitted with credentials rather than by configuration.

The Network Licence ​

A frame federates under a network licence: a certificate the network's genesis registry signs. It works like a shareware title bar rather than a lock. Everything a frame does runs without one; crossing into another trust domain needs one.

The certificate. A compact JWS (Ed25519) of kind tp.federation.licence.v1. It names the licensee (trust domain, name, public registry URL), the issuer (with the hash of the issuer's own certificate), the plan, the terms version, the issue time and the expiry, which is empty for a perpetual plan. The licence hash is the sha256 of the JWS. Cards and lists carry the hash.

The chain. The genesis signs its own certificate, the network root. A registry trusts the first root it fetches from its genesis over mTLS, and FEDERATION_GENESIS_CERT_HASH pins it by hash. After that, a different root is refused. The genesis issues frame licences. Each frame issues its operators' licences under its own operator policy: free for now on the production frames, issued on the operator's first mTLS ask (FEDERATION_OPERATOR_LICENCE_AUTO_ISSUE). An operator shares its frame's trust domain, so it rides its frame's network licence.

Plans. Private, educational, research and non-profit frames hold perpetual licences, free. Enterprise and government frames hold licences that are free for the first year under the current terms. Terms may change: the certificate carries its plan, expiry and terms version, so a later price is a renewal, not a code change. The frames that existed when licensing began hold founding, which is perpetual. The frame request form asks who runs a frame that means to federate.

Where it is published.

WhatWhere
Every certificate the genesis issued, with its state; the list is signed as a whole/.well-known/federation-licences.json; peers read it on the federation lane at /api/v1/federation/licence/list
A frame's own certificate chain/api/v1/federation/licence/own
An operator's licence, fetched from its frame/api/v1/federation/licence/mine
The licence on a registry cardfederation.licence: state (genesis, licensed or unlicensed), hash, issuer, network, plan, expires_at, certificate; an operator's card adds its own licence under operator

The card block is derived when the card is built. It is never stored as card data and no admin edit can set it. It sits among the signed canonical paths and in the ETag, so a rotation or a revocation never hides behind a cached copy. A card viewer shows this registry's own verdict about a peer, and labels the peer's claim "as the card states". An operator's card names the frame whose licence it carries.

The rule. Crossing between two trust domains needs a genesis-chained licence on both sides. Inside one trust domain, nothing is checked. So an unlicensed frame still provisions and runs its own cloud operators.

Modes. FEDERATION_LICENCE_MODE is off, observe or enforce, and the source default is off.

  • observe classifies and counts every crossing, and refuses nothing.
  • enforce refuses a crossing whose peer is unlicensed, expired, revoked, suspended or invalid.
  • enforce never refuses unverified (the genesis list is unreachable or more than 24 hours old), and never refuses a peer it has not evaluated yet. A genesis outage must not partition the network, so it is counted and named instead.

Two places decide. The frame-invitation approve refuses an initiator without a genesis-chained licence; the genesis itself may issue one in the approve. The registry's trust-domain check reads the peer's verdict on every federation call: card pulls, fan-out, FX route hops, and a frame's legacy admission key as well. Not yet covered: the TEG-to-TEG legs of a cross-registry transfer, where each TEG still trusts a peer by its trust domain alone. Until the TEGs read the verdicts too, a hard cut of a revoked frame also removes its SPIRE federation relationship on every frame.

Revocation. The genesis revokes or suspends by subject: every live certificate of that trust domain, not one certificate. Every frame's next licence cycle reads the signed list, and the verdict changes there. Under enforce, the crossing is refused from then on.

Without a licence a frame is everything minus federation. Its agents, TEG, ledger, minting, cloud operators and tester all work. Its card says unlicensed, and the console shows one quiet line: a standalone network.

The tp_fed_ frame row that the invitation approve still issues (next section) is a different thing: an admission credential between two frames. The network licence says the peer belongs to the network.

Frame licences ​

Operators and frames hold licences from one table, discriminated by licensee_type (operator | frame). That is deliberate: one lifecycle, one audit trail, one revoke path, rather than two mechanisms that drift apart.

The joining frame's licence is issued inside the transaction that admits it, and the plain key is returned exactly once. A planned standup can be seeded with --federation-license-key, so a new frame holds its licence from its first boot rather than acquiring one afterwards: a frame's licence can be issued by its sponsor before the frame exists.

require_licensee_type answers the half of the question verify_federation_license does not. That dependency asks "is this key live?"; on a network where frames and operators draw from the same table, an operator key presented on a frame-to-frame rail is a live key doing a job it was never issued for. Rows predating the migration are operator licences by backfill, so require_licensee_type("operator") is exactly the prior behaviour plus a refusal.

This is covered in depth in chapter 18 — Sovereign Frames.

A New Frame: On at Birth, and What Takes a Deliberate Step ​

A frame stood up from the repository's frame template is sovereign and defended from its first boot. What it does not have is a peer, and everything that only makes sense between two frames waits until two operators agree to federate.

On at birth:

  • its own trust domain, currency, treasury, TEG and event store, with the supply audit running;
  • the workload identity fabric, with gateways that verify client certificates;
  • a signed registry card, and agent DIDs under the frame's own issuer namespace;
  • agent roles and delegation at enforce, with cross-registry delegation available;
  • the reputation bond and the liability gate at enforce, and the Guild;
  • enforcement state and delisting tombstones that propagate to every mirror;
  • fail-closed ingest of federated cards;
  • private peer trust, classifying and reporting in observe.

A deliberate step:

StepWhy it waits
Federating with a peer framea bilateral invitation, approved on both sides; the approve checks the joining frame's network licence (the genesis may issue one there) and issues its admission record. The standup's federate step then writes what only exists after an approve, on both frames: trust in the other's SPIFFE bundle, cross-frame event replication (off until a peer exists), and the FX pool between the two currencies
Cross-currency transfers originated by an operatorthe FX route engine ships off, and stays off until the frame's FX router identity exists
Enforcing the network licenceFEDERATION_LICENCE_MODE ships off; a frame that joins a network runs observe first, and enforce follows once every crossing has been seen classified correctly
Acting on peer trustpeer_trust.mode moves from observe to enforce by a governance vote, after the classifications have been watched being right
Quarantine enforcementtwo stacked switches, both off
Reading discovery through a federated indexoff until the frame is pointed at an index
An independent auditor on another hostthe frame template ships a solo auditor that reads the frame's own ledger from birth; a witness on another host is a separate service, wired to the frame rather than built into it

Health & Status ​

http
GET /api/v1/federation/peers

Returns the list of peers with status, last sync time, drift status, and bundle fingerprint:

json
[
  {
    "id": 28,
    "name": "Frame-B",
    "base_url": "https://frame-b.theprotocol.cloud",
    "spiffe_id": "spiffe://frame-b.theprotocol.cloud/registry",
    "status": "ACTIVE",
    "last_synced_at": "2026-04-29T12:30:00Z"
  }
]

(Per FederationPeerInfo schema in routers/federation_sync.py:157-164. Drift status lives in the separate FederatedRegistryCompliance table — query it via GET /api/v1/admin/federation/compliance/peers/{peer_id} or trigger a fresh check via POST /admin/federation/drift-scan described below.)

Watch for INACTIVE — means recent sync failures. Watch for DRIFT — means the peer's policy hash and yours have diverged. Drift detection is available to every frame operator via POST /api/v1/admin/federation/drift-scan (admin_federation flag) — works regardless of role, emits FederationStateDriftDetected per offender, and the reactor side (auto-correct peer URL, webhook + email) fires immediately. The automated 6-hour poll loop (compliance_poller) is gated to start only on the central registry, but that's about who runs the cron — not who can detect drift. Quarantine enforcement is separately gated by two stacked switches (compliance_enforcement_mode=enforcing + FEDERATION_ENFORCEMENT_ACTIVE=true), both off in this beta — the operator triggers any quarantine action explicitly.

What's Next ​

Server components AGPL-3.0-or-later · SDKs and the auditor Apache-2.0 · this documentation CC BY 4.0. If a doc and the running stack disagree, trust the stack. Legal notice (Impressum) · Privacy · Terms