Operate bay
Host a trunk without a forge
Keep trunk in a plain bare Git repository over SSH, and let the host itself refuse any rewrite of trunk.
bay is in development. These pages describe bay as it will ship.
bay host init creates a bare Git repository to serve as your remote. Its trunk is append-only, and Git on the host enforces that on every push. With a key registry, the host also refuses any trunk commit that is not an executor's landing. Use it when you have no forge, or no route to one.
It is plain Git, not a server. bay adds no protocol, no accounts and no daemon. Git over SSH carries every push and fetch. Who may push at all stays with SSH and the host's file permissions.
When to host a trunk
- No forge: you keep this repository on no GitHub, GitLab or other forge.
- Air-gapped machines: the machines have no route out. Move changes with
bay share --bundle. - A bare repository over SSH: you already keep repositories on a server you run.
- Rules on the host: you want the host itself to refuse a rewrite of trunk.
Set up the host
- 01
Check Git
The host needs Git 2.54 or newer. Older Git ignores hooks that live in configuration. Below 2.54,
bay host initrefuses withGIT_BELOW_FLOORand creates nothing. - 02
Create the repository
Run
bay host initon the host. The path must be empty or absent.--trunknames trunk,mainby default.--object-format sha256makes a SHA-256 repository.sh bay host init /srv/git/app.git --trunk main - 03
Check the host
Run
bay doctorin the new repository. Itstrunk-hookrow must be green.sh cd /srv/git/app.git && bay doctor - 04
Point your clones at it
Add the host as a remote, push trunk, and name that remote in
.bay/config.toml.sh git remote add origin ssh://git.example.com/srv/git/app.git git push origin main - 05
Add the registry
Push the first trunk before the first registry. With a registry, each new trunk commit must be an executor's landing, the first ones too. The Keys page shows how to write the registry.
What bay host init sets
The hooks call the bay that ran bay host init, by its full path. Keep bay at that path on the host.
| Setting | What it does |
|---|---|
--ref-format=reftable | Stores refs in reftable, whose updates of several refs in one push are atomic. |
hook.bay-trunk on update | Keeps trunk append-only. |
hook.bay-scope on pre-receive | Judges every bay ref in a push, once the host holds a key registry. |
bay.trunk | The trunk name that both hooks read. |
core.logAllRefUpdates = always | Keeps a reflog for every ref. The hook reads it to judge an adoption by the host's clock. |
receive.advertiseAtomic = true | Lets a landing move trunk and its landed marker in one atomic push. |
What the trunk hook refuses
- A push that is not a fast-forward of trunk.
- A deletion of trunk.
- A new trunk after the old one held landed changes.
- Any trunk update the hook cannot judge. An unknown outcome is a refusal.
What stays rewritable
The trunk hook judges trunk only. refs/bay/* and refs/notes/bay/* stay rewritable, so retention cuts still work. So bay does not set receive.denyNonFastForwards or receive.denyDeletes. Those would block retention cuts too.
With a key registry
Once the host holds refs/bay/registry, the scoped hook judges every push. Git does not tell a hook who pushed. So the hook judges the signatures on what the push adds. Any refusal fails the whole push.
- Only an executor key can move trunk. Each new trunk commit must be an executor-signed landing.
- Each landing needs a current agent-signed request and passing producer-signed evidence.
- Each landing must create its landed marker in the same push, and a change lands only once.
- Only a key in the current
registryfile can advancerefs/bay/registry, and only by fast-forward. - A live ref takes only a summary of its own machine, signed by the agent key it names.
- Every bay ref name must use bay's fixed alphabet.
Compared with a forge
| Rule | On a trunk from `bay host init` | On a forge |
|---|---|---|
| Trunk is append-only | The trunk hook refuses a rewrite or a deletion. | Turn on the forge's branch protection for trunk. |
| Only landings reach trunk | With a registry, the scoped hook refuses any commit that is not an executor's landing. | Let only the executor's identity push to trunk. |
| A revoked key | Refused on every later push. | bay's own checks apply it after their next fetch. |
Share a change
bay share hands a change and its evidence to another actor. It pushes refs/bay/changes/<change> and every evidence notes ref to a remote. --remote names that remote. The default is the remote in .bay/config.toml. --bundle writes the same refs to one file instead, for a machine with no route. The other actor fetches the refs with plain Git, then runs bay start --resume <change>. No bay test covers the bundle path yet, so check the fetched refs before you rely on them.
bay share c7k2m4 --bundle c7k2m4.bundle
# on the other machine
git fetch c7k2m4.bundle 'refs/bay/changes/*:refs/bay/changes/*' \
'refs/notes/bay/evidence/*:refs/notes/bay/evidence/*'
bay start --resume c7k2m4Refusals
bay host init refuses below the Git floor. The host refuses a push with the reason on stderr.
| Code | Meaning |
|---|---|
GIT_BELOW_FLOOR | Git on the host is older than 2.54. bay host init created nothing. |
HOOK_TIMEOUT | The scoped hook ran past its time bound and refused the push. bay retries the same candidate. |
QUEUE_HELD | Another operation holds the head of the queue, so this landing waits. |
REGISTRY_CHANGED | The registry revoked a key behind this landing after the executor signed it. |
HOOKS_MISSING | bay doctor found a bare repository with no trunk hook. |
Not built yet
- No bay command writes
refs/bay/registry. A registry-key holder edits it by hand. - On a forge, bay does not yet report a trunk commit that did not come through bay.
- Compacting old landed markers into index refs once a repository holds many bay refs.
Limits
- Git on the host: 2.54 or newer.
- Hook time: the scoped hook judges a push for at most 30 seconds by default. Set
bay.hookTimeoutSon the host. - Live refs: a host holds at most 256 live refs.
- Operator names: machine, executor and producer names use
a-z,0-9and-, at most 32 characters.