Architecture and Guarantees

One page for what nfltr guarantees, which part guarantees it, and what it does not. Every guarantee is a rule the code enforces and a test protects; the evidence section says how strong each kind of test is. Claims the sources do not support are listed under Not guaranteed and Known gaps.


At a glance

This page describes main as of 2026-10-26. The code is the source of truth. Each claim rests on nfltr's internal specification, which cites the code and the test for every rule, and on the reference pages generated from code. The code and the specification are not public; this page states their rules in plain words.

Architecture

 you (terminal, Claude Code)                 paired phone or browser
        |                                            |
        v                                            | sealed envelopes
+-----------------------------------------+          | (end to end)
| HUB  nfltr mcp --toolset hub            |          |
|  Claude session <-> hub tools           |          |
|  hub engine: state file, task store     |          |
|  (orchestration layer)                  |          |
+--------------------|--------------------+          |
                     | one outgoing connection       |
                     v                               v
+----------------------------------------------------------+
| RELAY  nfltr.xyz or your own relay                       |
|  routes frames by agent id, checks API keys,             |
|  lists connected agents (presence), mailboxes, blobs     |
|  runs no model, holds no task content by default         |
+----------|-----------------------------|-----------------+
           | outgoing connection         | outgoing connection
           v                             v
+------------------------+     +------------------------+
| NODE  nfltr node join  |     | NODE                   |
|  starts agents on ask  |     |  ...                   |
|   +-> AGENT (worker)   |     +------------------------+
|        +-> Claude Code |
|             and tools  |-----> git host (allow-listed repos)
+------------------------+
 data plane between machines: link, multiplexed sessions
 with per-session flow control (tunnels, TCP, p2p)

One task, start to finish

This is the path of one spawn_agent call on a hub with one joined node. Each step says what is made durable and what happens when the step fails.

  1. The hub records the turn. spawn_agent writes the agent and its turn as pending into the hub state file. The file is replaced atomically and synced before the call returns. If the hub dies now, the next process with the same hub id re-runs the turn.
  2. The hub asks a node for a slot. It sends a launch request over the relay to a node that matches the constraints. The node answers with a worker id, or a refusal: unsatisfiable, at_capacity, bad_request, failed, monitors_not_allowed, sender_refused. The launch itself has no idempotency at the protocol level. The hub books each launch and treats a worker that never appears as lost.
  3. The agent connects. The node starts a worker process. It connects to the relay with its own id and a per-process instance id. A second connection with the same id and another instance replaces the first.
  4. The hub assigns the task. It sends task.assign to the worker through the relay. If the worker is not yet registered, the relay waits up to 30 seconds, then answers NotFound. Only a dial that never opened a peer stream is retried, so the first frame is delivered at most once. The hub then marks the turn running and saves.
  5. The agent works. It clones through the node's repository mirror, which refreshes from the origin first. It sends progress. The hub can steer or cancel.
  6. The agent reports. It sends task.result or task.error. The worker keeps the terminal frame and replays it on every reattach until it gets a matching task.result_ack.
  7. The owner accepts. The task store writes the terminal state, then acks. A second terminal for the same task is recorded only as task.duplicate_terminal evidence. Only the current {owner, epoch} may write a task.
  8. The hub completes the turn. The first recorded outcome wins and takes the next sequence number. The hub saves it as undelivered.
  9. The hub delivers. wait_for_agents marks the completion delivered and saves before it returns. If the save fails, the marks are rolled back and the call returns an error.

What happens when a step breaks:

FailureResult
Relay restarts or is killedStreams drop and redial under the same instance id. Agents keep running and replay buffered progress and the retained terminal.
Hub killedThe next process with the same hub id takes over the dead owner's claims, re-watches unfinished turns and returns each missed completion once.
Node stopped with Ctrl-CAgents fail their tasks node_lost, wait for the ack and exit.
Node killed or crashedAgents notice the parent change within 250 ms. If they cannot report, the hub declares node_lost after the node and agent were absent for 30 s plus a 2 s grace. A completed or cancelled status already recorded wins over node_lost.
Agent frozen past the heartbeat windowThe relay evicts it after 180 s idle. The hub records node_lost and supersedes the attempt. When the process thaws, the hub stops the superseded attempt. A late result arrives once, flagged superseded, as evidence only.
Agent leaving while a dispatch arrivesThe agent refuses with worker_drained before acceptance, or runs the dispatch that got in first. The hub places the turn elsewhere. A SIGKILL does not drain.

