fleet-deploy
August 10, 2026 · View on GitHub
You fixed something. It has to land on eleven machines - laptops, a couple of servers, the box in the other office, the two Macs. You copy it around, you post "please apply this", and a week later you find out that four of them never did, one applied a half-broken version, and nobody can tell you which four.
The usual answer is a config management tool, and if you can run one, run one. This is for the other case: a small heterogeneous fleet, no agent to install, no inventory server, machines that are asleep half the day - and a change that has to be provably on each of them.
The tool is one stdlib-only Python file. The idea it enforces is one sentence:
"Sent" is not "done", and silence is not consent.
A change counts as rolled out on a machine only when a command ran on that machine and read back a fact - a hash, a value, a marker. Everything else gets counted in a separate column, out loud.
Built and running daily at Palo Alto AI Research Lab, where a fleet of autonomous agents rolls its own fixes to itself.
$ python fleetdeploy.py board
FLEET ROLLOUT BOARD 2026-08-10T18:13:39Z
rolled out (PROVEN by a machine reading the fact): 1 of 2 node-parcels
still pending: 1
!! note-v2 heartbeat note v2
wave 0: HUB-1=applied
wave 1: NODE-2=PENDING
NODES
HUB-1 last seen 0.0h ago
NODE-2 last seen 0.0h ago
behind on: note-v2(pending)
NOT DONE. `done` means every target is applied or explicitly not-for-me.
Exit code 1. It stays 1 until the last machine reads the fact back.
See it work in 30 seconds
git clone https://github.com/tonydzi/fleet-deploy
cd fleet-deploy
python examples/demo/run_demo.py
Two simulated machines in a temp directory, a real (harmless) artifact, and every interesting moment printed: the second node refusing to jump the queue, the canary proving the change, a double-apply doing nothing, the board going green. Nothing is installed outside the temp dir.
The part that matters
The plumbing - copy a file, run a command - is the easy half. Here is the half that cost us months.
1. A verify that reads a flag is not a verify
Our first version had a done-marker that looked byte-for-byte identical whether a machine
had verified the fact, a human had said "yes I did it", or someone had passed --force and
skipped the check. Four code paths wrote that marker, each with its own wording.
So when we said "rolled out to every machine", we could not defend the number. When we finally split it, a large share of our historical "applied" turned out to be nothing of the kind.
Now there are two words and they never get added together:
| applied | a machine command read a fact on that node and exited 0 |
| claimed | somebody says so: prose steps, a hand-confirmation, a no-op check |
claimed is legal - work that needs human hands exists, and hiding it would be worse. It just
does not get to inflate the number, and it never unblocks the next wave.
2. An apply that does not deliver the artifact cannot be registered
This is the specific bug that produced 128 stuck parcels in our fleet. A parcel said the file would arrive over the sync share. For one node, that share did not exist. The parcel was undeliverable by construction - green at registration, impossible at the far end - and it sat there, along with the next hundred like it, until a node refused to fake a verify.
Registration now blocks:
- a step that promises delivery ("will arrive via sync") while shipping nothing;
- a step that calls a file existing neither locally nor in the payload - the parcel that quietly assumes;
- a step hardcoding
/home/OPERATOR/...orC:\Users\...while targeting other machines; - PowerShell aimed at a Linux node,
chmodaimed at a Windows one - judged by the OS mix of the targets, because one non-Windows target breaks a PowerShell parcel exactly as badly as ten; - a node name the registry does not know. A typo used to create a queue file that no machine ever reads: invisible by construction, and nobody owns the gap.
Every gate has a named escape hatch, and every escape prints a loud line. A bypass that leaves no trace brings back the whole class.
3. Canary first - "all machines at once" is itself the failure
We spent months building watchdogs against common-mode failures while creating one by hand every time we pushed a change to five machines simultaneously. When it broke, it broke everywhere at once, and there was no healthy machine left to repair from.
wave 0 one canary, SAME class as the real consumers
wave 1 a node of a DIFFERENT class or OS <- mandatory, not a nicety
wave 2 everybody else
A later wave cannot start until the earlier ones are applied. Three details we paid for:
- the canary must match the consumers' class - a headless box is a bad canary for a bug that only bites in a desktop session, and the reverse;
- hence wave 1 is deliberately a different class: the canary shares the canary's blind spots, including its warm cache, so the cold-start surprise lives in wave 1;
- if the change touches a lock, a lease, a shared counter or one database, a single run is
blind to races -
--concurrentputs two nodes in wave 0.
And the rollback is named before the rollout, at registration, or it is not a rollback - it is an improvisation you will attempt on a broken fleet at 2am.
4. A node has the right to say "not mine"
python fleetdeploy.py not-for-me gpu-driver-bump --reason "no GPU on this box"
Without that, unapplicable parcels pile up and the board is permanently red - and a permanently red board trains its readers to stop looking, which is worse than having no board. The reason is mandatory: an unexplained refusal is indistinguishable from a machine that quietly stopped applying things.
5. "That node is dead" expires
One refused connection once became a line in our notes, and a perfectly healthy machine was excluded from every rollout for weeks - because the note was the reason nobody retried. When somebody finally did, it worked on the first attempt in ten seconds.
python fleetdeploy.py node-down NODE-5 --reason "ssh refused" --recheck "ssh node-5 true"
The recheck command is required, and the verdict expires in 30 days. After that the board stops honouring it and tells you to re-measure. A negative result is a measurement taken on one day, not a property of the world.
Usage
python fleetdeploy.py init # create the bus + a starter fleet.json
Then describe your machines in fleet-bus/fleet.json - class and os are what the canary
picker reasons about:
{
"nodes": {
"HUB-1": {"os": "windows", "class": "desktop", "aliases": ["hub", "hub-1.local"]},
"NODE-2": {"os": "linux", "class": "headless", "aliases": ["vps"]},
"NODE-3": {"os": "darwin", "class": "desktop", "aliases": []}
}
}
Register a parcel on the hub. install and check-file are built in so you do not have to
guess whether the receiver speaks cp or copy:
python fleetdeploy.py register watchdog-v3 \
--title "watchdog v3 (fixes the restart loop)" \
--payload ./dist \
--apply '"$PYTHON" "$FLEETDEPLOY" install payload/watchdog.py "$FLEET_HOME/bin/watchdog.py"' \
--verify '"$PYTHON" "$FLEETDEPLOY" check-file "$FLEET_HOME/bin/watchdog.py" --md5 4f2b...' \
--targets all \
--rollback 'reinstall watchdog v2 from parcels/watchdog-v2/payload'
On each machine:
python fleetdeploy.py status # what is waiting for me, and what I am waiting on
python fleetdeploy.py apply watchdog-v3
And from anywhere:
python fleetdeploy.py board --html board.html # who is behind, by name
python fleetdeploy.py audit # re-read the fact for things already applied
audit is not optional in the long run. A marker short-circuits every check forever, so a
parcel that was applied and then quietly reverted stays green until something asks again. We
found one live: the marker existed, the file it installed did not.
$VAR and %VAR% both work, everywhere. The engine expands its own placeholders
($PARCEL, $PAYLOAD, $FLEET_HOME, $FLEET_NODE, $FLEET_BUS, $PYTHON, $FLEETDEPLOY)
before handing the line to a shell - because $X is not a variable to cmd.exe and %X% is
not one to sh, and a parcel is one string that has to run on both.
Transport
The engine never opens a socket. The whole system is one directory - the bus - and making that directory look the same everywhere is your existing plumbing's job. Three drivers are included, all stdlib, all short enough to read:
| transports/fileshare.py | you already sync a folder (Syncthing, Dropbox, NFS, SMB) - adds the delivery receipt a share does not give you |
| transports/ssh_push.py | the hub pushes, applies remotely, and pulls the proof back |
| transports/git_bus.py | the bus is a git repo: history and an audit trail for free |
See transports/README.md for the contract if you want to write your
own. One rule is not optional: never --delete another node's state directory - you would
erase the evidence that it was already done.
Install
Copy fleetdeploy.py to your machines. That is the install. Python 3.8+, no dependencies, no
daemon, no database, nothing to keep running.
Docs
docs/ROLLOUT-MODEL.md- the four states, what makes a parcel provable, the wave rules, exit codesdocs/GOTCHAS.md- 16 dated field notes, each one a bug we shipped firstdocs/SECURITY.md- read before pointing this at real machines: a parcel is remote code execution, so whoever can write to the bus owns your fleetPROMPT.md- paste into Claude Code or Codex and it wires this into your fleet
Tests
python tests/test_fleetdeploy.py # 35 checks, stdlib unittest, no network
The header of that file lists two one-line mutations that must turn the grid red. Run them before you trust it: a suite that cannot fail on broken code proves nothing.
License
MIT.
Part of the kit series by Palo Alto AI Research Lab. Closest neighbours:
verified-ops-starter - same discipline,
different half of the job: its rollout check asks whether a fix is present on each box
(hub-side, read-only, no state), while this kit runs the rollout - gates, payload, canary
order, per-node markers, refusals. Use that one to audit a fleet you deploy to some other way;
use this one when you want the deploy itself to refuse to lie. Also
claude-consensus
(how the machines agree on what to roll out before anyone rolls it), and
agent-approval-gate (when a parcel needs a
human's + and nobody is at the terminal). Also
telegram-mcp-kit,
whatsapp-mcp-kit and
mcp-daemon-diet. Questions, or a step that
does not work? Open an issue - we answer within 24h.
Publishing your own internals? This repo was sanitized for release with
oss-publish- our substitution pipeline: personal data is replaced by plausible fakes of the same shape (never<REDACTED>), and a fail-closed gate re-scans the whole tree before the push. Free, MIT.
🧰 Connector & Ops Kits
Eight kits, all published 2026-08-10, each lifted out of the same live fleet after it survived production rather than written as a demo. They are independent: take one, ignore the rest. All stdlib-only Python, all free.
| kit | what it solves |
|---|---|
telegram-mcp-kit | Connect your agent to your own Telegram account in ~15 minutes, with the production patches and every gotcha |
whatsapp-mcp-kit | Link WhatsApp, using a live self-refreshing QR page that makes pairing actually work |
mcp-daemon-diet | One shared MCP daemon per machine instead of a stdio copy in every session, with a watchdog that will not blind your live sessions |
agent-approval-gate | Your agent needs a human's OK and nobody is at the terminal: the ask goes to a messenger, the answer comes back into the run |
fleet-deploy | Roll a fix to N machines and prove it landed on each one: canary waves and a verify that must read a fact back |
secondop-panel | Nobody reviews themselves, and one reviewer model is one blind spot: fan a change out to several model families with quorum and honest skips |
oss-publish | Open up internal work without leaking it: plausible substitutions of the same shape, then a fail-closed gate over the whole tree |
llm-spend-audit | What your own wiring charges on every session, and which paid subscriptions are going undrawn |
🧩 One piece of a working system
This repository is one piece lifted out of a live operation: one non-technical founder, an AI cofounder, and a fleet of machines that reach consensus with each other and wake the human only for money or the irreversible. It was extracted after it survived production, not written as a demo — and it runs on its own: nothing here phones home to the rest.
See how the whole thing fits together → SYSTEM.md
AI contributors
This project is built by a human + AI team, and the git log says so: Claude writes most of the code, Codex and Grok review it, Gemini feeds the research. Each is credited on a commit only if its output changed that commit's content — no decorative credits. Lab-wide policy, one source for every repo: AI-CONTRIBUTORS.md.