Putting roost-session on a shed or machine¶
S5 of the Roost Pivot (epics/roost-pivot.md) lets the shed desktop and mobile apps install
and start a roost roost-session daemon on a shed or a
machines: entry that doesn't already have one — over the target's own SSH, with the same
choreography roost's own UI uses. This turns a shed's codex and cursor rows from
liveness only into roost-sourced activity, matching what a machines: entry already gets —
demonstrated live, not only in the hermetic tests that cover the mechanism this page describes:
a real shed's codex row carries roost-sourced activity with source: "roost"
(epics/roost-pivot.md's S5 row).
This page describes the mechanism the clients drive: where the bytes come from, what consent
promises, what the rollback guarantee actually covers, and the edges (shed reset, two
clients, mini3-style read-only hosts). The button, the dialog, and the toast are the desktop
UI's concern (Tauri roost.probe/roost.preview/roost.bootstrap/roost.launch — see
Desktop IPC); this page is about what happens once you click it.
The source ladder¶
An install streams a roost-session binary at the target. Where those bytes come from is
decided in order, and the first rung that can answer wins:
- Override — the file named by
$ROOST_SESSION_INSTALL_BIN(roost's own environment variable; shed reads it itself). A binary of the wrong OS or architecture is refused rather than silently skipped, because an explicit override that cannot be used means you asked for a specific binary — installing a different one instead would be worse than failing. - Sibling — the
roost-sessionsitting beside the client binary itself, desktop only. This requires the desktop to be running on Linux (there is no local candidate to compare against on macOS) and a localidentifyagainst that sibling reporting session protocol 5 — the same protocol this build speaks. - Release asset — a
roost-session-<version>-linux-<amd64|arm64>tarball plus its.sha256sibling, fetched over HTTPS and checksum-verified, behind a version pin (below). Both clients (desktop and mobile) use shed's own fetch here, not roost's owncurlrung — the descriptor shed hashes must be the exact descriptor it streams to the target, and roost's own download path keeps that open, hashed handle private. Contract: HTTPS only, redirects followed only to HTTPS, the asset capped at 256 MiB and its checksum file at 4 KiB, downloaded into a private, freshly created directory with partial files removed on any failure. - Nothing — the target is left completely untouched, and the client shows the sentence in the next section.
The release rung is real code, but it has never run against a real release¶
RELEASE_PIN is None today, because no published roost release speaks session protocol
5 — the latest, 0.0.19, speaks protocol 2. This is checked against roost's own repository
at the time of writing, not assumed. The message a client shows when every rung has failed:
no roost release speaking session protocol 5 is published yet (the latest, 0.0.19, speaks 2). On a Linux machine with a protocol-5 roost installed the desktop uses that roost-session; otherwise point
ROOST_SESSION_INSTALL_BINat a protocol-5 build.<target>was left untouched.
Flipping RELEASE_PIN once roost ships a protocol-5 release is a one-line change (it sits
beside the version/protocol pair the message above is built from). Say this plainly: the
release-asset rung is implemented and unit-tested against a loopback HTTP fixture, but it has
never been exercised against a real, published roost release, and nothing in this codebase
claims otherwise. Until that pin flips, the only ways a fresh Linux target gets a
roost-session are the override variable and — desktop only — the sibling rung.
One source-related decision in this design is recorded as not yet confirmed by the
project owner rather than settled: fetching the release asset with shed's own HTTP client
instead of roost's curl rung (above). It is implemented as described here; it should not be
read as a closed decision.
Compatibility: protocol only, not roost's exact triple¶
roost's own UI refuses a roost-session unless its version, protocol, and embedded ghostty
snapshot all match exactly. shed's compatibility rule is narrower and different: a target is
compatible if its session.identify reports session_protocol == 5, full stop. shed does
not build roost, never negotiates a ghostty snapshot, and cannot know a future release's exact
build fingerprint in advance — the protocol number is the one thing this codebase can commit
to checking. The accepted, documented consequence: a roost UI that later connects to a
shed-installed session applies its own stricter exact-triple rule and may offer to reinstall
something shed considers perfectly fine.
What actually happens (the plan matrix)¶
| The target looks like | The action |
|---|---|
| Nothing there | Install, then Start |
| A stale or incompatible binary, nothing running | Update (backup + replace) then Start |
| A compatible binary, nothing running | Start |
| A session already running protocol 5 | Nothing — status only |
| A session running any other protocol | Report only. Never stopped, never restarted. |
| No source available (see the ladder above) | Unavailable — the ladder's own sentence in place of a button |
Every one of these is preceded by a fresh probe (read-only: OS/arch check, candidate binaries, session state) and, for Install/Update, a consent step naming what will happen, where, and where the bytes come from — nothing is downloaded or written before that consent is given, and the install re-probes and refuses if the target changed since the card was shown.
The rollback promise, stated exactly as narrow as it is¶
An Install or Update follows roost's own staged order: prepare a temporary file beside the destination, stream the binary onto it, verify the staged file's identity, commit it into place (a rename), verify the installed file's identity, then discard the pre-commit backup. The rollback guarantee tracks that order precisely — it is not "any failure leaves the target unchanged":
- A failure at prepare, stream, verify, or commit removes the temporary file and leaves the incumbent binary exactly as it was (putting it back first, if the commit's rename had already landed before the failure was detected).
- A failure during the post-commit identify restores the incumbent when there was one to
restore, and says plainly whether that restoration succeeded (
restored: true/false) — including naming the path a binary is stranded at if it could not be put back. - Once the pre-commit backup has been discarded, the guarantee ends. A
roost-session startfailure — or a failure in the identify that follows a successful start — after that point leaves the new binary in place; nothing rolls the install back. The copy at that point reads: "installed but wouldn't start: tryroost-session starton<target>."
In short: the rollback promise covers everything up to and including the discard of the backup. After that line, a failure is reported honestly, but the new binary stays.
Hooks: the step that makes the payoff real¶
After a Start that the client itself performed, it sends
session.set_agent_hooks {mode: "auto", skip: [], client: "shed-desktop"} (or "shed-mobile")
to the fresh session — one wire call, nothing in front of it. Session protocol 5 dropped the
lease entirely, so there is no session.connect to open first and nothing to hold or lose
before sending it.
This is a dotfile mutation, performed by the host session, not by shed. shed sends one
wire call; roost-session on the target is the process that writes into the configuration
files of whichever agents it finds already configured there — claude, codex, cursor,
opencode, and grok — and nothing else. The consent copy shown before Install/Update/Start
names this plainly: "roost-session will also wire its hooks into the agents already configured
there — claude, codex, cursor, opencode, grok — and nothing else."
When it recurs: mode: "auto" only wires an agent whose configuration directory exists
at that moment. An agent set up later is not retroactively wired by this one call — it gets
wired the next time hooks are (re)sent, which is:
- every
shed start(each one performs a fresh Start-and-hooks cycle if a bootstrap runs), and - every successful cycle of the watcher for a target this client bootstrapped — re-sent on every reconnect, not only once at install time.
One op, no token, last writer wins. Every client that wires a target sends the identical
call — the desktop, the phone, and any roost UI that connects all say set_agent_hooks
{mode: "auto"} — and the host session applies whichever one it heard most recently. That is
the design, not a gap in it: one user owns every client that talks to their sheds, so hook
wiring is open to all of them and none of them needs to ask first. There is no session-level
owner left to displace and nothing a second client could "take over" from a first — roostctl
agent status on the host is where you read which client wired an agent's hooks last.
The PATH warning: named, never acted on¶
Immediately after Start, the bootstrap re-runs the same command -v roost-session check the
probe used. If the answer differs from — or is missing compared to — the binary that was just
installed and started, that fact rides into the result as a warning string surfaced in a
toast. Nothing edits a dotfile or a shell profile to fix it. Per roost's own precedent
(and pin P5 of this design), shed follows roost's PATH lead exactly and never writes into
.bashrc, .profile, or any other dotfile on the target — a mismatch is reported so you know
to fix your own PATH, and left at that.
shed reset¶
shed reset wipes a shed's writable upper filesystem, including ~/.local/bin — which is
where an install normally lands. There is no special-case handling for this: the next probe
after a reset simply reports Missing again, exactly as it would for a shed that never had
roost-session installed, and the client offers Install again from scratch. Nothing about the
hooks wiring or the source ladder needs to know a reset happened.
Two clients, one target¶
The desktop app and the mobile app can both bootstrap the same target. Nothing coordinates
between them beyond the filesystem itself: concurrent installs are last-writer-wins, using
the same .bak.<pid> backup-chain naming roost's own installer already uses, with the
post-commit identify as the only detector that something unexpected landed (a different
protocol-5 build than the one this client just streamed). This is treated as the normal
case, not a hazard to guard against further — a person with both apps open, bootstrapping the
same shed from their phone and their laptop, is exactly the scenario this is built for.
A read-only example: mini3¶
A target that is already running a roost-session speaking a protocol other than 5 (mini3, at
the time of writing, runs a release build of protocol 2) gets the Report row from the plan
matrix above and nothing else — probing it is read-only, and its daemon's start time is
unaffected before and after a probe. This is pin P6: an existing session is reported, never
restarted, no matter how out of date it is. The message names both protocol numbers and tells
you to upgrade whichever side is older.
See also¶
- Desktop IPC — the
roost.probe/roost.preview/roost.bootstrap/roost.launchops the desktop app calls to drive this. - The
shedroost provider — S4, the palette entry that opens a tab once a session exists. epics/roost-pivot.md— the Roost Pivot's tracking table (not a published doc page).