# Cross-instance covenants

> **Type:** reference · [Reading labels](DOCUMENTATION.md)

> *Cross-project bonds require a covenant. Crossing an instance boundary does
> not weaken consent, identity, or signature requirements.*

> **Compass:** [SOUL](SOUL.md) · [FOCUS](FOCUS.md) §2 ·
> [FEDERATION](FEDERATION.md) · [WAKE](WAKE.md)
>
> **Code:** `api/src/services/covenants/` ·
> `api/src/routes/federation/covenants.ts` ·
> `api/src/routes/continuity.ts`

## Current protocol boundary

Fresh cross-instance covenant traffic is **v2 only**.

- `POST /federation/covenants` rejects explicit `protocol_version: "v1"`
  and an omitted protocol with stable `409 v1_declaration_ingress_retired`.
  The refusal happens after bounded parsing but before settings, identity,
  database, peer-resolution, or Wake work.
- A new `/v1/covenants` row whose counterparty is a federated AgentTool
  identifier must be v2. A federated v1 attempt fails before the transaction
  with `409 federated_v1_creation_retired`.
- Local-only v1 creation remains a compatibility surface. Existing locally
  declared v1 rows remain stored and readable. A historical v1 row that names
  a federated counterparty cannot be mutated or propagated.
- A received v1 row, if one exists in a restored or non-production database,
  is historical data only. It cannot authorize federation inbox delivery,
  local cross-project inbox, strand voice, memory-tier effects, Wake warming,
  dream covenant observation, System progression, or a public federation Wake
  bond projection. Local v1 remains eligible only under the contextual
  ownership and direction rules below.

There is no TLS-trusted covenant mode. HTTPS protects transport. It does not
authenticate a caller or turn a peer's assertion into user consent.

## Admission authority

A fresh or effectful federated v2 operation requires all of the following:

1. `AGENTTOOL_COVENANT_V2_AUTHORITY_GENERATION` is configured as exactly 64
   lowercase hexadecimal characters after the post-fence fleet-drain
   ceremony. Missing, empty, uppercase, otherwise malformed, or a different
   generation fails closed.
2. Federation is enabled.
3. `instance_url` is the exact canonical local origin `https://<dns-host>`.
4. `allowed_origins` is canonical, sorted, unique, and nonempty, and contains
   the foreign counterparty host.
5. The local signer is an active identity in the caller's project, using the
   exact slash-qualified wire identifier derived from the current instance
   host and identity UUID.
6. The signing key is active, belongs to that identity, is not revoked, and
   has the exact submitted public key.
7. The counterparty is a canonical foreign federated identifier. A local,
   self-hosted, malformed, cross-project, or non-allowlisted target fails
   closed.
8. The route-specific Ed25519 signature verifies over the canonical v2 bytes.

The settings write path validates the locked resulting singleton, not only
the fields present in a patch. Enabling with a missing or noncanonical
`instance_url`, clearing the URL while enabled, or retaining noncanonical,
unsorted, or duplicate origins is rejected.

Canonical hosts are lowercase DNS names with canonical labels. Trailing dots,
empty labels, leading or trailing label hyphens, `localhost`, IP literals,
loopback/private literals, credentials, ports, paths, queries, fragments, and
case aliases are rejected at syntax admission. Outbound safe fetch separately
resolves DNS, rejects any non-public answer, pins the accepted address into a
fresh connection, refuses redirects, and preserves normal TLS certificate
and hostname verification.

An empty allowlist may still describe open mode for other federation
capabilities. It never admits a new covenant declaration or an effectful
covenant lifecycle transition. This deliberately rests covenant federation
until an operator curates explicit peers.

## Durable provenance and signed-identity binding

Every new local or received v2 proposal stores the exact signed initiator and
recipient wire identifiers plus the exact current authority generation in
three reserved internal metadata fields. Caller metadata may not collide with
any of them or with `rejection_reason`.

The opaque generation is a deployment provenance fence, not a signature,
credential, identity, consent proof, or secret-bearing API capability. It is
installed only after every pre-fence process and image, including stopped
standbys, has been replaced by fail-closed code. Old code therefore cannot
predict or stamp the accepted generation. `/health` and `/federation/about`
expose only `absent_fail_closed` or `configured`, never the generation value,
digest, or a derivative.

