Run the control plane in the cloud¶
The control plane that Agents Host runs on your Mac can only be reached while the Mac is awake and online. Run it on a machine you rent instead, at a domain of your own, and it is always there. Windows, phones and hosts reach it at one address from anywhere. Its certificate comes from Let's Encrypt, so it is trusted like any website's, and codes carry no pin.
This guide runs one copy, with its store on the machine's own disk, behind Caddy. For several copies over a bucket, see Run the control plane as several copies.
Before you start¶
- A clone of the Agents repository on a Mac with Docker running, to build the Linux programs: they are Go, built in a container.
- A Linux machine you rent, on x86-64 or ARM64, with Docker and Docker Compose. The smallest size a provider offers is plenty: 1 CPU and 1 GB of memory. The control plane runs no agents itself.
- A domain you control, such as
example.com, so that you can add DNS records for two names likeagents.example.comandviews.example.com. The second is where the web page draws MCP Apps views. - An email address for Let's Encrypt to write to about the certificate.
- A Google Cloud project with Cloud KMS turned on, and the
gcloudcommand signed in to it. Cloud KMS keeps the key that seals the control plane's own key, so that key is never on the machine's disk in clear.
Google Cloud¶
On Google Cloud, three scripts do most of the steps below for you: they make the machine,
its firewall, its address, the Cloud KMS key and the DNS records, then build and start the
control plane. Each script looks first for what it makes, so you can run it again safely.
Each takes --dry-run, which prints every command and runs none.
What you make first¶
- A Google Cloud project, with a billing account linked to it. Link it in the console
(Billing ▸ Link a billing account). A billing account that is not linked to the
project is not sufficient: without the link,
up.shcan't turn on Compute Engine or Cloud KMS. - In that project, a service account for the scripts, with these roles: Service Usage
Admin, Compute Admin, Cloud KMS Admin, Service Account Admin and Service
Account User. Make a JSON key for it, and put the key in
~/.agents/secrets/gcp-agents-control.json(chmod 600). The scripts read the project from this file. They use a gcloud configuration of their own, in~/.agents/gcloud/agents-control, and do not change yours. - The domain, such as
agentloops.dev, on Cloudflare's DNS, and a Cloudflare API token from the Edit zone DNS template, for that zone only. Put the token in the Keychain:
security add-generic-password -a "$USER" -s agents-cloudflare-token -w
- For notices on iPhone and iPad with no Mac host: the APNs
.p8key, as in Build and copy what it runs. Its name gives the Key ID. - On your Mac:
gcloud(brew install --cask gcloud-cli) and Docker, running, with thebuildxplugin. The image is forlinux/amd64, and on an Apple silicon Mac only BuildKit can build it.docker buildx versiontells you if you have the plugin. If not:
brew install docker-buildx
mkdir -p ~/.docker/cli-plugins
ln -sfn "$(brew --prefix)/opt/docker-buildx/bin/docker-buildx" ~/.docker/cli-plugins/docker-buildx
Run the scripts¶
In the repository on your Mac:
- Look at what
up.shwill do:
deploy/gcp/up.sh --dry-run
- Make the machine:
deploy/gcp/up.sh --apns AuthKey_KEYID.p8
It turns on Compute Engine, Cloud KMS and IAM. The IAM API needs about a minute after
it is first turned on, so up.sh waits until IAM answers. Then it makes:
- the Cloud KMS key ring agents and key control;
- the service account agents-control-vm, which may only encrypt and decrypt with that
key. The machine runs as this account, so no key file for it is on the machine;
- the network agents-control, with ports 80 and 443 open to all, and port 22 open
only to your Mac's IPv4 address;
- a static IPv4 address;
- an e2-micro machine in us-west1-b, running Debian, with a 30 GB standard disk.
A startup script puts Docker on it.
It prints the machine's address, then stops: the names do not point at it yet.
3. Make the DNS records, with the address that up.sh printed:
deploy/cloudflare/dns.sh <the address>
It makes an A record for control.agentloops.dev and one for
views.agentloops.dev, DNS only (not proxied), and never prints the token. Use
--zone and --names for other names.
4. Run up.sh again, with the same options:
deploy/gcp/up.sh --apns AuthKey_KEYID.p8
When both names point at the machine, it builds the Linux host and the control
plane's image on your Mac, copies them, deploy/compose.public.yaml and the secrets to
/opt/agents on the machine, and starts the control plane and Caddy. It waits until
https://control.agentloops.dev/healthz answers. Let's Encrypt's address for notices is
your git email, or give --email. Use --domain and --views-domain for other names.
To keep the control plane key from your Mac, give --seal-key control-key at this
step, before the first start. See
Move the control plane from your Mac.
5. Get the first code with the command that up.sh prints, then go on at
Pair your first window.
To upgrade, run up.sh again: it builds and copies the new image, and keeps the store
and the APNs key.
To remove it all, run deploy/gcp/down.sh. It asks first. The store is on the
machine's disk, so after down.sh every window, phone and host must pair again. The
Cloud KMS key stays, unless you give --destroy-key. Remove the DNS records at Cloudflare
yourself.
What it costs¶
| Item | Cost |
|---|---|
| The static IPv4 address | about $3.65 a month |
| The e2-micro machine in us-west1 | free tier (one a month) |
| The 30 GB standard disk | free tier (30 GB a month); a balanced disk, the console's default, is billed |
| The Cloud KMS key | about $0.06 a month, and $0.03 for each 10,000 uses |
| Traffic out | free tier (1 GB a month), then billed |
Steps¶
On another provider, or to do it by hand, follow these steps.
Get the machine ready¶
- Rent the machine, and note its public IPv4 address (and its IPv6 address, if it has one).
- Open ports 80 and 443 to the internet, in the provider's firewall and in the machine's own if it has one. Caddy needs both: 443 for connections and for proving the name to Let's Encrypt, and 80 for its other challenge and its redirect to HTTPS. Keep every other port closed. The control plane's own port, 8791, is only reached by Caddy inside Docker.
- Install Docker and the Compose plugin, following Docker's guide for the machine's distribution.
Point the name at it¶
- At your DNS provider, add an A record for the name, such as
agents.example.com, with the machine's IPv4 address. If the machine has IPv6, add an AAAA record too. If it has no IPv6, add no AAAA record: Let's Encrypt prefers IPv6, and a record that reaches nothing makes the certificate fail. - Add the same records for the views name, such as
views.example.com. The web page draws MCP Apps views from this name, because a view must have an origin of its own, apart from the page. A CNAME record to the first name is also correct. - Check both from your Mac:
dig +short agents.example.com
dig +short views.example.com
Each prints the machine's address. Wait for that before you start Caddy: until the name resolves, Let's Encrypt can't check it, and too many failed tries make it wait an hour.
Make the key that seals the control plane's key¶
The control plane's key proves to every window, phone and host that it is your control plane. On the machine it is sealed in the store by a key in Cloud KMS, which never leaves Google Cloud. The machine gets only the right to use that key.
On your Mac, with gcloud (put your project's id in place of my-project):
- Make a key ring and a key:
gcloud kms keyrings create agents --location global --project my-project
gcloud kms keys create control --keyring agents --location global \
--purpose encryption --project my-project
- Make a service account that may use only that key:
gcloud iam service-accounts create agents-control --project my-project
gcloud kms keys add-iam-policy-binding control --keyring agents --location global \
--member serviceAccount:agents-control@my-project.iam.gserviceaccount.com \
--role roles/cloudkms.cryptoKeyEncrypterDecrypter --project my-project
- If the machine is not a Google Compute Engine VM that runs as that service account, make a key file for the service account:
gcloud iam service-accounts keys create sa.json \
--iam-account agents-control@my-project.iam.gserviceaccount.com
On a Compute Engine VM that runs as the service account, make no key file: the VM gets its right to use the key from Google Cloud, and no secret of any kind is on its disk.
Build and copy what it runs¶
On your Mac, in the repository:
- Build the Linux host:
scripts/build-linux-agentsd.sh
It goes in App/Resources/servers/, from where the control plane hands it to every
Linux server that joins. The control plane also hands each server the runtimes'
toolsets in App/Resources/toolsets/, so the server installs Claude by itself. The control plane itself is built in its image, on the
machine, from Go/ and Web/.
2. Say which key seals the control plane's key, and give the service account's key file:
deploy/make-secrets.sh --public \
--kms projects/my-project/locations/global/keyRings/agents/cryptoKeys/control \
--service-account sa.json
This writes AGENTS_CONTROL_KMS in deploy/.env, and copies the key file to
deploy/secrets/gcp-service-account. On a Compute Engine VM that runs as the service
account, leave out --service-account. Then delete sa.json: anyone who has it and a
copy of the store can open the control plane's key. Never commit either.
For notices on your iPhone and iPad when no Mac host relays them, give the APNs key from your Apple developer account (Certificates, Identifiers & Profiles ▸ Keys, with Apple Push Notification service on). Do it now or later, with or without the rest:
deploy/make-secrets.sh --public --apns AuthKey_KEYID.p8
This copies the key to deploy/secrets/apns-key, and writes AGENTS_APNS_KEY_ID (read
from the file's name) and AGENTS_APNS_TEAM_ID in deploy/.env. Give --apns-key-id
when the file has another name. Without it, the control plane sends no pushes, and its
log says so once when it starts. Never put the key in a code, the store or the repo.
3. Copy the repository's deploy/ folder (with deploy/secrets), Go/, Web/,
App/Resources/servers/ and App/Resources/toolsets/ to the machine, keeping the same layout. For example:
rsync -a --relative --exclude node_modules --exclude Web/dist --exclude Go/out \
deploy Go Web App/Resources/servers App/Resources/toolsets you@agents.example.com:agents/
Start it¶
On the machine, in the folder you copied to:
- Say the two names and your email, in
deploy/.env:
AGENTS_DOMAIN=agents.example.com
AGENTS_VIEWS_DOMAIN=views.example.com
ACME_EMAIL=you@example.com
Caddy gets a certificate for each name.
- Start the control plane and Caddy:
docker compose -f deploy/compose.public.yaml up -d --build
The control plane says its version in Settings ▸ Control plane. Put it in
AGENTS_VERSION (in deploy/.env or the environment), such as the output of
git describe --tags --always. Without it, the version is dev.
At its first start the control plane makes its key and seals it in the store. To keep a key from elsewhere, seal it before this step: see Move the control plane from your Mac.
- Within a minute Caddy has its certificate. Check from your Mac:
curl https://agents.example.com/healthz
It prints ok. If it doesn't, look at what Caddy says:
docker compose -f deploy/compose.public.yaml logs caddy.
Pair your first window¶
The first code comes from the machine. After that, every code comes from a paired window.
- On the machine:
docker compose -f deploy/compose.public.yaml exec control /agents-control code --client
It prints a code, which works once, for five minutes. Its eighth field is -: it
carries no pin, because the certificate is publicly trusted.
2. In Agents on your Mac, choose Connect… under Connect to a control plane,
paste the code under CODE, and click Connect.
The window is paired. It has no hosts yet.
Pair your iPhone and iPad from that window, as in Connect a window or phone.
Use Agents in a browser¶
The control plane serves the Agents page at its own address, https://agents.example.com/,
so a browser anywhere can use it. A browser pairs by a code from a window or phone that is
already paired:
- In Agents on your Mac, open Settings ▸ Control plane ▸ Clients and click Add a Browser…. On an iPhone or iPad, tap Add a Browser… at the foot of the sidebar.
- In the browser, open
https://agents.example.com/, paste the code, and click Connect. The code works once, for five minutes.
The browser shows in Clients as Chrome on the web (or its own name). It can do all a
window can. To cut it off, click Forget… on its row. To serve no page, set
AGENTS_CONTROL_PUBLIC_WEB= (empty) in deploy/.env. Sign-in with an account comes later
(#995).
Enrol a Linux server¶
- In the window, open Settings ▸ Control plane, click Show beside Hosts, then Add a Server…, and Copy the command.
- On the server, as the user your agents should run as, run it. It looks like this:
curl -fsSL https://agents.example.com/v1/install.sh | sh -s -- 'agents-control:2:h:…'
There is no --pinnedpubkey in it: curl, and then the host, check the certificate
with the server's own trusted roots. The server needs them installed, which most
distributions do by default; on a minimal image, install ca-certificates first.
The command says devbox joined the control plane. The server has a heading of its own in every window.
See Add a server for the rest: installing over ssh, projects on the server, and its Claude sign-in.
Enrol your Mac¶
Your Mac becomes a host of the control plane in the cloud with Agents Host.
- In the window, open Settings ▸ Control plane ▸ Hosts, click Add by Code…, then Copy.
- On the Mac, open Agents Host. Under Join one elsewhere, paste the code and click Join.
Agents Host says This Mac runs its agents for a control plane elsewhere. The Mac's projects appear under its own heading in every window.
If Agents Host already runs a control plane on this Mac with hosts and devices paired to it, joining one elsewhere starts afresh: those devices pair again. To move an existing control plane across and keep what is paired, see the next section.
Move the control plane from your Mac¶
If Agents Host already runs the control plane on your Mac, you can move it to the machine and keep every window, phone and host paired. The machine gets this Mac's control plane key, so everything that is paired trusts it. Agents keep working while it moves.
Get the machine ready first, as above, with its names and certificate. If the control plane on the machine has already started (it has a store and a key of its own), empty it first.
- On the machine, stop the control plane and remove its store:
docker compose -f deploy/compose.public.yaml down
docker volume rm agents-control_data
This removes everything that the machine's control plane keeps: its hosts, its
clients and its own key. The certificate is in another volume and stays.
2. In deploy/.env on the machine, add this line. It starts the control plane empty, to
wait for this Mac's records:
AGENTS_CONTROL_RECEIVE=1
- In Agents Host on your Mac, click Move to Another Machine…. The sheet opens at step 1.
- Click Save Key…, and save the key as
control-key. Then seal the key in the machine's store, with the key piped in over ssh, so that it is never written on the machine:
ssh you@agents.example.com 'cd agents && docker compose -f deploy/compose.public.yaml build control'
ssh you@agents.example.com 'cd agents && docker compose -f deploy/compose.public.yaml \
run --rm -T control key seal --key-fd 0' < control-key
It says the control key … is sealed in the store by gcpkms://…. On Google Cloud,
deploy/gcp/up.sh --seal-key control-key seals the key, and starts the control plane.
Then delete the saved control-key on your Mac. Anyone who has it can pose as your
control plane.
On Google Cloud, deploy/gcp/move.sh receive control-key does steps 1, 2, 4 and 5 in
one command. It saves the machine's old store to /opt/agents/saved/ on the machine
before it empties it, and it deletes control-key when the key is sealed. After
Move Now, deploy/gcp/move.sh done takes AGENTS_CONTROL_RECEIVE=1 out again.
5. Start the control plane on the machine, if it is not running:
docker compose -f deploy/compose.public.yaml up -d
- In the sheet, click Continue. Type the machine's address, such as
https://agents.example.com, and click Check. Each line must get a tick: It answers, Its certificate is publicly trusted, It holds this control plane's key and It's empty, and waiting for this one. - Click Tell Everyone. Every window, phone and host that is paired gets the new address. The sheet counts who knows. You can move when some have not heard yet: this Mac sends them on later.
- Click Move Now. The sheet shows each stage: records held still, records copied, the machine takes over, this Mac's host and relay go there, and this Mac forwards anyone who still comes to it.
When the move is done, Agents Host says Joined and the machine's address. Windows, phones and hosts connect to the machine in a few seconds, and nothing pairs again. This Mac's copy of the control plane keeps running only to send members on to the machine, for 30 days. It does this again after the Mac restarts.
After the move, take AGENTS_CONTROL_RECEIVE=1 out of deploy/.env. On a store that
it has already taken, the setting has no effect. But if the store is lost, a control plane
with this setting waits for a move and does not start a store of its own.
Bring it back to your Mac¶
- In Agents Host, click Run It Here Again…. The sheet checks both ends: the machine answers and holds this control plane's key, and this Mac's copy is ready and empty.
- Only if the machine made a key of its own (it started with an empty store and no key sealed), the sheet says so. Then write that key to a file on your Mac, piped out over ssh, so that it is never written on the machine:
(umask 077; ssh you@agents.example.com 'cd agents && docker compose -f deploy/compose.public.yaml \
run --rm -T control key export --key-fd 1' > control-key)
Click Use Key… in the sheet, choose the file, and then delete the file. Agents Host keeps the Mac's earlier key aside in the keychain. Only a shell on the machine can export the key: no request to the control plane gives it out. After a move from your Mac, the machine holds this Mac's key, and you do not do this step. 3. Click Tell Everyone, and then Move Now.
Every window, phone and host comes back to the Mac, with nothing paired again. Away from home, the iPhone and iPad then reach the Mac only through your iCloud, and only while Relay for my devices is on.
Notifications¶
With the APNs key (--apns), the control plane sends notifications to the iPhone and iPad
through Apple, so that a phone can get them with no Mac host. In v0.5.0 this does not work
yet: with Relay for my devices off, a need from a host does not get to the locked phone
(#1509, to be fixed in v0.5.1). Until then, keep Relay for my devices on, on a Mac host.
With only Linux hosts, devices reach the control plane at its address and get no
notifications.
What to know¶
- The certificate renews itself. Caddy renews it about a month before it runs out. Nothing needs doing: no client or host holds a pin, so a renewed certificate, even with a new key, is accepted like the old one, and live connections carry on.
- The page and the control plane are one version. The image builds the page from the same checkout, so an upgrade changes both. A browser that has the page open loads it again when it reconnects.
- MCP Apps views on the page come from the views name. At
views.example.comthe control plane serves only the two files that hold a view (/sandbox.htmland/sandbox.js), and only the page may frame them. Atagents.example.comit refuses them. If the views name has no DNS record or no certificate, the page loads, but each view stays empty. - Don't set
AGENTS_CONTROL_PINwith a publicly trusted certificate. Caddy makes a new key at each renewal, so a pin would stop every window, phone and host from connecting after the first renewal. - What's kept, and where. The store, with every host and client and the sealed
control plane key, is in the Docker volume
agents-control_data. The certificate and the ACME account are inagents-control_caddy. Back up the store's volume. Never destroy the Cloud KMS key: without it, the sealed key doesn't open, and every window, phone and host must pair again. - If the machine or its disk is lost, remove the service account's key in Google Cloud (IAM ▸ Service accounts ▸ agents-control ▸ Keys). A copy of the disk then can't open the control plane's key.
- Upgrading. Build the Linux host again on your Mac, copy
deploy/,Go/,Web/,App/Resources/servers/andApp/Resources/toolsets/across, and run theup -d --buildline again. Hosts and clients reconnect within seconds. Linux servers update their host the next time they connect.
If it doesn't work¶
- The control plane stops at start with
could not find default credentials. It has no right to use the Cloud KMS key:deploy/secrets/gcp-service-accountis empty and the machine isn't a Compute Engine VM with the service account. Runmake-secrets.shagain with--service-account. - It stops with
PermissionDenied. The service account may not use the key. Give it the roleroles/cloudkms.cryptoKeyEncrypterDecrypteron the key, as above. -
It stops with
the key given is not the one sealed in the store. The store already has a control plane key, andkey sealwas given another one. Give the store's own, or start with an empty store. -
curlsays the certificate can't be verified, or Caddy's log sayschallenge failed. The name doesn't point at the machine yet, or port 80 or 443 is closed. Checkdig +shortand the firewall, then restart Caddy:docker compose -f deploy/compose.public.yaml restart caddy. - Caddy's log says
rateLimited. Let's Encrypt allows only a few failed tries an hour. Fix the cause, then wait for the time it gives. - The install command says
This machine has no CA certificates. Installca-certificateson the server, and run the command again with a fresh code. - A host's log says
CERTIFICATE_VERIFY_FAILED. The server's trusted roots are missing or out of date. Updateca-certificatesthere.