docs/AGENT_QUICKSTART.md
Rama main · réplica de lectura del repositorio original.
# Ferryman for agents
**You are the intended reader.** This page is written for an AI agent that has been
told to put a project on Ferryman, with no human available to answer questions. Every
step below is non-interactive, idempotent, and safe to retry.
If you are a human: this works for you too, but [the README](../README.md) is friendlier.
---
## What you are setting up, in one paragraph
Ferryman lets agents on different machines hand each other work without a server. An
order is a file, a result is a file, a review is a file; Syncthing copies the folder
between machines. You will install a binary, run one command in the project directory,
and end up with a signed identity on a channel. Nothing you do touches the project's own
code or git history.
## Step 1 — install
One line, no toolchain and no compile. On macOS or Linux:
```sh
curl -fsSL https://raw.githubusercontent.com/estejosh/ferryman/main/scripts/install.sh | sh
```
On Windows, in PowerShell — the line above cannot run there, because a default
Windows machine has no `sh`:
```powershell
irm https://raw.githubusercontent.com/estejosh/ferryman/main/scripts/install.ps1 | iex
```
Both verify the release checksum before installing, and install for the current user
without asking for administrator rights.
Both verify the download's SHA-256 before installing anything, and abort on a mismatch
rather than warning. If you are tempted to work around a checksum failure: don't, and
report it.
Building from source also works, and needs a Rust toolchain plus, on Linux, D-Bus
headers for the OS credential store (`libdbus-1-dev pkg-config` on Debian/Ubuntu,
`dbus-devel pkgconf` on Fedora):
```sh
cargo install --git https://github.com/estejosh/ferryman ferryman-cli
```
Or the container, which carries everything including Syncthing:
```sh
podman run -d --name ferryman -v ~/ferryman-channels:/channels:U \
-p 22000:22000/tcp -p 22000:22000/udp ghcr.io/estejosh/ferryman:latest
```
Check it:
```sh
ferry --version
```
## Step 2 — enable the project
From inside the project directory:
```sh
ferry enable --email you@example.com --json
```
The email is required. Free production use is conditioned on registering one
(LICENSE section 3), and it is how the Licensor counts Seats and can tell you when a
deployment goes over. **If you do not have an address to give, ask your human for one
— do not invent one.** `FERRYMAN_EMAIL` works instead of the flag.
Nothing about your code, your channel, or your agents' work is ever transmitted. See
[PRIVACY.md](../PRIVACY.md) for the exact payload, and run
`ferry license checkin --dry-run` to see it before anything is sent.
That is the whole setup. It prints one JSON object:
```json
{
"enabled": true,
"project": "myproject",
"agent": "wisp",
"channel": "/path/to/myproject/.ferryman/ferryman",
"syncthing_folder_id": "myproject-ferryman",
"review": "confirm",
"public_key": "710836e3...",
"already_configured": false,
"files": [{ "what": "channel config", "path": "...", "created": true }]
}
```
Read `already_configured` to tell "I just set this up" from "it was already done". If
you cannot remember whether you ran this, **run it again** — it will not overwrite an
edited config and will not rotate your signing key.
Useful flags, all optional:
| Flag | Default | Why you would change it |
|---|---|---|
| `--project` | the directory name | The directory is named something unhelpful |
| `--agent` | this machine's name | Several agents on one machine |
| `--role` | `worker` | This one hands out and reviews work: use `orchestrator` |
| `--command` | `claude` | You run a different agent CLI; known engines (`opencode`, `codex`, …) get their correct non-interactive args written automatically — see [ENGINE_SETUP.md](ENGINE_SETUP.md) |
| `--email` | *(required)* | No default is possible; ask your human |
| `--review` | `confirm` | See the risk section below |
| `--dashboard` | off | Mark that the human wants the web dashboard (`ferry dashboard`) to approve work from a browser |
| `--dashboard-operator` | email local part | The operator's username, for the interactive prompt only |
Exit code is 0 on success, non-zero with a message on stderr otherwise. It never
prompts, never opens an editor, and never waits on a terminal.
Right after enabling, run:
```sh
ferry doctor
```
It checks what the first task would otherwise discover the hard way: the config
parses, the engine resolves on PATH, the signing key and roster entry exist,
Syncthing answers. Every failing check states its remedy. A missing engine is
the single most common failed first task — enable warns about it in its output,
and doctor confirms the fix worked.
The dashboard operator's password is **never handled by an agent**. `enable` does
not accept one, and you must not type one into a flag, an environment variable, or a
config file. When the human wants a dashboard, run `enable --dashboard`, then start
`ferry dashboard` and hand the human the URL: they create their operator (name +
password) in the browser, out of your sight. A human running `enable` themselves at a
terminal is instead prompted for the password, typed without echo.
## Step 3 — join the other machines
**`enable` already did this.** It registered the channel folder with the local Syncthing
and shared it with every device that Syncthing already trusts. Check `syncthing` in the
JSON:
- `available: false` — Syncthing is not installed or not running. The channel still
works locally. Report the `note` field to your human; installing Syncthing is not
your call to make silently.
- `shared_with: []` — nothing else is paired with this Syncthing yet. Normal on a first
machine.
- `device_id` — this machine's Syncthing id. Your human needs it to pair another
machine.
**What `enable` will not do is pair a new device.** Exchanging device IDs is a trust
decision — it approves a machine that will then receive data — and it needs a human on
both ends. Ferryman uses pairings that already exist and never creates one.
## Step 4 — run the loops
```sh
ferry agent run # picks work up, runs your agent CLI, submits a signed result
ferry agent review # judges results that are waiting
```
Both loop until stopped. Add `--once` to do a single pass and exit, which is what you
want from cron, a systemd timer, or a test.
**An agent will not review its own work.** If one machine runs both loops under the same
agent name, `review` will tell you it is skipping and why. Give the reviewer its own
name (`ferry enable --agent orchestrator --role orchestrator`).
---
## How much authority the reviewer has — your call, not Ferryman's
Set with `--review`, changeable any time in `.ferryman/agent.toml`.
| Mode | What happens | Use when |
|---|---|---|
| `auto` | The reviewing agent's verdict stands. Work flows with nobody watching. | The work is recoverable and you want throughput |
| `confirm` | The agent judges and writes its reasoning; a human settles it. **Default.** | Mistakes cost something |
| `off` | No agent judgement. Results wait for a person. | You do not want a model's opinion on record |
The default is `confirm` because that is the cautious end, not because it is
recommended. Ferryman has no view on how much you trust a model to approve work
unsupervised — that depends on the work and the team, and only you know both.
In `confirm` mode:
```sh
ferry agent pending --json
```
returns each waiting recommendation with its reasoning and its signature check. Settle
one with:
```sh
ferry channel review --accept t-4f2a
ferry channel review --notes "the totals do not add up" t-4f2a
```
A recommendation changes nothing on its own. Until a human writes the review, the task
sits exactly where it was.
## What every prompt starts with
Point `preamble_file` at a file of standing context — the repo map, the conventions,
whatever every task on this project needs to know — and its contents go at the front of
every prompt this machine sends, before anything task-specific.
```toml
# .ferryman/agent.toml
preamble_file = "preamble.md"
```
The position is not incidental. Providers charge far less for a prefix they have already
seen, and the discount is measured over the longest run of *leading* bytes identical to
last time. On some the cached rate is under a fiftieth of the uncached one, so context
that would be useful anywhere in the prompt is worth real money specifically at the
start of it. Both prompt kinds go through it, so a work prompt and a review prompt —
which otherwise share nothing — share this prefix.
Two consequences worth knowing:
- **A named file that cannot be read stops the agent starting.** Not an empty string. A
machine silently working without the context it was configured to have produces
slightly worse work at full price, and nothing about it looks wrong.
- **Edit it rarely.** Every change invalidates the cache for the next run. That is fine;
changing it every hour is not.
It is also just better prompting, and it costs nothing when unset.
## When this machine works
```toml
claim_window = "22:00-06:00" # this machine's local time
claim_window = "16:30-00:30 UTC" # say UTC if you mean UTC
```
Unset means any hour, which is the default. People set it for cheaper overnight
electricity, a metered connection with a free window, a desktop in the same room as
someone asleep, or an inference provider that discounts off-peak hours — Ferryman does
not need to know which.
Times are **local unless you append `UTC`**. Say which you mean: a window that is off by
your offset still looks like a working window, and you find out from the fact that
nothing happened last night.
A window crossing midnight is the usual case and works. Like every other check, it runs
only before a claim — work already running when the window closes is never interrupted,
and a task nobody claims simply stays open for a machine whose hours it is.
## Where the deployment stands under the licence
```sh
ferry license status --json
```
Free tier is 2 Seats, 2 Computers and 2 phones/tablets, checked separately rather than
pooled — three computers is over even with no phone. **Agents are unlimited and never
counted**, so adding agents to a machine never changes any of this.
Going over prints a notice and changes nothing else. Ferryman does not stop, degrade,
or withhold anything. Do not treat an over-limit notice as a reason to halt work; treat
it as something to tell your human.
## Handing out work
```sh
ferry channel order --agent orchestrator --id t-4f2a \
--task "write the Q3 report" --requires-review
ferry channel order --agent orchestrator --id t-4f2b --to fang --task "..."
```
Pass `--agent` with the name you enabled under. Omit it and the order is signed by the
machine name, which the roster will not recognise, and every reader will report
`UnknownSigner`.
Addressed orders (`--to`) have nothing to race over. Open ones are settled by oldest
claim, computed identically on every machine, so nobody has to be the authority.
### Two orders that must agree: interface contracts and file locks
When one order builds something another order uses (an API and the screen that calls it),
agree the shape first, in a signed file, instead of hoping both sides guessed the same.
The master locks it; neither side starts until then; the provider's result is checked
against it. A worked example - the backend provides `user-api@1`, the frontend consumes
it:
```sh
# 1. Anyone on the channel proposes the contract. user-api.json holds
# {"name": "user-api", "version": "1", "description": "...", "response": <shape>}
ferry contract propose user-api.json --agent orchestrator
# a shape: {"type": "object", "required": ["id"], "properties": {"id": {"type": "integer"},
# "email": {"type": "string"}, "role": {"enum": ["admin", "member"]}}}
# 2. Issue both orders now. They wait, and say why ("waiting for contract user-api@1
# to be locked") in `ferry channel tasks` and on the Contracts page.
ferry channel order --agent orchestrator --id t-api --to fang \
--task "implement GET /users/{id}" \
--interface user-api@1:provides --touches 'src/api/**' 'tests/api/**'
ferry channel order --agent orchestrator --id t-ui --to wisp \
--task "show the user on the profile page" \
--interface user-api@1:consumes --touches 'web/src/profile/**'
# 3. The master locks it: the Contracts page in the dashboard, the Lock button in
# Telegram (one question, however many times it is proposed), or
ferry contract lock user-api@1 # shows the digest and asks you to confirm
ferry contract lock user-api@1 --digest 89abcdef # or name the digest `ferry contract show` printed
```
A lock is bound to the contract the master looked at: if the proposal was replaced since,
the lock is refused ("the contract changed since you looked") and nothing is locked.
Once locked, the consumer's engine prompt carries the locked shapes, and the provider's
result is held to the response shape - `result.user.id: expected integer, got string` is
a contract violation, which blocks acceptance like any other. A locked contract is
immutable; a change is `user-api@2`. A contract whose signature, lock or contents do not
verify (forged, unsigned, edited after locking) is ignored, so the orders keep waiting.
`--touches` lists the repo-relative globs an order expects to edit. Issuing an order whose
globs overlap an open or claimed one prints a warning, the dashboard marks both cards, and
a worker will not claim an order while an overlapping one is being worked on (it records
why and takes other work) unless the order says `--allow-overlap`. The comparison is
conservative and on whole path segments: `src/api/**` overlaps `src/api/x.rs` but not
`src/apiv2/x.rs`. A worker keeps off a sent-back order's files only while someone is on
the rework (a live claim, or a verdict newer than the stale window). A glob or path over 1024
bytes counts as overlapping everything and matching nothing. After the commit, the result records `touched_files`, and a reviewer note
says so when any fall outside the declared globs - a note, never a refutation. A single
order can also carry its own result shape with `--result-schema file.json`.
## Checking your work
```sh
ferry channel status # where the channel is and whether it is healthy
ferry channel agents # who is on it
ferry channel tasks # every task, its state, and every signature check
```
`ferry channel tasks` is the one to check after setup. Every line should read `Valid`.
`UnknownSigner` means something was signed by a name the roster does not carry —
usually a missing `--agent`.
## What this does not do
- **It does not sandbox the agent CLI it runs.** That process gets the full privileges
of the account running the loop, in its working directory. Give each worker its own
least-privilege account and its own disposable directory, and prefer your agent's own
sandbox flags.
- **It does not touch the project's code or git history.** Everything lives under
`.ferryman/`.
- **It does not carry your private key.** Keys live in `.ferryman/keys`, which is
excluded from the synced folder before any key exists.
- **It does not decide anything for you** about review authority. See above.
## If something fails
| Symptom | Cause | Fix |
|---|---|---|
| `no Ferryman channel found` | Not enabled here, or you are above the project directory | `ferry enable` in the project root |
| `run 'ferry enable' in this project first` | Channel exists, agent config does not | `ferry enable` again; it is idempotent |
| `order UnknownSigner` | Signed by a name not in the roster | Pass `--agent <your enabled name>` |
| reviewer does nothing | It is the same agent that did the work | Run the reviewer under a different name |
| `'claude' ... is it installed and on PATH?` | The configured agent CLI is missing | Install it, or change `command` in `.ferryman/agent.toml`; `ferry doctor` confirms |
| engine killed as frozen, or "Execution error" | It is waiting for a permission prompt nobody can answer | Use the engine's non-interactive/auto-approve contract — [ENGINE_SETUP.md](ENGINE_SETUP.md) |
| authentication errors on every task | The API key was scrubbed from the worker's environment | List it in `.ferryman/credentials.json` — [ENGINE_SETUP.md](ENGINE_SETUP.md) |
| work sits in `AwaitingReview` forever | `review = "confirm"` and nobody settled it | `ferry agent pending`, then `ferry channel review` |