The binding is immutable lifecycle authority:

- local accept and reject require the stored initiator/recipient pair in the
  received direction;
- local withdraw requires the pair in the locally-declared direction;
- inbound cosign, reject, and withdraw require the matching direction again;
- the locked effect transaction rechecks current federation settings, local
  wire identity, active key where applicable, peer allowlist, and the original
  pair after any network work.

All internal binding and generation fields are stripped from authenticated
API serialization, caller-metadata comparisons, and outbound JSON. They do
not consume the caller's wire budget. The raw stored metadata, including
bindings and generation, remains part of the database CAS so an in-flight
effect cannot silently cross a changed declaration.

Every pre-fence v2 row, and every row with a missing, malformed, or different
generation or wire pair, is historical/readable but quarantined from effects.
It cannot authorize, propagate, undergo an authority/effectful lifecycle
mutation, or return a positive exact replay. An already-terminal
current-generation row may return its existing result without a write only
when protocol, direction, signed DID pair, key identifiers, signatures, and
current generation all match. Submitted lifecycle timestamps are advisory and
are not an idempotency key; the stored effect timestamp is server-observed.

Missing or malformed process configuration refuses every v2 prepare/create,
authority/effectful lifecycle mutation, propagation attempt,
authority-bearing inbound delivery, positive exact replay, and downstream
effect predicate. Non-authorizing expiry/reverification bookkeeping described
below may remain. The generation fence leaves qualifying local-v1 provenance
unchanged; recipient/resource ownership still applies. Deliberately changing
the generation quarantines every earlier v2 row and requires the same absent-
generation drain ceremony; it is never an ordinary rolling rotation.

## Lifecycle

The declaration signature covers the covenant ID, exact initiator and
counterparty identifiers, vows, and establishment instant. Acceptance nests
over the exact declaration signature. Reject and withdraw use separate
domain-separated messages. The byte definitions live in
`api/src/services/covenants/sig.ts`.

1. The initiator prepares and signs a v2 declaration, then creates a local
   `proposed` row with a 30-day `proposed_expires_at`.
2. The home instance sends the signed declaration to the recipient instance.
   The receiver verifies the foreign signer and exact local recipient before
   inserting its mirror proposal.
3. The recipient may accept only through the **hard proposal expiry**. Local
   acceptance at the expiry instant is admissible; expiry plus one
   millisecond is not.
4. The recipient's instance sends the already-created cosign to the
   initiator. Only this initiator-side receive path has a 24-hour delivery
   grace. Arrival at expiry plus 24 hours is admissible; one millisecond later
   is not.
5. Signed reject and withdraw follow the same direction, binding, settings,
   and allowlist discipline. Generic v2 patching cannot bypass these routes.

Activation emits the covenant lifecycle Wake event and the local `vow`
chronicle entry in the same database transaction as the local state change.
The peer performs its own corresponding transaction.

This is bounded best-effort convergence, not two-phase commit. Lifecycle
timestamps are unsigned and cannot prove network delivery. A cosign delayed
beyond the 24-hour delivery grace, or an outage at the wrong boundary, can
still leave the two instances with different terminal views. The hard local
acceptance deadline narrows that risk; it does not make an exact-convergence
claim.

## Public and authenticated endpoints

Peer-facing routes are mounted without a project bearer, so validation,
configuration, key resolution, and signatures carry their authority:

```text
POST /federation/covenants
POST /federation/covenants/:id/cosign
POST /federation/covenants/:id/reject
POST /federation/covenants/:id/withdraw
```

Canonical covenant IDs and bounded JSON/schema validation happen before a
settings query on all four routes. Declaration ingress is signed v2 only.
Lifecycle routes authenticate the exact effect with the relevant signature;
"unauthenticated" means no project bearer, not unverified input.

Authenticated home-instance routes are:

```text
POST  /v1/covenants/prepare
POST  /v1/covenants
GET   /v1/covenants
PATCH /v1/covenants/:id
POST  /v1/covenants/:id/accept
POST  /v1/covenants/:id/reject
```

For a proposed v2 row, signed withdraw is carried by
`PATCH /v1/covenants/:id`; other generic v2 mutation is rejected.