Which layer guarantees what

Three layers give guarantees. A guarantee of a lower layer is not a promise of a higher one: link says nothing about tasks, and the relay says nothing about whether an agent did its work.

link: the data-plane library

link multiplexes sessions over one connection that the caller dials and secures. It imports only the Go standard library. The wire format is version 2.

The relay

Orchestration: hub, nodes, agents

What "exactly once" means

nfltr uses "exactly once" for three narrow things. It never uses it for what an agent does.

  1. Acceptance. A task has at most one accepted terminal result. The worker replays its terminal result until it gets a matching acknowledgement. The owner acknowledges only after a durable write. A second terminal is stored as evidence and never becomes the outcome.
  2. Delivery to the hub. The hub marks a completion delivered and saves before wait_for_agents returns it. After a crash, the completion is returned once more only if the save did not happen. This covers completions, late results, owner messages and hub events.
  3. Dispatch. A task assignment frame is delivered to a worker at most once. A dispatch the transport cannot confirm is never re-sent. The hub resolves it by asking the worker.

What it does not mean:

Production guarantees and other evidence

A guarantee here is a rule the code enforces and a check protects. How strong the check is differs. This table keeps the kinds apart.

Kind of evidenceWhat it showsWhat it does not show
Unit and contract tests, run on every change. Examples: delivery of a missed completion exactly once after a hub restart, a durable save before a call returns, the mailbox and blob contract tests, one conformance suite every blob backend passes.The rule holds in the tested cases.Behaviour under a real model, a real network or a real fleet.
Fake-agent end-to-end tests and soaks. A scripted stand-in for Claude Code replaces every model call. Real relays, nodes and hub processes run, and faults are injected: an end-to-end suite of the hub, and fault soaks of the hub with nodes and of a single relay.Orchestration under process kills, freezes, restarts and deploy windows. The history audit checks invariants after each run.Anything the model does. Rates measured this way are rates of nfltr, with zero model calls.
Linearizability check. A Porcupine check runs of the fenced task stores under concurrent owners, crashes and lease expiry.Histories of the tested workloads are linearizable.A proof for all executions. No TLA+ or other model checker is used.
Chaos lab on Kubernetes (kind). Experiments f1 to f9, r29a, r29b kill relays, Redis and workers, partition and delay the network, and skew clocks. Shared steady-state probes check the result.Real relays under network faults.It spends model money unless run with the stand-in. There is no unit-level clock-skew test, and the hub is excluded from clock skew.
Hosted status test and canary. An hourly test connects two machines through nfltr.xyz, starts a job on each with a stand-in and checks that each result arrives once. A production canary checks exactly-once delivery, the sha on the origin and cleanup.The hosted path works from one test machine. A gap means that machine was offline.Real model runs. One vantage point.
link fuzzing and soak. Decoders are fuzzed (4.85 hours in total, no crasher in the library). Soaks under faults ran 2.89 hours on macOS and Linux CI with leak and integrity checks.The wire decoders and sessions survived what was run.The project's own target for both is 24 hours. It has not been met.

Two things are missing from this evidence:

Trust boundaries

Who trusts whom, and what each party can do if it misbehaves.

