# Production readiness plan **Written:** 2026-08-13 (Friday) — for orchestrator review Monday. **Last updated:** 2026-08-14 (end of session). **Branches (dependency order):** `feat/portable-auth-v2` → `feat/groundcrew-borrows` → `feat/always-on-fleet` → `feat/learn-and-observe` → `feat/skills` → `feat/ready-hardening`. This is the single source of truth for what it takes to reach production/live status. When an item changes, update this file — not the chat. --- ## 0. Correct the framing (do this first) `docs/PUBLIC_RELEASE_PLAN.md` and the July review call this a **"local single-node preview."** That is wrong and it has been wrong for a while. Ferryman is a **multi-machine, multi-agent team coordination system**: - Syncthing-carried shared channel + private-Git backstop, - signed portable messages with replay protection and a trust store, - an append-only attribution ledger, - a master operator with signed grants and delegation, - shared project memory. Correct label: **"self-hosted, local-first team coordination — reference implementation."** Fix it in `PUBLIC_RELEASE_PLAN.md`, `THREAT_MODEL.md`, the repository banner, and the README. --- ## 1. Done this cycle - **Signed v2 portable messages** — Ed25519 over RFC 8785 canonical JSON, domain separation, trust store, `key_version` + revocation, replay ledger (with atomic save + per-project lock). - **Attribution ledger** — append-only, hash-chained, signed; git-backstopped. - **Full-channel git backstop** — every portable file (tasks, ledger) snapshots to private Git on each ledger append, so a Syncthing deletion is recoverable. - **Team memory** — project memory bank (Syncthing-synced, gitignored, with a `README.md` review order) + the agent-memory tier defined. - **Master model** — explicit `ferry enable --master`, signed declaration in the shared channel, disclaim/transfer, master-signed member grants. - **Team-awareness + enforcement** — `integration_mode` read; `grants = "required"` gates the agent loop on master grants; `grants = "open"` (default) is full permissions. --- ## 1.5 Session-2 status (2026-08-14) One session added five feature branches and three production blockers: - **Built:** pluggable runner (`none`/`podman`/`docker` + `net=` egress policy), task sources, worktree-per-task, interrupt mode, always-on triggers, result contracts, learning DB + `ferry bench`/`ferry channel stats`, OTLP export, `SKILL.md` skills, repo roadmap. - **Ready blockers done this session:** B1's sandbox half (runner) and independent approval (`requires_approval`, no self-approval), B5 (git subprocess secret sweep), B7 (label fix). - **Still open:** B3 (worker credential separation), B4 (distributed message claims), B6 (release checklist). ### B4 — distributed message claims: deferred with a design note `claim_message_v2` writes its processed-marker to the machine-local `attachment/runtime/processed`. Fixing the double-claim needs the marker moved into the synced channel, keyed per recipient. This is subtle because a message to `all` must be processed once *per machine*, whereas a message to a named agent must be processed once *total* — the claim model differs by recipient class. This needs an orchestrator decision on broadcast semantics before the change; documented rather than rushed, because the adjacent code is replay-protection-critical. ### Next build sequence ("better", agreed 2026-08-14) — all built 1. ~~Plan/spec artifact + dependency DAG~~ — `Order.depends_on` (signed); `work_for` waits for dependencies. 2. ~~Session trajectory capture/replay~~ — `channel::trajectory` (prompt digest + output). 3. ~~Credential injection~~ — `credentials.json` injected into the sandboxed agent CLI. 4. ~~Native event adapters~~ — `channel::events` GitHub `gh` adapter → signed orders. 5. ~~Eval scorer command~~ — bench tasks accept a `scorer` (exit 0 = pass). 6. ~~Web dashboard~~ — `ferryman-server::dashboard` read-only axum router. 7. ~~Cost/token accounting~~ — `channel::cost` per-engine usage + estimated cost. 8. ~~BYO-key manager~~ — `channel::keys` KeyStore add/list/remove (no `.env` editing). 9. ~~At-rest/payload encryption~~ — `channel::encrypt` (chacha20poly1305 + sha256 key). 10. ~~Cross-project master console~~ — `ops::console` `fleet_summary`. 11. ~~hone integration~~ — `docs/HONE_INTEGRATION.md`. --- ## 2. Remaining blockers, prioritized ### B1 — Worker sandboxing + independent approval (CRITICAL) **Status (2026-08-13):** the container-sandbox layer is implemented — set `sandbox = ""` in `agent.toml` (or `ferry enable --sandbox `) and the agent CLI runs inside it: `podman run --rm -v :/workspace:Z -w /workspace `. Empty `sandbox` (default) runs bare. Image curation/maintenance is requested from the community in [issue #8](https://github.com/estejosh/ferryman/issues/8), reviewed before any merge. Remaining: the per-platform bind-mount wrinkles (SELinux/macOS/WSL), independent approval, and credential separation (B3). **Problem.** The reference worker runs a coding agent (`claude`/etc.) with the full privileges of its OS user. One bad or malicious prompt is machine-wide damage. This is the last thing standing between "works" and "safe to run on real work." **Proposed fix (four layers, not one):** 1. **Dedicated unprivileged account.** Run the worker under its own OS user (or a container/VM) that can reach only the project workspace and a scoped credential set — never the operator, memory-write, recovery, or repository credentials. 2. **Sandbox the agent CLI.** Wrap the spawned coding agent in a sandbox with a filesystem allowlist (the workspace only) and a network policy: Linux → containers / `firejail` / `bubblewrap`; macOS → `sandbox-exec`; Windows → Job Objects / AppContainer. 3. **Independent approval authority.** For `requires_approval` work, the approve decision comes from a *separate* principal — the master/human via the channel — never the agent that did the work. The agent can propose, not self-approve. 4. **Credential separation** (B3 below) so the worker holds only short-lived, scoped leases. This is the hardest item because it is OS-level and platform-specific, not just Rust. It needs an operator decision (container vs. dedicated user vs. sandbox-exec) per platform. ### B2 — Merge `feat/portable-auth-v2` → `main` The orchestrator decides this Monday. Everything above is on the branch and green; nothing in `main` carries the signed-message/ledger/master work until the merge. ### B3 — Worker credential separation, with a full-permissions opt-in Default: workers get short-lived, scoped lease tokens (not operator/project tokens). **Explicit requirement:** a user must be able to opt into full permissions for their own projects — that is already the shape of the model (`grants = "open"` = full permissions; `grants = "required"` = master-gated). The lease-token mechanism itself (short-lived, scoped, minted by the master) is still to be built on top. ### B4 — Distributed *message* claims Order/work claims already use the optimistic-claim + deterministic-winner model. The portable *message* claims (`claim_message_v2`) are still machine-local, so two machines can double-claim a message. Fix: apply the same model — write the claim into the synced channel, oldest-claim-wins with a name tiebreak, and the loser backs off. (This is the "merge is being requested" item: no, it is not about the branch merge — it is about making message claims distributed, same as order claims already are.) ### B5 — Finish git subprocess secret isolation Already scrubs known sensitive env vars; finish the *strict allowlist* so a project-local git hook cannot inherit anything it was not given. ### B6 — Public-surface release checklist From `PUBLIC_RELEASE_PLAN.md`, still open: a real security-reporting path, startup production-config validation, black-box HTTP integration tests, OpenAPI completion, dependency/secret/license scans + SBOM in CI. ### B7 — Label + docs Carried from section 0: fix the "single-node" framing everywhere. --- ## 3. Decisions the orchestrator needs to make Monday 1. **Merge timing** — approve `feat/portable-auth-v2` → `main` (B2). 2. **Sandbox mechanism per platform** — container vs. dedicated user vs. `sandbox-exec`/Job Object (B1). 3. **Token model** — confirm: default BYO (each member's own key) + optional master-provisioned pool; jobs shared, secrets not (this is the "share tokens vs jobs" question). 4. **Full-permissions default** — confirm `grants = "open"` is the default and `required` is opt-in (already implemented). --- ## 4. Recommended sequence (Monday onward) 1. Orchestrator reviews + merges `feat/portable-auth-v2` (B2). 2. Worker sandboxing + independent approval (B1) — the real safety gate. 3. Distributed message claims (B4). 4. Worker credential separation with full-permissions opt-in (B3). 5. Git subprocess allowlist (B5). 6. Release checklist + label fix (B6, B7). --- ## Product roadmap (agreed 2026-08-13) Decisions from the "what would make it compelling" review, in build order. 1. **Signed audit report** — BUILT (`ferry channel report`, commit `61989be`). Signed, standalone, per-entry verification; JSON or human-readable. 2. **Web dashboard** — build. Users are uncomfortable with a CLI; this is the biggest usability unlock. Ledger, tasks, memory in one pane. 3. **Cost/token accounting + reduction suggestions** — analysis of token usage with suggestions to reduce (route selection to cheaper models). Reference tools mentioned: omniroute/caveman, ponytail. 4. **Multi-provider BYO-key manager** — per-agent keys, vendor independence. 5. **At-rest / app-level payload encryption** — Syncthing already encrypts in transit; the gap is at-rest. Use Syncthing "Receive Encrypted" for offsite backup devices, plus per-project payload encryption (master mints the key, shares it encrypted to members' device keys — ADR 0009 applied to content). 6. **Cross-project master console + master memory** — one view over every project the master runs, with persistent cross-project memory so the master does not restart context per project. 7. **Multi-messenger approval + 2FA** — keep Telegram; plan for other messengers (Signal, etc.); a "confirm on a second channel" 2FA mode (issue on Telegram, confirm on Signal). 8. **Agent/memory marketplace via hone** — hone hosts the marketplace; consider a "memory marketplace" (expert context from other work). 9. **Autonomous orchestrator** — the long-term goal; build towards full integration into hone. Must remain human-gated for destructive work. --- ## Competitor learnings → build plan (groundcrew/ClipboardHealth) Reviewed `ClipboardHealth/groundcrew` and `prolego-team/groundcrew` (`memory-bank/competitors.md`). Four borrows, planned: 1. **Pluggable runner abstraction.** Generalize the current `sandbox=` into `local.runner` with `none` (bare) / `podman:` / `docker:` and per-platform auto-resolution (Linux→podman, macOS/Windows→docker or a native sandbox). Keep the "none" escape hatch + a network-egress allowlist. First step: rename/extend `AgentConfig.sandbox` into a `runner` enum. 2. **Task-source adapters.** Add pluggable task sources (Linear, Jira, custom shell) that map external tickets into signed orders. Reuses the adapter definition pattern from groundcrew; Ferryman's value-add is that the imported ticket becomes a signed order with a ledger entry. 3. **Worktree-per-task, made better.** Borrow groundcrew's `git worktree` per task, but tie it to our trust model: the worktree branch derives from the signed order id + agent (deterministic, idempotent re-dispatch), the worktree is recorded in the attribution ledger, and the signed result carries the worktree HEAD so a reviewer can verify the work matches the commit. Cleanup (teardown of worktree + branch) is a ledger-recorded, idempotent operation. 4. **Interrupt mode.** Today the Telegram gate approves/denies *parked* work; groundcrew lets you take over a *running* agent. Add an interrupt path — a signed "interrupt/pause/steer/kill" order that the worker honors between its poll ticks, surfaced through Telegram and `ferry`. This is the missing analogue of groundcrew's live terminals. ### Status (2026-08-14): all four built on `feat/groundcrew-borrows` 1. Runner abstraction — `agent::Runner` (`Bare`/`Podman`/`Docker`), `sandbox` config now accepts `none`/`podman:IMG`/`docker:IMG`/`IMG` (legacy podman). 2. Task sources — `channel::source` (`TaskSource::Shell` → signed orders), `ferry channel source`. 3. Worktree-per-task — `channel::worktree` + `AgentConfig.worktree`; branch derives from signed order + agent; head signed into the result. 4. Interrupt mode — `channel::interrupt` (kill/pause/steer, signed, acked), honored in `do_work`, `ferry channel interrupt`. ## Broader competitive research → analysis and outcome A parallel agent reviewed 11 adjacent projects (`memory-bank/competitive-research.md`). Seven upgrades were assessed; two were built, five deferred. ### Built (materially better, and self-contained) 1. **Always-on triggers** (research #2) — the gap between Ferryman and an "always-on fleet." `sources.toml` lists sources; `ferry agent` now re-polls them on their interval and imports new tickets as signed orders, and `ferry channel watch`/`sources` give a standalone path. Intervals survive restarts via a last-import marker; imports are race-tolerant and idempotent. 2. **Result contracts** (research #4) — an order can `--require` top-level keys in its result. The contract is signed into the order (backward-compatible: no contract → identical signature bytes), `ferry channel tasks` flags missing keys, and `ferry channel review --accept` refuses malformed deliverables mechanically. ### Deferred (assessed, not built this cycle) 3. **MCP providers** — real leverage, but needs an external MCP crate (none in the dependency tree) and a trust decision on arbitrary MCP servers. Blocked on a dependency/architecture decision, not effort. 4. **Approval-mode ladder** — Ferryman already has `review = auto|confirm|off` and, with the runner, `none|podman|docker`. The finer edit-permission ladder belongs in the runner; no separate build this cycle. 5. **Worker eval harness** — a separate project (dataset curation + scoring), not a feature to bolt on. Roadmap, not this branch. 6. **OTLP trace/metrics** — needs an OTLP dependency + a collector. The ledger already answers *attribution*; trace observability is a later, larger piece. 7. **Repo-map pre-context** — most agent CLIs build this themselves; marginal for a provider-neutral shell wrapper. Lowest priority. --- ## 5. Quick answers to the questions asked Friday - **"Assuming all changes pass muster, are we ready?"** — The coordination and identity layer is ready (signed messages, replay, attribution, master/grants, memory). The *execution-safety* layer is not: B1 (sandboxing), B3 (credential separation), and B4 (distributed message claims) are the remaining gaps. So: ready as a team-coordination product; not yet safe to run untrusted work on machines you care about. - **"You still have not fixed sandboxing?"** — Correct, and it is deliberate scope: it is OS-level and platform-specific, and it needs an operator decision (container vs. dedicated user). The fix is fully specified in B1 above. - **"Single node? That is not what we built."** — Agreed; the label was inherited and wrong. See section 0. - **"Merge is being requested?"** — B2 (the branch merge) is the orchestrator's call; B4 (distributed message claims) is a code change, not a merge request.