Federation¶
Federation lets multiple kata daemons share selected projects while each user keeps a local daemon and local database. It is opt-in per project.
For a new relay connection, start with Share projects between hubs. It explains the roles and walks through preview, connection and recovery. Project relays require Kata 0.19.0 or later. Direct-spoke enrollment remains the bootstrap path for hubs using daemon-owner credentials outside identity mode.
Use this page for operator details:
| Task | Start here |
|---|---|
| Connect with an ordinary hub account | Relay setup guide |
| Use the bridge HTTP API | Connect through the daemon catalog |
| Restrict a project to teams | Team visibility |
| Choose credentials | Token boundaries |
| Set up a direct spoke with hub-owner credentials | Worked example |
| Understand storage growth and recovery limits | Delivery retention and checkpoint capacity |
| Reuse vectors across replicas | Sharing embedding vectors |
| Recover, disconnect or rotate keys | Reset recovery, disconnect, and root signing key |
| Require signed HTTP requests | Federation request signing |
Use a shared daemon when clients need one central copy and fresh online reads. Federation instead favors local availability and durable offline queues. Each participant may use SQLite or PostgreSQL independently; no backend-specific federation flags are needed.
Workspace aliases and per-issue recurrence schedules stay local. Issues created by a recurrence synchronize as ordinary content. Native cron job and workflow definitions and run evidence do synchronize; only the owning hub delivers their recurring child issues. Local catalog, scheduling and claim audit records remain on their source daemon.
Roles¶
Roles apply to a project, not to an entire daemon.
| Term | Meaning |
|---|---|
| Root hub | The project's authority for accepted history, signed receipts, leases and purge/reset decisions. |
| Hub | The immediate upstream daemon. It may be the root or a relay. |
| Spoke | A daemon holding a local replica connected to an upstream hub. |
| Relay | A spoke that also serves the selected project to downstream daemons. |
| Leaf | A spoke that does not serve the project onward. |
| Bridge | The configured connection from one local project to its upstream hub. |
| Binding | The local record of a project's federation role and synchronization state. |
| Enrollment | The upstream grant for a particular spoke, project and set of transport capabilities. |
| Origin instance UID | The durable identity of the daemon that wrote an event. It stays with the event across relay hops. |
| Cursor | A position in a delivery stream. Direct spokes use event IDs; relays keep separate positions for each hop, stream and epoch. |
| Root receipt | Signed proof that the root accepted an event and its accountable actor. A hop acknowledgement alone is not this proof. |
| Checkpoint | A signed recovery snapshot and proofs that replace history no longer available for replay. |
| Epoch | A numbered generation of one relay connection's delivery streams, changed during reset. |
| Replay horizon | The event boundary before which a direct spoke needs baseline snapshots instead of ordinary replay. |
| Lease | An optional reservation for coordinating edits to an existing issue. The API capability and stored events use the name claim. |
| Quarantine | A recorded batch rejection requiring operator recovery. A relay quarantine stops automatic sync for that project. |
Moving issues between projects¶
Cross-project issue moves involving a federated project are unsupported on hubs and spokes. Federation replay cannot apply the move event. Disabling sync retains the federation binding and replica state, so it does not enable moves.
kata move <issue-ref> <project> --dry validates this restriction through the
daemon and returns the same federated_move_unsupported conflict as a real move.
The preview does not change issues or refresh claims. It validates the current
state without reserving a future move. See the CLI reference
for the other move restrictions.
Connect a project through the daemon catalog¶
For the CLI procedure, see Share projects between hubs.
The equivalent API lets a daemon owner connect one project to a configured hub using
POST /api/v1/federation/bridges. Select the catalog entry that holds the
ordinary hub account credential. The daemon resolves that entry alone; it does
not forward its own API token or return either credential.
The receiving daemon requires owner authority. Its catalog entry must hold a writable, ordinary account token with access to the existing hub project; issue-scoped, bootstrap and enrollment tokens cannot replace it. An expiring account token is accepted while valid; expiry suspends transport until the account credential is replaced. Environment variables and signing-file paths are resolved by the receiving daemon.
{
"hub_catalog": "example-hub",
"hub_project": "shared-project",
"project_name": "shared-replica",
"actor": "local-member",
"serve_downstream": false,
"preflight": true
}
The preview returns the hub instance, selected project, upstream account,
local account and bidirectional direction. It rejects read-only hub accounts,
unsupported relay,
provenance or embedding artifact protocols and local project-name collisions
before enrollment. Set preflight to false to connect. serve_downstream
defaults to false. Set it to true only when this daemon will relay the
project to downstream daemons. Each root-to-leaf path supports up to eight
hubs, including the root, plus one final leaf that does not serve downstream.
A hub that requires request signing advertises
federation_signing_required in GET /api/v1/instance. Preview and connect
refuse such a hub with hub_signing_required unless the bridge has a signing
source: pass signing_key_id with signing_key_env or an absolute
signing_key_file on the daemon, or select one for an already retained bridge
credential with kata federation signing configure --project-uid <hub-project-uid>.
The daemon validates the source before saving anything, keeps a retained
credential's source, and sends the hub no secret. The hub operator then
registers that key for the bridge's enrollment ID; see
Bridges.
Connecting issues a narrow claim,pull,push enrollment bound to the upstream
credential's account. The local account remains separate. The daemon
pins the root authority and installs the negotiated relay path before activating
transport. The root establishes its first project pin from the daemon's existing
owner-only signing key at enrollment. A different retained public pin requires
root key recovery; enrollment does not replace it. It retains
the candidate token in the owner-only credential file
before enrollment, so retrying the same connection after a lost response uses
the same grant. Pending candidates do not sync, forward claims or generate
shared-project embeddings. A changed local credential prevents an in-flight
response from replacing it; resolve that conflict before retrying.
To replace an expired or revoked hub account credential, update the selected catalog entry and repeat the same bridge connect. This explicitly rebinds the retained narrow token to the new live credential for the same upstream account. It preserves the enrollment, relay binding, reset epoch, cursors and queued work. An explicitly revoked enrollment stays revoked. Normal sync never falls back to the catalog credential.
Direct enrollment callers must set relay.rebind_parent: true and present both
the replacement account bearer and the exact retained narrow token. Ordinary
creation retries still require the original parent. The legacy enrollment
rotation endpoint rejects negotiated relay grants with 409 before changing
state; use bridge connect for this recovery.
The target must be a root HTTP(S) origin. Non-loopback plain HTTP requires the catalog's
explicit allow_insecure setting, and redirects are rejected. Preview and
connect require daemon-owner authority; ordinary browser sessions and scoped
user credentials cannot use this administrative operation. When the hub rejects
a request, bridge connect and disconnect report it as an upstream failure with
the hub's status in data.hub_status: a hub 401 or 403 returns 502 with
code hub_credential_rejected, any other hub 3xx or 4xx returns 502 with
code hub_request_rejected, and a hub 5xx keeps that status. The local daemon
accepted the caller; the hub refused what the daemon sent.
Read one bridge's locally recorded state with
GET /api/v1/federation/bridges/{project_name} using daemon-owner authority.
The response includes the local and upstream accounts, negotiated relay path,
credential status and existing federation diagnostics. It reports a pending
enrollment even before a replica exists, and never returns tokens. Status does
not contact the hub: connected reflects the configured connection and last
recorded outcome, while offline, paused and revoked describe observed local
state. revoked records an upstream 401, 403 or 404 other than a recognized
signature failure. It can clear after access returns. Explicit enrollment revocation, such as
kata federation revoke <id> on the hub, is permanent and requires a new
enrollment.
signing_rejected means the hub refused or could not verify this
bridge's request signature; the grant is intact, so fix the signing source on
this daemon or the key registration on the hub instead of reconnecting. Sync
timestamps show how recent that observation is. An ambiguous pending project
name returns a conflict instead of choosing a credential.
Disconnect one selected bridge with daemon-owner authority using
POST /api/v1/federation/bridges/{project_name}/disconnect. Send
{"preflight":true} to check the retained credential and local lifecycle
without contacting the hub or changing state. Finish retained deliveries and
remove downstream enrollments before disconnecting. The actual request drains
transport, durably marks the credential as leaving, revokes only that saved
narrow grant, detaches the replica and removes the exact observed credential.
It preserves the local project and issues. A lost response or interrupted
cleanup retains retry state; repeat the request to resume. A concurrent
credential change returns a conflict and preserves the replacement. Repeating
completed local cleanup succeeds without another upstream call.
An archive with pending local work is blocked. If a replica was already
archived with retained delivery, restore it with kata projects restore, sync,
and retry disconnect. Relay archive/restore events retain the source instance's
local lifecycle audit; they do not archive another instance's project catalog.
A relay can revoke its own narrow transport grant with
POST /api/v1/projects/{project_id}/federation/relay:disconnect, using that
project's transport bearer and spoke_instance_uid. This operation only revokes
that exact grant; it returns no enrollment inventory or project data. Repeat
calls remain safe after parent credential expiry or membership removal. A
credential for another project, peer, or legacy spoke cannot revoke this grant.
Detach a bridge whose hub is gone¶
When the hub is gone for good, an ordinary disconnect cannot finish: it needs the hub to confirm revocation and waits for retained deliveries. Detach the bridge locally instead:
kata federation bridge disconnect --project shared-replica --local-only --preflight
kata federation bridge disconnect --project shared-replica --local-only
The API form is the same disconnect request with "local_only": true. It
requires daemon-owner authority and never contacts the hub. It discards this
bridge's undelivered relay work (its relay outbox, inbox and cursors), removes
the binding and any retained credential, and keeps the project and its issues
as a standalone local project. Local edits that never reached the hub stay in
the local project only. The response sets local_only and carries a warning:
the hub grant was not revoked. Ask the hub operator to revoke the bridge's
relay enrollment with kata federation revoke <id> on the hub.
A local-only disconnect resolves the bridge from its database binding and does
not need the saved hub credential. A database restored without its credential
file can still be detached, and so can a bridge whose hub vanished while its
enrollment was still pending; the detach then removes only the binding and
relay state. A saved credential must belong to this bridge (a manual credential
for the binding's hub and hub project); otherwise the request is refused with
federation_credential_conflict.
A local-only disconnect is refused with federation_downstream_enrollments
while downstream relays still receive this project through the bridge. List
them on this daemon with kata federation enrollments list, revoke each with
kata federation revoke <id>, and retry. It also resumes an ordinary
disconnect that is stuck because the hub stopped answering. After it completes,
repeat bridge connect to reconnect the project to a hub. Edits made while the
hub was gone or while the project was detached are offered to the hub by the
new binding before any reset, so reconnecting does not delete them.
Team visibility¶
Daemon owners can restrict a project to selected teams. Membership belongs to
the credential's canonical actor; an issue author or teammate label grants no
access. Project policy also intersects existing issue, host and enrollment
grants. A teams policy with no teams denies ordinary access. Removing a team
does not change the project to all visibility.
Create a team and select it for a project:
kata teams create engineering
kata teams members add engineering --actor member
kata projects access set shared-project --visibility teams --team engineering
kata projects access show shared-project
Use kata tokens create --actor member --team engineering to issue a credential
and enroll its actor together. See the CLI reference
for the remaining commands.
The administrative API uses the same owner capability as token administration:
| Operation | Route |
|---|---|
| Create or list teams | POST or GET /api/v1/teams |
| Read a team and its members, or delete it | GET or DELETE /api/v1/teams/{team_uid} |
| Add or remove a canonical actor | PUT or DELETE /api/v1/teams/{team_uid}/members/{actor} |
| Read or set project visibility | GET or PUT /api/v1/projects/{project_id}/access |
Create a team with {"name":"engineering"}. Set visibility with
{"visibility":"teams","team_uids":["<team_uid>"],"revision":<current_revision>}.
Use all with an empty team list to allow ordinary project access. A stale
revision returns 409 revision_conflict; reload the policy before editing.
Ordinary members cannot administer teams or policy. Browser sessions retain
their existing owner and read-only restrictions.
In the web UI, open Credentials on the daemon you want to administer.
A daemon-owner login with the local target selected shows Teams and
visibility below the credential inventory. Create a team, select it to add or
remove member accounts, then select a project and choose all or the teams that
may access it. An empty selection under teams denies ordinary member access;
daemon owners retain administration access. Saving includes the loaded policy
revision. If another owner changed it, the UI keeps your draft and reports the
conflict; Reload visibility reads the current policy before another edit.
These settings use the browser's source daemon. They are unavailable while a
remote target is selected, the connection is stale, or the credential is
expired, or the daemon itself is read-only. Identity-mode bootstrap logins
have the separate access_admin capability for these team and visibility
settings; they remain read-only for ordinary attributed issue work and cannot
administer tokens through the browser. Direct local browser sessions cannot
administer teams; use an owner credential or the owner CLI/API. A daemon switch or lost authority clears pending settings reads; a
normal authority refresh keeps the selected team and project with controls
disabled until the refresh succeeds.
Token creation accepts team_uids alongside actor. The token and initial
membership commit together; invalid teams leave neither a token nor partial
membership behind. Team membership changes apply to all credentials for that
canonical actor.
Token boundaries¶
Federation has two bearer-token systems.
Daemon API tokens identify clients talking to normal daemon routes. They come
from KATA_AUTH_TOKEN or [auth].token, and DB-backed identity tokens are
managed with kata tokens ....
Federation enrollment tokens authorize spoke-to-hub transport routes. Bridge connection or direct enrollment creates them. They are stored hashed on the hub and plaintext only in spoke federation credentials. They authorize pull, push, join metadata fetches and forwarded lease actions.
Each enrollment is bound to one account. Direct-spoke transport authors local events and lease requests as that account; the hub rejects a different event actor. Negotiated relays preserve the original source actor across hops. Their authenticated connection and root receipts establish accountable attribution.
Enrollment tokens are not general daemon API tokens.
kata federation enroll is a normal daemon API call to the hub, not a
spoke-to-hub transport call. The command sends that hub API call to
--hub-url and authenticates it with --hub-token-env or a daemon-catalog
credential whose URL has the same origin. With neither configured, the request
is unauthenticated. The local daemon's global KATA_AUTH_TOKEN or
[auth].token is never sent to the hub. The CLI's default daemon should be the
spoke being enrolled; that spoke can be the implicit local daemon or a remote
daemon selected by KATA_SERVER or .kata.local.toml. enroll uses the
default/spoke daemon only to detect whether the named project already exists
on the spoke and should print --adopt-existing. The generated token printed
in the kata federation join ... command is a separate spoke transport
credential.
Without a project argument or --project, enroll finds the project from the
client's .kata.toml or Git alias and sends only that name or alias to the
hub. An unbound directory fails with project_not_initialized, and a missing
--workspace path fails with a validation error. The hub is never asked to
search client directories, even when --hub-url is a loopback address.
A DB-backed account token from kata tokens create cannot create or rotate a
legacy enrollment, whatever the hub's [auth].require_token_identity setting:
the hub answers 403 federation_enrollment_requires_relay, because a legacy
grant would outlive the account token that issued it. An account token can
only create a credential-bound relay enrollment, which kata federation bridge
connect does from a hub account selected in the local daemon catalog.
kata federation enroll always contacts --hub-url over HTTP, so running it
on the hub host does not bypass the hub's authentication. On a hub outside
identity mode, create a legacy enrollment by authenticating with the hub's
daemon token ([auth].token) through --hub-token-env or a catalog entry
that holds it; only a keyless loopback hub accepts the command without a
credential. In identity mode the daemon token is a bootstrap token: it can
mint account tokens, but it cannot perform the attributed federation-enable
step that kata federation enroll runs, so use kata federation bridge
connect with an account token instead. If you only have the bootstrap token,
first mint an account token as described in
Identity tokens.
--hub-token-env replaces --hub-token for enrollment and leave. Pass only
an environment variable's name; Kata reads its value inside the process.
An unset or empty selected variable is an error and does not fall back to
catalog credentials.
Credential-bound relay transport¶
A client can narrow an existing ordinary user token to one project with
POST /api/v1/federation/enrollments. Set project_id, the receiving
spoke_instance_uid, capabilities: "claim,pull,push", and
relay: {"protocol_version": 1}. Add "serve_downstream": true only when
the receiving daemon will relay the project to its own spokes; it defaults to
false. The actor comes from the user token;
a different supplied actor is rejected. Subtree tokens, bootstrap credentials
and wildcard project grants cannot enroll a relay. On a relay, only the local
account that connected the bridge may enroll a downstream daemon with
serve_downstream: true; another member's request is refused with
relay_downstream_forbidden, and that member can still enroll a leaf.
The response contains a separate transport credential and a relay handshake
with the binding UID, upstream instance UID, reset epoch, authority path and
root public-key pin. Retain the credential through the existing federation
credential store. An exact retry with the same caller-supplied token returns
the same live grant. Parent-token expiry or revocation, team removal, project
archive and bridge suspension stop subsequent transport requests.
After membership is restored, the existing origin-pinned metadata handshake can resume a locally suspended relay with the same credential. It verifies the project, peer, relay path and root pin before clearing suspension. Old-epoch work drains before a required reset. A committed reset whose activation reply was lost resumes its retained new namespace on retry. Installation checks the reset blockers before changing the projection or delivery state. Existing downstream enrollments are carried through the reset, not detached. A validated live regrant permits existing descendants again while that reset is blocked.
The enrollment credential authorizes these project routes:
| Route | Purpose |
|---|---|
GET /api/v1/projects/{project_id}/federation/relay?stream=events&limit=100 |
Offer a retained, emitted prefix. The limit is 1–1024. Receipts use their own stream=receipts. |
POST /api/v1/projects/{project_id}/federation/relay:accept |
Atomically accept a batch, its immutable source identity and root provenance, then queue onward delivery. |
POST /api/v1/projects/{project_id}/federation/relay:ack |
Acknowledge the exact emitted endpoint by stream, epoch, sequence and digest. |
GET /api/v1/projects/{project_id}/federation/relay/reset |
Retrieve a retained root-signed checkpoint with an authenticated immediate-hop cursor translation. |
Each pushed delivery lists its relay path, starting at the instance that wrote it. A hub accepts a push from a downstream enrollment only when the work began below that hub:
- A leaf enrollment (
serve_downstream: false) may push only its own events. - A relay enrollment may push events from itself and its own descendants.
- The hub rejects a push whose path names the root or any hub above the receiving hub. Root-origin and upstream-origin work reaches a hub only through that hub's own upstream connection.
Cron definitions and run observations pushed over a relay pass the same replay validation as a direct spoke push: a definition version or run reference that names an unrelated event or another project's definition is refused.
A rejected push returns a validation error and changes nothing.
Delivery acknowledgements and retention¶
Lost replies replay the same bytes and identities. An acknowledgement confirms commit at the immediate hop; a forwarded root receipt confirms accountable root acceptance. Neither an actor label nor a numeric project ID grants access to another project. Ordinary direct-spoke enrollment retains its existing actor and origin checks.
Relay delivery rows live as long as their binding. For every live binding a daemon keeps every outbox envelope it emitted, including acknowledged ones, every inbox envelope it accepted, and one cursor row per stream and epoch. The retained bodies are what a lost reply replays with the same identities. While the binding lives they are also the fallback copy of a compacted source: a receipt that arrives for an event whose local row was already compacted recovers its creation reference from any binding's retained outbox envelope. Acknowledgement and compaction do not delete them, and no time or size limit prunes them: growth is proportional to delivered work, one outbox row per delivery per downstream binding and one inbox row per accepted delivery, and it is unbounded by design.
Ending a binding removes its rows in the same transaction. Revoking a relay
enrollment, with kata federation revoke <id> on the hub or the relay's own
relay:disconnect request, deletes that grant's outbox, inbox and cursor rows.
kata federation leave and kata federation bridge disconnect delete the
bridge binding's rows once its deliveries are resolved;
kata federation bridge disconnect --local-only deletes them without waiting.
Other bindings on the same project keep their rows. Receipts and creation
references already recorded stay in the provenance tables, so a later
checkpoint for a live binding does not depend on a removed binding's rows. A
receipt that arrives after both the local event and the last retained copy of
its source are gone is kept without a creation reference. Restore skips a
grant's delivery rows when the backup itself records that grant as revoked,
including backups from older builds that retained those rows. A backup written
before the revocation still records the grant as live, so restoring it brings
back both the grant and its rows.
Archiving a project keeps its binding and its rows for restore; the rows for a
project are also removed when adoption gives the project a new UID.
Sync scheduling¶
The runner polls every 30 seconds by default. Set
KATA_FEDERATION_PULL_INTERVAL_MS on the daemon process to change that interval.
Shorter intervals increase wakeups and database traffic.
Sync runs in turns. One turn for a project pushes and pulls at most 8 batches per stream and direction (1000 deliveries per event or receipt batch, 32 manifest offers per artifact batch) and then yields, so the runner can serve the next project and local indexing can run. When a turn stops at that budget with deliveries still pending, the runner gives that project another turn at once instead of waiting for the sync interval; projects whose turn finished wait for the next interval or wake as usual. A backlog on one stream therefore delays the other streams, the other projects and local indexing by at most one bounded turn, and it does not make the runner poll unrelated hubs more often. A turn that stops before its outbound intent is drained does not request a signaled reset checkpoint; a later turn does.
Relay checkpoint capacity¶
A relay must be able to recover after the root purges or compacts history. Recovery uses a root-signed checkpoint containing retained portable history, current issue and cron-definition state, creation proofs and artifact manifests. The complete checkpoint must fit in 64 MiB. Ordinary sync uses batches and does not enforce a project-size limit.
The root returns 409 relay_project_too_large when a new relay enrollment or
an existing relay's reset would exceed this capacity. The error includes the
measured size and limit. Existing relays can continue ordinary sync, but cannot
recover from a later reset until the project fits again.
kata federation status estimates checkpoint size for each locally rooted
project with a live relay enrollment. It reports the exact retained-history
size plus an upper bound for newly captured state and receipts, allowing for
the largest encoded live relay account name. The estimate covers a checkpoint
captured from that state; it does not reserve capacity against future writes.
| Status field | Meaning |
|---|---|
relay_checkpoint_bytes |
Estimated bytes needed for the checkpoint. |
relay_checkpoint_capacity_bytes |
The 64 MiB limit. |
relay_checkpoint_error |
Why the estimate could not be calculated. Other status remains available. |
Human output shows relay checkpoint: <size> of 64.0 MiB, warns within 10% of
the limit and warns again above it. A failed estimate shows not measured.
Only removing content shrinks the checkpoint. kata purge <ref> removes an
issue's retained history and current-state record. Purge is irreversible;
export anything you need first. It waits for pending relay deliveries across
the whole project and for prepared resets to activate
(409 federation_pending_delivery). See Purge and reset
before retrying.
Archiving or deleting without purge retains history. Moving issues is not a remedy: cross-project moves involving federated projects are unsupported, and historical moves do not remove the source project's retained proofs.
Signed receipts are never removed. Purging lowers size only toward the floor set by accepted-event receipts. Once receipts alone exceed the limit, the project cannot enroll new relays or supply reset checkpoints to existing ones. Plan a new project before a busy project reaches that point. Purging more issues cannot remove that receipt floor.
Sharing embedding vectors¶
A shared project can choose one daemon to generate embeddings and send the results to its replicas. An artifact is the complete saved vector output for one issue and input. A manifest describes it without the vector bytes. A recipe identifies the model and all settings that affect those vectors. Recipients reuse an artifact only when its input and recipe match exactly.
Start with Select an embedding producer. Use Embedding status to distinguish normal waiting from a failed producer check. The rest of this section describes transfer and reuse.
Select an embedding producer¶
A root with a configured encoder chooses itself when its worker first needs shared-project vectors. To use a relay instead, enroll that relay for this project and configure its encoder. On the intended producer, select its Kata home and export the configured recipe and daemon identity:
embedding-recipe always writes JSON. It reads local home configuration,
including the model revision, dimensions, normalization and chunk recipe. It
makes no daemon or provider request and exports no API key or endpoint. A remote
server selection does not change which local configuration it reads. Use the
home that runs the intended producer and ensure its running encoder matches
that configuration.
At the root, use its existing owner-authenticated project metadata API:
POST /api/v1/projects/{project_id}/metadata. Set patch.federation_embedding
to an object with producer_instance_uid from that producer's identity and
recipe containing the complete exported JSON. Send the current project ETag
as If-Match so another owner's change cannot be overwritten.
Generate the request body from the exported recipe rather than typing its fingerprint or omitting fields:
PRODUCER_UID=01J00000000000000000000003
jq --arg producer "$PRODUCER_UID" \
'{actor: "example-owner", patch: {federation_embedding: {producer_instance_uid: $producer, recipe: .}}}' \
recipe.json > producer.json
Replace the example UID with that producer's actual instance UID and send
producer.json to the root's metadata route with its owner credential and
current project ETag. Ordinary project credentials and replicas cannot change
this reserved key. Select an enrolled relay with current project access and the
same exact configured recipe. A selected relay must have an authorized upstream
connection before generation. A different recipe leaves vectors pending; the
worker does not silently substitute a model. If a selected relay's configured
model or recipe changes, its project federation status reports
reason: producer_recipe_changed. Export the relay's recipe again and repeat
this selection at the root to resume generation. Confirm the assignment and state
with kata federation status at each participant and find the shared project row.
An explicit metadata change can choose another producer. Existing compatible
artifacts remain reusable. Setting the reserved key to null removes the
selection; a capable root can then choose itself again. An outage alone never
changes the assignment. An in-flight request or an explicit changeover can
incur duplicate spend; this policy does not promise exactly-once provider billing.
A hub configured for another embedding producer keeps missing shared vectors pending. Waiting shared content does not block private local embeddings. Compatible retained artifacts are reused before the worker checks generation eligibility. An unavailable producer does not trigger local generation. The daemon owner controls producer configuration at the root. Only root-origin events can change it. Writable replicas cannot change it through metadata writes, and a hub rejects any root-origin event pushed by a downstream enrollment. Root-origin changes continue downstream through each hub's authenticated upstream relay connection. Invalid producer configuration is rejected before a metadata change commits. Malformed configuration retained by an older backup leaves that project's vectors pending while private projects continue indexing. Ordinary enrollment and content synchronization continue. Clear or repair the root's configuration to resume. A configuration that names the root itself is replaced by the root's worker with its current recipe. Pausing transport preserves legacy and local root generation; a negotiated relay waits for an active authorized upstream.
Embedding status¶
Project federation status includes the selected producer and a bounded window of
up to 32 retained artifact identities. generated and reused describe the
artifact's origin relative to this instance. waiting means a producer is
selected without a retained artifact in that window. incompatible identifies
changed input (input_changed), a different exact recipe (recipe_mismatch), or
an artifact this daemon skipped because it failed validation
(invalid_artifact). stored_unindexed means the complete
artifact remains portable but this daemon cannot index it, including PostgreSQL
recipes above 4000 dimensions. limited indicates the window may omit older
artifacts. Status omits vector bytes and provider credentials and requires the
same current project access as other federation reads.
When this daemon cannot generate for a project it is selected to produce for,
status adds a reason:
producer_recipe_changed: this daemon's configured recipe differs from the selected recipe. After an upgrade that changes the recipe version, the selection can no longer be read andstateis alsorejected. Select the producer again at the root, as described in Select an embedding producer.producer_unavailable: the last producer check failed, for example because the upstream hub was unreachable, the federation credential was missing, or the artifact sync failed.last_errorholds the failure, and the daemon log records each one. The worker retries on its next fill and clears the reason after a check succeeds. A failure is shown only while the selection it was observed under is still current.
Without a reason, waiting means the producer is expected to supply vectors
and nothing has failed on this daemon.
kata federation status and the TUI federation detail show the producer,
preferred model and dimension count, and counts from this retained window.
The agent output adds a separate embedding row for each project, including
reason and last_error. A limited
inventory can omit older artifacts. These counts describe stored artifacts,
not provider requests or billing. JSON preserves the exact preferred recipe
and each retained artifact's digest and state.
The web project's sync summary shows pending delivery, the last successful sync, credential state, producer and retained-artifact counts. Use Refresh sync to read the current state. This read requires current project access and does not require credential inventory or administration. The summary clears when the session expires, the project leaves the visible catalog, or connection authority changes. It is available on the source daemon, including its local catalog entry. Remote workspaces do not delegate federation reads through their target credentials. Retained-window counts describe stored artifacts, not provider requests or billing.
Embedding transfer and reuse¶
The artifact stream accepts bounded manifest offers separately from issue
and receipt delivery. New relay enrollments atomically queue manifests for
already retained artifacts that match each issue's current content. Installing
an outgoing relay binding also queues those current artifacts already computed
locally; configuration retries retain their delivery identities. Expiring staging is excluded. Each manifest identifies the exact complete portable
artifact and its ordered chunks, without vector bytes. Its recipe identity
covers the configured model, revision, dimensions, normalization, document and
query roles, preprocessing, and the 2000-rune/200-overlap chunk window. Credentials,
batching, timeouts and unpinned endpoint addresses do not change that identity.
The response lists
missing_digests; a retry may include complete artifacts for those digests in
artifacts. An exact validated artifact in durable storage needs no vector
payload. A different digest or temporary pre-content staging remains a miss.
Negotiated sync exchanges these manifests in both directions after ordinary
issue and receipt delivery. Complete misses use the same enrolled relay route:
GET /api/v1/projects/{project_id}/federation/relay?stream=artifacts&epoch=1&artifact_digest=<digest>.
Repeat artifact_digest for multiple misses, up to 32. The sender serves only
exact previously emitted, unacknowledged offers in that binding's current epoch.
Every download rechecks enrollment, parent credential and project access. A
lost response after durable acceptance resumes with a manifest hit and needs
no second upload of vector bytes.
A known issue tombstone retires an outstanding artifact offer without storing
or downloading its vectors. This records the original hop identity and terminal
acceptance, rather than claiming that the recipient has the artifact. Later live
artifacts can advance the emitted prefix. Unknown issues still use bounded
pre-content staging and cannot advance that prefix. If deletion wins after an
offer, downloads return retryable 409 artifact_miss; if it wins after download,
the receiving transaction discards the payload. Portable bytes already retained
at the sender remain available for backup and restore. Restoring an issue
reoffers retained artifacts using a new delivery identity that combines the
unchanged artifact digest with the restore event UID. Previous offers and their
acknowledgements remain immutable. Durable artifact presence still uses the
exact original artifact digest, so recipients download vectors only for misses.
The existing embedding worker reconciles retained compatible artifacts before filling missing vectors and rechecks them after each pending-document scan. It reads artifacts only for pending documents; unchanged indexed issues cause no artifact reads or vector-index rewrites. Imports validate the full current input and configured recipe and preserve every chunk under the local content-revision fence. Semantic retrieval can use received vectors without another document embedding request. Portable storage remains separate from the backend index conversion. New local document generation within portable bounds retains the scanned input and complete original float32 vectors before index conversion, including PostgreSQL halfvec conversion. A concurrent edit can leave a valid stale portable artifact; the local revision fence prevents those old vectors from stamping the edited issue. Local results above portable artifact limits stay in the local index and do not become partial transfer artifacts or trigger repeated generation.
A root with a configured encoder selects its own instance and exact recipe
when its existing worker first encounters shared content without a producer.
The selection is a committed project metadata change and reaches live clients
and the existing federation handshake. An explicit selection stays stable.
When the root itself is the selected producer and its configured model or
recipe changes, including a recipe version change after an upgrade, its worker
records the new recipe through the same metadata change before generating.
A selected relay checks its current authenticated upstream connection, pinned
root, project access and preferred recipe before generating missing vectors.
That check is one metadata read per project in each fill; it never
synchronizes the project itself. The federation runner delivers artifacts
through its ordinary sync turns, and the hub's handshake reports
pending_artifact_offers while it still holds offers the relay has not
accepted. While offers are pending, the worker leaves the project's documents
pending and reports producer_unavailable with waiting for federation sync
instead of paying to generate vectors the hub already has; the next sync turn
imports them and wakes the worker. That deferral is bounded: if the hub still
reports pending offers 10 minutes after this relay first observed them, for
example because the hub cannot serve one artifact's bytes, the worker
generates locally and the stale offers keep retrying through sync. The wait is
measured by the relay from its first observation; it ends when a sync turn
finds no more offers to pull, so a later episode of offers starts a new wait,
and it starts again when the daemon restarts. Before each paid encoder
request, it
rechecks current upstream authority once for each project in that request,
not once per document; pending offers do not defer that recheck. A failed
connection leaves the project's remaining documents pending until the next
fill; it does not repeat the failed connection attempt for every document.
The worker rechecks canonical content before dispatch. A document that changed
since the page was scanned stays pending until the next mirror refresh. A
document whose artifact arrived meanwhile is imported instead. Neither is sent
to the provider, and the fill continues with the other documents. Artifact-only
arrivals wake the existing worker after commit, including a sync that loses its
acknowledgement.
A retained artifact that fails validation against the current issue input is skipped and logged. Its issue stays pending, so the selected producer embeds it locally, and reuse continues for other documents.
Artifact retention and transfer limits¶
Adopting an unfederated populated project preserves its current and stale portable artifacts in the same transaction as the project UID change. Adoption updates each artifact's project UID and digest while preserving its complete chunk bytes, input identity and producer attribution. A failed adoption restores the original artifacts and project identity.
Retaining an artifact for an issue's current content removes that issue's artifacts for older content, so edits do not accumulate full copies. An artifact still offered to a live peer, in that peer's current or prepared reset epoch, or on the outgoing upstream binding stays until the offer is acknowledged; the issue's next retained artifact removes it then. An artifact for older content that arrives after the current one is kept until that next retention. An artifact offer is named after its digest and the issue's latest content event, so when an issue returns to earlier content, its artifact has the same digest but a new offer identity, and a peer that pruned the artifact after an intervening edit is offered it again, whether or not it acknowledged the earlier offer and whichever instance authored the edits. The same re-offer happens when the producer reuses the retained bytes for the returned content instead of embedding it again; that retention runs before the document is indexed, so a failed re-offer is retried.
Permanently purging an issue removes all of its portable artifact bytes in the same transaction, including artifacts for older inputs. Sibling issues retain their artifacts.
Artifact acceptance permits at most 32 offers, 2 MiB of manifest bodies and 64 MiB of declared uncompressed vectors per batch. Complete artifacts retain the 16 MiB vector limit. Acceptance commits portable bytes and hop mappings in one transaction. A later durable hit cannot advance the artifact cursor past an earlier miss. Staging never acknowledges an artifact or blocks ordinary issue delivery. After the issue arrives, a manifest retry can promote matching, validated unexpired staged bytes without downloading vectors again. Every retry checks current enrollment and project access.
Project boundaries and attribution¶
Negotiated enrollment and populated-project adoption reject existing links to another project before changing project identity or history. Remove those links explicitly before sharing the project. New links and initial issue links must stay inside the selected project. A signed bootstrap omits whole historical records that reference excluded endpoints; the owner store retains their original bytes.
A fresh negotiated enrollment pins the public signing-key history received from its selected trusted hop, validates the complete signed rotation chain, and advances to the advertised current key before accepting receipts. This preserves creation proofs issued before a rotation. Subsequent metadata uses the existing pin and signed forward transitions; it cannot inject unknown historical keys. Private signing keys never travel with enrollment or normal backups.
For a negotiated replica, pending push counts local sources awaiting an exact root receipt. An immediate-hop acknowledgement does not clear that count. Compaction preserves pending intent in the relay outbox; a compacted source has no local event ID to report in the pending high-water field.
Fresh local issues and comments on a negotiated replica expose
verification: pending until their signed root receipt arrives, including
while offline or paused. They retain their source actor and teammate without
asserting a certified account. Receipt arrival changes the creation projection
to verified; historical rows without proof remain legacy. Complete owner
backups retain local pending intent. Project-only exports exclude it.
kata show displays this creation status for the issue and each comment,
including source actor and teammate labels. A verified projection also names
the accountable actor certified by the root receipt. pending and legacy
projections display only source labels; those labels do not establish an
accountable identity. Human output keeps creation details separate from comment
Markdown. Agent output adds Creation for the issue and a creation field on
comment rows. JSON retains the daemon's structured fields. Responses from older
daemons that omit this projection retain the previous text format.
The TUI shows the same creation status, source labels and verified accountable actor in the issue detail header and above each comment. Labels wrap to the terminal width; pending and legacy rows do not assert an accountable actor.
The web issue detail and comment list display the same creation projection. Source actor and teammate labels remain visible when proof is pending or legacy. The accountable actor appears only after the daemon verifies the root receipt.
Reset recovery¶
Once a relay observes upstream credential or project-access rejection, it persists that state and stops serving downstream updates. Temporary network or server failures keep queued work. Negotiated replicas refuse legacy cursor-only resets before deleting data or changing their delivery state.
Before detach or archive, resolve retained deliveries through root acceptance and explicitly detach or revoke downstream enrollments. A hop acknowledgement alone cannot discard work still awaiting its root receipt. Detach and archive recheck these blockers in the native transaction. Reset installation keeps downstream enrollments; see Downstream reset recovery. Refused changes keep the project, credential and descendant grants available for retry.
Installing a reset is blocked by:
- Unacknowledged deliveries to the upstream hub, in any stream.
- Local events still awaiting a root receipt.
- Open lease requests that are neither resolved nor rejected.
- Event deliveries this relay still owes a downstream enrollment that the root has not accepted.
- Active quarantine or a retained external-root binding.
A matching event UID and content hash attested by the incoming checkpoint can satisfy the local or downstream event check. Downstream receipt deliveries stay owed; artifact offers are replaced by the forwarded checkpoint. Neither independently blocks installation.
When the upstream advertises a changed relay epoch, the existing sync connection retrieves its signed checkpoint and installs the snapshot, creation proofs and three stream baselines in one transaction. The snapshot carries the root's current issues, cron job and workflow definitions, and cron run history as snapshot records, so a replica that installs it holds the same definitions and runs as the root. The rebuild replaces relayed and issue-scoped history only; daemon-local catalog history such as project renames and alias removals has no root receipt and stays in place. New enrollments also use this path when a purge boundary or missing creation history makes ordinary replay incomplete. Uncompacted enrollment keeps the existing small-batch replay.
The signed snapshot includes complete ordered manifests for retained artifacts that match each issue's current content, without vector bytes. Reset installation preserves validated portable bytes for retained issues, removes bytes for excluded issues, and reconciles unexpired staging against the new issue projection. A manifest alone cannot supply or acknowledge vectors. Root and forwarded resets reoffer retained artifacts in the new child epoch; normal manifest-first exchange transfers only exact misses.
Relays forward the original root manifest and snapshot unchanged, with their own authenticated child translation. The issuing hub prepares the checkpoint while keeping the old hop epoch live. Work arriving before installation drains with its original delivery identity; that fresh intent can require a new capture. After local installation, the first authenticated request for the prepared epoch activates it upstream. A lost activation request resumes the installed namespace. Writes committed upstream after capture remain owed and are offered in the new epoch.
Retries without fresh intent retain the same checkpoint bytes and identities until a later root purge requires a new snapshot. The signed root history boundary detects those later purges independently of hop epochs. Before requesting a signaled new epoch, sync drains the existing source and receipt streams through their usual commit and emitted-prefix checks. Every download checks current enrollment, parent credential and project access. The blocker checks above run before data changes. Snapshots carry artifact manifests; complete vector misses use the ordinary artifact stream.
Root resets on a relay with downstream enrollments¶
A relay that serves downstream enrollments installs a root reset without detaching them. Routine root maintenance such as an issue purge therefore flows through the whole hierarchy on the next sync of each hop:
- The relay installs the root checkpoint and replaces its projection as usual.
- In the same transaction it prepares a forwarded checkpoint for the next epoch of every active downstream enrollment. The forwarded checkpoint keeps the root's manifest, snapshot and signature; only the immediate-hop translation changes. Root-accepted work that arrived after the checkpoint was captured is queued in that next epoch. The enrollment itself stays in its current epoch until the downstream activates the new one.
- The downstream's next sync sees
reset_required, drains its own pending pushes, pulls the receipts it is still owed, downloads the forwarded checkpoint and installs it. Its first request for the new epoch activates that epoch on the relay. Creation receipts and the downstream grant survive unchanged.
A downstream enrollment with an expired or revoked parent credential does not by itself block its relay's reset. The relay retains a prepared checkpoint for that child, which must restore access before downloading or activating it. Unaccepted event deliveries owed to that child still block installation.
Event and artifact deliveries the downstream had not acknowledged when the relay installed the reset are superseded by the forwarded checkpoint: the checkpoint carries the root's state for every event, and the forward reoffers the manifests of every retained artifact in the new epoch. Superseded outbox rows stay in place as history but are not offered again, in the old epoch or the new one, so a purged issue cannot come back through an old delivery. Receipts are never superseded: a receipt is root proof the downstream may need before it is allowed to install, so unacknowledged receipts stay owed in the old epoch and follow the grant into the new one. Work the relay accepts after the installation is also still owed in the old epoch until activation.
Until the downstream activates the new epoch, the relay records what it
superseded in the daemon-local meta row
relay_reset_superseded.<project_uid>.<binding_uid> with the old and new
epochs, the checkpoint's snapshot UID, the last superseded outbox row ID and
the event and artifact counts. Activation deletes the row. A complete owner
backup includes it; project-scoped exports and standalone imports omit it
like the retained reset checkpoints next to it. If the root resets again
before the downstream activates, the relay replaces the earlier forward's
unemitted rows for that next epoch with rows derived from the newer
checkpoint.
For downstream deliveries, the blocker is an event this relay still owes a
downstream enrollment that the root has not accepted: an event without a root
receipt that the incoming checkpoint does not attest either. No checkpoint can
stand in for that work, and the relay could not forward a checkpoint to that
downstream either. Installation then fails with
federation reset blocked by pending push: downstream enrollment <binding_uid>
still holds events deliveries the root has not accepted; let it synchronize
or revoke it, then retry. Let the hierarchy sync so the root accepts the
work and its receipt reaches the relay, or revoke the enrollment with
kata federation revoke <id>, then let the relay sync again.
External-agent onboarding without hooks¶
For an account-token relay, have the daemon owner follow Share projects between hubs, then give the agent access to the resulting local project. The enroll/join procedure below is for a direct spoke on a hub using daemon-owner authentication outside identity mode.
An external coding agent needs no session-hook integration to use federation.
Run kata quickstart at session start, and keep ordinary commands pointed at
the intended spoke daemon throughout this workflow. For a registered local
profile, pass its --daemon selector explicitly. The hub administrator can run
enrollment from an environment that can select the spoke daemon; do not give
the agent the hub's administration token.
- On the spoke, read its durable instance identity:
Send the returned instance_uid to the hub administrator, along with the
intended project and actor. This UID identifies the spoke daemon, not an
actor or workspace. Use the spoke's identity rather than the hub's.
- The hub administrator uses a configured catalog entry with hub
administration credentials to enroll that spoke. Keep the selected daemon
pointed at the spoke because it identifies the local project. The exact
--hub-urlselects credentials from a hub catalog entry with the matching origin; do not select the hub profile with--daemon:
On this request-actor hub, --actor external-agent selects the enrollment
actor. Ordinary account tokens cannot create this legacy enrollment; use
a project bridge for an identity-mode hub.
kata --daemon <spoke-profile> federation enroll hub-project \
--hub-url https://hub.example \
--spoke-instance <spoke-instance-uid> \
--actor external-agent
Check the returned actor and capabilities. Enrollment returns a separate transport token and a generated join command for the agent; treat that command as a secret when transferring or storing it. It does not grant general hub API access.
- On the spoke, run the generated
kata federation joincommand. Keep its hub URL, project ID, actor, and capability values intact. Join reads the current project UID and replay/baseline cursors from the hub when it runs. A manual command has this shape:
kata federation join --project spoke-project \
--hub-url https://hub.example \
--hub-project-id <hub-project-id> \
--token <enrollment-token> \
--capabilities pull,push,lease \
--actor external-agent --push
Use --adopt-existing only for an existing standalone spoke project, with
the administrator's matching adoption-enabled enrollment. A new replica
needs neither prior kata init nor adoption. Use the operator's HTTPS
hostname that matches the certificate: substituting an IP address can fail
certificate validation. --allow-insecure does not bypass TLS certificate
checks; it is a plaintext private-network opt-in.
- Set the intended actor according to the spoke's authentication mode, then inspect status before taking work. On a request-actor spoke, use the actor from the generated join command:
# Request-actor spoke only.
export KATA_AUTHOR='<actor-from-generated-join-command>'
export KATA_INBOX_USER=external-agent
kata whoami
kata federation status --project spoke-project --json
kata inbox --project spoke-project --for external-agent --json
kata events --project spoke-project --after 0 --limit 100 --json
Replace the KATA_AUTHOR placeholder with the actor from the generated
join command. Identity-mode spokes ignore KATA_AUTHOR and use the identity
token's actor; that actor must match the enrollment actor for push-enabled
bindings. Confirm pull/push health and inspect existing work instead of
creating a practice issue. Inbox and event reads are local to the selected
spoke and may lag the hub.
For an agent without hooks, its harness polls the exact inbox address while
idle. Save the returned event cursor and resume with --after <cursor>; keep
cursors scoped to the selected daemon and project. On reset_required, discard
cached issue state, refresh reads, and resume from the reset cursor. A live
kata events --tail --json stream can wake the harness too; reconnect using
--last-event-id <cursor>. Reading an inbox never clears its request. Clear only after
handling the work, then read the inbox again:
kata notify abc4 --project spoke-project --to external-agent --clear
kata inbox --project spoke-project --for external-agent --json
Map teammate recipients such as external-agent/teammate-1 separately; the
actor inbox does not aggregate child inboxes. Attention signals can be replaced,
so coalesce updates and reconcile state after each wakeup. kata quickstart
prints this contract without installing hooks, polling processes, or
configuration. Configure optional embeddings on the intended search daemon;
see first-run embeddings setup.
Config-driven enrollment¶
For direct-spoke setup with a hub-owner administration credential outside identity mode, declare
project mappings in the spoke's <KATA_HOME>/config.toml. Account tokens cannot use this
legacy enrollment path. For an account-token relay, use a catalog entry and
bridge connect without a [[federation.project]] mapping:
[[daemon]]
name = "team-hub"
url = "https://hub.example"
token_env = "KATA_TEAM_HUB_TOKEN"
[[federation.project]]
hub = "team-hub"
spoke_project = "spoke-project"
hub_project = "hub-project"
actor = "user-a"
Set KATA_TEAM_HUB_TOKEN in the spoke daemon's environment to a normal hub
administration credential. The reconciler uses only the selected catalog
entry's credential for that hub origin; it never substitutes the spoke
daemon's global bearer token. A database-backed account token is refused with
federation_enrollment_requires_relay.
On daemon start, the mapping ensures or creates hub-project, enables it for
federation, creates a project-scoped claim,pull,push enrollment, and binds
spoke-project with push enabled. A missing local project is created
automatically; an existing standalone project is adopted. After ensuring the
hub project, the spoke generates and durably reserves an enrollment secret
under that hub project's UID when no compatible credential exists, then asks
the hub to enroll it. The secret lives only in the owner-only federation
credential file and is safely reused after a restart or lost response. Every
credential mutation uses failure-atomic file replacement.
The reservation is tied to the resolved hub UID. If a named hub project is
deleted and recreated, the replacement has a different UID and reconciliation
reports a conflict instead of silently enrolling it. Before local adoption the
category is configuration_conflict; after binding it is binding_conflict. Run
kata federation leave <spoke-project> to clear the old managed reservation,
verify the mapping, and restart the daemon to enroll the intended project.
Reconciliation starts in the background after storage and the federation
runner are ready. The daemon listener and normal project work do not wait for
the hub. Runtime failures are fail-open and each mapping retries independently,
starting after one second and backing off to at most five minutes. Inspect the
sanitized aggregate under federation_config in GET /api/v1/health;
ok remains true during hub outages, authentication failures, and conflicts.
An unset selected token_env is reported as hub_authentication and follows
the same retry path.
The config is startup-only. Restart the spoke daemon after adding or changing a mapping. Once a mapping succeeds it stays quiet until the next restart. Removing the mapping and restarting does not revoke the enrollment or convert the replica back to standalone; configuration removal is not teardown. Leave explicitly when that is the intended operation:
Leave also cleans up exact config-managed credential reservations from an interrupted startup reconciliation, before or after adoption. It fails closed and retains any conflicting or manual credential rather than deleting it. If a leave races with reconciliation while it is doing hub enrollment or rotation I/O, leave first records a durable leave marker and waits for the earlier request to finish. Reconciliation records and revokes any enrollment that completed, without recreating the credential that leave removes. The enrollment ID remains in the marker until local teardown, which makes a retry or restart safe. If the daemon crashed before recording the ID, it replays the reserved token to recover the exact enrollment and revoke it. A completed leave suppresses that mapping until the daemon restarts.
See Configuration for validation rules and HTTP API schema for the health and credential-rotation contract.
External credential providers¶
A hub may provide a local helper that arranges project access. Kata can use that helper instead of asking you to copy a token or supply hub administration credentials:
[[daemon]]
name = "team-hub"
url = "https://hub.example/tasks"
[[federation.project]]
hub = "team-hub"
spoke_project = "spoke-project"
hub_project = "hub-project"
intent = "collaborate"
credential_provider = ["access-helper", "credentials", "--profile", "work"]
Use the helper command supplied by your hub operator. Kata runs the listed
program directly, without a shell. Do not set actor, token, or token_env
for this mapping; the provider supplies the approved identity and permissions.
read_onlystarts an empty local replica that can pull tasks.collaboratestarts an empty replica that can also push work. The hub may additionally allow it to claim tasks.migraterequests Kata's adoption workflow: import an existing project's tasks and authors, then use the hub's project identity for federation. It is not an automatic fallback when an ordinary connection finds local data.
Kata saves its candidate credential before asking the helper for approval.
Pending approval, sign-in waits, and temporary failures retry the same request,
including after restart. A denied or conflict decision stops automatic
authorization retries and appears as a conflict in daemon health. Restarting
does not clear that decision. Remove the mapping and restart to release the
saved request before configuring a new one.
Once approved, ordinary synchronization uses the saved credential directly;
it does not run the helper for every task.
The provider decides approval and whether credentials expire. Kata displays
approval status and any supplied expiry in kata federation status, without
displaying credentials or helper arguments.
To disconnect, run kata federation leave spoke-project. Kata stops local
synchronization before asking the provider to release that exact connection.
If the provider is unavailable, cleanup remains pending. Retry the command
when it is available. --local-only cannot discard a provider-owned request.
Local tasks remain available after disconnection.
Provider mappings differ from catalog-admin mappings when removed:
- Remove the
[[federation.project]]block and restart Kata to release its provider connection and detach the replica. Cleanup retries in the background, even when you removed the last mapping. - A completed explicit leave keeps a secret-free closed marker until you remove the mapping. Restarting with that same mapping does not request new access.
- Other projects and manually stored credentials are not removed or revoked.
- Cleanup follows the saved project identity after a rename or after an unbound project was purged. A temporary database failure retries that cleanup.
- If startup reports
credential_io, correct the owner-private credential file and restart the daemon. Kata does not guess how to repair invalid credentials.
Move an existing spoke to a new HTTPS endpoint¶
Catalog edits are intentionally not applied to existing bindings. After
changing the named [[daemon]] entry and restarting the spoke daemon so it has
loaded the new catalog, migrate one spoke explicitly:
Or attempt every local spoke independently:
--daemon selects the spoke daemon that owns the binding and credential;
--hub selects a catalog entry in that daemon's config. The request carries
only the catalog name. The daemon captures the existing binding and credential
state and compares them internally before updating the endpoint.
The spoke daemon requires the entry to be remote HTTPS,
then deliberately sends the existing enrollment token to that configured
origin and fetches the same hub project's federation metadata. It updates
local state only if both the numeric project ID and stable project UID match.
The catalog administration token and token_env are not used.
Rebind keeps the old binding and credential expectations through the update; a changed principal, catalog target, binding revision, or project identity refuses the replacement. Shared cron definitions and run observations use the ordinary federation binding and stable project identity, so rebind needs no cron-specific step.
For a config-managed spoke, reconciliation reports binding_conflict and
changes nothing between the catalog edit and successful rebind, including
across a restart. Rebind is the designed resolution for this state. It does not
reenroll, rotate the enrollment token, reset cursors, alter capabilities or
actors, or require a database migration. If the two local stores were updated
only partially, rerunning the same command resumes safely; a fully migrated
retry returns unchanged. Plain HTTP targets are rejected even when the old
binding allowed insecure transport.
The daemon drains any project sync already using the old endpoint before it changes either local store and holds new sync work until both stores converge. This prevents an old-origin response from applying events or advancing a cursor after cutover; the next sync rereads the new binding and credential.
TUI enrollment workflow¶
This flow uses direct-spoke enrollment with a hub-owner credential outside identity mode. An ordinary account token cannot use it; use bridge connect for account-token enrollment.
The TUI federation view is scoped to the active daemon. Press F from the
queue or project selector to open federation for that daemon, then press n to
enroll a spoke project into a hub from the daemon catalog. The hub browser uses
catalog entries without switching the active daemon, so spoke and hub auth stay
separate.
The screenshots in this section are generated from disposable simulated daemons, hosts, actors, and projects. Generate local preview assets with:
Regenerate the SVGs and update the local single-commit docs-assets branch
with:
The first screen identifies the active spoke daemon and the selected local project before enrollment starts:
The enrollment flow starts by selecting a hub daemon from the catalog. The
active spoke is shown but blocked as a hub target; the catalog hub keeps its own
URL, auth, and allow_insecure setting:
After the hub daemon is selected, choose the hub project behavior. The default row creates or enables the hub project that matches the local spoke project; an existing hub project can be selected when the local project should adopt into a different hub project:
The preview is the mutation boundary. Confirm the operation type, local spoke
project, hub daemon, hub auth state, requested actor, capabilities, push
setting, and allow_insecure value before pressing Enter:
On success, the TUI shows the actor returned by the hub, adoption status, snapshot count, and hub project metadata, then refreshes the spoke federation list:
Worked example: direct token-auth hub¶
Use this runbook for a direct spoke joining a hub with daemon-token
authentication and require_token_identity = false. Use the hub's configured
[auth].token, not a database-backed account token. For an identity-mode hub,
use project relay setup instead.
Fill in these values once:
| Placeholder | Value in this example | Where it comes from |
|---|---|---|
<hub-api-url> |
http://100.64.0.5:7787 |
URL your CLI already uses for direct hub access. |
<hub-project> |
fedlab |
Project name on the hub. |
<spoke-project> |
local-tool |
Existing spoke project to federate. |
<spoke-worktree> |
~/src/local-tool |
Worktree for the spoke project when the spoke is a local workstation. |
<actor> |
member |
Actor bound to this direct-spoke enrollment. |
<hub-admin-token> |
secret | The hub's configured daemon token, outside identity mode. |
<spoke-instance-uid> |
01H... |
Printed by kata federation identity on the spoke daemon. |
<enrollment-token> |
secret | Printed by kata federation enroll on the hub. |
Do not infer these values from one another. In particular, the direct hub token and the enrollment token are different secrets, and the spoke instance UID must come from the spoke daemon, not the hub.
Before starting, make sure normal kata ... commands target the spoke being
enrolled, not the hub. For a remote spoke, set KATA_SERVER to the spoke URL.
For an implicit local spoke, leave KATA_SERVER unset. federation enroll
reaches the hub through --hub-url; identity, spoke project detection, and
join use the default/spoke daemon.
Step 1: get the spoke instance UID from the spoke daemon:
Expected shape:
Step 2: create the hub enrollment from the same machine. Leave normal kata
commands pointed at the spoke daemon; --hub-url is the explicit hub API
target for this command. Authenticate with --hub-token-env or a same-origin
daemon-catalog credential; the spoke daemon's global token is not sent to the
hub. If <hub-project> does not already exist on the hub, this command creates
it before enabling federation and creating the enrollment:
export HUB_ADMIN_TOKEN="<hub-admin-token>"
kata federation enroll --project <hub-project> \
--spoke-instance <spoke-instance-uid> \
--hub-url <hub-api-url> \
--hub-token-env HUB_ADMIN_TOKEN \
--actor <actor>
Expected shape:
enrolled <spoke-instance-uid> for <hub-project>
join: kata federation join --project <hub-project> --hub-url <hub-api-url> --hub-project-id <hub-project-id> --token <enrollment-token> --capabilities pull,push,lease --actor <actor> --push --adopt-existing
When a spoke project named <hub-project> already exists, the printed command
includes --adopt-existing. If no spoke project with that name exists, the
printed command omits --adopt-existing and join creates a new spoke
replica. If the spoke project name differs from the hub project name, replace
the printed --project <hub-project> with --project <spoke-project>, and
create the enrollment with kata federation enroll --adopt-existing so the
token is marked for adoption snapshots. That permission preserves historical
issue, comment, and link authors across every chunk of the initial baseline,
including resumed transfers. The event actor still identifies the enrolled
installation's actor. Completing the baseline consumes the permission; later
writes must use the bound actor.
Before enrollment, clean current-state data that should not appear in baseline
snapshots. Use kata comment edit <ref> <comment-uid> --body ... to replace
comment text while preserving the comment's UID, author, and timestamp. Use
kata projects rewrite-author <project> --from <old-author> --to
<new-author> to rewrite exact current-row author identities across issue
authors, issue owners, comment authors, and link authors. These are
pre-federation hygiene tools; they do not redact historical event logs that
have already been exported or shared.
Step 3: run the printed join command against the spoke daemon:
cd <spoke-worktree>
kata federation join --project <spoke-project> \
--hub-url <hub-api-url> \
--hub-project-id <hub-project-id> \
--token <enrollment-token> \
--capabilities pull,push,lease \
--actor <actor> \
--push \
--adopt-existing
Step 4: verify from both sides. First create or inspect work locally:
Then inspect the central hub using your normal direct hub administration
access, and confirm the new issue appears in <hub-project>.
For a new empty spoke replica, do the same runbook but skip --adopt-existing
in Step 3. The printed --project <hub-project> can be used unchanged unless
you intentionally want a different spoke project name.
For a plaintext private-network hostname, --hub-url is both the hub API URL
used for enrollment and the URL the spoke stores for later pull, push, and
lease requests. Use this only on trusted private networks; use HTTPS for public
networks.
export HUB_ADMIN_TOKEN="<hub-admin-token>"
kata federation enroll --project <hub-project> \
--spoke-instance <spoke-instance-uid> \
--hub-url http://hub.internal:7787 \
--hub-token-env HUB_ADMIN_TOKEN \
--actor <actor> \
--allow-insecure
The printed join command should include --allow-insecure; keep it when
joining locally:
cd <spoke-worktree>
kata federation join --project <spoke-project> \
--hub-url http://hub.internal:7787 \
--hub-project-id <hub-project-id> \
--token <enrollment-token> \
--capabilities pull,push,lease \
--actor <actor> \
--push \
--adopt-existing \
--allow-insecure
Hub setup¶
These enroll/join commands use direct-spoke transport with hub-owner credentials outside identity mode. For ordinary account credentials, use the relay setup guide.
Create or register the project explicitly when you want a separate setup step:
Enable federation explicitly when you want a visible enable step:
Enrollment creates the hub project if it does not already exist, and auto-enables the project if it is not already federated.
Get each spoke's instance UID from that spoke daemon:
Create one enrollment per trusted spoke. --hub-url selects the hub daemon for
this command. Supply the hub's admin credential with --hub-token-env, or add a
same-origin daemon-catalog entry with token or token_env. The spoke's
global daemon token is never sent to the hub:
export HUB_ADMIN_TOKEN="<hub-admin-token>"
kata federation enroll --project fedlab \
--spoke-instance 01H... \
--hub-url http://100.64.0.5:7787 \
--hub-token-env HUB_ADMIN_TOKEN \
--actor member
The hub binds the direct-spoke enrollment to the requested actor. Treat the actor and capabilities in the returned join command as the enrollment contract.
The --hub-url value is the URL the spoke will store and use later for pull,
push, and lease transport.
Manual --hub-url values may include a reverse-proxy path prefix, such as
https://hub.example/kata; the prefix is retained for later federation
requests. They must be HTTP(S) base URLs without user info, a query, or a
fragment.
The CLI prints a pasteable kata federation join ... command containing the
generated token. Treat that command as secret-bearing material.
CLI and TUI output use lease for coordination. The API and JSON capability
fields use the canonical claim name; both spellings are accepted as input.
Spoke setup¶
Run the join command printed by enroll against the spoke daemon:
kata federation join --project fedlab \
--hub-url http://100.64.0.5:7787 \
--hub-project-id 1 \
--token ... \
--actor member \
--push
By default, join fetches hub project metadata using the enrollment token, so
the hub must be reachable and the token must include pull. The command
creates a spoke replica project bound to the hub project UID and replay horizon, stores the hub
URL/project/token locally, and enables push only when --push is present.
--actor is required and should be copied from the printed join command.
Scripts can supply metadata explicitly:
--hub-project-uidsupplies the hub project's durable UID.--replay-horizonsupplies the first event ID available for replay.--baseline-throughsupplies the hub's baseline event marker.
Supplying both a non-empty --hub-project-uid and a positive --replay-horizon
skips the metadata request; include --baseline-through too if the script
needs that value. Otherwise, the request fills an empty project UID and replaces
nonpositive replay-horizon and baseline values, preserving positive values.
The initial pull cursor is one below the replay
horizon, with a minimum of zero; --baseline-through does not select it.
When the hub is reached over plain HTTP through a private overlay hostname rather than a literal non-public IP address, opt in explicitly:
kata federation join --project fedlab \
--hub-url http://hub.internal:7787 \
--hub-project-id 1 \
--token ... \
--actor member \
--push \
--allow-insecure
--allow-insecure is stored with the local federation credential so later
background pull, push, and lease requests can keep using that hub hostname.
Origin pinning still applies, so enrollment tokens are not sent across
cross-origin redirects. Use HTTPS instead when the hub is not on a trusted
private network.
Enrollment capabilities and local spoke behavior are separate:
--capabilities pull,push,leaseon the hub says what the token may do;--pushon the spoke says this replica should actually push local-origin events back to the hub.
If a token has push but the spoke joins without --push, the spoke remains
pull-only and the CLI prints a warning.
Adopting an existing project¶
If a spoke already has a non-federated project that should join the hub, use an
enrollment token created with adoption enabled and add --adopt-existing to the
join command. Adoption requires --push:
kata federation join --project fedlab \
--hub-url http://100.64.0.5:7787 \
--hub-project-id 1 \
--token ... \
--actor member \
--push \
--adopt-existing
The spoke and hub project names do not have to match. For that flow, run
kata federation enroll --adopt-existing, then select the spoke project with
--project in the printed join command and the hub project with the hub
selector.
Adoption preserves the current state of local issues, including closed and soft-deleted issues, comments, labels, metadata, priority, owner, and links. It does not preserve the old local event history. Instead it removes those pre-adoption local events, queues fresh snapshots for the hub with each issue's links embedded in the snapshot payloads, and reports how many snapshots were queued. The hub stores a cross-project link event even when its peer issue has not arrived yet and materializes the edge after both endpoint projects reach the same hub. Until then the edge is absent. Spoke projects that share links must use the same hub URL origin; different DNS names or IP aliases are intentionally separate federation groups. Adoption snapshot event actors are the bound federation actor. Snapshot payload authors and comment authors are preserved, so adopted issues keep their original displayed content authors. New snapshots also preserve relationship creation dates through rebuilds on SQLite and PostgreSQL. Older snapshots without those dates keep their existing insertion-time behavior; Kata does not invent missing dates. PostgreSQL rebuilds skip unchanged comments so they do not delay unrelated writes by rewriting the same rows.
Preserving the pre-adoption timeline: Adoption is a cutover, not an in-place history merge. If you need the old local event timeline for audit or rollback context, run
kata --project <project> export --output <path>.jsonlbeforekata federation join --adopt-existing. kata does not currently keep a separate in-product archive of pre-adoption events.
Adopted issues become ordinary federated spoke issues. You can keep editing them locally; acquire a hub lease only when you want exclusive coordination.
Leaving a federation¶
kata federation leave is the inverse of join: it revokes the spoke's hub
enrollment, then tears down the local spoke state so the project becomes an
ordinary standalone local project again.
A project connected with kata federation bridge connect is a relay, and
kata federation bridge disconnect is its inverse. leave on a relay is
refused with federation_lifecycle_blocked while the relay holds deliveries
its hub never acknowledged or active downstream enrollments; neither leave
nor leave --local-only discards that work. When the hub is gone for good,
run kata federation bridge disconnect --project <project> --local-only
instead; see Detach a bridge whose hub is gone.
By default this detaches: the local federation_bindings,
federation_sync_status, and quarantine rows are removed, the stored hub
credential is deleted, and all of the project's issues and current state are
kept. Leaving is revoke-first: the hub enrollment is revoked before any local
teardown, so a hub failure leaves local state intact for a clean retry.
An in-flight sync cannot recreate sync-status or quarantine rows after the binding has been removed. Its database writes recheck the binding before recording a late result.
For config-driven federation, leave also removes the exact managed credential
reservation whether reconciliation stopped before or after adoption. If the
credential changes while leave is cleaning it up, the API returns
federation_credential_conflict; resolve the conflict in credentials.toml
and retry kata federation leave <project>.
After confirmation, the CLI asks the spoke daemon to prepare the leave before it contacts the hub. Preparation durably marks a config-managed reservation, blocks new reconciliation work for that mapping, and waits for earlier hub enrollment or rotation requests to finish. Any completed enrollment is recorded and revoked idempotently before local teardown. If the command or daemon stops midway, rerun the same leave command; the marker prevents automatic re-enrollment and carries the cleanup forward. A successful leave suppresses the still-configured mapping until the daemon restarts.
Add --delete to also archive the now-standalone project (reversible with
kata projects restore); --delete --force archives even when the project has
open issues:
If the project still has open issues and you do not pass --force, the leave
is refused (project_has_open_issues) by a daemon preflight before the hub
enrollment is revoked: the binding, the credential, and the hub enrollment
all stay intact. The same preflight runs for plain detach leaves too, so any
local refusal the daemon can predict surfaces before hub contact. Close the open issues (or re-run with
--force) and run the leave again. The preflight is advisory (the
authoritative check runs inside the archive transaction itself, which executes
after the revoke), so an issue opened in that small window can still land the
spoke "hub-revoked, locally intact"; re-running leave --delete --local-only
(or --force) completes that teardown.
Hub admin auth for the revoke is resolved, in order: --hub-token-env, the
--hub <name> daemon-catalog entry, then the catalog entry whose URL matches
the binding's hub URL. With no hub credential the revoke request is sent
unauthenticated: the local daemon's global KATA_AUTH_TOKEN /
[auth].token is never sent to the hub origin implicitly, so a token-protected
hub requires --hub-token-env, a catalog entry, or --local-only. The hub URL
itself always comes from the binding, and a catalog token is only ever sent to
the origin its entry is configured for: a --hub <name> entry that is missing
or whose URL does not match the binding's hub URL is rejected, so a catalog
admin token cannot leak to a different hub. Use --hub-token-env when you
deliberately need to present a token the catalog does not associate with that
hub.
For plain-HTTP overlay hubs joined with --allow-insecure, the transport
opt-in is recorded on the binding itself (as well as in the credential), so
the leave-time hub client can carry a bearer token to the plaintext hostname
even after a partial leave lost credentials.toml. A same-origin catalog
entry with allow_insecure = true also restores the opt-in, and
kata federation leave --allow-insecure asserts it explicitly when no local
record of it survives. Without one of those, a token-bearing request to a
plaintext hostname is refused before any network I/O. Leave also warns when the hub holds a
matching global enrollment (no project scope) for this spoke: it still
authorizes the left project but is not auto-revoked, since it may serve the
spoke's other projects.
If no active enrollment matches this spoke's instance UID but project-scoped
enrollment(s) still authorize the hub project, the leave aborts and names
them instead of treating zero matches as success: the instance UID can change
after a clone/import, or the enrollment may have been created for another
instance (including another spoke of a shared hub project). Revoke the right
one with kata federation revoke <id> on the hub, or rerun with
--local-only.
If the hub is unreachable, --local-only tears down the local spoke without
contacting the hub. The enrollment token then remains valid until you run
kata federation revoke <enrollment-id> on the hub yourself:
Leaving is idempotent: running it on a project that is already standalone
reports success and finishes any cleanup a failed earlier leave left behind
(such as a stale hub credential in credentials.toml), and leave --delete
on a standalone project still archives it for the daemon owner. A project
access holder may use --delete only while the project is federated; on a
daemon with a host access controller that controller also authorizes the
archive. An archive-leave retry on an
already-archived project also resumes instead of erroring: it detaches a
surviving binding, deletes a stale credential, and reports archived=false
for that call. The leave command resolves archived projects for its argument
and --project forms, so rerunning the same kata federation leave
<project> completes the pending cleanup directly. A binding surviving on an
archived project takes the normal bound path: the hub revoke runs (it is
idempotent, so a retry whose enrollment was already revoked is a no-op, while
a spoke archived via kata projects remove, which does not revoke, gets
its enrollment revoked instead of silently stranded), then the local teardown
finishes. --local-only remains the unreachable-hub escape.
In the TUI federation view, press x on a spoke row to open a leave preview
(the mutation boundary), toggle detach/archive and local-only, then confirm.
Removing the spoke's already-pushed data from the hub project is a separate
hub-admin action. A user-facing project purge is not yet available; use
--delete (archive) for now.
Rejoining after a leave¶
Leaving keeps the local project's identity: it still shares the hub project's
UID. A later join for that hub project recognizes this and rejoins,
rebinding the existing local project instead of creating a second replica:
kata federation join --project <spoke-project> --hub-url <url> \
--hub-project-id <id> --token <fresh-enrollment-token> --actor <actor> --push
Rejoin also recovers a join that created the local project but failed before saving its binding. Repeat the join for the same local name and hub project identity to bind that existing project.
Pull restarts from the hub's replay horizon (already-applied events deduplicate by event UID), and a push-enabled rejoin re-offers local-origin events from the beginning; the hub deduplicates what it already has and absorbs any edits made while the project was standalone. Rejoin with the same actor the enrollment is bound to; events authored as a different actor are rejected by the hub and quarantined.
A join that names a different project while a local project still holds the
hub project's UID is refused with federation_rejoin_name_mismatch, which
names the holder; rerun the join with --project <holder> to rejoin it. An
archived holder must be restored (kata projects restore) first.
In the TUI, selecting a hub project whose identity is already held by a local unbound project presents the operation as rejoin of that project rather than a new local replica.
Adoption confirmation in the TUI¶
Because adoption rewrites the local project's event history, the TUI enroll preview no longer executes an adoption on a bare Enter. Enter opens a confirmation screen that states the operation (federate local project X INTO hub project Y) and requires typing the local project's name. Creating a new replica and rejoining stay single-step confirmations.
Sync model¶
This section describes direct-spoke transport. Negotiated relays use credential-bound relay transport.
A spoke polls the hub for events after its pull cursor. It applies hub events in order, deduplicates by event UID and content hash, folds portable payloads into the local projection, and advances its pull cursor only after successful application.
A push-enabled spoke scans for local-origin events above its push cursor and sends them to the hub as an all-or-nothing batch. The hub authenticates the enrollment token, verifies project scope and capability, checks that each event belongs to the bound spoke origin and actor, verifies schema version, deduplicates same-hash retries, rejects same-UID/different-hash conflicts, materializes the batch, and returns the advanced push cursor.
Ingest work is bounded by the accepted batch: metadata, comment, label, status, and claim-only batches skip the federation group's link rebuild because they cannot change link state. Link mutations, snapshots, and issue lifecycle events that can change endpoint resolution still run the full reconciliation.
Federation push and poll requests have a 60-second default budget, separate
from the shorter interactive CLI default. KATA_HTTP_TIMEOUT overrides that
budget when a large, healthy hub needs more time. A timed-out request leaves
the cursor unchanged and retries through the normal sync loop.
If a response is lost after the hub commits, retrying the same batch is safe. Permanent validation failures or hash conflicts record a quarantine on the spoke instead of retrying forever.
Leases and write gates¶
Lease commands first authenticate to the local daemon through its ordinary API. In identity mode, use an ordinary account token for these attributed commands. Enrollment tokens authenticate only the subsequent transport between daemons.
The root arbitrates leases. A direct spoke forwards acquire, renew, release and
status requests to its hub with an enrollment token that has claim capability.
The hub derives holder_instance_uid and the holder from that enrollment;
client-supplied holder strings do not change its authority.
Negotiated lease requests continue through relays to the root. Each authenticated hop retains a device-specific holder identity under its upstream account. Two leaves using the same account and client label cannot renew or release each other's root lease. Copying a displayed holder label or client identity grants no authority.
Use leases when an agent or operator wants to say "I am actively working this
issue; avoid overlapping non-comment edits until I release it." Holding a lease
gives temporary exclusivity against other non-comment mutations while the lease
is live. It also gives status and audit surfaces a clear current holder for
coordination. It does not grant durable ownership, replace the issue owner
field, serialize all collaboration, or act as a prerequisite for ordinary
edits.
For federated projects, ordinary issue edits are local-first and converge by LWW. Creating new issues also stays local-first. A lease is optional coordination: when another holder has a live lease on an affected existing issue, non-comment mutations are denied until the lease is released or expires. Comment creation and comment body edits bypass leases because they remain comment-level collaboration and maintenance actions rather than leased issue work.
Check the owner and current lease with kata status <issue-ref> --agent.
To keep a timed lease you already hold, renew it before it expires:
Renewal keeps the same lease and sets its expiry to 30 minutes from the hub's
current time. It does not add 30 minutes to the old expiry. The duration must
be a whole number followed by s, m, or h, from 60s through 24h
inclusive. A lease without an expiry does not need timed renewal.
Before an ordinary issue mutation, each spoke lease-status refresh gets a 500 ms budget. If the hub stalls or is unavailable, the spoke checks its cached lease state and continues the local operation. A cached live lease held by another principal still denies the edit. A received hub rejection remains an error, even if its response body stalls. The budget applies to each refresh, not the complete mutation; relationship edits can check several endpoints.
When offline, cached hard leases can still be used as a continuity hint, but they are not proof that exclusivity still holds. Timed leases expire by hub time and stop blocking edits once expired.
A complete issue read on a spoke, including kata show, makes an optional
lease-status refresh from the hub. That refresh gets a 500 ms budget; if it
times out or the hub is unavailable, the read still returns the locally cached
lease state.
This is a budget for the optional hub refresh, not a deadline for the complete
issue read. Project-local projections such as kata meta get do not perform
the refresh.
On a hub, kata show and kata status read cached lease state without taking
the database write lock. A timed lease can remain in the response after its
expiry time until the hub persists expiry; kata status reports it as
hold=expired. The hub's background sweeper checks every 30 seconds and
publishes expiry events. The lease-status API
(GET /api/v1/projects/{project_id}/issues/{ref}/lease) also persists expiry
before returning authoritative state. Hub issue reads do not expire leases or
publish expiry events, and unresolved violations remain visible until expiry
is persisted.
The hub checks pushed work against live lease state at ingest time. Work that
conflicts with another holder's live lease is kept, but the hub records
claim.violated. Work on unleased issues is normal and is not a violation.
Link mutations check both materialized endpoints in the compatible federation
group, expire timed leases in each endpoint's owning project, and record any
violation in that owning project.
Ingest audits current lease state, not authority at the time an offline edit was made. An edit covered by a lease locally can arrive after another holder acquires a lease and be recorded as a violation.
Root signing key¶
A hub signs a root receipt for every attributed write to a project it roots.
The project's first signed write or first relay enrollment pins the project to
the daemon's public key. The private key is federation-root.key in the
daemon's namespace data directory, <KATA_HOME>/runtime/<storage-id>/. JSONL
backups never contain it, so back it up with your other secrets. Every daemon
process that serves the same database needs the same key file.
When the daemon starts:
- With no project pinned to one of its keys, it creates the key file if it is missing.
- With a pinned project, it never creates a replacement. If the key file is missing, unreadable, or holds a different key, the daemon still starts and prints a warning. Projects pinned to different keys are handled the same way.
While the key is unavailable, signed writes to each affected hub project fail
with 409 root_signing_key_unavailable. The error names the project and the
recovery command. Standalone projects, spoke replicas, and other writes that
need no root receipt keep working. The condition appears in:
kata daemon statusandkata daemon diagnose, as a warning line and theroot_signingJSON field;GET /api/v1/health, as theroot_signingdiagnostics block;kata federation status, asroot signing key: unavailableon each affected hub project.
Recover in this order:
- Restore the key. Copy the backed-up
federation-root.keyinto the data directory with owner-only permissions (chmod 600) and restart the daemon. Pins do not change, and downstream relays keep working. - Rotate from the previous key. When the daemon now holds a different key
and you still have the previous key file, run
kata federation root-key repin --project <project> --previous-key-file <path>. The path is on the daemon host. The previous key signs a rotation to the loaded key, and downstream relays accept it on their next sync. - Repin without the previous key. When the key is lost, run
kata federation root-key repin --project <project>. The daemon createsfederation-root.keyif it is missing, retires the project's old pin, and pins the loaded key. Receipts signed before the repin stay verifiable with the retired key. New relays can connect and pin the new key. Downstream relays that pinned the old key reject receipts signed by the new key and can no longer sync the project. Re-enrolling does not help: a relay keeps its existing pin and accepts a new key only through a signed rotation. Kata has no command yet that moves an existing relay to the new key, so use this step only when the previous key is gone for good. While the project has active relay enrollments, the command refuses with409 root_repin_strands_relaysand changes nothing. Pass--strand-relaysto accept that those relays can no longer sync the project; the repin reports their count and never revokes them.
Run the command with --preflight first. The preview reports the method
(signed_rotation, unsigned_repin, or pin), whether it would create the
key file, both key IDs, the number of active relay enrollments, and the
consequence, including whether the repin needs --strand-relays, without
changing state. Repin
each affected project; after the first repin creates a new key, later repins
use it. A retired key cannot be pinned again. Repinning is owner-only, and
--previous-key-file requires daemon-owner transport or token. The command
fails with root_key_not_local for a project that this daemon does not root.
Operator commands¶
kata federation identity
kata federation enable --project <project>
kata federation enroll --project <project> --spoke-instance <uid> --hub-url <url> \
--actor <actor> [--hub-token-env <env-name>] [--allow-insecure]
kata federation join --project <project> --hub-url <url> --hub-project-id <id> \
--token <token> --actor <actor> [--push]
kata federation enroll --project <project> --spoke-instance <uid> \
--hub-url <http-hostname-url> --actor <actor> --allow-insecure
kata federation join --project <project> --hub-url <http-hostname-url> \
--hub-project-id <id> --token <token> --actor <actor> --allow-insecure [--push]
kata federation join --project <existing-project> --hub-url <url> \
--hub-project-id <id> --token <token> --actor <actor> --push --adopt-existing
kata federation enrollments list
kata federation revoke <enrollment-id>
kata federation leave <project> [--delete [--force]] [--local-only] [--hub <name>]
kata federation rebind <project> --hub <name>
kata federation rebind --all --hub <name>
kata federation root-key repin --project <project> [--preflight] \
[--previous-key-file <path>] [--strand-relays]
kata federation status
kata federation status --json
kata federation lease acquire <issue-ref> [--ttl 30m]
kata federation lease renew <issue-ref> --ttl 30m
kata federation lease release <issue-ref>
kata federation enroll --project <project> creates <project> on the hub
when it does not already exist, then enables federation and creates the
enrollment.
kata federation status reports local bindings, enabled/push state, cursors,
pending push depth, sync timestamps, enrollment counts, lease counts,
quarantine counts, reset blockers, recent lease violations, and whether this
daemon can sign root receipts for each hub project it roots. Use
kata federation quarantine list to inspect every active quarantine and
kata federation quarantine show <id> to see its complete retained event UID
list and error. The TUI shows the same retained errors in a spoke row's detail
view.
Compatibility and rolling upgrades¶
Push requests declare the spoke's local storage schema version. A hub accepts
older positive schema versions, but rejects a spoke schema newer than its own
with unsupported_federation_schema. The spoke treats that error as transient
version skew: it records the sync error, leaves the push cursor unchanged, does
not create quarantine, and retries on later sync passes.
Operationally, upgrade hubs before push-enabled spokes. If a spoke runs ahead
of its hub, kata federation status will show pending push and the last
schema-skew error until the hub catches up. After the hub is upgraded, the next
sync retries the same pending events automatically.
Malformed schema declarations are not rolling-upgrade skew. Missing schema
versions fail request validation, and explicit non-positive schema versions
return invalid_federation_schema.
Legacy v0.9 directional unlink payloads are also outside the supported ingest
contract. blocks and parent unlinks must include storage-oriented
link_from_uid and link_to_uid; the hub rejects events that omit them and
never guesses orientation from the current graph. Such a validation quarantine
is not released by upgrading or retrying because kata does not rewrite the
stored event. Handle it through the normal explicit quarantine disposition
workflow.
Older kata builds may already have quarantined a batch after a transient schema-skew rejection. After upgrading the hub, upgrade and restart each spoke on a build with this compatibility behavior. The next sync auto-releases that legacy schema-skew quarantine and re-sends the same events without advancing the push cursor.
Quarantine¶
A spoke records active quarantine when it sees a permanently poisoned push batch. Quarantine blocks further push and can block reset.
Inspect the retained event range and error before choosing any disposition:
Do not edit kata's SQLite database to clear a quarantine or advance a cursor. Those changes bypass the retry and audit transitions and can silently strand events. A compatible spoke automatically releases and resends a push quarantine whose retained error has the exact former cross-project peer validation shape. Current hubs accept a missing link peer as deferred state, so the original batch can advance and the edge materializes after both endpoint projects reach the same hub federation group.
Unknown primary issues are different: errors such as issue.updated references
unknown issue still indicate a poisoned mutation, remain quarantined, and stop
before another network request. Fix the root cause and explicitly retry other
recoverable push quarantines:
kata federation quarantine retry <id> \
--confirm "RETRY FEDERATION BATCH <id>" \
--reason "hub upgraded"
For legacy spokes, retry supports push quarantines. It marks the quarantine
resolved without advancing the push cursor, so the same local events are sent
again on the next sync. Retrying a legacy pull quarantine returns
federation_quarantine_retry_unsupported.
Negotiated project relays quarantine rejected canonical data in either direction. The retained range names hop sequence numbers, and the event UID list identifies the unchanged source events. Quarantine stops automatic delivery attempts; status still exposes the unresolved batch. Upgrade the incompatible recipient or resolve the validation failure, then explicitly retry. Retry preserves every source UID/hash, reset epoch and emitted-prefix acknowledgement. If the recipient still rejects the batch, it is quarantined again. Local project catalog, transport and root claim audit records stay in their own journals.
Relay quarantine cannot use legacy skip: the request returns
federation_quarantine_skip_unsupported without changing delivery state.
Advancing a legacy cursor cannot acknowledge rejected relay data. Keep the
original batch and retry after compatible recovery. A stale push
quarantine created by older builds for unsupported_federation_schema is
released automatically on sync after the spoke runs a fixed build; manual retry
is for other fixed push-quarantine root causes. Older peer-validation
quarantines also auto-release after both hub and spoke run compatible builds;
their retry transition preserves the cursor and resends the original events.
Intentionally skip only when the operator accepts that local events will not be federated:
kata federation quarantine skip <id> \
--confirm "SKIP FEDERATION BATCH <id>" \
--reason "operator accepted the skipped outbound batch"
For a legacy spoke, skipping advances its push cursor past the quarantined event range. It does not delete local events and it does not make skipped work appear on the hub.
Purge and reset¶
Hard purge is hub-admin-only for federated projects. A spoke rejects hard purge
with federated_admin_required. A hub purge uses normal local/admin daemon
auth, exact confirmation, and the same live-lease conflict gate as other issue
mutations. Purge also waits for every non-revoked enrollment to acknowledge the
project's current event, receipt and artifact deliveries and activate any
prepared reset. This covers the whole project, including deliveries for other
issues. A 409 federation_pending_delivery preserves the issue and
its downloadable vectors. Sync the retained deliveries or explicitly revoke
their enrollments, then retry. Retired epoch history does not block cleanup.
An issue purge removes the links from the issue and its comments to their signed creation receipts, so attribution lookups for them return not found. The signed receipts for the purged issue's events stay. Each receipt carries the accountable actor, source actor, teammate handle and content hash of one event. Receipts stay because their per-project sequence numbers are the cursor that downstream hubs follow; deleting one would let a later receipt reuse a sequence number that a downstream hub already holds.
When a hub purge removes replay history, it records a reset boundary and writes
a fresh federation baseline for remaining project state. A spoke whose pull
cursor is below that boundary receives reset_required and re-bootstraps from
the current federation horizon.
Relays recover from that boundary through a signed checkpoint. Purging issues
also removes their history from that checkpoint, which is how a root hub
brings a project back under the
relay checkpoint capacity; the purged events'
receipts remain in it.
A direct, push-enabled spoke refuses reset while it has unaccepted local-origin events or active quarantine. Negotiated relays use the full reset blocker checks.
Consistency limitations¶
Federation has expected stale or deferred states:
- Spokes read local state and can be behind the hub.
- Local spoke writes happen before hub acceptance.
- Offline cached hard leases can later be superseded.
- Lease violation signals are best-effort at ingest time, not proof of causal authorization at original edit time. Unleased edits are expected and are not violations.
- Poisoned push batches require operator choice.
- Hub outages degrade lease acquisition, pull, push, and status freshness.
- Future-schema push skew pauses push until the hub is upgraded. Stale schema-skew quarantines from older spoke builds auto-release after the spoke is upgraded and restarted.
- Stale peer-validation quarantines from older builds auto-release when a compatible spoke syncs against a compatible hub. Unknown-primary and other validation failures remain blocked for operator diagnosis.
- Purge causes spoke re-bootstrap.
- Enrollment creation uses normal daemon auth; the generated enrollment token only authorizes the spoke transport grant and is not a user daemon API token.
- Direct-spoke push binds event actors to the enrollment actor. A spoke that pushes a different actor is rejected and records the batch in quarantine. Negotiated relays preserve source actors separately from accountable attribution; see Project boundaries and attribution.
- Direct-spoke links may span projects; negotiated relays reject cross-project links. For direct spokes, a missing peer does not quarantine push: the event and cursor advance while the edge remains absent. The edge materializes when both endpoint projects are enabled through the same hub federation group. Pulling clients must enroll both projects and use the same normalized hub URL origin to project it locally. Unfederated peers, different hub origins, and peers that never arrive stay absent. Malformed events, unknown primary issues, actor violations, and hash conflicts remain permanent validation failures and can quarantine push.
Cutover notes¶
The actor-bound federation schema is a v13 JSONL cutover. Existing unbound hub
enrollments are not imported because they cannot be made actor-safe; create new
enrollments with kata federation enroll --actor <actor>. Existing
push-enabled spoke bindings without a stored actor are imported with push
disabled, so those spokes must re-run kata federation join --actor <actor> --push
before local-origin work can sync to the hub again.
Use a shared daemon when those trade-offs are unacceptable.