Add a dedicated runner
Use this recipe to give a deployed Happy Agent node — standalone or team — a separate machine that does its work. It is strongly recommended for every remote node. The node keeps the database, provider credentials, API token, and inference; the runner holds the project folders and runs everything that touches them: agent file tools and commands, Git, workspace checkouts, setup commands, terminals, file browsing, previews, and MCP stdio servers. An agent command that goes wrong, or a dependency that runs hostile code, then lands on a machine with none of the node's secrets.
Read Runners in the configuration guide for the settings, and the
installed release's API.md (in the source checkout packages/happy-agent/API.md) for exact
requests. The walkthrough targets a fresh Linux runner host with systemd. Execute it end to end,
as the recipe rules describe; ask only for the choices below that are still missing.
1. Resolve the choices
- Which node gets the runner, and which machine and SSH account host it? Confirm the runner host's key, operating system, architecture, and permission to install a persistent service there. The runner host must not be the node's own machine; that would defeat the separation.
- A short runner ID (
[a-z][a-z0-9_-]{0,63}, for examplebuild-box) and a friendly name. - Which runtimes and package managers the user's projects need on the runner. Git is always required. Docker is needed only for container workspaces (step 7).
- The Git name and email commits on the runner should use — the same identity the node's own Git used, unless the user wants another.
A runner holds no provider credentials, no Happy data, and no API token. Do not copy any of them to it. Repository tokens stay on the node too: Git on the runner reaches remotes through the runner connection, and the node adds the token itself.
2. Generate the runner's token and add it to the node
Generate a fresh token privately on the node — 32 random bytes as 43 base64url characters — and never print it, put it in a command argument, or reuse another token:
umask 077
head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n' > ~/.runner-token-build-box
Merge the runner into the node's machine happy.toml, writing the token from that file with a
TOML serializer rather than pasting it, and preserving every unrelated setting:
[runners]
default = "build-box"
[runners.build-box]
name = "Build box"
token = "REPLACE_WITH_THE_GENERATED_TOKEN"
default is optional with exactly one runner; with several it names the runner used for new
projects that name none, for the home project, and for bot folders. Each token must differ from
[api] token, every connection token, and every other runner's token.
Once any runner is configured, the node runs nothing on its own machine. Projects registered on the node's own disk stay listed so they can be archived, but their agents, terminals, and files refuse to work there. Tell the user before restarting, and plan to register or clone those projects again on the runner (step 6). Apply the change by draining and restarting the node as in its deployment recipe; keep the node's Tailcat identity and data.
3. Prepare the runner host
Install the same Happy Agent release as the node, with the same download and checksum steps as Deploy a standalone remote agent. Use a dedicated unprivileged account that owns the runner's folders:
sudo apt-get update
sudo apt-get install -y ca-certificates curl git
sudo useradd --system --create-home --home-dir /var/lib/happy-runner \
--shell /bin/bash happy-runner
sudo chmod 0700 /var/lib/happy-runner
sudo install -d -m 0700 -o happy-runner -g happy-runner /var/lib/happy-runner/.happy-runner
Agent commands run in the sandbox on the runner, so on Ubuntu 24.04 follow the
Ubuntu AppArmor host prerequisite
for the runner's executable, exactly as for a node. Install the projects' runtimes for the
happy-runner user, and set its Git identity:
sudo -u happy-runner -H git config --global user.name "CONFIRMED_NAME"
sudo -u happy-runner -H git config --global user.email "CONFIRMED_EMAIL"
4. Give the runner its token and the node's address
Transfer the token to the runner host over the approved private path, straight into the token file, and delete the node's temporary copy afterwards:
sudo install -m 0600 -o happy-runner -g happy-runner /dev/stdin \
/var/lib/happy-runner/.happy-runner/token < runner-token-from-the-node
The runner dials the node; it needs no inbound port. Read the node's Tailcat address and port from
its private .happy/agent/tailcat/address and tailcat/port files — the same values a primary
installation's connection uses — and write the endpoint into a private environment file. Both
standalone and team nodes are reached this way; a node that is already reachable over a private
network may use https://host or http://host:port instead.
printf 'HAPPY_RUNNER_ENDPOINT=tailcat:%s:%s\n' "$NODE_TAILCAT_ADDRESS" "$NODE_TAILCAT_PORT" |
sudo install -m 0600 -o happy-runner -g happy-runner /dev/stdin \
/var/lib/happy-runner/.happy-runner/endpoint.env
The runner reads the token from that file and removes both variables from its own environment, so nothing it starts inherits them, and agent commands cannot read its private directory.
5. Install the runner service
Install /etc/systemd/system/happy-runner.service:
[Unit]
Description=Happy Agent runner
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=happy-runner
Group=happy-runner
WorkingDirectory=/var/lib/happy-runner
Environment=HOME=/var/lib/happy-runner
EnvironmentFile=/var/lib/happy-runner/.happy-runner/endpoint.env
ExecStart=/usr/local/bin/happy-agent runner
Restart=always
RestartSec=5
KillMode=control-group
[Install]
WantedBy=multi-user.target
The token and endpoint stay out of the world-readable unit. Folders live under the runner's home
unless --home <directory> names another absolute directory. Start it and wait, with a bounded
deadline, for the connection:
sudo systemctl daemon-reload
sudo systemctl enable --now happy-runner
sudo journalctl -u happy-runner -n 50 --no-pager
The log says Connected to the daemon. A refused token, a node without runners configured, or an
unreachable endpoint is logged in plain words; the runner keeps retrying, so fix the cause rather
than restarting in a loop.
6. Verify from the node
Through the connection the user will use:
GET /v0/runnerslists the runner asconnected, with its version, platform, and home. The home project and bot folders now report arunnercompute.- Follow Testing a deployed node with a temporary project
registered or cloned on the runner — pass its
runnerId, or rely on the default. Confirm the project's compute is{ "type": "runner" }, that the agent's shell reports the runner's hostname, that a terminal opens in the folder, and that the file tree lists it. Archive the project afterwards. - Re-register or clone the user's real projects on the runner when they ask. A clone through the node's repository credentials proves Git access without any token on the runner.
Stopping the runner makes its folders' work fail with runner_unavailable until it reconnects;
nothing falls back to the node.
7. Optional: container workspaces
To run a project's workspaces in containers on the runner, install Docker there and let
happy-runner use it. Membership in the docker group is root-equivalent on that host; say so and
get approval first. Then set the project's defaultWorkspaceCompute to
{ "type": "docker", "image": "..." }. Each new workspace's agent tools run in a container of that
image with the folder mounted at the same path; Git, terminals, and previews use the runner's copy.
More runners, rotation, and removal
Add more runners with their own IDs and tokens and choose one per project with runnerId. Upgrade
runners with the node, keeping one release across both. To rotate a token, write a new one to both
the node's happy.toml and the runner's token file, then restart both. To remove a runner, delete
its entry and restart the node: the node refuses its token and tells it to stop everything it held.
Then disable happy-runner on that host.