Install, upgrade and release
For the operator: the person who installs whr on the Mac mini and cuts releases. A draft: nothing on this page has been run against a release yet (unverified; the first install is issue #62, the first release v0.1.0 follows the slice demo, issue #28). The design is D24 and Releases.
Where whr comes from
The supervisor always runs an installed binary built by CI from a signed tag on main, never a working tree (D34). The installer puts three files in a prefix that the whr user cannot write, so nothing running as whr, an agent’s escape included, can replace the binary:
| File | Role |
|---|---|
<prefix>/bin/whr | the CLI and whr serve (macOS, arm64) |
<prefix>/libexec/whr/whr-shim-linux-arm64 | the in-guest launcher |
<prefix>/libexec/whr/whr-proxy-linux-arm64 | the egress allowlist proxy |
Before v0.1.0: a draft release
Until the first release, a signed prerelease tag v0.1.0-alpha.N on a green commit of main gives a dogfood build. Its draft is never published. Install it as the administrator, not as whr:
make install-release VERSION=v0.1.0-alpha.1 # prefix /opt/whr
make install-release VERSION=v0.1.0-alpha.1 PREFIX=/some/prefixYou need gh (brew install gh), signed in as a writer of the repository: a draft can be downloaded only by a writer. The script downloads the macOS archive (whr_<tag>_darwin_arm64.tar.gz), the guest archive (whr-guest_<tag>_linux_arm64.tar.gz) and checksums.txt, checks both archives against the checksums and against the build-provenance attestation of this repository’s release workflow, and installs nothing unless every check passes. It refuses anything but macOS on Apple silicon. It reads the installed version from <prefix>/libexec/whr/VERSION, which it writes after a verified install, and never runs the installed whr before the checks; an older tag, or an install without that file, needs --allow-downgrade (make install from source removes that file, so the version after a source install is unknown) (make install-release ... ALLOW_DOWNGRADE=1). WHR_RELEASE_REPO=owner/name changes whose attestations are trusted (a fork); the script refuses it unless you also pass --trust-release-repo to scripts/install-release.sh.
Verify a download yourself
The release signature is the keyless Sigstore build-provenance attestation the release job makes (D24): it binds each file to the release workflow, the tag and the tagged commit. The attestation bundle is attached to the release as whr_<tag>.intoto.jsonl. Pin the workflow, the tag and the commit, not just the repository: without --source-ref and --source-digest, an older release’s archive with its own checksums.txt passes (a downgrade).
tag=v0.1.0-alpha.1
commit=$(gh api repos/wstein/workharbor/commits/refs/tags/$tag --jq .sha)
gh attestation verify <file> --repo wstein/workharbor \
--signer-workflow wstein/workharbor/.github/workflows/release.yml \
--source-ref refs/tags/$tag --source-digest "$commit" \
--deny-self-hosted-runners
# offline, with the attached bundle:
gh attestation verify <file> --repo wstein/workharbor --bundle whr_$tag.intoto.jsonl \
--signer-workflow wstein/workharbor/.github/workflows/release.yml \
--source-ref refs/tags/$tag --source-digest "$commit" \
--deny-self-hosted-runnersAlso check the file against checksums.txt (shasum -a 256 -c). make install-release does both. The exact gh flags (--source-ref, --source-digest, --deny-self-hosted-runners included), the offline check with --bundle and whether OpenSSF Scorecard counts the attached bundle as a signature are unverified until a real draft release has been checked (#180).
Then, as whr, build the tool store with the guest launcher (the script prints the exact command):
/opt/whr/bin/whr tools build -store <tool store> -shim /opt/whr/libexec/whr/whr-shim-linux-arm64make install builds from the source tree of a clone of the repository instead. It refuses a dirty tree and a commit that is not on origin/main, and is for a developer’s machine, not the supervisor.
For a development installation, make install defaults to $HOME/.local.
Select that installation explicitly when running setup or doctor (provisional;
unverified on the reference host):
whr setup --dev --user "$USER" --only config-base --dry-run
whr setup --dev --user "$USER" --only config-base
whr doctor --dev --user "$USER"config-base writes the usual ~/.config/whr/config.json; it does not install
or start a service. Remove --only config-base to run the other setup steps.
--dev prints a warning: a supervisor in a user-writable prefix can be replaced
by that user and lacks the managed installation’s replacement protection.
The account and remote-access checks still apply, and running as root or from a
Git working tree is still refused.
An explicit --prefix /absolute/path takes precedence over $HOME/.local.
Use the same prefix with make install, setup and doctor. Symlinks are resolved
before checking the binary’s location. The prefix must be a directory of its
own (not /, your home or any directory above your home), and the binary, the
prefix and every directory above it must belong to the account that runs whr
or to root and be closed to group and other writers. One exception: a sticky
directory above the prefix, such as /tmp, may be writable by others, because
it only lets an owner replace its own entries; a sticky directory between the
prefix and the binary is still refused. Without --dev, setup retains the
managed prefix list; --prefix alone selects a custom managed installation,
whose ownership doctor checks separately.
Development mode is chosen by the flag --dev or by one key in the
configuration, never by an environment variable (WORKHARBOR_DEV and the like
change nothing). An explicit whr setup --dev remembers the choice: after you
confirm a diff, it writes development_prefix, the absolute prefix, as a
top-level key of config.json, and says so. From then on whr setup,
whr doctor and whr service install read the key as --dev --prefix <value>
and whr serve only logs a warning at start, so --dev need not be typed
again; an explicit --dev or --prefix wins, and --prefix without --dev is
a managed call that ignores the key. whr doctor reports warn on every run
while the key is set, naming the key, the file and the way out. The key loosens
nothing beyond --dev: whr setup and whr doctor run every check of the
prefix again on each read (owner, writer, home, binary under the prefix), and
whr service install runs the configuration’s checks of the key and its file,
the refusal of a managed whr and the test that its binary lies under the prefix,
but not the owner, writer and home walk.
The key is refused, as a configuration error (setup and doctor fail, whr serve
does not start), when its value is not an absolute path or is a managed prefix
(/opt/whr, /opt/homebrew, /usr/local), when whr itself runs from a
managed prefix, or when the configuration file is not a regular file with one
link, owned by you or root, closed to group and other writers, outside every
workspace root and git working tree. The file is opened without following a
link and checked on the open file. These checks guard against a mistake: the
account can write its own configuration, so anything running as it can write
the key too, and the workspace-root check reads its roots from the same file, so
it catches a stray file, not a crafted one. What limits the key is that a
managed installation refuses it.
To leave development mode run whr setup --managed (--only development-key
does just that step). It shows a diff, removes development_prefix after your
y, keeps every other key, and then checks the managed prefix; it cannot be
combined with --dev. From a whr that is not an installed binary (a source build
or a user-writable one) it runs only --only development-key: every other step
is refused, so install the release first. It removes every spelling of the key
that differs only in case. Deleting the key by hand does the same. --dry-run writes
nothing, and whr doctor and whr serve never write the key.
When setup reaches service-install, it passes the chosen executable through
whr service install --whr <binary>, and whr service install with the key set
requires that binary under development_prefix. The LaunchAgent retains that
executable path, so stopping and starting it needs no development flag. The default production
procedure remains the administrator-owned release installation above.
From v0.1.0: the tap
A published, non-prerelease release updates the Homebrew tap, so the tap never points at a draft:
brew install wstein/tap/whrThe formula installs whr, the guest binaries in libexec/whr and the shell completions. The formula route is unverified until #62 installs it. The host’s Homebrew packages are pinned and not upgraded unasked (HOMEBREW_NO_AUTO_UPDATE=1, see Prepare the Mac mini); upgrade whr on purpose.
brew upgrade whr is not blocked by HOMEBREW_NO_INSTALL_UPGRADE: Homebrew’s manual says that variable only stops brew install from upgrading an installed formula. brew pin does hold a formula through brew upgrade, so if whr was pinned, run brew unpin whr first and pin it again afterwards (the manual page of brew; not run against a whr formula, unverified).
The formula has depends_on arch: :arm64, so on an Intel Mac Homebrew stops with its own message about the unsupported architecture (unverified: read from the formula, not run). Release 1 supports Apple silicon only.
Back up, upgrade and restore
One procedure serves all three: stop, copy or replace, start. Run it as the whr user, from its desktop session (Prepare the Mac mini, step 2). The paths below are the defaults; where the configuration sets state_dir or other roots, use those.
What to copy
| What | Where | Rule |
|---|---|---|
| Configuration directory | ~/.config/whr (the configuration file) | A plain copy. It also holds secret files by default (below). |
| State directory | ~/.local/state/whr, or state_dir | Holds the database workharbor.db with workharbor.db-wal and workharbor.db-shm. The database runs in WAL mode, so a copy of workharbor.db alone, or of any of the three taken while whr serve runs, can be inconsistent. Copy all three with whr serve stopped. The audit log is stored with the database; whether a separate audit file exists is unverified. api.sock in the same directory is a socket: skip it. The same directory holds topics (the supervisor’s own repositories: the commits the supervisor has imported from an agent’s exported bundle, D42, design §4.5; commits the agent made after the last export are not in it), mirrors (the cache of the forge repositories: rebuildable, can be large, so skipping it is fine) and the editor copies (open, the copies you open in your editor). Back up topics with the database. Whether to keep the editor copies is your call (unverified: their directory name is read from the code, not from a restore). |
| Secret files | Every file the configuration names: api_token_file, github.key_file, agent_api_key_env_file, bot_signing_key_file, console.ssh_ca_key_file, the ntfy files | Only into an encrypted backup (a FileVault volume or an encrypted disk image). They may live outside ~/.config/whr, so read the paths from the configuration. Keep mode 0600 when you restore them. |
| Workspace folders | Every folder below roots.workspaces | The agents’ working folders and their session state. An agent’s commits live in the workspace’s agent clone until the supervisor exports them as a bundle into topics (above), so these folders hold the unexported commits as well as uncommitted changes and the session state; topics alone can miss committed work. Restore them from the same backup as the database, so session and database agree (unverified). |
| Tool store | roots.tool_store | Not copied. Rebuildable with whr tools build, and whr serve verifies it at start against the hashes recorded when it was built (design §5.6); it refuses to start when it does not verify. |
| Not copied | The agent-home volumes and container images | Exclude them from every backup. An agent-home volume holds the agents’ sessions and the subscription logins (D40); no whr command copies one, and you should not either (design §7.3). Images are rebuilt. |
The procedure
- Stop the supervisor.
whr service uninstallunloads the job (the logs stay); if you runwhr serveby hand, stop that. Check that nowhr serveprocess is left. Stopping the supervisor does not stop the environments: an agent can keep running in its environment whilewhr serveis down, so the workspace folders andtopicsmay change under your copy (design §4.1). Stop the environments first (whrhas no single command for it in this draft; stop each workspace’s environment, unverified), or accept that the copy may be mid-change. Runs resume after the start (unverified). - Back up every path in the table, to an encrypted destination for the secret files. Prefer a manual copy of the paths above while the supervisor is stopped. Time Machine cannot be timed to a stopped supervisor, and whether its snapshot covers the database and its two WAL files at one instant is not measured (unverified).
- Upgrade (skip for a plain backup). Read the release notes and the upgrade notes below, then install the new version as the administrator (
make install-releaseorbrew upgrade whr). - Restore (skip unless restoring). With the supervisor still stopped, put back the configuration directory, the state directory (the database file with its
-waland-shmfiles, together from one backup, or none of the three), the secret files at the paths the configuration names, and the workspace folders. Remove stale-waland-shmfiles that do not belong to the restored database. - Start the supervisor.
whr service install(thewhr servicegroup is provisional, like every command here) writes the plist for the current binary; the database migrates on the first start of a new version. - Check.
whr doctor, thenwhr version.
What a restore loses
The agent-home volumes are not in the backup, so a restore on the same Mac keeps the volumes that still exist, and a restore on another Mac or after the volumes were removed has none. Then:
- Agent logins are gone. Sign in again inside each environment, through the vendor’s own flow (D40);
whrnever stored the login. - Logins, not sessions, are in the agent home. Session state (transcript and agent session ID) lives in the workspace folder (design §4.3), so it comes back with the folder. It is lost only where the folder is not restored or the agent no longer knows the session: a run whose session the agent cannot resume (
ErrNoSession), or that never reported one, endsfailedand waits on a retry-or-cancel Decision (design §5.3). Start a new run from the workspace; the agent’s files and commits in the folder are still there. - Runs and tasks come back as the database recorded them at the time of the backup, so a run that finished later is unknown to the restored supervisor. A Decision that was open in the backup may be superseded by the restart rule (design §4.2); the agent asks again.
- Resume after a restore. A run recorded
starting,runningorpausedbecomesinterruptedand resumes from its session, if the restored workspace folder still holds it, with a briefing from the supervisor (D27); otherwise it endsfailedas above. The restored database may be older than the workspace folder and its session, so check the workspace before you let the agent repeat anything.
The restore itself, and what resumes afterwards, has not been rehearsed on the real setup: unverified.
The database migrates on the first start of a new version. There is no downgrade: keep the backup from the procedure above. Versions are 0.x until the API, the adapter contract and the migrations are stable, so read every upgrade note.
Upgrade note: the integration branch is part of the workflow policy (migration 0016)
Since migration 0016, the recorded workflow of a repository includes its integration branch (main or develop) next to the preset. On the first start after the upgrade, a branch that was recorded as empty and is now read from the configuration counts as a policy change, and whr serve stops with a message that names the repository and both values. Check them, then do one of:
- start once with
whr serve --accept-workflow-change; or - with a passkey enrolled, start normally and confirm the change on the web page Changes; it applies at the next start.
Tasks already started keep the policy they started under. Repository names are now matched without regard to case, so a name written in another case is the same repository.
Cut a release (the maintainer)
Only a human tags, signs and publishes (D24, §6); an agent never does.
make release-prep VERSION=vX.Y.ZregeneratesCHANGELOG.mdand commits it aschore(release). It does not tag.- After CI is green on that commit of
main, push a signed, annotated tagvX.Y.Z. The release workflow checks the signature against.github/release-signers, that the commit is onmainand that CI passed, then builds into a draft:whr, the guest binaries,checksums.txt, an SBOM, a build-provenance attestation and its bundle (whr_<tag>.intoto.jsonl). - Check the draft: install it on your own prefix with
make install-release VERSION=vX.Y.Z, which verifies the checksums and the attestation, and read the notes. - Publish it. For a release (not a prerelease) the
tapworkflow renders the formula and pushes it towstein/homebrew-tap; a prerelease never updates the tap.
Set up the tap once (the maintainer)
The tap workflow pushes the formula with a deploy key, which issue #62 installs. unverified until #62 is done, and the details below come from .github/workflows/tap.yml, not from a run:
- The private half is the secret
TAP_DEPLOY_KEYin thehomebrew-tapenvironment ofwstein/workharbor, so only that workflow job can read it. - The public half is a deploy key with write access on
wstein/homebrew-tapand on no other repository. - To rotate it, generate a new key pair, add the new public key to
wstein/homebrew-tap, replace the secret, run the workflow once, then delete the old deploy key. Never put the private key in a file in the repository or in a chat.
make release-snapshot builds the artifacts locally into dist/ without publishing, to test the pipeline.