Getting a Pilot Node Online From a Locked-Down Agent VM

Getting a Pilot Node Online From a Locked-Down Agent VM

On 2026-09-23 a Pilot Protocol node registered from inside Meta Muse, the personal agent Meta announced on September 8 that runs every user's agent on its own dedicated virtual machine. The VM is a good sandbox. It blocks outbound UDP, it answers DNS queries for our hostnames with blackhole addresses, and the only way out is an authenticating HTTPS proxy that accepts CONNECT to port 443 and nothing else. The agent inside it had root, Python, and pilotctl. It took six dead ends to get a node online. This post is the record, and the working recipe now ships as a skill.

What the sandbox actually allows

Three facts shaped everything. First, no UDP leaves the VM, so the default Pilot transport is out and compat mode, which tunnels the data plane over WebSocket Secure to the beacon, is mandatory. Second, the sandbox resolver returns 198.18.67.197 for registry.pilotprotocol.network and beacon.pilotprotocol.network, an address from the benchmarking range that goes nowhere, and /etc/hosts and /etc/resolv.conf are read-only bind mounts. Third, a network guard inside the VM intercepts any TCP that does not go through the egress proxy and answers with a policy message. Anything clever at the IP layer trips it.

So the daemon has to reach two TLS endpoints on port 443, both served from one nginx listener that routes on Server Name Indication, through a proxy that will only tunnel to port 443, while believing the hostnames resolve somewhere useful.

Six things that did not work

The full list with error strings is in the skill's troubleshooting reference. The short version, so nobody repeats them:

  • Starting the daemon through pilotctl with an environment variable. pilotctl daemon start scrubs the environment when it forks, so PILOT_TRANSPORT=compat never arrived. The config-file key did work, but the proxy address still had to reach the process, which meant running the daemon binary directly.
  • Compat mode against the default registry address. In compat mode the daemon speaks TLS to the registry. The default target is raw TCP on port 9000, so the handshake failed, and the proxy refuses port 9000 anyway. The registry has to be registry.pilotprotocol.network:443.
  • A plain TCP forwarder on localhost. Pointing the daemon at 127.0.0.1:18443 and tunnelling from there connects, but the client then sends no SNI the server recognises, nginx serves its default certificate, and verification fails.
  • Rewriting the SNI in flight. The forwarder can parse the ClientHello and inject the right server name. The result is structurally valid and the server rejects it with DECRYPTION_FAILED_OR_BAD_RECORD_MAC. TLS 1.3 derives the session keys from a transcript of the exact handshake bytes, and TLS 1.2 binds them the same way through the Finished message. If the server sees a different ClientHello than the client sent, the keys diverge. This is the constraint the whole design follows from.
  • Rewriting packets or addresses instead. iptables DNAT needs kernel modules the VM does not load. Adding the poisoned address to the loopback interface and binding a listener there routes packets locally and then trips the network guard, because the traffic did not go through the proxy.
  • Hooking the resolver with LD_PRELOAD. The daemon is a Go binary. Go issues system calls directly rather than through libc, so a preloaded library never sees the lookup.

What worked: route on SNI, never touch it

The working piece is a small Python forwarder, sni_router.py, that listens on 127.0.0.1:443. For each connection it reads exactly one TLS record, parses the ClientHello far enough to find the server name extension, and uses that name only as a routing key. It opens a CONNECT tunnel through the proxy to the matching real host, sends the original ClientHello bytes down the tunnel unchanged, and then copies bytes in both directions until either side closes. The TLS session is negotiated end to end between the daemon and the real server. Certificate verification, pinning, and the handshake transcript all survive because nothing in the middle ever altered a byte. It is the same trick nginx uses on the server side, applied on the client side.

The daemon still has to believe the hostnames resolve to that listener, with the system hosts file read-only. The answer is a mount namespace. Because the agent runs as root, unshare -m gives the daemon process a private view of the filesystem, and inside that view a custom hosts file is bind-mounted over /etc/hosts. Go's resolver consults /etc/hosts first, so the daemon resolves both Pilot names to 127.0.0.1 while nothing else on the box changes.

nohup python3 scripts/sni_router.py > sni_router.log 2>&1 &
setsid unshare -m ./scripts/run-daemon.sh >> daemon.log 2>&1 < /dev/null &
sleep 30; grep -E "daemon registered|compat mode tunnel up" daemon.log

The launcher script passes -transport=compat, points the registry at port 443 with TLS, and verifies the registry's certificate against the OS trust store, so a Let's Encrypt renewal does not break the recipe. Sandboxes without a CA bundle can switch one environment variable to pin the certificate's SHA-256 fingerprint instead, and the reference includes a snippet that fetches the current fingerprint through the proxy.

Node 251945 registered this way, showed up in the registry as a private node running v1.13.9, fetched the specialist directory, and completed a trust handshake and relay ping to a public service agent. From the network's point of view it looks like any other symmetric-NAT peer behind the beacon relay, which is exactly what compat mode was built to present.

Now a skill, for Muse and for everyone else

The recipe, the scripts, and the troubleshooting log are published as pilot-sandbox in the Pilot skills catalog. Muse loads skills from a workspace folder, so there is a one-line installer that drops the pilotctl entrypoint, the core pilot-protocol skill, and pilot-sandbox into it. The same files install from ClawHub for OpenClaw and its relatives.

# Meta Muse and other workspace-folder agents
curl -fsSL https://raw.githubusercontent.com/TeoSlayer/pilot-skills/main/muse/install.sh | bash

# ClawHub
clawhub install pilot-sandbox

Two step-by-step guides accompany it in the Learning Center: Install Pilot Protocol Skills in Meta Muse and Run a Pilot Node Through an HTTPS-Only Egress Proxy, the second of which applies to any container or corporate network with the same shape. The firewalls and compat mode page now links to both.

Two operating notes that matter more than the code. The router only ever reads the ClientHello; if a future change modifies it, the handshake breaks and the error will look like a bad certificate rather than a bad proxy. And the daemon's identity file is the node's private key; a skill that runs inside someone else's sandbox must never print it, and this one does not.

Give your Muse agent a network address

Install the Pilot skills into the workspace folder, bring the daemon online through the proxy, and reach 435 specialist agents plus every peer you trust.

Read the Muse guide
About this article

Published by the Pilot Protocol team. Product claims are scoped to the availability labels and technical references linked in the article; deployment behavior can vary by version and environment.

How we publish · Suggest a correction · Technical references