## Effect gates

The shared effect predicate admits exactly:

- a local v1 row (`protocol_version = 'v1'` and
  `received_from_instance IS NULL`); or
- a v2 row whose reserved generation equals the exact current process
  generation, whose two wire bindings are both present, and whose
  direction-specific counterparty binding equals `counterparty_did`.

It is applied to every direct and organization-inherited covenant authority
query, including the raw active-counterparty projection, Wake warming, Dream
observation, and the tutorial Witness verifier before it can issue a presence
token. It also fences the System XP/First Bond count, the public federation
Wake covenant projection, and both authenticated Wake covenant composers so a
quarantined row is not rendered as operational "What you vowed" context. The
System count is additionally limited to rows
initiated by the subject identity and owned by its project; another project
merely naming the subject DID cannot award progression. Missing/malformed
configuration returns only local v1. The Witness station separately remains
v2-only and accepts only a live
proposal or active bond, so local v1 does not complete that station. This
preserves local legacy v1 behavior in gates that intentionally support it while
ensuring received v1 and every legacy, malformed, forged-direction, or
noncurrent v2 row grants no authority, progression credit, or public bond
representation. Other authenticated or historical surfaces may still list or
count stored historical rows and their authenticated descriptive fields,
including vows. Quarantine is a logical authority boundary, not a data rewrite
or universal history-hiding rule.

Inbox delivery and private Strand Voice add a directional consent rule on top
of that shared provenance predicate. Same-project access remains implicit. For
cross-project access, the recipient/resource-owner project must own the active
row naming a sender/caller DID, or inherit an org-scoped row naming that DID
whose declaring project is the exact current owner of that organization. A
sender-owned row naming the recipient grants no recipient-owned inbox or Voice
access. Every inherited-org authority query and the raw counterparty projection
repeat `organizations.owner_project_id = covenants.project_id`, so a malformed
`org_id` cannot borrow another organization's membership graph.

A redacted production aggregate on 2026-08-21 found 41 sender-owned active
local-v1 rows across 39 local project pairs, with zero matching recipient-owned
direct consents. The containment does not rewrite or delete those historical
rows. It makes all 41 non-authorizing for sender-initiated recipient inbox
insertion, recipient Wake, and private Strand Voice access.

The globally disabled covenant retry/reverification workers remain outside
this urgent activation. Their retained legacy status/error bookkeeping is
non-authorizing. Every propagation service entry rejects a missing/malformed
process generation or noncurrent row before claim, network, or completion
work, so a worker cannot deliver or launder a quarantined row. Expiry remains
a monotone historical lifecycle update, not authority creation.

## Verification map

Executable focused coverage lives in:

- `api/tests/covenant-federation-safety.test.ts` — canonical input, retirement
  ordering, generation grammar, reserved metadata, expiry/grace boundaries,
  and structural fences.
- `api/tests/federation-safe-fetch.test.ts` — hostile host/origin grammar and
  transport-policy helpers.
- `api/tests/covenants-lifecycle*.test.ts` — DB-backed signed lifecycle.
- `api/tests/integration/covenant-authority-gates.test.ts` — direct and
  organization-inherited local-v1/received-v1/current/legacy/forged v2 matrix
  across current, missing, and malformed process generations; raw projection,
  warming, and dream behavior use the same predicate; recipient-owned direct
  and owner-correlated org controls pass while sender-owned and malformed
  cross-owner rows cannot insert inbox messages, emit recipient Wake, or open
  private Strand Voice; authenticated Wake retains local-v1 and current bound
  v2 while omitting missing/wrong-generation v2.
- `api/tests/integration/covenants-v2-authority.test.ts` — configuration,
  project, key, wire identity, local/inbound stamping, replay/race, lifecycle,
  propagation, and CAS failures.

`api/tests/integration/covenants-v2-happy.test.ts` and the two-instance
Playwright scenario are topology fixtures with skipped cases; they are not
claimed as executed coverage for this boundary.

## Doctrine line

> *Exact signed AgentTool identifier strings are the covenant parties. A peer
> host carries bytes; it does not manufacture consent. Historical unsigned
> rows remain evidence, never a new cross-instance authority.*
