Provisioning files into a run
What --provision accepts, what travels, what the record says afterwards, and when a citation refuses.
What --provision accepts, and what the record says afterwards
--provision <source>=<workdir-relative-dest> is repeatable, and <source> may be either
a file or a directory. Whichever it is, the bytes leave this host in exactly one
way: a signed provisioning pack, streamed to the execution host and unpacked into the
workdir there. There is no second transport and nothing about your invocation selects one —
in particular it does not matter whether the far worktree already existed or was created by
the same dispatch. On a machine whose transport is local the same set is written straight
into the workdir, with the same members, modes and citations.
A directory is walked recursively in name order and every regular file inside it lands
under <dest>, nested paths included.
Modes. One bit travels: the owner-execute bit. An executable source arrives 0755, a
non-executable one arrives 0644. Everything else is normalised, on purpose — group and
other bits are an artefact of this host's umask and are not a policy for the leg's host,
and setuid/setgid/sticky are dropped unconditionally, because provisioning writes as the
far user and minting a privileged file on another machine is not a provisioning act. If you
need a specific mode beyond "executable or not", set it in the leg's own work, not here.
⚠ Normalisation can WIDEN, not only narrow. A 0600 source arrives 0644: it is not
executable, so it lands at the non-executable mode, which is readable by group and other on
the far host. If a file's restrictive mode is what protects it, --provision is the wrong
carrier for it — scp it and set the mode yourself on the far side.
Symlinks refuse. A symlink named as a <source>, or found inside a provisioned
directory, is refused by name (provision-source-kind) — neither followed nor carried. A
followed link would land another file's bytes under a name meaning something else on the far
host; a carried link would dangle or point outside the workdir over there.
Destinations are confined, lexically. A destination that normalises outside the
workdir refuses by name (provision-dest-escape), naming both the normalised path and the
workdir, and nothing in the set is written — not even the entries that were fine. An absolute
destination and one with a .. path component are refused before that. The channel namespace
(_*) and the engine's own .saphan-kickoff-brief are likewise not legal destinations, and
two entries naming one destination refuse as provision-dest-collision.
⚠ "Confined" here means joined-and-cleaned, not resolved. The engine does not follow symlinks when it makes this decision, and it cannot: your leg's workdir is on the execution host, so resolving this machine's filesystem would answer a question about the wrong one. Do not rely on this check to contain a destination you did not write yourself.
A symlink in the destination's path refuses, and your link is left alone. On a remote
machine that decision is made on the execution host, where the workdir actually is, and it is
made before anything is unpacked: if any component of a destination under the workdir is a symlink — including the
final component, the destination itself — the apply refuses, names that path
(SAPHAN-PROVISION:DESTSYMLINK <path>) and surfaces as provision-carry-failed, with nothing
written and nothing removed. What to do when it refuses: the engine will not choose between
your link and your bytes. Either name a destination that does not cross the link, or move the
link aside yourself on the execution host and re-dispatch. Re-running unchanged refuses again,
identically.
On a machine whose transport is local the write walks the destination one directory at a
time and never follows a link: a symlinked directory on the way, or a link standing at the
destination itself, is refused and names what stands there, and nothing lands outside the
workdir.
Bounds. A provisioning set is capped at 32 MiB and 4096 members, all entries summed.
Past either, the dispatch refuses naming the size it measured and the cap, and the
row records that refusal — it is never an unexplained error. Your sources have been read into
memory by then, but nothing has been packed, dialed or written on either host. The remedy is
not a bigger cap: scp the bulk material into the worktree's .provision/ on the execution host
before dispatching, and keep --provision for what the order actually cites.
Why the cap is about your set and not about your command line. Provisioned bytes ride the transport's stdin; the command the engine composes for the far shell names the pack file and the workdir and nothing else. So that command's size does not vary with what you provisioned — it is the same at 1 KiB and at 4 MiB, so a large set can never break through the far kernel's argument limit. The property is that invariance under payload; it is not a constant, and no fixed byte figure for that command is worth quoting, because its length does move with your workdir's path and the pack file's name.
What the run record carries. A file entry is cited as <dest>@sha:<12 hex> — the
destination and the sha256 of the source bytes on this host. ⚠ It is not a far-side
re-measurement per file. What is checked on the far side is stronger in aggregate and weaker
per file — the whole pack's sha256 is verified on the execution host before its bytes are renamed
into place, so one comparison covers every member and the manifest declaring them, but no
line in the record says "this individual file measured the same over there". A directory
entry is cited as
<dest>@tree:<12 hex>:<member count>, where the digest is over the pack manifest's own
declaration of that tree (each member's path, mode and sha256). A directory has no single
content digest, so the record does not pretend to one; sending the same tree twice produces
the same digest.
When a provisioned citation refuses
A run that was given a file with --provision <source>=<dest> — a permission class, a
canon document — carries a citation on its run record: the destination plus the sha256 of
the bytes. Teardown refuses to record the decision unless it can still find those bytes,
because a teardown is the moment they stop being reachable. It looks in four places, in
this order:
- the archive this teardown just wrote from the live worktree;
- a known-good copy you supplied with
--provisioned-from <dest>=<absolute path>; - the stream's own repository history;
- the source the run record remembers — the path the bytes were read from at spawn, and the commit they stood in.
Place 4 is why an ordinary teardown of a stream provisioned with a permission class needs no flag from you. Three things it deliberately does not do, each of which you will see named in the refusal when it happens:
- A source that stood in no commit when it was read is never resolvable from place 4, even if the file is still on your disk. Uncommitted bytes are not a durable place to point at, and that is the case this invariant was written for.
- A remembered source that is there but no longer hashes to the cited digest refuses by sha, and no further place is consulted. The engine will not pick whichever copy happens to pass.
- A run record written by a release that did not yet record provisioning sources has
nothing to look for in place 4; it refuses and points you at
--provisioned-from, and the refusal says why.
--provisioned-from remains the corroborated way out of all three: the bytes you supply are
hashed and must match the cited digest, so it satisfies the invariant rather than bypassing
it.
Finally, closed-without-merge exists as a first-class decision: a stream can end
honestly without landing — the record then says so, rather than the stream lingering as a
ghost or being quietly deleted.