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
- Results arrive once, side effects do not. The hub receives each agent result exactly once, even across a hub crash, a relay restart or a node loss. What an agent did to the outside world (a pushed branch, an API call, a sent message) is not made exactly once by nfltr. What exactly once means.
- The relay carries messages and decides nothing. It runs no model, holds no task content by default and derives identity from the credential, never from what a client says. With end-to-end encryption on, it sees who talks to whom, when and how much, not what. Threat model.
- Most reliability evidence comes from stand-ins. The fault-injection soaks, the chaos lab, the hourly hosted test and the load measurements use a scripted stand-in for Claude Code. They test nfltr, not the model. No formal model checking is used. Evidence.
- Your machines are the trust boundary. An agent runs as the OS user of its node. Whoever controls a node controls what its agents can reach. Whoever holds an account API key can act as that account. Trust boundaries.
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)
- Hub. A Claude session with the hub tools. It decides which agents to start, where, and what to do with results. It keeps its state in a file and a task store on its own machine. It is the only owner of task state.
- Relay. Transport and directory. It routes frames between authenticated agents of one account. It also stores mailboxes and blobs, and answers presence queries.
- Node. A machine you joined with
nfltr node join. It connects out, so it needs no open port. It starts agents only when a hub of the same account asks, within the limits its operator set. - Agent. A worker process on a node that runs Claude Code for one piece of work. It clones the repository it needs, works, pushes its own branch and reports.
- link. The data-plane library under tunnels and TCP sessions. It multiplexes sessions over one connection with per-session flow control. Orchestration frames travel over gRPC streams to the relay, not over link.
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.
- The hub records the turn.
spawn_agentwrites the agent and its turn aspendinginto 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. - The hub asks a node for a slot. It sends a
launchrequest 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. - 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.
- The hub assigns the task. It sends
task.assignto the worker through the relay. If the worker is not yet registered, the relay waits up to 30 seconds, then answersNotFound. 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 turnrunningand saves. - 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.
- The agent reports. It sends
task.resultortask.error. The worker keeps the terminal frame and replays it on every reattach until it gets a matchingtask.result_ack. - 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_terminalevidence. Only the current{owner, epoch}may write a task. - The hub completes the turn. The first recorded outcome wins and takes the next sequence number. The hub saves it as undelivered.
- The hub delivers.
wait_for_agentsmarks 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:
| Failure | Result |
|---|---|
| Relay restarts or is killed | Streams drop and redial under the same instance id. Agents keep running and replay buffered progress and the retained terminal. |
| Hub killed | The 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-C | Agents fail their tasks node_lost, wait for the ack and exit. |
| Node killed or crashed | Agents 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 window | The 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 arrives | The 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.
- A slow or stuck reader stalls only its own session. Each side announces a window per session and a sender may have at most that much unacknowledged.
- A dead peer surfaces as an error on every session with it, on both ends, within
Interval + Timeoutof its last frame (4.5 s to 60 s by preset, 15 s by default). io.EOFmeans the peer ended its writes on purpose. Any other cut reads an error.- Bytes of one session arrive in order and intact.
- The wire is held byte for byte by test vectors and a recorded session. A change is a new version negotiated in the hello.
The relay
- Identity comes from the credential, never from client metadata. The derived agent id overwrites any client-supplied one.
- A frame routes only between agents of one account (the source's key namespace or an agent of its owner). A refusal looks like a missing target.
- The first frame of
task.assignis delivered at most once. - After a worker's
agent.drainis acked, no new call reaches it. Calls to a draining or departed id getUnavailableand are not delivered. - A hub refuses every task route the relay's own agent asks. The relay cannot read a hub's run through the task API.
- Mailbox, presence and blob contracts: see Contracts per primitive.
- A relay restart ends no running agent. Registrations are not kept, so every client redials under the same instance id and the relay treats it as a reconnect.
Orchestration: hub, nodes, agents
- Exactly-once acceptance. A task has at most one accepted terminal result.
- Exactly-once delivery. Each completion, late result, owner message and hub event reaches the hub model once across restarts.
- First answer final. A turn's first outcome wins. Superseded attempts are never revived.
- A turn runs once. A send the transport cannot confirm is never re-sent. The hub asks the worker instead.
- Fenced ownership. Only the current owner and epoch may write a task. A stale writer is rejected and counted.
- Ordered event cursor. Per-task event
sequencestarts at 1 with no gap or repeat. - Liveness. Every turn ends: completed, failed, cancelled or
node_lost. The fault soaks require it within 2 minutes of a fault. - Hub state survives a machine crash. A save that returned survives power loss. The hub acks a mailbox or monitor event only after it saved.
- Node capabilities cannot be widened by the hub. The labels a node advertises are reset from its flags, so
--labelscannot fake them.
What "exactly once" means
nfltr uses "exactly once" for three narrow things. It never uses it for what an agent does.
- 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.
- Delivery to the hub. The hub marks a completion delivered and saves before
wait_for_agentsreturns 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. - 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:
- Agent side effects are not exactly once. The hub chooses to retry. After
node_lostor a freeze, it starts a new attempt while the old process may still be running. A superseded attempt that the hub cannot reach keeps running and can push, write or call out until it is stopped. nfltr gives agents no mechanism to deduplicate external effects. Use idempotent operations (a branch named by the task, a request with an idempotency key) in what agents do. - Delivery to the hub is not processing by the model. Without
ack, a returned completion counts as delivered. A program client that passesackgets the completion again until it acknowledges, which is at-least-once for that client. - Mailboxes are at-least-once. The hub turns that into once by saving a cursor before acking. A reader of a mailbox that does not do this sees repeats.
- A duplicate result is kept, not hidden. A late or second terminal from a superseded attempt is delivered once with
superseded: true. It is evidence for the hub, not an outcome.
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 evidence | What it shows | What 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:
- No measurement with a real model. The recorded reliability rates were taken with the stand-in. The sources contain no failure rate of agents running real Claude Code.
- No production history on your own relay. It is a beta. A smoke test covers its feature matrix and restart. It has one process and no failover.
Trust boundaries
Who trusts whom, and what each party can do if it misbehaves.
| Party | Trusted to | Not trusted to | Control |
|---|---|---|---|
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. |
| Relay | Authenticate 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 provider | Run 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 repositories | Nothing. | 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 host | Serve 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:
- Hub to relay. The hub refuses every task route the relay's own agent asks. Hubs, nodes and the other samples do not accept relay-originated plaintext requests, so the relay cannot start work on a node by posing as its own agent. A worker accepts relay-owned dispatch (
nfltr orch task) unless a pairing key is set. - Node to agent. The node sanitises the agent's environment, uses a clean Claude config unless
--inherit-host-claude-configis set, can apply a tool policy and can run agents in a PID namespace (Linux). The relay account key is held in memory and is unreadable by the agents of a node started from its environment. A readable key file is not protected:nfltr node joinwarns when it finds one. - Agent to model. The agent's Claude Code process holds the provider's credential. It sends prompts, repository content and tool output to the provider. What the provider keeps is the provider's policy, not nfltr's.
- Agent to tools. An agent with
--allow-all-toolsruns commands as the node's OS user. The node's tool grant is the boundary, not the git allow-list.
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)
| Question | Contract |
|---|---|
| Backpressure | Each 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. |
| Ordering | Bytes of one session arrive in order. There is no ordering between sessions. |
| Cancellation | Reset 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. |
| Termination | io.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 peer | Detected within Interval + Timeout. Idle connections cost a few bytes. |
| Limits | Sessions 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 relay | A 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 streams | When 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.
| Question | Contract |
|---|---|
| Delivery | At least once. A pull never acknowledges. An envelope has a relay-assigned increasing seq. |
| Lost acknowledgement | The 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 gone | Envelopes stay until acknowledged or expired. A reader that is away loses nothing before expiry. A full mailbox refuses the append with MAILBOX_FULL. |
| Expiry | A 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 sender | The 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. |
| Limits | Payload 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. |
| Webhooks | A 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 use | The 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.
| Question | Contract |
|---|---|
| What it is | A snapshot of connected agents plus a cursor taken before the snapshot. Watching from that cursor misses nothing. |
| Staleness | An 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 dead | A 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. |
| Feed | Watch 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 reset | If 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 replicas | With 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. |
| Labels | Labels 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.
| Question | Contract |
|---|---|
| Authorization | A 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. |
| Integrity | The 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 uploads | Parts 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. |
| Expiry | A 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 limits | A 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.
| Class | Meaning | Examples |
|---|---|---|
| Transient | The 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). |
| Permanent | The 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-safe | The 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:
- An unconfirmed send is never re-sent. It becomes a resumable
stream_errorand the hub adopts the task. - A terminal failure code can be revived by later worker evidence only for codes in the revivable list; every other failure is final. The catalog marks each code's
RetryableandRevivablevalues. - A client redials on every error except a closed fatal list (identity refused, version refused, invalid configuration, session replaced, own context cancelled). Error text is never consulted.
- A result that misses the hub's declared output fields fails as
contract_violation, with the answer kept as the partial result.
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
| Party | Sees task content (prompts, results, repository text)? |
|---|---|
| The hub machine and the hub's Claude session | Yes. It sends the prompts and receives the results. |
| A node and its agents | Yes, for tasks they run. |
| The model provider | Yes, 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 agent | Yes. A2A payloads are plaintext to it. |
| The dashboard | No. 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 browser | Owner 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
| Key | Where it lives | Revocation or rotation |
|---|---|---|
| Account API key | Your 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 session | A 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 keys | A 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 passkey | Browser storage (non-extractable WebCrypto) and the platform authenticator. | Approvals and instructions need a passkey assertion with user verification, bound to the request. |
| Pairing link | One 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 key | The 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 key | A file you copy to each machine. | Replace the file on every machine. The sources describe no revocation of a single machine's copy. |
| Webhook secret | A 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 certificate | Its 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 key | Held 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 compromised | What an attacker gets | What limits it |
|---|---|---|
| A node | Everything 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 key | The 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 machine | The 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 relay | Page 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 relay | It 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
| Where | What | How 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 pins | Monitor event lines; the device pin file. | The per-event log append is not fsynced. |
| Hub secret files | Hook secrets, as 0600 files beside the state. | Until the hook is revoked. |
| Node task directories | Each 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 mirrors | Bare mirrors of allow-listed repositories. | They only grow (gc.auto=0). |
| Node approval ledger | One file per spent approval grant. | Pruned once the grant expires. |
| Artifact store | Blobs 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-03 | Accounts, 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 relay | A 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
| Threat | What stops it | What 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
- Exactly-once side effects of agents. See What exactly once means.
- That a model does what you meant. nfltr runs the agent and delivers the result. It does not check the answer against the goal, except for the output fields a hub declares.
- Availability of one relay. nfltr.xyz runs as a single relay host. A deploy window is down for 30 to 90 seconds. Running agents continue and reconnect. Your own relay is one process with no failover.
- Delivery by the relay when it is hostile. It can drop or delay frames. It can always deny service.
- Confidentiality against a relay that substitutes keys, unless you set a pairing key. And a pairing key does not distinguish machines.
- A boundary between agents and their node's OS user. Same-user agents can read the user's files. The tool policy and the git helper are not hard boundaries for an agent with a shell.
- Ordering between sessions, mailboxes or tasks. Order is per session, per mailbox
seqand per-task eventsequence. - Clock-independent timing in all cases. Leases and bounds use durations. There is no unit-level clock-skew test.
- Retries by nfltr. The hub decides how and whether to retry. Startup recovery only re-attaches and never starts a replacement attempt.
- Agents other than Claude Code. The hub is a Claude session and agents run Claude Code. The spec describes what a second runtime must implement and marks its adapter seam as unverified in production dispatch.
Known gaps
These are open findings and claims the sources do not support. They are listed so you do not assume them.
- No task-only credential (audit F-20, residual). An account key can pick its own agent id, and an agent of the same OS user can read the operator's files.
- Open findings of the security audit: F-19 per-agent metric label cardinality, F-18
GET /logout, F-24 admin-list fail-open and a full key scan per invalid key, F-26 limiter map caps, F-33trust_loopbackbehind a reverse proxy, F-25 and F-27 WebSocket limits, F-36 minor items. The audit is the source of truth; this list is as of the date above. - Per-machine keys. The A2A pairing key is shared by all machines. Per-machine signing keys pinned on first use are the stated next step.
- No revocation of a single machine's A2A pairing key. The sources describe replacing the key file everywhere. They name no test for
nfltr orch hub devices --revoke. - Blob quota and retention. The sources give object and part sizes and a retention that every PUT refreshes. They do not give a per-account total quota or the retention period.
- Hosted retention and backups. The sources record a daily relay database backup and name retention values marked "to confirm" (account rows, logs, the backup lifecycle rule). This page makes no promise about them.
- Claude Code's own files on nodes and the hub are not described by the sources.
- No formal model checking. The strongest check on the fenced task stores is the linearizability test.
- No real-model failure rates. All recorded rates used the stand-in.
- link's own targets. 24 hours of cumulative fuzzing and soak are targets, not results. The API is pre-1.0.
- Disk full. A task-store write that hits a full disk fails and is logged, but is not classified as a disk-full error. A node has no free-space floor before it creates a mirror or keeps a directory.
- The monitor log's per-event append is not fsynced. A host crash can lose the newest unpulled lines.
- Clock skew. Covered only by the chaos lab, not by a unit test, and not for the hub.
- Your own relay (beta). There is no dashboard, WireGuard tunnel, Postgres or Redis, or automatic certificate; share links need a domain of yours and a wildcard certificate on the machine that shares; it has one process.
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:
- Reference pages generated from code and kept current by the build: the hub tool descriptions, the task error codes and the task event types.
- The hub tools page and the CLI reference, every command and flag named here.
- End-to-end encryption: what the relay can and cannot read.
- Protocol packages named on this page:
nfltr.mailbox.v1,nfltr.presence.v1,nfltr.blob.v1and theErrorReasonenum of the relay protocol; link's wire format and itspeerlinkcontract.
Next
- Join Machines as Nodes: every node flag.
- Run Your Own Relay (Beta): step by step.
- Use nfltr from Claude Code: the hub inside your own session.