Skip to content

Agent of Empires

The phone control plane for ruche, the disposable agent Host: tmux sessions that know whether an agent is idle, working, blocked or done, plus a web dashboard that installs as a PWA and pushes a notification when a session blocks or finishes (ADR-0054). Upstream: agent-of-empires/agent-of-empires

  • URL: https://essaim.{agents_domain} — tailnet only, on the agent tier’s own DNS zone (ADR-0068)
  • Data: nothing backed up. Sessions, worktrees and the push keypair are all rebuildable or disposable, which is the whole point of the Host
Terminal window
auberge deploy aoe -H ruche # this App alone
auberge deploy ruche -H ruche # the whole agent tier

[!IMPORTANT] The role is guarded on when: "'agent' in group_names", so the Host must carry tags = ["agent"] in hosts.toml or the deploy is a green no-op that installs nothing. auberge host list shows the TAGS column. Not to be confused with tailnet_tag = "agent", an unrelated field naming the ACL tier.

Set up the agent tier’s DNS zone first — the vhost needs a certificate for essaim.{agents_domain}, and DNS-01 is the only challenge a Host with no public ingress can answer.

Key Purpose
agents_domain the agent tier’s own domain; essaim.{agents_domain} is the dashboard
agents_cloudflare_dns_api_token answers DNS-01 for that zone, and for no other
aoe_subdomain the label, and the agent tier’s serving gate — host-scope it
aoe_tailscale_ip the Host’s tailnet IPv4: what Caddy binds and what Blocky publishes
aoe_passphrase the login passphrase, minimum 8 characters

aoe_tailscale_ip is fleet-wide, never host-scoped: the Blocky that publishes the name runs on another Host and cannot read a value scoped to this one (ADR-0059). aoe_subdomain is the opposite — host-scope it, because it also decides which Host’s Caddy holds the agent tier’s ACME token (ADR-0072).

aoe_tailscale_ip = "100.64.0.9"
aoe_passphrase = "!<your store command>"
[hosts.ruche]
aoe_subdomain = "essaim"

Signing in takes both:

Factor Where it comes from
a bearer token generated by aoe serve, in the URL, rotates every four hours
the passphrase aoe_passphrase, typed on the login page

The token is not settable and not stored in config.toml — there is no flag or environment variable for it. Read the current pairing URL on the box:

Terminal window
ssh ruche 'aoe url' # the full URL, token included
ssh ruche 'aoe url --token-only' # just the token, to paste into the PWA

Pairing binds the device, and a bound device stays signed in across token rotations. Changing aoe_passphrase signs every device out.

[!TIP] Wrap the two commands above into one-command pairing — open the dashboard directly, or print a QR code (--qr) for a phone, which beats typing a 64-character token on a keyboard that isn’t there. -h/--help prints usage without touching the network:

#!/bin/bash
readonly AOE_URL="https://essaim.{agents_domain}"
main() {
local token url
case "${1:-}" in
-h | --help)
echo "Usage: $(basename "$0") [--qr]"
exit 0
;;
esac
token="$(ssh ruche 'aoe url --token-only')" || exit 1
url="${AOE_URL}/?token=${token}"
if [[ "${1:-}" == "--qr" ]]; then
qrencode -t ansiutf8 "$url"
else
xdg-open "$url"
fi
}
main "$@"

aoe serve binds loopback and Caddy is the only thing in front of it. Three flags make that work, and one is deliberately absent:

Flag Why
--behind-proxy Caddy terminates TLS: cookies get Secure, the forwarded client IP is trusted
--allowed-host essaim.{agents_domain} aoe’s DNS-rebinding gate rejects a proxied Host it was not told about
--allowed-origin https://essaim.… the browser Origin is the https one, not the loopback one aoe binds
--remote never. It publishes the dashboard through a public Funnel or Cloudflare tunnel

HTTPS is functional here, not cosmetic: PWA install and web push both require a secure context, which is why the tailnet-only dashboard still carries a real certificate.

  • Install from Chrome on Android: open the dashboard, menu → Add to Home screen.
  • Push is offered after install. The VAPID keypair is server-generated and held on the box; nothing provisions it and nothing backs it up. Losing it means every device re-subscribes — acceptable under the Host’s disposability rule.
  • An installed PWA binds to its exact origin. Treat essaim.{agents_domain} as frozen once anyone installs it; changing the domain forces a delete-and-reinstall on every device.

Push subscriptions are bound to the bearer token and dropped when it rotates past a grace period. Login sessions are independent (30-day sliding window, persisted across restarts), so rotating the token kills push, not sign-in.

Sessions survive both a client detach and an SSH disconnect: the dashboard runs in the unprivileged agent user’s own systemd manager with lingering enabled, so nothing is torn down at logout.

Terminal window
ssh ruche 'systemctl --user status aoe'
ssh ruche 'journalctl --user -u aoe -f'
ssh ruche 'aoe serve --status' # the daemon's own view: PID, mode, URLs, log path

MemoryHigh is declared at 4G and MemoryMax deliberately is not. MemoryHigh throttles and reclaims; MemoryMax is where the kernel kills. aoe supervises tmux sessions running agents it did not fork, and until it is observed which cgroup those land in, a kill line risks OOM-killing an agent mid-run. Set one once that is known.