PartyTrusted toNot trusted toControl
Hub (your Claude session and the nfltr process)Decide what runs where. Own all task state.Widen a node's capabilities.Node flags are the operator's. The hub cannot change them.
Node (an operator's machine)Start agents inside the limits its operator set.Accept work from anyone but a hub of its account.The node refuses empty senders, the relay's own agent and node-launched workers as senders (sender_refused). --serve-hubs narrows hubs but is not an authentication boundary.
RelayAuthenticate keys, route frames inside an account, hold mailboxes, blobs and presence.Read content, decide, or vouch for a peer's key.End-to-end encryption of A2A frames is on by default. A pairing key makes the key exchange resistant to a hostile relay.
Model providerRun the model CLI on the machine where Claude Code runs.Receive nfltr relay credentials.The relay credentials are stripped from the model CLI's environment. The provider's own credential is kept for the CLI by design.
Tools and repositoriesNothing.Everything they return is untrusted data.Results, artifacts and monitor lines reach the hub model framed as untrusted content. The framing informs the model. It does not bound what the model does.
Git hostServe allow-listed repositories.Be reached by an agent beyond its grant.Allow-list and a scoped credential helper. Not a hard boundary for an agent with a shell.

Details:

Contracts per primitive

Each contract states the guarantee, the bound and the failure. Where the sources give no rule, the row says so.

Streams (link sessions)

QuestionContract
BackpressureEach side announces a per-session window (128 KiB to 1 GiB). A sender may have at most the peer's window unacknowledged. The receiver returns credit as bytes leave its buffer. A slow reader stalls only its own session.
OrderingBytes of one session arrive in order. There is no ordering between sessions.
CancellationReset ends a session and the peer reads an error. CloseWrite half-closes: the peer reads io.EOF and can still write. Abort reaches the far end through every hop as a typed code. Deadlines are each side's own; link carries none from hop to hop.
Terminationio.EOF is a deliberate end. A reset, a closed connection, a dead peer or a drain deadline reads an error that is not io.EOF. After a go-away, no new sessions open; open ones run to their end.
Dead peerDetected within Interval + Timeout. Idle connections cost a few bytes.
LimitsSessions per connection are bounded by the peer's announced maximum. An open past it ends the connection (ErrFull). A data payload is at most 1 MiB per frame.
HTTP through the relayA client that opts in has at most 4 response chunks and 4 upload chunks in flight per transaction. Any exit before the end sends a cancel frame. Clients that do not opt in get bodies whole.
A2A streamsWhen an agent's connection ends, every pending call ends with ErrConnectionClosed (gRPC Unavailable, retryable), never io.EOF.

Not specified in the sources: a fairness guarantee between sessions beyond "every session keeps getting its turn", and any end-to-end encryption in link itself. link does no encryption. Confidentiality needs TLS or an encrypted payload inside the session.

Mailbox

A mailbox is a named, ordered queue of opaque envelopes owned by one account. Only the owner's principals append, pull or acknowledge. The relay does not read payloads.

QuestionContract
DeliveryAt least once. A pull never acknowledges. An envelope has a relay-assigned increasing seq.
Lost acknowledgementThe envelope is returned again with a higher delivery_count. The reader dedups by seq. An ack removes every envelope up to and including through_seq.
Consumer goneEnvelopes stay until acknowledged or expired. A reader that is away loses nothing before expiry. A full mailbox refuses the append with MAILBOX_FULL.
ExpiryA time-to-live up to 7 days. Unset or 0 means 7 days. After expires_at the relay drops the envelope, and a pull returns only unexpired ones.
Duplicates from the senderThe same idempotency_key from the same sender within 24 hours returns the first seq with duplicate = true and stores nothing. At most 1000 keys are remembered per mailbox.
LimitsPayload 1 MiB. 500 envelopes and 8 MiB per mailbox. 64 MiB and 256 mailboxes per account. A pull returns at most 100 and waits at most 25 s.
WebhooksA POST is at most 64 KiB and 600 per minute. Refusals are counted (dropped_rate_limited, dropped_mailbox_full, rejected_too_large) and the next envelope carries dropped_before.
Your own relay (beta)Mailboxes and webhooks are kept in the relay's data directory. A restart keeps them, with their unacknowledged envelopes.
Hub useThe hub saves its cursor before it acks. An envelope at or below the cursor is skipped, so redelivery does not repeat an event.

Presence

Presence lists the connected agents an account may reach, with their labels, and a change feed.

QuestionContract
What it isA snapshot of connected agents plus a cursor taken before the snapshot. Watching from that cursor misses nothing.
StalenessAn agent counts as connected until the relay ends its stream. Heartbeats run every 15 s. With heartbeat checks on, the relay ends a session that sent no frame for 45 s. Its reaper evicts agents idle for more than 180 s (it scans every 60 s). A frozen process can stay listed until then.
A listed agent may be deadA dispatch to a frozen but listed agent is answered NotFound before acceptance, so nothing ran. The turn ends at once as worker_not_connected and is placed again.
FeedWatch is a long poll that waits at most 25 s and returns at most 256 events, oldest first. The revision orders events within one relay process.
Cursor resetIf the cursor is older than the relay's history, or from another relay process, the response sets reset. Changes may be missing. The client must list again.
Several relay replicasWith Redis down, a replica's feed shows only its own agents. A hub waiting on another replica's agent wakes at its 30 s backstop.
LabelsLabels are advertised data. The relay filters on exact values and never interprets keys. Labels set by the operator at connect cannot be changed by UpdateLabels.

Blobs

A blob is a content-addressed object named by the SHA-256 of its bytes. The relay offers the store only when its blob backend is configured. The relay keeps no orchestration artifacts; an artifact names its blob by hash.

QuestionContract
AuthorizationA blob belongs to the account that put it. Only that account reads it. Anyone else gets 404 BLOB_NOT_FOUND, the same answer as for an unknown blob.
IntegrityThe relay verifies each part, then the whole object against the hash in the path. A mismatch is 409 BLOB_HASH_MISMATCH. Reads carry ETag and Repr-Digest.
Concurrent and resumed uploadsParts are verified, held durably, idempotent and resumable from any relay replica. A part that starts beyond the bytes held gets 409 BLOB_UPLOAD_GAP; resume at Upload-Offset. A probe with bytes */TOTAL returns the bytes held. Putting a blob you already hold only refreshes its retention.
ExpiryA blob has expires_at. The relay drops the owner's hold when it lapses, unless the owner puts the blob again. Every PUT refreshes it.
Size limitsA part is at most 64 MiB. An object is at most 1 GiB by default, set per deployment by blob_store.max_object_bytes. A transfer that moves no bytes for one minute fails. Over a limit: 413 LIMIT_EXCEEDED.

Not specified in the sources: a per-account total storage quota, what two different accounts uploading the same hash share, and the retention period itself. See Known gaps.

Typed errors

Clients branch on a machine-readable reason, never on message text.

ClassMeaningExamples
TransientThe sources mark the error retryable. The same call may succeed later.Relay errors AGENT_OFFLINE, RELAY_STARTING and DIRECTORY_UNAVAILABLE (marked retryable), RELAY_DRAINING (carries a retry hint). Task codes with Retryable: yes: stream_error, worker_disconnected, at_capacity, startup_timeout. link: ErrGoAway (open on another connection; Pool and Node do), ErrConnClosed (open a new session).
PermanentThe call is refused or wrong, and the sources mark no retry. Change the request or the setup.UNAUTHENTICATED, LIMIT_EXCEEDED, BLOB_HASH_MISMATCH, INVALID_ARGUMENT. Task codes with Retryable: no: no_worker_satisfies_requirements, harness_refused, workspace_artifacts_failed, max_runtime_exceeded. A replaced session (FailedPrecondition) is final.
Retry-safe (nothing ran)The error says the task never started, so a retry cannot repeat work.dispatch_transport_unavailable (nothing ran), worker_not_connected (relay could not deliver the first frame), worker_drained and at_capacity (refused before acceptance).
Not retry-safeThe task may have reached the worker. nfltr does not re-send; the hub resumes or asks.stream_error after a send, ErrSendUnconfirmed, node_lost, worker_disconnected.

Rules that protect the classes:

The full lists are generated from code: the task error codes and the ErrorReason enum in the relay protocol.

Threat model

Scope: the hosted relay at nfltr.xyz, your own relay (beta), the nfltr command with the hub, nodes and agents, and the dashboard. The full model, with a table per boundary, is internal; this section summarizes it.

Who can read task content, and what the relay sees

PartySees task content (prompts, results, repository text)?
The hub machine and the hub's Claude sessionYes. It sends the prompts and receives the results.
A node and its agentsYes, for tasks they run.
The model providerYes, for every machine that runs Claude Code: prompts, repository content and tool output.
The relay, with end-to-end encryption on (the default for hubs, nodes and agents)No. It sees routing fields.
The relay, with it off, or on a path to the relay's own agentYes. A2A payloads are plaintext to it.
The dashboardNo. The hub's plaintext dashboard surfaces were removed: the dashboard reads no hub, and the hub refuses every task route the relay's own agent asks.
A paired phone or browserOwner questions, answers and a live snapshot, sealed per device.

What the relay always sees: the account, agent ids, labels and capabilities, which machine connects to which, when, frame sizes and timing. For p2p it also sees both agent ids, the session id and key material that is public by design. For tunnels, it sees the traffic of ordinary HTTP tunnels and shares (TLS ends at the relay) and raw TCP bytes. It does not see end-to-end TLS passthrough content, and a WireGuard tunnel is sealed between its two peers.

The hub's own run state stays on the hub machine. Run state is shown in your hub; a paired device gets a live snapshot sealed to that device.

Caveat on "end-to-end encryption on": the first request to a peer is encrypted to the static key that peer advertises, and the relay vouches for that key. Without a pairing key, a relay that swaps keys is not detected and can read the frames. With a pairing key (a file of random bytes copied to each machine yourself), the handshake carries a MAC and a relay that swaps a key cannot complete it. A pairing key proves a peer holds the key. It does not say which machine; any holder can impersonate any other.

Key management and revocation

KeyWhere it livesRevocation or rotation
Account API keyYour environment or saved config. The relay stores only its SHA-256.Revoking the key, or its expiry, ends the streams it opened with Unauthenticated. A dashboard session made by that key ends with it.
Dashboard sessionA signed cookie naming a server-side session, 24 hours by default.Logout, revoking the key that made it, or removing the account from the allow-list ends it. A WebSocket opened before logout stays open until it closes (finding F-27).
Hub identity and epoch keysA key file and a pin file on the hub machine. The pin file holds at most 8 devices.Epoch keys rotate every 24 hours. nfltr orch hub devices --revoke <id> unpairs one device: later messages from it are refused. nfltr orch hub pair --rotate-identity makes a new hub key and forgets every device. A corrupt pin file refuses everything.
Device keys and passkeyBrowser storage (non-extractable WebCrypto) and the platform authenticator.Approvals and instructions need a passkey assertion with user verification, bound to the request.
Pairing linkOne link valid for 10 minutes with a secret in the fragment.Pairing needs y at the hub's terminal. A leaked link cannot finish it.
Approval signing keyThe owner's machine. Nodes hold the public key only.A grant is bound to one node, one prompt and an expiry. It is single use on its node.
A2A pairing keyA file you copy to each machine.Replace the file on every machine. The sources describe no revocation of a single machine's copy.
Webhook secretA bearer secret is hashed at the relay. A GitHub secret is derived from the relay's seal key.revoke_hook removes the hook. Rotating the seal key means re-creating every GitHub hook.
Your own relay's key and certificateIts data directory. Clients pin the certificate or trust a CA.Stop the relay, delete its api-keys and admin-api-key files (a new key) or its tls/ directory (a new certificate), start it and repeat the client step on each machine. Certificate files you provide can be replaced in place: the relay serves the new pair within about 10 seconds, without a restart. Each key in the file is its own account.
Release signing keyHeld outside the repository; the public keys are embedded in install.sh and nfltr update.The installer verifies the signature before the checksum.

Device compromise

What is compromisedWhat an attacker getsWhat limits it
A nodeEverything the node's OS user can read, including repositories, cloud credentials and the harness credential. Agents run as that user.Run nodes as a dedicated OS user or in a container. Keep the account key in memory only (the start environment), not in a readable file. Tool policy, the git allow-list and a PID namespace narrow what agents do. They do not bound an agent with a shell.
A node, and the account keyThe key acts as the account. It can pick its own agent id. There is no task-only credential.Revoke the key. This is audit finding F-20, residual.
The hub machineThe hub state file, which holds prompts and results in plain text, the hub identity key and the pin file.Disk encryption and file permissions are yours. Remote answers need a pinned device.
A paired phone (unlocked)It can answer open questions.Revoke it with nfltr orch hub devices --revoke <id>. Approvals need the passkey's user verification. Instructions need a passkey assertion and are accepted only in the all remote mode. The hub rate-limits each device.
A browser page served by a hostile relayPage code that holds device keys could use them.Hub pages run under a strict policy, but no code pinning exists. This is an accepted gap in the decision on remote access.
The relayIt can deny service, drop or delay frames, observe metadata and route frames inside an account. It cannot forge remote answers or approval grants. It could serve a tampered install.sh.Release signature checks in install.sh and nfltr update. A pairing key and end-to-end TLS inside sessions.

What persists locally

WhereWhatHow long
Hub state file and task store (SQLite beside it)Agents, turns, completions with delivered marks, monitors, hooks, schedules, remote requests; tasks and their events. The state file is plain text, mode 0600.The 128 newest settled agents are kept. Older ones fold into usage totals. The sources give no retention for the task store.
Hub monitor logs and pinsMonitor event lines; the device pin file.The per-event log append is not fsynced.
Hub secret filesHook secrets, as 0600 files beside the state.Until the hook is revoked.
Node task directoriesEach agent's working directory, including uncommitted files.Moved to a kept directory when the agent ends. Removed when the hub archives the agent, and by the node's next start. Kept directories have no size cap.
Node repository mirrorsBare mirrors of allow-listed repositories.They only grow (gc.auto=0).
Node approval ledgerOne file per spent approval grant.Pruned once the grant expires.
Artifact storeBlobs on the relay; the hub's own copy of artifacts.Blob retention is set by the relay and refreshed by each PUT.
Relay (hosted), as inventoried on 2026-10-03Accounts, key hashes, machine records, mailbox envelopes until acked or expired, weekly activity, page-visit counts, contact messages, service logs, daily database backups.Contact messages 180 days. Mailbox envelopes at most 7 days. Retention of the others is not confirmed in the sources.
Your own relayA data directory with its key, API-key hashes, certificate and file-backed stores.Yours. A data directory readable by group or others is refused.

Claude Code keeps its own session files on the machines that run it. The sources describe nfltr's directories, not Claude Code's files. See Known gaps.

Node impersonation and misuse of delegated capability

ThreatWhat stops itWhat does not
A second process claims an existing node id.The join check refuses it while the first node is listed. A takeover needs the same API-key hash. Another key gets AlreadyExists.A holder of the account key can take over its own account's ids.
A stolen key registers an agent that poses as a node.The relay derives identity from the key. Labels that grant capabilities are reset from flags on the node.The relay cannot tell a genuine node from any program that holds the account key: the threat model lists a compromised node as able to send any A2A frame and to control its labels and description. A pairing key adds proof that a peer holds the key, not which peer it is.
Another account reaches your node.Frames route only inside one account.Nothing between members of one account.
A peer in your account sends launch to your node.The node refuses senders that are not hubs. --serve-hubs and --require-pin narrow which hubs and machines.--serve-hubs is not an authentication boundary: any same-account sender can claim a hub id.
The hub (or a prompt-injected hub) asks for more than the node allows.Capabilities are node flags: repositories (--allow-repo), tool policy, max agents, monitors, file inspection. A task cannot raise allow_all_tools.A node with --allow-all-tools runs what the agent decides, as its OS user.
A rule a node denies must be lifted.An unless_approved rule lifts only with an owner grant: an ed25519 signature over node id, expiry and prompt digest, spent once on that node, also after a restart.The ledger is a directory of the node's OS user. An agent of the same user can read it.
Injected text steers the hub.Results, artifacts, monitor lines and webhook bodies are framed as untrusted. Owner messages use their own delimiters.The framing informs the model. It does not enforce anything. Node descriptions and resource labels are framed in list_nodes; list_worker_peers shows labels unframed (F-28).

Not guaranteed

Known gaps

These are open findings and claims the sources do not support. They are listed so you do not assume them.

Sources

Each claim above is a rule of nfltr's internal specification. The specification cites the code that enforces each rule and the test that protects it, and a check fails the build when a cited file or symbol no longer exists. The code, the specification, the security audit and the threat model live in private repositories, so this page states their rules instead of linking them. What is published:

Next