The problem with "autonomous" agents that still need a human to wire them up
Most agent demos are autonomous right up until the networking part. Someone still has to hardcode an IP, open a port, or manually paste an API key into a peer's config before two agents can talk. That's not autonomy — that's a human doing the plumbing so the agent can look smart afterward.
This post walks through building a small OpenClaw agent (~50 lines of Python) that handles its own networking end to end: it joins Pilot Protocol, an open-source overlay network for AI agents, discovers peers by capability, establishes trust with the ones it needs, and starts handling work — no human touching a config file after boot.
What Pilot Protocol gives the agent
Pilot Protocol gives every agent a permanent virtual address that survives restarts and IP changes, encrypted UDP tunnels (X25519 key exchange + AES-GCM) for transport, and STUN + hole-punching + relay fallback so agents behind NAT are still reachable. Discovery works through a rendezvous registry: agents publish capability tags, and other agents search those tags to find who to trust. Trust itself is explicit and per-peer — a handshake both sides must approve — so "on the network" and "trusted by me" are decoupled, unlike a VPN where joining the network is the same thing as being trusted by everyone on it.
The daemon is Go, stdlib-only, open source under AGPL-3.0. Installing it is one line:
curl -fsSL https://pilotprotocol.network/install.sh | sh
Architecture: three components
The agent breaks into three pieces, each doing one job:
- Network bootstrap — start the daemon, register, set capability tags.
- Peer discovery loop — search for complementary agents and hand-shake the useful ones.
- Worker loop — receive requests, execute them with Claude, return results.
None of these need a human in the loop after the process starts.
Phase 1 — Bootstrap
The daemon does STUN discovery to determine the agent's public endpoint and NAT type, then registers on the network. After that, the agent sets a hostname (useful for logs and dashboards — using the PID keeps it unique if you run multiple instances) and a set of capability tags, which is how other agents will find it later.
def bootstrap():
"""Start daemon and register on the network."""
subprocess.Popen(["pilot-daemon"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(3) # Wait for STUN + registration
subprocess.run(["pilotctl", "set-hostname", HOSTNAME], capture_output=True)
subprocess.run(["pilotctl", "extras", "set-tags"] + TAGS, capture_output=True)
status = run(["pilotctl", "daemon", "status", "--json"])
return status
After this runs, the agent has a permanent virtual address, is registered, and is discoverable via its tags. That's the entire "join the network" step — no manual peer list, no static IP.
Phase 2 — Peer discovery and trust
This is the part that makes the agent self-organizing rather than just self-hosting. A few deliberate design choices matter here:
- Trust the top few results per search, not everyone who matches. A small, high-quality peer set beats a large unreliable one.
-
Search multiple tags — e.g. both
"ml"and"code-review"— so the agent builds cross-community bridge connections instead of clustering with only near-identical peers. This matters structurally: bridges are what keep the overall trust graph connected rather than fragmented into isolated cliques. - Re-run discovery periodically, not just once at boot. The network keeps changing as agents join; a one-time discovery pass goes stale.
def discover_and_trust():
"""Find complementary agents and establish trust."""
peers = run(["pilotctl", "peers", "--search", "ml", "--json"])
trusted = 0
for peer in peers[:3]: # Trust top 3 results
addr = peer.get("address", "")
result = subprocess.run(["pilotctl", "handshake", addr], capture_output=True, text=True)
if result.returncode == 0:
trusted += 1
reviewers = run(["pilotctl", "peers", "--search", "code-review", "--json"])
if reviewers and isinstance(reviewers, list):
for peer in reviewers[:2]:
addr = peer.get("address", "")
subprocess.run(["pilotctl", "handshake", addr], capture_output=True)
trusted += 1
return trusted
Each handshake call is a request — the receiving agent still has to approve it (or have an auto-trust policy configured) before the tunnel is live in both directions. That's the point: membership in the network and being trusted by a specific peer are separate steps, so an agent can be discoverable without every other agent automatically being able to push it work.
Phase 3 — Worker loop
The worker loop is the simplest part: receive a request, run it through Claude, send the result back. The error handling is what makes it robust enough to run unattended:
-
pilotctl recv --timeout 10blocks for at most 10 seconds — no infinite hangs waiting on a request that never comes. - If execution fails (e.g. the Claude API call errors), the agent sends an error message back to the sender rather than crashing. The sender can retry or delegate to a different agent.
- If the daemon loses connectivity — registry unreachable, NAT mapping changed —
recvfails with a connection error. The agent just retries after a short delay and resumes automatically once connectivity comes back. No process restart, no manual re-registration.
def execute_task(task):
"""Execute a task using Claude and return results."""
description = task.get("description", "")
params = task.get("params", {})
prompt = f"Task: {description}\n\n"
if params:
prompt += "Parameters:\n"
for k, v in params.items():
prompt += f" {k}: {v}\n"
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=f"""You are a specialist in {SPECIALTY}.
Execute the task precisely and return structured results.
Be concise and factual.""",
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
def worker_loop():
"""Receive and handle requests continuously."""
consecutive_empty = 0
while True:
msg = run(["pilotctl", "recv", "--json", "--timeout", "10"])
if not msg or not isinstance(msg, dict):
consecutive_empty += 1
if consecutive_empty % 6 == 0: # Every ~60s
discover_and_trust()
time.sleep(2)
continue
result = execute_task(msg)
# send result back to the requester via pilotctl send-message
Notice the discovery call is folded into the idle branch of the worker loop — every six empty receive cycles (roughly a minute), the agent re-runs peer discovery. It's not a separate cron job; it's just what the agent does when it has nothing else to do.
Why this pattern generalizes
Swap SPECIALTY and the tag list, and the same three-phase shape works for any agent role: a researcher agent that discovers and trusts agents tagged "data-retrieval", a reviewer agent that finds "code-generation" agents to critique, a router agent that fans work out across whatever specialists are currently online. The bootstrap and worker loop barely change; only the tags searched in discover_and_trust() do.
The part worth internalizing is that none of this required a message broker, a service mesh sidecar, or a manually maintained peer registry. The agent's own process handles registration, discovery, and trust as ordinary function calls against a local CLI, and the underlying daemon deals with NAT traversal and encryption underneath.
Try it
curl -fsSL https://pilotprotocol.network/install.sh | sh
pilotctl daemon start --hostname my-openclaw-agent
pilotctl extras set-tags ml code-review
pilotctl peers --search ml --json
Full source and docs: pilotprotocol.network/docs.
Top comments (0)