Multi-host fleet¶
Adding a second host to an existing fleet, renaming one after an OS hostname change, and relocating a live agent from one host to another.
One shared GitOps repo drives many hosts. agents.yaml pins each agent to a host via
host:; on each host apply provisions only its own agents and self-registers that
host. Host identity is always the real os.Hostname() (no host.name scalar in the
committed yaml), so host: pins and the hosts: registry keys are the machines' actual
hostnames.
host:
teams_gateway: teams-a # hostname of the machine that runs the Teams gateway
hosts: # fleet age-recipient registry (each host self-registers on join, and on apply)
teams-a: age1… # keyed by os.Hostname()
teams-b: age1…
agents:
agent-a: { runtime: pi, channel: teams, owner_aad: <objectId>, host: teams-a }
agent-b: { runtime: pi, channel: teams, owner_aad: <objectId>, host: teams-b }
Joining a new host¶
Easier path: the
topweb dashboard's ⛓ join fleet wizard orchestrates this whole sequence over SSH (prepare deploy keys → seed/adopt → the age-recipient handoff → apply). The manual runbook below is the same steps by hand.
NEW = the joining host, REC = any host already a secrets recipient. Steps are
idempotent. The only unavoidable manual hops are the one GitHub deploy key and the
pull/apply handoff that exchanges age recipients.
# ── on NEW ────────────────────────────────────────────────────────────────────
# 0. Install the agentctl deb (apt / dpkg -i).
# 1. Fleet git deploy key (the one GitHub step). Generate a key, then register its
# PUBLIC half as a READ-WRITE deploy key on the fleet repo (writeback needs push):
sudo install -d -m700 /root/.ssh
sudo ssh-keygen -t ed25519 -N '' -C "$(hostname)-fleet" -f /root/.ssh/fleet_deploy
sudo cat /root/.ssh/fleet_deploy.pub # → GitHub repo ▸ Settings ▸ Deploy keys ▸ Add (Allow write)
sudo git config --global core.sshCommand \
'ssh -i /root/.ssh/fleet_deploy -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new'
# 2. Join + provision. init --git clones the FILLED ledger (don't init empty) and
# registers this host: age key, hosts.<hostname>, pushed. Same as seed/adopt do.
sudo agentctl init --git git@github.com:<you>/<fleet-repo>.git
sudo agentctl provision
sudo agentctl apply # converges. Re-registers if step 2 could not (idempotent).
# Agents pinned to OTHER hosts are skipped.
# ── on REC (an existing recipient, e.g. the first/gateway host) ────────────────
sudo git -C /opt/agentctl pull --rebase
sudo agentctl apply # sees 2 recipients → re-wraps the secrets.d/ shards to include NEW, pushes
# ── back on NEW ────────────────────────────────────────────────────────────────
sudo git -C /opt/agentctl pull --rebase # NEW can now decrypt the shared store
# 3. Add this host's agents ON this host (auto-pinned host: <hostname>):
sudo agentctl agents add agent-x --channel teams --runtime pi \
--owner-aad <owner-object-id> --provider openrouter --model <id> --api-key <key> --no-admin
sudo git -C /opt/agentctl push # publish the new agent (agents add commits but doesn't push)
# ── on the gateway host (Teams only) ───────────────────────────────────────────
sudo git -C /opt/agentctl pull --rebase && sudo agentctl apply # gateway learns the new agent's route
- Writeback: with
originset (via--git),applyautopull --rebase+pushes. It refuses to push a tree with merge-conflict markers (resolve withgit checkout -- <file>, then re-apply). - Secrets: the per-owner
secrets.d/*.sops.yamlshards are each encrypted to every host's age recipient; the re-wrap runssops --config <repo>/.sops.yaml updatekeysper shard (the--configis required). Per-owner sharding means concurrent secret writes to different agents touch different files, with no shared-blob conflict. The age private key stays host-local. - Teams cross-host: the gateway binds
0.0.0.0; a remote agent dialsws://<teams_gateway>:3978/registerover the private network. Keep that port off the public IP (NSG/firewall); the public proxy fronts only/api/messages. Multi-host works forclaudeagents too (only the gateway piece is pi-specific).
Renaming a host¶
The stable identity is the host's age recipient (the private key survives an OS rename), so after hostnamectl set-hostname <new> just run:
sudo agentctl host rename # finds the old name by pubkey match, new = os.Hostname()
sudo agentctl apply # converge units to the new identity
sudo);
the ledger commit + push then happens in agentd's git credential context, like every other
ledger mutation.
It rewrites the hosts: registry key, every agent's host: pin, and host.teams_gateway, then commits + pushes. Use --from <old> / a positional <new> to override the auto-detection.
Agent relocation¶
agentctl migrate relocates one agent to this host from a source agents.yaml, over SSH or from a local path. It does a fresh agents add on the destination, re-activates the agent's declared tools / skills / subagents / cron, rsyncs live state (workspace + git, Claude sessions/transcripts, native memory with the project-dir key remapped), and moves the OAuth tokens, so no re-login. It excludes node_modules, caches, browser profiles, and host-bound secrets. --cutover stops the source first (agents stop-self over SSH); without it the source keeps running so you verify the destination first. Per-agent MCP secrets aren't auto-carried; migrate prints exactly which secret set --agent calls the destination needs.
agentctl migrate you@<host>:22 ./agents.yaml --agent assistant -i ~/.ssh/id_ed25519