Skip to content

Latest commit

 

History

History
133 lines (106 loc) · 8.24 KB

File metadata and controls

133 lines (106 loc) · 8.24 KB

Compatibility contract

This reference states which of ferry's surfaces are stable, what a release may change, and how ferry treats a state file written by a different version. It describes what ferry guarantees; it prescribes no procedure.

Compatibility surfaces

ferry has three surfaces a user or an automation depends on:

Surface What it is
CLI The commands, subcommands, flags, exit codes, and the machine-relevant wording of their output.
ferry.toml schema The manifest keys and value shapes documented in configuration and the agents domain[manage], [agents], [terminals], and their tables.
On-disk state The files ferry keeps under its state directory (~/.local/state/ferry) — the last-applied store, the agents target record, the backup journal, the immutable baseline, the restore and receive snapshots, and the work-domain state — plus the files ferry work writes into the configured cargo store, which cross account and machine boundaries.

The pre-1.0 rule

Before v1.0.0, a minor release may break any of the three surfaces. A breaking change carries a Breaking entry in the changelog that names the surface and the change. There is no deprecation window before v1.0.0: the changelog entry is the whole notice.

One guarantee holds even pre-1.0: defaults never change silently. A change to a default value — what a key means when it is absent, which domains or harnesses are managed when a list is omitted — is itself a breaking change and carries its own Breaking changelog entry. A machine that pins nothing still behaves predictably across an upgrade, or the release says so in words.

The deprecation contract from v1.0.0

From v1.0.0, a CLI or ferry.toml surface is removed only through deprecation:

  • The old form keeps working for at least one minor release after it is deprecated. It is not removed in the same release that deprecates it.
  • Using a deprecated form prints a warning that names the replacement verbatim — the exact command, flag, or key to use instead — so that an automation or an agent that hits the warning can correct itself from the message alone, without consulting the changelog.
  • Removal happens no earlier than the following minor release, and carries a Breaking changelog entry.

On-disk state versioning

Each state file in the envelope scheme carries an integer version field at the top level of its JSON. This lets a newer ferry recognise an older file and a downgraded ferry recognise a file it cannot safely read.

The versioned files are:

File Contents
dotfile-last-applied.json Per managed target, the content hash ferry last wrote — the middle term of the three-way apply comparison — and, from schema version 2, the exact bytes it last deployed (the last-deployed baseline), content-addressed by that hash so ferry can reconstruct and diff what it last wrote.
agents-targets.json Every $HOME destination the agents domain has applied on this machine, so ferry restore agents reverts what was actually applied.
journal/<run>/manifest.json One apply run's record of prior states and actions, used to roll back an interrupted run.
work/<key>.json Per-project work-domain state on this machine: the last pack and receive this account performed.
The cargo bundle's ferry-work.json manifest and the store's claim.<account>.json files The manifest is written by ferry work pack (as a member of each .ferrywork bundle) and read by ferry work receive; the claim files are written by pack, receive, and take-back alike — each account appends only to its own claim.<account>.json, merged on read by receive and work status. These files routinely cross to a different account or machine, which makes their version envelope the most compatibility-relevant in ferry.
.abcd/.work.local/.ferry-handover.json The project-side handover marker pack records (never a cargo-store file); receive consumes and removes it.

Two stores sit outside this envelope scheme and are read version-independently:

  • The immutable baseline — ferry's record of true pre-ferry state, which restore reverses to — a separate, content-addressed store.
  • The snapshot store (snapshots/<id>/manifest.json, plus one blob per affected path), written before a restore mutates anything and by every ferry work receive.

Neither carries a version field, so the refuse-a-newer-file rule below does not apply to snapshots: restore --undo <id> replays a snapshot without a version gate, whichever version of ferry wrote it. That is why the snapshot manifest's shape is treated as frozen.

Reading an older file

A file with no version field is the original form that predates versioning and reads as version 1. This applies only to the files that ever had a pre-versioning form — the last-applied store, the agents target record, and the journal manifest. The work-domain files shipped versioned from the start, so for them an unversioned or unrecognisable payload is refused as corrupt, never guessed at. A file at a versioned-but-older version (for example the last-applied store at version 1, once its current schema is version 2) is recognised the same way. Migration is forward-only and happens on read, on a mutating command (apply, capture) — a read-only command (status, diff) reads the older form in memory and writes nothing. An older last-applied store migrates forward with every recorded hash preserved; the last-deployed baseline it gains starts empty and is re-established target by target on the next apply.

Before a migrating command rewrites a file into the current form, it preserves the pre-migration bytes in a sibling copy named <file>.pre-v<n>.bak, where <n> is the version being migrated away from (for example dotfile-last-applied.json.pre-v1.bak). The backup is written once and never overwritten, so the first, genuinely-original copy is the one kept. This migration copy is distinct from the backup baseline: it preserves ferry's own bookkeeping, which is not managed $HOME state and never enters the baseline a restore reverses.

The last-applied store and the agents target record follow this back-up-then-rewrite path. The journal manifest is the exception: a journal run is per-apply and short-lived, so an older manifest is never rewritten in place. An older complete run stays in its original form (it is inert — restore reverses through the baseline, not the journal), and an older interrupted run is rolled back and cleared as usual.

ferry applies no retention policy to this state: complete journal runs and restore snapshots accumulate — one directory per mutating run — and nothing prunes them. Snapshots back restore --undo <id> at any later time, so they are deliberately kept; complete journal runs are inert. The all-or-nothing restore --purge-without-recovery is the one command that clears the store.

Reading a newer file

A file whose version is higher than the running ferry understands is refused, not read and not rewritten. The command stops with an error that names the file and both versions — the version found and the highest this ferry supports — and the file is left byte-for-byte untouched. Upgrading ferry to a version that understands the file is the way forward. A downgraded ferry can therefore never silently corrupt state that a newer ferry wrote.

This refusal covers the interrupted-run path too: an incomplete journal run whose manifest a newer ferry wrote is neither rolled back nor deleted — it is reported and left in place.

A rollback the containment guard refuses

Rolling back an interrupted run re-checks, at the write boundary, that each path still resolves inside $HOME and outside ~/.ssh. If a recorded path no longer does — its parent has since been replaced by a symlink that escapes — the rollback skips that one write, finishes the rest, and keeps the run's record, and every subsequent apply repeats an error naming the refused path. Removing the redirecting symlink and re-running clears it; deleting the named run directory under the ferry state directory instead discards the record of the change ferry could not revert.