# Changelog ## v0.6.3 - 2026-10-27 v0.6.3. v0.6.2 was not published separately; its changes (below) ship in this release together with everything merged since. Also in this release. A native MCP server (beta, ADR 0028): `ferry mcp serve` lets an AI app join a project as an agent over stdio (join, catch up, orders, talk, primary, approvals), with a seat check, signed-only updates, redaction of outbound text, and approvals refused from agents (docs/MCP.md). Opt-in expiry for hand-ins and the outbox: a project's master can set `ferry channel ttl` and `ferry channel expire --dry-run` shows what would go; nothing expires by default (docs/TTL.md). A Hive rescue channel (opt-in, feature-gated crate `ferryman-hive-rescue`, `ferry rescue hive status|post|read|mock`): a cut-off master posts one sealed, master-signed custom_json to a fleet Hive account and cut-off machines verify it against the roster and add the rescue relay to the live iroh lane; signing uses k256 0.14 (crypto-bigint 0.7.10, RUSTSEC-2026-0387). Dependencies: zstd 0.14.3 (RUSTSEC-2026-0391, -0398 and zstd-safe advisories). Fixes: the setup dry-run snapshot test no longer flakes (#101), and the counter allocation lock waits up to 60 seconds (`FERRY_COUNTER_LOCK_SECS`) so a loaded machine does not fail it. A quiet worker no longer burns CPU re-reading what has not changed. The agent roster was read from disk on every ask (dozens per task, about 60 a second on a real fleet), each time opening every agent file, both pins of each agent and the whole fleet roster, and every signature was checked again on every pass. Rosters are now remembered while none of their files changed (name, size and modified time, the same rule the task cache uses, and never for a file written in the last two seconds), and a signature already found good is not checked again for the same key, signature and bytes. Pins still override a forged roster entry. On a 50-project root this was about half a core. Timed access for invited people (ADR 0029). Access an invitation gives is now either timed or open, and the master chooses on every invitation: `ferry team invite create` requires `--access ` (a machine added with `--as-identity` takes none; it lapses with its owner), and the dashboard's invitation form has a required "Access lasts" choice next to "Code valid for", which is the code's own lifetime as before (`--expires-days`, default 7). Timed access ends N days after the master's grant is signed, for the person and their agent alike, and counts as revoked everywhere a revocation does: the gate, workers, the iroh lane, delegations, the doctor. A few days before it ends (three, or a third of the term for terms under nine days) the master is asked Renew or Let it end, on Telegram and on the Team page; the answer is their signed answer, renewing one more term, and no answer lets it end. The agent loop unshares the Syncthing folder from the devices of people whose time ran out. `MasterGrant` gains a signed `expires_at` and `Invite` a signed `access_days`, both appended to the signed bytes only when present, so grants and invitations written before this verify unchanged. What had already synced stays on their disk, and secrets sealed to them should be rotated. New API: `POST /api/questions/{id}/answer`; `/api/team` rows gain `access_ends`, `access_lapsed` and `pending_access`; `POST /api/team/invite` requires `access_days` or `access_forever`. New projects reach every machine of the fleet, connectors are seated in them, and a running worker serves them without a restart (ADR 0030). A project made on one machine used to stay there: nothing filed it on another, the iroh lane only carries projects both sides already serve, and first contact needs the peer's worker on the roster on both sides. Now a machine's directory entry in the fleet folder also says where the project's clone came from (`git_remote`, credentials never published) and who masters it (`master`), and its machine record names its all-projects worker (a name and a public key) and what it excludes. Every worker pass puts the other admitted machines' workers on the rosters of the projects it serves, and files a project another admitted machine advertised (live, the fleet master's, not excluded here, not swarm-off) as a channel with both workers on its roster, then clones and attaches it, at most two clones a pass and each git command on a clock (`repos::Options::git_timeout`; a failing remote is retried after five minutes). All the new fields are signed and optional, so old records still verify and old binaries ignore them. `[seat] agents = ["claude-ai-josh"]` in the root's `ferry.toml` lists identities this machine serves to apps; the worker seats the key the machine already holds (another attachment or the machine key store, never a new one, never into a checkout git would commit, refused when two different keys exist) in every project it serves and in each fresh clone, and `ferry root seat ` / `ferry fleet setup --seat ` add a name and seat it everywhere at once. The grant still comes from the master (`team approve --all`). The running worker also says why a project that appeared after it started is not served yet (the first reason used to be recorded in silence), and adds the fleet-home route to the lane when the folder appears after the lane started. Lanes: several agents, one repository. `ferry lanes new --owns ` gives each agent or task its own git worktree (`/.lanes/`, branch `lane/`, excluded from the main checkout's status) and a signed record of the paths it owns in the project's channel; a per-worktree pre-commit hook (`extensions.worktreeConfig` and `git config --worktree core.hooksPath`) refuses staged paths another lane owns, allows and reports unowned paths and shared files (DECISIONS.md, README.md, CHANGELOG.md), and takes `FERRY_LANE_OVERRIDE=why` as a logged override. `ferry lanes land ` rebases the lane in a scratch worktree and fast-forwards its base, one landing at a time (an OS lock plus a signed claim in the channel); on a conflict it aborts, leaves the lane and the base untouched, lists the files and posts a notice to the lane's owner; `--push` uses the sealed `GH_TOKEN` from memory, handed to the one git process as `GIT_CONFIG_*` environment and never written. `ferry counter next ` hands out sequential ids (decision numbers, asset ids) through signed claim files: strictly unique on one machine, final across machines after a settle window, with a master-signed allocator or disjoint ranges where that is not enough. `ferry lanes list|enter|leave|retire`, `ferry counter list|check|policy`, lanes in `ferry root show`, and `ferry doctor` warns about two agents sharing a working tree and about stale or far-behind lanes. `ferry lane iroh` is unchanged. See docs/LANES.md and docs/adr/0031-lanes.md. ## v0.6.2 - 2026-10-23 v0.6.2 was not published separately; its changes ship in v0.6.3. Fleet hardening (v0.6.2). A worker that updates itself now comes back by itself, the iroh lane says when it is unhealthy, it works with or without a relay of our own, and `ferry lane iroh status` says what each peer is doing. Restart after a self-update, whatever supervises the worker. The worker used to stop with exit status 0 after installing a release, which `Restart=on-failure` and a Windows task's restart-on-failure read as "done", and with no supervisor it stayed stopped. It now asks to be restarted and `main` carries it out once its locks and lane are gone: on Unix it `exec`s the new binary in place with the same arguments (the same process id; a binary replaced while running is found at its plain path, not ` (deleted)`); on Windows under the scheduled task it exits 75 (`EX_TEMPFAIL`, which the wrapper passes on and the task restarts; the wrapper now sets `FERRYMAN_SUPERVISED`, and a wrapper written before 0.6.2, which does not, is recognised as the task by the wrapper script among the worker's parent processes and is marked in place when the worker starts and before it restarts, the old file kept as `ferryman-worker.ps1.before-supervised`, so the first auto-update on such a machine does not leave the worker stopped); on Windows with no supervisor it starts a detached copy that waits until a time five seconds ahead (`FERRYMAN_REEXEC_NOT_BEFORE`, a time that delays nothing once passed, so nothing the copy starts later waits), then exits 0. A fleet `restart-worker` order goes the same way. A loop guard in `reexec.json` (machine state) allows three restarts an hour, and none at all when there is no machine state directory to keep the count in; a fourth is refused and the worker carries on with the version it has. docs/FLEET_SETUP.md has the recommended supervisor settings (`Restart=always` for systemd; the task's restart-on-failure, 1 minute, 999 times, no time limit). Lane health alerts. The worker works out each pass whether a peer has failed every exchange for two minutes, has had no good sync for `alert_after_minutes` (default 15; counted from the lane's start for a peer never reached), or whether a relay is meant to be in use and none has connected for two minutes. A peer both failing and quiet is one alert. Each alert is raised once and cleared once (state in `lane-alerts.json`, machine-local): the worker logs it, `ferry doctor` has a `lane` line, `ferry lane iroh status` an `ALERT` line, the dashboard's `/api/fleet` a `lane_alerts` object, and the fleet gets `lane-health/.json` in the fleet folder, signed by the machine's own identity about its own machine only, listing the alerts standing now (an empty list is the all-clear). It is a folder of its own and not a notice kind: machines on 0.6.1 and 0.5.30 reject a notice of an unknown kind and show it as ignored, and they never read that folder. A machine the master has not admitted cannot write it and says so. There is no Telegram push: the Telegram bridge only relays orders and results. Alerts stand across a restart of the worker and are cleared only on positive evidence (a successful sync since the worker started, a connected relay, a good configuration), so a restart neither clears nor re-raises them; peers that come and go are named in `intermittent_peers` in `[iroh]` and never raised on, every other peer is expected to be on all the time. A faulty fleet relay list is an alert too (`relay-config`). Relays: ours, iroh's public ones, or none. `[iroh]` takes `relay_url` and an ordered `relays` list (ours, primary first, pinned by `relay_cert_sha256`), `fallback_public_relays` (off unless set: iroh's public relays are added only while none of ours has connected for two minutes, and are checked against the public CA roots while ours still need the pin), `direct_only`, and `alert_after_minutes`. With no relay named the lane has no relay and says so (it fails closed, as before); iroh's public relays are used only with an explicit `public_relays = true`. The relay token stays optional everywhere: it is put on the relays this machine's own `ferry.toml` names when the file exists, never on a public one and never on one that only the fleet's list names. The worker publishes the relay it is homed on in its TRANSPORTS record and republishes when it changes. The public-relay paths are covered by tests of configuration, the relay map and the certificate check, not exercised against n0's servers. A fleet relay list. The master signs an ordered list of relays into the fleet folder (`fleet/relays.json`, kind `relays`; `ferry fleet relays [list|set|add|remove]`, entries `https://host[:port]/` with `=` for a certificate no public CA signed). Every machine knows the relays named in its own `ferry.toml` first, then the list in the master's order, as candidates: iroh picks the home relay among them by latency and moves when that one stops answering, so the order is not a promise of which relay carries a connection. The worker republishes the relay it is homed on. Only the master's list counts (a file signed by anyone else, or edited, is ignored and `ferry doctor` says so); an entry that is not an `https://` address or has a bad pin is dropped. A pin in the list is the master's word for that relay's certificate and applies to that host alone, so a second relay with its own certificate needs no change on any machine but the master's; an entry with no pin must present a certificate a public CA signed. The list is read when the worker starts: a changed list is noticed and takes effect on restart (`ferry fleet order restart-worker`), and the worker says so. The list fails closed: one that is missing, unsigned, edited, empty or older (by `written_at`) than the last one accepted is not used, the last good list (`fleet-relays-seen.json`, machine state) stays in use or, with none, the lane has only the relays `ferry.toml` names or none, and a `relay-config` alert is raised; it never falls back to public relays, and an empty list cannot be published. With the fleet list and nothing in `ferry.toml`, the plan is the list, not iroh's public relays; `direct_only` still means none. Public relays remain an opt-in fallback (`fallback_public_relays`) and are off for a setup that pins a relay. Running a relay on a fleet machine is not built; see docs/adr/0027. `ferry lane iroh status` now prints, per peer: last success (and how long ago), last error (and since when it has been failing), whether the exchange went direct or through a relay and which, the version the peer runs (told in the exchange; older builds say none), and files each way. `--json` has the same fields, the alerts, the relays allowed and the version. ## v0.6.1 - 2026-10-21 The fleet has a home of its own, and a project can say where it is. `ferry fleet init` (the master, once) makes `/fleet/`, a synced channel with the Syncthing folder id `fleet-ferryman` that the iroh lane carries too, separate from every project, for what is true of the whole fleet: `directory//.json` (where a machine keeps a project: code, channel, hand-ins, data, env, work, placement, layout, live or archived), `machines/.json` (version, layout, root, OS, engines, projects served, last seen), `notices/` (layout-change, deprecation, update-available, migration, general, each with a severity and an optional expiry), `orders/` (fleet orders in the ordinary order format: update, migrate, restart-worker, to one machine or all), `releases/`, and a `librarian/` folder that only holds a README. A file counts only when it is signed, verifies against the key the folder's roster pins, and its signer has standing: the master for anything, a machine the master admitted (`ferry fleet admit`) for its own entries and its own layout-change or migration notices, never a Worker-level agent. An unsigned, forged, edited or unadmitted file is ignored by every reader, and `ferry doctor --layout` (the new `fleet` check) and `ferry doctor` (a `fleet_home` line) say what was ignored. A path in the directory is data: one with `..`, a control character, a UNC or device prefix, a drive-relative form, an alternate data stream or a reserved device name is refused, and nothing ever opens or runs one. No secret goes in the folder; a project's `.env` is recorded as a path only. The directory is one small file per project and machine, plus a master-signed `.override.json` that wins, so no file has two writers. `ferry where [--machine M] [--json] [--kind code|channel|handins|data|env]` prints that place and nothing else, from any folder (`--all` lists every project on every machine); this machine's own manifest answers for this machine, so the answer is right the moment a migrate finishes, and the directory answers for every other machine. `ferry where --session-hint ` prints a short block for a CLAUDE.md or AGENTS.md, and `ferry fleet write-hints` shows, and with `--apply` writes, that block between `` and `` in each clone's CLAUDE.md (made if missing) and AGENTS.md (where there is one): it touches only its own markers, keeps the file's line endings, refuses damaged markers, and never stages, commits or pushes. Every worker publishes its machine record and directory entries when it starts and on each pass, and `ferry enable`, `ferry root adopt`, `ferry root data` and `ferry root migrate --apply` publish at once; `root migrate --apply` updates the directory and then posts a signed layout-change notice listing every old and new path. A worker says each unread high-severity notice once, says when a live `update-available` notice names a newer release and whether this machine updates itself (`FERRYMAN_NO_AUTO_UPDATE`), and acts only on fleet orders the master signed: `restart-worker` stops it between passes for its supervisor, `update` installs the newest release unless auto-update is off here, and `migrate` is reported and left to a person, since a migrate refuses to run while a worker does. `ferry fleet init|admit|publish|notices|notice|order|orders|machines|write-hints` is the command set, and the dashboard's Fleet page shows the notices, the machines (version, layout, last seen) and a directory you narrow with the filter box. Release requests, approvals and denials are written to `fleet/releases/` when the root has the folder, and read from there and from the `ferryman` project channel's `release/`; the fleet folder's copy wins a tie, the old place is kept current too while older machines are about, and nothing is deleted from it. The pinned keys in `comms/agents-pinned/` are mirrored into `fleet/local/agents-pinned/` (the old copy stays the authority while it holds a pin, and is never removed); `comms/runtime/` is not moved, because its worker locks only exclude a second worker if every binary looks in the same place. The folder is additive, and a binary that does not know it never reads it. Each project also gets a `data/` place, `projects//data` by default (the same place in layout 1 and 2), for a database, a model or a dataset. It is machine-local and never synced: the Syncthing guard refuses it and any folder that holds it or lies inside it, wherever it is recorded; the iroh lane keeps it out of a share it sits in and exchanges nothing if it is the share; `ferry enable --data PATH` and `ferry root data [PATH|--reset]` record another place and refuse one inside or around a synced folder; and `ferry doctor --layout` has a `data` check and `--fix` makes the folder. See docs/adr/0026-a-home-for-the-fleet.md, docs/LAYOUT.md, docs/FLEET_SETUP.md and docs/IROH_LANE.md. ## v0.6.0 - 2026-10-18 `ferry release status` no longer refuses a release because a request names a commit by its full SHA and the check spelled the current commit short (or the other way round). `release prepare --commit ` wrote the full SHA into the request, `release status` compared it as text with `git rev-parse --short HEAD`, and the two never matched, so the approval looked stale. Both sides are now resolved to full SHAs with `git rev-parse` before they are compared, in the approval check and in the denial check, and a commit that cannot be resolved is never a match. Only a commit's own name is resolved (4 to 40 lowercase hex digits): `main`, `HEAD`, a tag or any other ref moves, so it is never a match, not even against itself. The signed request, approval and denial formats are unchanged. An idle worker no longer re-reads and re-verifies every file on every poll. A pass used to open, parse and signature-check every file of every task, and to read the roster, the master and the grants again for each task. Now the roster, grants and master are looked at once per pass, and a task folder whose files have the same names, sizes and modified times as the last read, under the same roster, Worker set and control files, is served from this machine's memory (`readcache`; machine state, never the channel, gone on restart). A change to the roster, a key, a grant, a revocation, a delegation, the head or the master changes the trust key, so every verdict is judged again; a file written in the last two seconds is never remembered; a task with an offer or a delegated review is read afresh each time. On a synthetic channel (100 tasks, 400 signed files) one poll fell from 3,675 ms to 94 ms, and one poll over 1,500 tasks (6,000 files) now takes about 820 ms. The iroh lane's BLAKE3 manifest already keyed hashes by path, size and modified time; it now also forgets files that have gone and does not remember a file written in the last two seconds. `cargo test -p ferryman-channel --release --test read_bench -- --ignored --nocapture` measures it. An iroh-only channel no longer talks about Syncthing. `ferry channel send` said "Syncthing could not confirm it syncs this channel, so it may not leave" and waited about two seconds on a Syncthing that does not run (the wait is Windows retrying a refused connection to its API), and `ferry channel status` named a Syncthing folder, even on a channel set up with `ferry enable --no-syncthing`. When this machine's iroh lane carries the channel (the lane is on in `ferry.toml`, the machine has an iroh key, and the channel holds a roster-honoured transports record naming it), `send` asks Syncthing nothing, does not wait, and says the iroh lane is carrying it, how many peers have synced it, or that the worker's lane has not run lately and the message goes out when it does; `channel status` gains a `transport` line and says Syncthing is not used when it has no folder for the channel; `channel order` prints the same line. A channel Syncthing carries behaves as before. One project's memory no longer reaches another project's prompts. A fleet worker is one agent, with one key, on every project's roster, and the signature on its profile (`memory-bank/agents/.md`, the "what you have worked on in this project before" block) named the agent and the bytes but not the project, so a copy of one project's profile in another's memory bank verified there and went into that project's prompts (quantly's into iroh-test's). New profile signatures (`ferryman-profile/v2`) cover the project id, so a profile signed for one project does not verify in another, for the agent's own profile and for the peer profiles and routing hint built from the bank; a profile signed for another project is set aside (`*.other-project-