<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Philip Stayetski</title>
    <description>The latest articles on DEV Community by Philip Stayetski (@pstayet).</description>
    <link>https://dev.to/pstayet</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896740%2Ffbf5015f-af90-4277-b187-3176bd60f441.png</url>
      <title>DEV Community: Philip Stayetski</title>
      <link>https://dev.to/pstayet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pstayet"/>
    <language>en</language>
    <item>
      <title>Bring Your Own Identity Provider for AI Agents: Binding an OIDC Identity to an Overlay Address</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Mon, 10 Aug 2026 20:39:58 +0000</pubDate>
      <link>https://dev.to/pstayet/bring-your-own-identity-provider-for-ai-agents-binding-an-oidc-identity-to-an-overlay-address-25gk</link>
      <guid>https://dev.to/pstayet/bring-your-own-identity-provider-for-ai-agents-binding-an-oidc-identity-to-an-overlay-address-25gk</guid>
      <description>&lt;p&gt;Your agents have keys. Your company has an identity provider. Right now, those two worlds don't talk, and it's your job to connect them.&lt;/p&gt;

&lt;p&gt;Here's the situation I kept running into: the platform team stands up a fleet of autonomous agents, each one minting its own cryptographic identity. Then the security team asks the inevitable question — how do we bring your own identity provider for AI agents, so an agent's identity means something to the rest of the org? They want the agent to authenticate against the same OIDC/OAuth IdP as every employee and service account. Same governance, same lifecycle, same audit trail.&lt;/p&gt;

&lt;p&gt;This post is a practical how-to for that exact problem: linking an agent's keypair-based identity to an existing IdP so the agent is addressable both inside the overlay network and inside your corporate identity system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an agent's identity starts as a keypair, not a login
&lt;/h2&gt;

&lt;p&gt;Agents on a peer-to-peer network don't get usernames. They get addresses derived from keys. On Pilot Protocol, for example, every agent holds a permanent virtual address that survives restarts, IP changes, and moves across clouds — the identity is the key, not the host.&lt;/p&gt;

&lt;p&gt;That's the right foundation: a machine identity that can sign and encrypt, that can't be spoofed by claiming a hostname. X25519 key exchange and signed handshakes beat password auth for unattended processes, because there's no human to type a password and no session to steal.&lt;/p&gt;

&lt;p&gt;But a keypair answers "who is this agent?" only in the cryptographic sense. It doesn't answer "is this agent allowed to touch our production data?" That's a question your IdP already answers for every other principal in the company. The trick is making the two answers agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-identity problem
&lt;/h2&gt;

&lt;p&gt;You now have two identity systems describing the same agent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;World&lt;/th&gt;
&lt;th&gt;Identity&lt;/th&gt;
&lt;th&gt;Issued by&lt;/th&gt;
&lt;th&gt;Verified by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IdP (OIDC/OAuth)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;client_id&lt;/code&gt; + claims&lt;/td&gt;
&lt;td&gt;Your identity provider&lt;/td&gt;
&lt;td&gt;Token signature (JWKS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overlay network&lt;/td&gt;
&lt;td&gt;Public key / virtual address&lt;/td&gt;
&lt;td&gt;The agent itself&lt;/td&gt;
&lt;td&gt;Peers, at handshake&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The overlap problem is real. An agent with address &lt;code&gt;N:1234.ABCD.5678&lt;/code&gt; might map to service account &lt;code&gt;agents/checkout-worker&lt;/code&gt; in your IdP — or it might be a rogue instance claiming to be that worker. Nothing in the overlay world knows about the IdP world unless you make it.&lt;/p&gt;

&lt;p&gt;The good news: you don't need to replace either system. You need a binding between them. Here are the three patterns I've actually used, in increasing order of ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Present IdP-verified claims at handshake time
&lt;/h2&gt;

&lt;p&gt;The overlay's trust model is an explicit handshake — two peers mutually approve before any traffic flows. That approval step is the natural enforcement point for IdP policy.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent authenticates to your IdP (OIDC client-credentials flow, or a device flow if a human bootstraps it) and receives an ID token.&lt;/li&gt;
&lt;li&gt;During the overlay handshake, the agent presents that token alongside its public key.&lt;/li&gt;
&lt;li&gt;The receiving peer (or a policy agent acting on its behalf) validates the token signature against your IdP's JWKS endpoint, checks the claims — &lt;code&gt;aud&lt;/code&gt;, expiry, group membership — and only then approves the handshake.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No new infrastructure. Your IdP stays the source of truth for "is this principal valid", and the overlay stays the source of truth for "can this address reach me". The binding is the signed token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: A broker agent vouches for its fleet
&lt;/h2&gt;

&lt;p&gt;If your agents are short-lived or spawn dynamically, making every peer verify JWKS signatures is noisy. Instead, run one well-known "registration agent" that holds a long-lived trust relationship with both worlds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agents authenticate to the IdP once, through the broker.&lt;/li&gt;
&lt;li&gt;The broker verifies the token, then handshakes the new agent and attests to the fleet: "I vouch for this address; it holds IdP claims for &lt;code&gt;agents/checkout-worker&lt;/code&gt;."&lt;/li&gt;
&lt;li&gt;Other peers trust the broker's attestation instead of doing their own token dance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same delegation pattern you already use for employee onboarding — a trusted authority performs the verification, everyone else trusts the result. It also gives you one place to revoke: kill the broker's attestation and the agent loses its standing in the fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Private networks with join rules
&lt;/h2&gt;

&lt;p&gt;For the coarsest control, don't let strangers onto the shared network at all. Pilot Protocol supports private networks with group-level connectivity and join rules — you define who may join, and agents outside the allowlist simply can't reach the group.&lt;/p&gt;

&lt;p&gt;This doesn't replace IdP binding, but it layers on top of it: the IdP decides &lt;em&gt;who&lt;/em&gt; the agent is, the join rules decide &lt;em&gt;whether it's in your network&lt;/em&gt;. Combined with pattern 1 or 2, you get identity verification at two independent layers, which is exactly what a zero-trust posture wants.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the handshake actually looks like
&lt;/h2&gt;

&lt;p&gt;Concretely, with Pilot's CLI, the enforcement point is the trust commands. On the receiving side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl pending                &lt;span class="c"&gt;# incoming handshake requests&lt;/span&gt;
pilotctl approve &amp;lt;node_id&amp;gt;      &lt;span class="c"&gt;# approve after your policy check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the initiating side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl handshake &amp;lt;peer&amp;gt; &lt;span class="s2"&gt;"agent authenticating as agents/checkout-worker (IdP token attached)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason this pattern works is that Pilot's trust model is &lt;em&gt;explicit&lt;/em&gt; — membership and trust are decoupled. Joining the network doesn't make you trusted; a peer has to approve you. That's the hook your IdP policy hangs on: approval can be gated on token verification instead of being automatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the two worlds meet, cleanly
&lt;/h2&gt;

&lt;p&gt;What I like about this setup is that neither system contorts itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent keeps its cryptographic identity — a permanent address it can be reached at, regardless of NAT or cloud moves. That's the overlay's job.&lt;/li&gt;
&lt;li&gt;The IdP keeps its job — issuing verifiable claims about principals, with expiry and revocation.&lt;/li&gt;
&lt;li&gt;The binding is a signed token exchanged at an explicit trust boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the full picture of the addressing and trust model — what an address is, how handshakes work, how private networks and join rules are configured — the &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;trust model documentation&lt;/a&gt; covers it in detail, including the exact commands for handshakes, approvals, and network join rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring your own identity provider for AI agents: the takeaway
&lt;/h2&gt;

&lt;p&gt;Bringing your own identity provider for AI agents isn't about choosing between cryptographic identity and corporate identity. It's about binding them at the right enforcement point. Handshake-time token verification, a vouching broker, and network join rules are three ways to do it, and they compose.&lt;/p&gt;

&lt;p&gt;The agents get addresses that survive infrastructure churn. The IdP gets a seat at the table for every agent that joins your fleet. And the security team gets an answer to their question that doesn't start with "well, actually...".&lt;/p&gt;

&lt;p&gt;If you're setting this up yourself, the install is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then handshake, verify, approve — and your agents are both addressable and auditable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>networking</category>
    </item>
    <item>
      <title>Consensus Between Three or More AI Agents Without a Leader: How Peer Agreement Works</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Sun, 09 Aug 2026 13:35:16 +0000</pubDate>
      <link>https://dev.to/pstayet/consensus-between-three-or-more-ai-agents-without-a-leader-how-peer-agreement-works-4ma0</link>
      <guid>https://dev.to/pstayet/consensus-between-three-or-more-ai-agents-without-a-leader-how-peer-agreement-works-4ma0</guid>
      <description>&lt;p&gt;When you have three or more AI agents that need to reach consensus without a leader — deciding task ordering, settling a value, or converging on shared state — the default move is to appoint a coordinator. One agent owns the decision, everyone else asks it. That works until the coordinator disappears, falls behind, or becomes the bottleneck every message has to route through. Leaderless consensus is the alternative, and it is far more practical than the theory makes it sound. This post is an explainer of what "without a leader" really demands, which existing tools fit, and where most peer consensus attempts actually fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "No Leader" Is a Different Problem
&lt;/h2&gt;

&lt;p&gt;Leader-based consensus (Raft, Paxos) is well understood. The catch: election itself is a consensus problem, and it assumes a topology that stays put. Autonomous agents violate that assumption constantly — they restart, move across clouds, change IPs, and drop off the network mid-conversation. If your consensus protocol depends on a stable leader, you have just re-imported the failure mode you were trying to avoid.&lt;/p&gt;

&lt;p&gt;Leaderless agreement quietly assumes three substrate properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stable identity.&lt;/strong&gt; A participant you can name today is the same participant tomorrow, even after a restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reachability.&lt;/strong&gt; Messages actually arrive, including for agents behind NAT or firewalls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verified trust.&lt;/strong&gt; You know who is sending you a vote, and they know you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most write-ups on agent consensus skip straight to the algorithm. In practice, the substrate is where these systems fall apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Existing Toolkit: Quorums, CRDTs, BFT
&lt;/h2&gt;

&lt;p&gt;You do not need to invent an algorithm. The pieces exist; the job is picking the right one for the kind of agreement you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quorum voting — the default shape
&lt;/h3&gt;

&lt;p&gt;Each agent broadcasts a proposal, peers respond with signed acks or counter-proposals, and the proposal applies once a threshold of the group has agreed. No agent is special; the quorum is a property of the group, not of any single member. This is the right shape for task ordering and value settlement between agents that already trust each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  CRDTs — agreement by merging
&lt;/h3&gt;

&lt;p&gt;For state convergence, you can skip consensus entirely. Convergent replicated data types let every agent apply updates locally and merge with peers; as long as everyone eventually exchanges deltas, the state converges without any vote. This is the cheapest form of "agreement" and the right default when the question is &lt;em&gt;what is the current state&lt;/em&gt;, not &lt;em&gt;whose proposal wins&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  BFT — only when you expect adversaries
&lt;/h3&gt;

&lt;p&gt;Byzantine fault-tolerant protocols (PBFT, HotStuff) handle malicious participants, and they cost accordingly — extra rounds, extra messages, and a minimum group size that grows with the number of tolerated faults. Worth it for open networks with untrusted actors. Overkill for a closed set of collaborating agents.&lt;/p&gt;

&lt;p&gt;The honest summary: for the common cases — ordering work, settling values, converging state among agents you know — you need a quorum loop, a merge function, and the substrate properties above. You rarely need Byzantine tolerance, and you never need a coordinator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Layer That Kills Peer Consensus Before It Starts
&lt;/h2&gt;

&lt;p&gt;Here is the part that rarely shows up in the algorithm papers. To run any of the above, every agent needs an address that survives restarts, a path to peers behind NAT, and a way to verify who it is agreeing with. If those three things are bolted on as an afterthought, your consensus loop will spend most of its time failing to deliver messages, not failing to agree.&lt;/p&gt;

&lt;p&gt;This is the gap Pilot Protocol fills, and it is an honest fit rather than a stretch: it is a networking layer built specifically so agents can talk to each other as peers. Each agent gets a permanent virtual address that survives restarts and IP changes. Transport is encrypted UDP tunnels (X25519 key exchange, AES-GCM) with STUN, hole punching, and relay fallback, so agents behind NAT are reachable. Trust is an explicit per-peer handshake where both sides approve — membership and trust are decoupled, which is exactly the pairwise-trust property a quorum needs. The network carries 243k+ agents, and it is open source under AGPL-3.0 with zero external dependencies.&lt;/p&gt;

&lt;p&gt;The commands are the substrate in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl handshake &amp;lt;peer-address&amp;gt; &lt;span class="s2"&gt;"collaborating on shared task state"&lt;/span&gt;
pilotctl peers          &lt;span class="c"&gt;# who you can actually reach, and how&lt;/span&gt;
pilotctl send-message &amp;lt;peer&amp;gt; &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;message&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handshake detail matters for consensus specifically: &lt;em&gt;joined&lt;/em&gt; does not imply &lt;em&gt;trusted&lt;/em&gt;. Each agent approves who it will accept messages — and votes — from, so your quorum is made of relationships the participants chose, not the network's default. And because addresses are permanent, the agent that voted in round one is the same agent in round ten. The &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol docs&lt;/a&gt; cover the addressing and trust model in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal Leaderless Agreement Loop
&lt;/h2&gt;

&lt;p&gt;Putting it together, a leaderless agreement loop for three or more agents looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Propose&lt;/strong&gt; — an agent broadcasts &lt;code&gt;{proposal, seq, signature}&lt;/code&gt; to every peer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collect&lt;/strong&gt; — each peer replies with a signed ack or a counter-proposal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt; — the proposer applies once it holds acks from a simple majority of the group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converge&lt;/strong&gt; — for shared state, merge CRDT deltas instead of voting on a single winner.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A sketch of the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;QUORUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;  &lt;span class="c1"&gt;# simple majority of a 3-agent group
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;acks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;          &lt;span class="c1"&gt;# signed, delivered over the peer tunnel
&lt;/span&gt;            &lt;span class="n"&gt;acks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;QUORUM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# durable, ordered, attributed
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;backoff_and_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transport here is whatever your peers speak; the loop is the point. What the transport must give you is addressing that survives restarts and verified sender identity. That is the substrate layer — and it is the part you should not have to build yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consensus Between Three or More AI Agents Without a Leader: What to Reach For First
&lt;/h2&gt;

&lt;p&gt;If you are designing a multi-agent system that needs leaderless agreement, the order of operations is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give every agent a stable identity and address that survives restarts and NAT.&lt;/li&gt;
&lt;li&gt;Establish trust pairwise and explicitly — no ambient "everyone on the network is trusted."&lt;/li&gt;
&lt;li&gt;Then pick the mechanism: quorum for ordering and settlement, CRDTs for state convergence, BFT only when you genuinely expect malicious actors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the loop is stuck, check the substrate before the algorithm. Nine times out of ten the failure is a message that never arrived or a sender that could not be verified — not a flaw in the agreement logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;Leaderless consensus between agents is mostly a substrate problem wearing an algorithm costume. Once addressing, reachability, and trust are real, the agreement layer reduces to a quorum loop and a merge function. If you want to see the substrate part working before you build on it, install Pilot, handshake a couple of peers, and look at who you can actually reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run a quorum loop over direct peer messages and watch how much of the hard part was already handled for you. The docs at pilotprotocol.network/docs are the reference for the addressing and trust model behind it.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>distributedsystems</category>
      <category>networking</category>
    </item>
    <item>
      <title>What an Agent Should Do When It Loses Connectivity to All Peers: A Graceful Degradation Playbook</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Sun, 09 Aug 2026 09:07:41 +0000</pubDate>
      <link>https://dev.to/pstayet/what-an-agent-should-do-when-it-loses-connectivity-to-all-peers-a-graceful-degradation-playbook-1heh</link>
      <guid>https://dev.to/pstayet/what-an-agent-should-do-when-it-loses-connectivity-to-all-peers-a-graceful-degradation-playbook-1heh</guid>
      <description>&lt;p&gt;Your agent depends on a network. Then one day every peer is unreachable at once. What should an agent do when it loses connectivity to all peers — keep retrying in a tight loop until it burns its rate limits, or degrade gracefully and come back when the network returns?&lt;/p&gt;

&lt;p&gt;I've watched autonomous agents handle total network loss in production, and most of them handle it badly. Not because the engineers who wrote them were careless, but because we design for &lt;em&gt;partial&lt;/em&gt; failure — one peer down, one timeout, one retry — and total failure is a different beast. This post is my opinion on what the right behavior actually looks like, shaped by what I've seen work and what I've seen fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Losing all peers is a different failure from losing one peer
&lt;/h2&gt;

&lt;p&gt;A single peer disappearing is a retry problem. You have a timeout, you have a backoff schedule, you have other peers to talk to while this one is away. The system keeps functioning; one leg of the mesh is just offline.&lt;/p&gt;

&lt;p&gt;Losing &lt;strong&gt;all&lt;/strong&gt; peers at once is a different failure class. It usually means one of three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your host's network path is down (the local uplink, a firewall change, a suspended VM),&lt;/li&gt;
&lt;li&gt;the network itself is partitioned, or&lt;/li&gt;
&lt;li&gt;the discovery/rendezvous layer you depend on is unreachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those get better by hammering the same failed request fifty times in ten seconds. Yet that's exactly what a lot of agent loops do, because the default loop is "try, fail, retry" with no branch for &lt;em&gt;total&lt;/em&gt; failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent should do when it loses connectivity to all peers
&lt;/h2&gt;

&lt;p&gt;My short answer: &lt;strong&gt;detect the shape of the outage, back off with exponential delay, operate locally, and reconnect with a defined recovery path.&lt;/strong&gt; Four phases, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect&lt;/strong&gt; — determine whether &lt;em&gt;you&lt;/em&gt; lost the network or &lt;em&gt;everyone&lt;/em&gt; did. Can you still reach the rendezvous/registry? Can you reach the internet at all? That single check tells you whether to keep trying or to stop trying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back off exponentially&lt;/strong&gt; — the first reconnect attempt is immediate, then the delay doubles each failure, with a cap and jitter so a fleet of agents doesn't reconnect in lockstep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate locally&lt;/strong&gt; — the agent still has work it can do that doesn't require peers: local computation, queueing intents, preparing state. Total disconnect is not total idleness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect deliberately&lt;/strong&gt; — probe at the backoff cadence, and when connectivity returns, resync state and drain the queue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important mental shift: reconnection is not "try harder." It's "try less often, more deliberately, while doing useful local work in between."&lt;/p&gt;

&lt;h2&gt;
  
  
  The backoff loop, concretely
&lt;/h2&gt;

&lt;p&gt;Exponential backoff with jitter is boring and correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reconnect_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_connected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt_connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_connected&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# steady-state health check
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;attempt_connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;  &lt;span class="c1"&gt;# jitter
&lt;/span&gt;        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cap matters. Without it, an agent that lost its uplink for three hours spends three hours sleeping for increasingly absurd stretches. With it, you get a steady, polite heartbeat — one probe every minute or so — and an agent that's ready to act the moment the network returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operate locally while disconnected
&lt;/h2&gt;

&lt;p&gt;The phase most agent designs skip entirely is &lt;strong&gt;local operation&lt;/strong&gt;. An agent that only reacts to network events is dead weight during an outage. An agent that &lt;em&gt;also&lt;/em&gt; does local work keeps earning its keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queue intents.&lt;/strong&gt; If the agent's job is to relay or transform messages, have it write outbound intents to a durable local queue instead of dropping them. When the network returns, drain the queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do local-only work.&lt;/strong&gt; Computation, data preparation, rendering, checks against local state — anything that doesn't need a peer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the outage state.&lt;/strong&gt; A clear "disconnected since X, reason Y, N queued items" is worth more than a wall of connection errors when you come back to debug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb I use: if a task doesn't strictly require a peer, it should not block on the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconnect to the same address, not a new one
&lt;/h2&gt;

&lt;p&gt;Here's where the network design underneath the agent starts to matter. If your agent's identity is tied to a machine, an IP, or a cloud region, then an outage that moves the machine means the agent comes back as a &lt;em&gt;different&lt;/em&gt; endpoint — and every peer has to re-learn who it is. That turns a connectivity blip into a trust and discovery problem.&lt;/p&gt;

&lt;p&gt;A design I've come to respect handles this by giving the agent a &lt;strong&gt;permanent virtual address&lt;/strong&gt; that survives restarts, IP changes, and moving across clouds. On Pilot Protocol, agents get exactly that: a stable address, encrypted UDP tunnels, and NAT traversal with relay fallback, so "reachable" doesn't depend on which network your agent happens to be behind. The recovery story becomes well-defined — when connectivity returns, the agent reconnects to the &lt;em&gt;same&lt;/em&gt; address, peers re-establish the &lt;em&gt;same&lt;/em&gt; trust relationship, and the mesh resumes where it left off. No re-discovery dance, no new identity to propagate. (Their &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; walks through the addressing and trust model in detail.)&lt;/p&gt;

&lt;p&gt;I'm not saying an overlay network makes outages impossible — networks fail regardless of how clever your addressing is. But it does make the &lt;em&gt;recovery&lt;/em&gt; deterministic, which is the part that's actually in your control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The opinion part: resilience is a behavior, not a network property
&lt;/h2&gt;

&lt;p&gt;Here's my real takeaway, and it's an opinion: we spend a lot of effort making networks resilient and very little effort making &lt;strong&gt;agents&lt;/strong&gt; resilient. The network either comes back or it doesn't — you can't control that. But the agent's behavior during the outage — whether it thrashes or waits, whether it drops work or queues it, whether it comes back clean or half-broken — that's entirely yours to design.&lt;/p&gt;

&lt;p&gt;So the next time you deploy an agent that depends on peers, ask yourself what it does when &lt;em&gt;every&lt;/em&gt; peer vanishes. If the answer is "retries in a tight loop," you haven't finished writing it. Add the exponential backoff, add the local work, add the deliberate reconnect. The outage will happen. Your agent's behavior during it is a choice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're building agents that talk to each other, the one-command way to give them stable addresses and encrypted tunnels is: &lt;code&gt;curl -fsSL https://pilotprotocol.network/install.sh | sh&lt;/code&gt; — then &lt;code&gt;pilotctl&lt;/code&gt; gets you connected to peers in minutes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Automate Agent Onboarding in a Multi-Agent Fleet: Address, Trust, Discovery — Done Once</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Sat, 08 Aug 2026 20:33:47 +0000</pubDate>
      <link>https://dev.to/pstayet/automate-agent-onboarding-in-a-multi-agent-fleet-address-trust-discovery-done-once-2pja</link>
      <guid>https://dev.to/pstayet/automate-agent-onboarding-in-a-multi-agent-fleet-address-trust-discovery-done-once-2pja</guid>
      <description>&lt;p&gt;Onboarding a new agent into a multi-agent fleet is the least-automated part of most agent deployments. You provision the container, install the runtime, and then the manual part starts: give the agent an identity, tell it which peers to trust, point it at the capabilities it's allowed to call. Two agents and it's a chore. A fleet and it's a project. The way out isn't a thicker runbook — it's recognizing that onboarding is a short, fixed sequence. Every new agent needs three things: &lt;strong&gt;an address, trust with its peers, and discovery of what it can call.&lt;/strong&gt; Automate that sequence once per deployment and every agent after the first is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  What onboarding actually is: address, trust, discovery
&lt;/h2&gt;

&lt;p&gt;Most onboarding checklists are really three different problems stapled together. Separate them, and each one has a clean answer — and each answer is automatable.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. An address that outlives the machine
&lt;/h3&gt;

&lt;p&gt;An agent in a fleet has to be reachable. Not just today, on this IP, in this cloud — but after restarts, IP changes, and moves between clouds. That's the property DNS doesn't give you: a DNS record is bound to a host or endpoint that changes underneath you. And if your agents sit behind NAT, there's no stable endpoint at all.&lt;/p&gt;

&lt;p&gt;What a fleet needs is a &lt;strong&gt;permanent virtual address&lt;/strong&gt; that belongs to the agent, not to the machine it's running on. The address survives restarts, IP changes, and cloud migrations; the transport underneath — encrypted UDP tunnels, with NAT traversal when the network demands it — is the runtime's problem, not yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Trust, established explicitly — not implied by membership
&lt;/h3&gt;

&lt;p&gt;The second thing a new agent needs is a set of peers it can actually talk to. This is where a lot of fleets quietly get the model wrong. In a typical overlay VPN, joining the network means being trusted: membership and trust are the same thing. For agent fleets, that's the wrong conflation — you often want agents from different teams or organizations on the same network without them trusting each other wholesale.&lt;/p&gt;

&lt;p&gt;The model that fits fleets is &lt;strong&gt;explicit, mutual trust&lt;/strong&gt;: a handshake between two agents that both sides approve, per peer. "Can reach" and "is trusted" stay separate questions, and each new agent gets exactly the trust it needs — no more.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Discovery: what can I call, and who else is here?
&lt;/h3&gt;

&lt;p&gt;The third piece is discovery, in both directions. A new agent needs to find the peers and capabilities it's supposed to use, and the fleet needs to know the agent exists. A rendezvous registry plus a nameserver that resolves names and tags to addresses covers the peer side; capabilities get their own layer, which we'll get to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating agent onboarding in a multi-agent fleet
&lt;/h2&gt;

&lt;p&gt;Once the sequence is defined, automating it is a provisioning problem, not a runtime problem. Because the address is permanent, onboarding happens &lt;strong&gt;once per deployment&lt;/strong&gt; — not on every restart. The agent's identity lives in a local keypair file, and everything after first start is idempotent: run the same steps again and nothing breaks.&lt;/p&gt;

&lt;p&gt;Here's the whole sequence as a script, with Pilot Protocol as the runtime — a Go daemon with zero external dependencies that gives each agent a permanent virtual address, encrypted UDP tunnels (X25519 key exchange + AES-GCM), and NAT traversal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Address — first start gives the agent a permanent virtual address&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
pilotctl daemon start

&lt;span class="c"&gt;# 2. Trust — explicit and mutual, per peer&lt;/span&gt;
pilotctl handshake &amp;lt;peer-address&amp;gt; &lt;span class="s2"&gt;"onboarding fleet member"&lt;/span&gt;
pilotctl trust          &lt;span class="c"&gt;# confirm the link is mutual before relying on it&lt;/span&gt;

&lt;span class="c"&gt;# 3. Discovery — find peers and capabilities by name or tag&lt;/span&gt;
pilotctl send-message list-agents &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'/data {"search":"weather"}'&lt;/span&gt;
pilotctl appstore catalogue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three commands cover the whole checklist. The handshake is the only interactive part, and even that is scriptable: your fleet's control plane approves incoming handshakes the way it approves any other request — &lt;code&gt;pilotctl pending&lt;/code&gt; to see them, &lt;code&gt;pilotctl approve &amp;lt;id&amp;gt;&lt;/code&gt; to accept. Write this once into your deployment pipeline, and "add an agent" stops being a human task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability onboarding: discover → install → call
&lt;/h2&gt;

&lt;p&gt;Address and trust get an agent &lt;em&gt;onto&lt;/em&gt; the network. The third step — capabilities — is where the fleet's actual work happens, and it deserves its own loop: &lt;strong&gt;discover → install → call&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An agent that joins a fleet needs to know what it can use. Pilot's app store makes capability onboarding look like package management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl appstore catalogue                    &lt;span class="c"&gt;# what's available&lt;/span&gt;
pilotctl appstore &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;                 &lt;span class="c"&gt;# install a capability app&lt;/span&gt;
pilotctl appstore call &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &amp;lt;app&amp;gt;.help         &lt;span class="c"&gt;# what does it do, what params&lt;/span&gt;
pilotctl appstore call &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &amp;lt;app&amp;gt;.&amp;lt;method&amp;gt; &lt;span class="s1"&gt;'&amp;lt;json&amp;gt;'&lt;/span&gt;   &lt;span class="c"&gt;# use it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The apps run locally on the agent's daemon as typed IPC services — JSON in, JSON out — and the daemon re-checks each app's pinned signature on every spawn. Permissions are granted at install time, scoped per app, so a new agent picks up capabilities without ambient authority. Install is an explicit, auditable event, the same shape for every agent in the fleet — and each app you publish is discoverable by the 243k+ agents already on the network.&lt;/p&gt;

&lt;p&gt;One fair comparison: MCP did the ecosystem a real service by standardizing how agents invoke tools. What an app store adds on top is packaging and trust — signature-verified adapters, scoped grants, a single install command. The two are complementary: the store is about how capabilities get installed and vetted, not how they're called.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from a pre-wired fleet and skip the sequence entirely
&lt;/h2&gt;

&lt;p&gt;If your fleet has a shape you keep re-deploying — a content pipeline, a code-review loop, a monitoring stack — you don't have to write the onboarding sequence from scratch at all. Pilot ships &lt;a href="https://pilotprotocol.network/plain/setups/" rel="noopener noreferrer"&gt;pre-wired multi-agent fleets&lt;/a&gt;: orgs where the agents, skills, and trust links are already wired, ready to deploy. The address-trust-discovery sequence is already done; you add your agents to a fleet that already knows how to behave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Onboarding is a script, not a ceremony
&lt;/h2&gt;

&lt;p&gt;The difference between a fleet you can grow and a fleet you're stuck with is whether adding an agent is a script or a ceremony. Address, trust, discovery — three steps, done once per deployment. Automate them and the marginal cost of a new agent approaches zero; leave them manual and every new agent is a new chance for drift.&lt;/p&gt;

&lt;p&gt;Try the sequence once with two agents on one host, then script it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, onboarding is the boring part of your fleet — which is exactly what it should be.&lt;br&gt;
.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Encrypted Tunnel vs TLS for Agent Communication: Same Crypto, Different Trust Model</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Fri, 07 Aug 2026 16:34:12 +0000</pubDate>
      <link>https://dev.to/pstayet/encrypted-tunnel-vs-tls-for-agent-communication-same-crypto-different-trust-model-3ge0</link>
      <guid>https://dev.to/pstayet/encrypted-tunnel-vs-tls-for-agent-communication-same-crypto-different-trust-model-3ge0</guid>
      <description>&lt;p&gt;Encrypted tunnels and TLS both give you encryption. If you're picking between them for agent communication, the cipher suite is the wrong place to look — the real difference is in the trust model and the connection model. TLS secures a stream and anchors identity in a certificate authority. An encrypted tunnel secures a channel and anchors identity in a mutual handshake between the two endpoints. Same crypto family, different answers to "who is this peer?" — and for AI agents, that answer drives everything from reconnects to zero-trust posture.&lt;/p&gt;

&lt;p&gt;I've spent the last few months wiring up agent-to-agent traffic, and this distinction keeps coming up in the wrong framing: people ask "which has stronger encryption?" and miss that both are strong. The question that actually matters is whether you want a stream or a channel, and whether you want a CA or the peer itself to vouch for identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TLS gives you: a secured stream
&lt;/h2&gt;

&lt;p&gt;TLS is a stream protocol. It runs on top of a reliable, ordered transport — almost always TCP — and it protects a byte stream between two sockets. The handshake negotiates a session key, and certificates bind public keys to identities. In the common case, the server presents a certificate signed by a certificate authority, and the client validates that signature against its trust store.&lt;/p&gt;

&lt;p&gt;It is excellent at what it was built for: browsers talking to web servers, APIs over the public internet, anything where a large population of clients needs to trust a small set of servers without prior coordination. The CA ecosystem does that job at planetary scale, and mTLS extends the model so both sides present certificates.&lt;/p&gt;

&lt;p&gt;The trade-offs show up when your endpoints are agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection-scoped.&lt;/strong&gt; A TLS session lives and dies with the connection. The agent restarts, the IP changes, the cloud migrates — the session is gone and the handshake has to happen again, certificates and all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reachability assumed.&lt;/strong&gt; TCP needs a listener your peer can actually reach. Agents behind NAT or firewalls aren't reachable by default, so you end up adding port forwarding, a relay, or a public-facing endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CA-anchored identity.&lt;/strong&gt; Even with mTLS, trust is anchored in a certificate authority — a root you may not control, plus certificate lifecycle to manage (issue, renew, revoke).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are failures. They're properties of a design aimed at the public web. But agent-to-agent traffic has different properties: peers are long-lived, mobile, frequently behind NAT, and mutually untrusted until they've explicitly agreed to talk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an encrypted tunnel gives you: a secured channel
&lt;/h2&gt;

&lt;p&gt;A tunnel is a different shape. Instead of securing a stream, it creates a persistent, encrypted channel between two endpoints, and whatever you send through it is protected. The channel has an identity of its own, independent of any single connection, so the endpoints can reconnect, restart, and move without re-establishing trust from scratch.&lt;/p&gt;

&lt;p&gt;Pilot Protocol is a concrete example of this model. It's an open-source overlay network for AI agents: every agent gets a permanent virtual address that survives restarts, IP changes, and moving across clouds. Traffic moves over encrypted UDP tunnels using X25519 key exchange and AES-GCM, with reliability handled in userspace rather than by TCP. NAT traversal is built in — STUN, hole-punching, and a relay fallback — so agents behind NAT are reachable without opening ports.&lt;/p&gt;

&lt;p&gt;The trust model is where it diverges most sharply from TLS. Instead of a certificate authority, identity is established by an explicit per-peer handshake: both sides mutually approve before any traffic flows. Membership and trust are decoupled — being on the network doesn't mean being trusted, which is a different posture from a VPN where "joined" effectively means "trusted." Discovery is handled by a rendezvous registry and nameserver, so agents find each other by name or tag rather than by hardcoded IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channel vs stream: why the shape matters for agents
&lt;/h2&gt;

&lt;p&gt;The stream/channel distinction isn't academic — it changes what breaks and what doesn't.&lt;/p&gt;

&lt;p&gt;With TLS, a connection is the unit of trust and state. If your agent's connection drops, everything riding on it — the session keys, the negotiated parameters, the application state that assumed a live socket — needs to be rebuilt. Retry logic, backoff, and reconnection are your problem to own.&lt;/p&gt;

&lt;p&gt;With a channel, the unit of trust is the endpoint pair. The encrypted tunnel persists as a concept even when individual packets or connections come and go. The agent reconnects to the same virtual address; the mutual trust already established doesn't evaporate with a socket close. That's a meaningful difference for autonomous agents that run for weeks, restart after updates, or get migrated between clouds mid-conversation.&lt;/p&gt;

&lt;p&gt;There's also a payload angle. TLS is protocol-specific in practice — you negotiate ALPN, you pick a protocol per connection. A channel is transport: once it exists, you can run HTTP, gRPC, MCP, or anything else through it. You establish trust once and multiplex whatever your agents actually speak.&lt;/p&gt;

&lt;h2&gt;
  
  
  CA trust vs mutual trust: who answers "who is this peer?"
&lt;/h2&gt;

&lt;p&gt;This is the question the "vs" in encrypted tunnel vs TLS really lives in.&lt;/p&gt;

&lt;p&gt;TLS answers it with a signature chain: a CA vouches for the server (and in mTLS, both sides). That's powerful when you want &lt;em&gt;many&lt;/em&gt; clients to trust &lt;em&gt;few&lt;/em&gt; servers they've never met — the browser scenario. It's heavier when every peer is both client and server and you'd rather not operate a private CA or pay for certificates per agent.&lt;/p&gt;

&lt;p&gt;Mutual handshakes answer it directly: each peer decides, for itself, whether to trust the other. No third party is in the loop, and trust is revocable per peer. For zero-trust agent infrastructure — where you assume no ambient trust and verify explicitly — that maps cleanly: an agent only talks to peers it has deliberately approved, and it can drop that approval at any time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encrypted tunnel vs TLS: side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;TLS&lt;/th&gt;
&lt;th&gt;Encrypted tunnel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stream, per-connection, over TCP&lt;/td&gt;
&lt;td&gt;Persistent channel, independent of any connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trust anchor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Certificate authority / PKI&lt;/td&gt;
&lt;td&gt;Mutual per-peer handshake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peer identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Certificates (CN/SAN), CA-validated&lt;/td&gt;
&lt;td&gt;Explicit mutual approval between endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;After a restart / IP change&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Session gone, re-handshake needed&lt;/td&gt;
&lt;td&gt;Same virtual address, trust persists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NAT traversal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Needs reachable listener or relay&lt;/td&gt;
&lt;td&gt;Built in (STUN, hole-punching, relay fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What you can run through it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One negotiated protocol per connection&lt;/td&gt;
&lt;td&gt;Anything: HTTP, gRPC, MCP, custom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which should you use for agent communication?
&lt;/h2&gt;

&lt;p&gt;Honestly: both, and they're not really competitors. TLS is the right tool when your agent talks to public web services, browsers, or anything where the CA model already fits — it's a solved problem and you shouldn't re-invent it. An encrypted tunnel is the right tool for the connections &lt;em&gt;between your own agents&lt;/em&gt;: long-lived, mutually untrusted until handshaken, behind NAT, moving between environments. Plenty of setups use both — a tunnel for reachability, TLS inside it when a specific protocol wants it.&lt;/p&gt;

&lt;p&gt;If you want to see the channel model in practice, Pilot Protocol is a working implementation: Go, zero external dependencies, AGPL-3.0, with the trust handshake, NAT traversal, and permanent addressing described above. The &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol docs&lt;/a&gt; walk through the model, and 243k+ agents are already on the network.&lt;/p&gt;

&lt;p&gt;The takeaway: stop comparing cipher strength and start comparing trust models. TLS asks "can this certificate be traced to a root we trust?" A tunnel asks "have these two peers explicitly agreed to talk?" For agent communication, that second question is usually the one you actually need answered.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Reference:&lt;/em&gt; try the tunnel model yourself — &lt;code&gt;curl -fsSL https://pilotprotocol.network/install.sh | sh&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>networking</category>
      <category>security</category>
    </item>
    <item>
      <title>Timeout and Retry Between Two Autonomous Agents: When No Broker Owns the Retry</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:36:02 +0000</pubDate>
      <link>https://dev.to/pstayet/timeout-and-retry-between-two-autonomous-agents-when-no-broker-owns-the-retry-57bh</link>
      <guid>https://dev.to/pstayet/timeout-and-retry-between-two-autonomous-agents-when-no-broker-owns-the-retry-57bh</guid>
      <description>&lt;p&gt;Two autonomous agents are talking. Agent A sends a request to Agent B and waits. Nothing comes back. Is B slow, or is B gone? How long does A hold the connection, and what does it do next? Timeout and retry between two autonomous agents is one of the most quietly important reliability problems in agent systems — because unlike a classic API call, there is no broker, no queue, and no load balancer that owns the retry. Somebody has to own it, and in a peer-to-peer setup that somebody is you.&lt;/p&gt;

&lt;p&gt;This post is the practical version: right-sized timeouts, exponential backoff with jitter, idempotency, and a retry budget — the logic, with code, that lets two agents survive each other's disappearances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Timeout and Retry Between Two Autonomous Agents Is Different
&lt;/h2&gt;

&lt;p&gt;In a traditional client–server architecture, retries are infrastructure's problem. A message queue redelivers unacked messages. A load balancer fails over to a healthy replica. A serverless platform retries the invocation for you. The broker owns the retry because the broker owns the request lifecycle.&lt;/p&gt;

&lt;p&gt;Two autonomous agents talking directly have none of that. The failure domain is bigger and fuzzier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The peer may be gone.&lt;/strong&gt; Crashed, redeployed, moved to another cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The peer may be alive but busy.&lt;/strong&gt; Agent B could be mid-task on something that takes minutes. From A's side, a slow response and a dead peer look identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody is watching the wire.&lt;/strong&gt; If the reply is lost in transit, nothing redelivers it; the request simply evaporates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both sides may be retrying at once.&lt;/strong&gt; A retries its request while B retries its own — the same message arrives twice, or the two agents hammer each other in a feedback loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you stop assuming a broker, every retry decision becomes an application decision. The good news: the toolbox is small and well understood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right-Sized Timeouts: Slow Is Not Dead
&lt;/h2&gt;

&lt;p&gt;The first mistake is picking a timeout out of thin air. Too short turns every slow-but-healthy peer into a "failure"; too long makes the whole system hang on a dead peer. The right size comes from measurement: log real round-trips between your agents, find the slow tail, and set the per-attempt timeout a comfortable multiple above it. And separate the two things you're actually measuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Round-trip time&lt;/strong&gt; — how long the wire takes. This is what you probe with a ping or heartbeat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation time&lt;/strong&gt; — how long the peer's actual work takes. This varies per request type and is what your per-call timeout should be based on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep them as &lt;em&gt;different&lt;/em&gt; values. A liveness probe with a short timeout tells you the peer is reachable; the operation timeout tells you the work finished. In a P2P or overlay setting, ping the transport before you assume the peer died — reachability and responsiveness are different facts.&lt;/p&gt;

&lt;p&gt;On top of per-attempt timeouts, set a hard overall deadline for the whole request. Per-attempt timeout says "this try gave up"; the deadline says "this request is over, stop spending." A minimal loop looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_with_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run coro with a per-attempt timeout and a hard overall deadline.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;total_deadline&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overall deadline exceeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt_timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# handled by the retry loop below
&lt;/span&gt;            &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry loop that wraps this is where backoff lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exponential Backoff With Jitter
&lt;/h2&gt;

&lt;p&gt;When a retry fails, you back off — but &lt;em&gt;not&lt;/em&gt; at a constant rate, and &lt;em&gt;not&lt;/em&gt; in lockstep with the other side. Exponential backoff with jitter is the standard answer for exactly this situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;backoff_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Full jitter: random delay in [0, base * 2**attempt], capped.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The exponent.&lt;/strong&gt; Each failed attempt multiplies the window, so the delay grows 0.2s → 0.4s → 0.8s → 1.6s… up to the cap. A peer that's restarting gets time to come back; a peer that's gone stops being hammered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The jitter.&lt;/strong&gt; Without randomness, every agent retries on the same schedule — a thundering herd of synchronized retries, and when both agents retry at the same cadence they keep colliding forever. Jitter breaks the symmetry. It's the one part people skip, and the part that prevents retry storms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cap the backoff and stop at a bounded number of attempts. Which brings us to the safety condition that makes retries legal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency Is What Makes Retries Safe
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable fact: a retry is a duplicate. If Agent B actually processed the request and only the reply was lost, the retry makes B do the work &lt;em&gt;again&lt;/em&gt;. For reads that's fine. For anything with side effects — a payment, a job dispatch, a state change — it's a bug.&lt;/p&gt;

&lt;p&gt;The fix is the same one distributed systems have used for decades: &lt;strong&gt;at-least-once delivery plus deduplication on the receiver&lt;/strong&gt;. Give every request a unique ID; the receiver remembers which IDs it has already processed and answers duplicates without re-running the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Receiver side: dedup by request id
&lt;/span&gt;&lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# already ran this one — reply from cache
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;do_side_effectful_work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sender generates the ID once and keeps it across all retry attempts; the receiver treats "same ID" as "same logical request." Retry becomes a transport concern instead of a correctness one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry Budgets and Circuit Breakers
&lt;/h2&gt;

&lt;p&gt;Left to itself, retry logic retries forever — which means the "reliability" mechanism becomes the outage. Give every call a budget: a maximum number of attempts and a maximum total time. The deadline in the first section is the total-time half; the attempt cap is the other half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt_timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_with_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt_timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max attempts exceeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beyond the per-call budget, add a &lt;strong&gt;circuit breaker&lt;/strong&gt; at the peer level: track consecutive failures to Agent B, and once they cross a threshold, stop sending for a cooldown window instead of burning each call's budget on a peer that is clearly down. In a brokered world the queue provides this backpressure. In a brokerless world, your circuit breaker &lt;em&gt;is&lt;/em&gt; the backpressure — the only thing standing between "B is having a bad minute" and "A spends its afternoon retrying B."&lt;/p&gt;

&lt;h2&gt;
  
  
  In a P2P World, the Other Side Retries Too
&lt;/h2&gt;

&lt;p&gt;Everything above assumed you control one side. In agent-to-agent communication, both sides run the same playbook — so both sides can be retrying the same logical exchange from opposite directions. That's how you get duplicate work on &lt;em&gt;both&lt;/em&gt; ends and message storms that neither side's backoff fixes on its own.&lt;/p&gt;

&lt;p&gt;The mitigations are protocol-level agreements, not library choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request IDs end to end&lt;/strong&gt;, generated by whoever originates, honored by whoever receives — on both sides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At-least-once semantics on both sides&lt;/strong&gt;, with dedup as the default assumption rather than the exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared timeout conventions&lt;/strong&gt;, so A's "slow" isn't B's "normal."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also where agent-native networking infrastructure earns its keep. If you're building agent-to-agent links over raw sockets or HTTP, you own the whole transport stack on top of the retry logic: addressing, reachability, the distinction between "peer gone" and "peer moved." An overlay network for agents — like &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol's docs describe&lt;/a&gt; — gives every agent a permanent virtual address that survives restarts and IP changes, so a retry to the same address still reaches the same agent, plus encrypted tunnels and NAT traversal so the peer is reachable at all. The transport handles the wire; your timeout and retry logic handles the operations. You can absolutely build this yourself — but it's worth knowing where the line between "application logic" and "transport plumbing" sits before you own both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Checklist
&lt;/h2&gt;

&lt;p&gt;The working version of this post is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt; real round-trips; set per-attempt timeouts above the slow tail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate&lt;/strong&gt; liveness (ping the peer) from responsiveness (time the operation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back off&lt;/strong&gt; exponentially, with jitter, capped, with a max attempt count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicate&lt;/strong&gt; by request ID on the receiver — retries are only safe if they're idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget and break&lt;/strong&gt; — a total deadline plus a circuit breaker so retrying never becomes the outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agree on semantics&lt;/strong&gt; with the peer, because they're running this same playbook at you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No broker is coming to save you. That's fine — the tools above are older than agents, and they still work.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Reference: &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol docs&lt;/a&gt; — addressing, transport, and the trust model for agent networks. Install: &lt;code&gt;curl -fsSL https://pilotprotocol.network/install.sh | sh&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>networking</category>
      <category>python</category>
    </item>
    <item>
      <title>Log Agent-to-Agent Message History for Debugging: When Both Agents Insist They're Right</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Fri, 07 Aug 2026 06:44:37 +0000</pubDate>
      <link>https://dev.to/pstayet/log-agent-to-agent-message-history-for-debugging-when-both-agents-insist-theyre-right-l19</link>
      <guid>https://dev.to/pstayet/log-agent-to-agent-message-history-for-debugging-when-both-agents-insist-theyre-right-l19</guid>
      <description>&lt;p&gt;Two agents, one conversation, and suddenly both of them insist they never said what the other one claims. Sound familiar? When you run multi-agent systems long enough, you will eventually need to log agent-to-agent message history for debugging — not because your agents lie, but because "what was actually sent" stops being obvious the moment a retry, a timeout, or a silent drop enters the picture.&lt;/p&gt;

&lt;p&gt;This post is a practical how-to: what to capture when two agents disagree, why the obvious "just add logging" answer keeps failing, and how to pick between shipping messages to external storage or using what the transport already keeps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just add logging" keeps failing
&lt;/h2&gt;

&lt;p&gt;The naive approach is to log at the application layer, inside the agent code. Each agent writes what it &lt;em&gt;thinks&lt;/em&gt; it sent. That's exactly the problem: the log ends up being a record of intentions, not a record of the wire.&lt;/p&gt;

&lt;p&gt;Three failure modes show up over and over:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The he-said/she-said.&lt;/strong&gt; Agent A logs "sent", Agent B logs "never received". Both logs are truthful from their own perspective. The retry logic, the timeout, or the queue that ate the message lives somewhere neither agent can see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wrong clock.&lt;/strong&gt; Timestamps generated at the application layer drift across hosts. When you line up two logs side by side, the ordering doesn't match anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The missing correlation.&lt;/strong&gt; Two agents talking over a mix of transports — webhooks, a message bus, a direct socket — produce four different log formats with no shared identifier. You can't join them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is to log at a layer that sees the actual exchange, with a consistent shape, and to decide up front &lt;em&gt;where the record lives&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log agent-to-agent message history for debugging: the five fields that matter
&lt;/h2&gt;

&lt;p&gt;Whatever mechanism you pick, every agent-message log entry needs these five fields to be useful later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp&lt;/strong&gt; — ideally captured by the logging layer, not the application, so clocks are consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From / to&lt;/strong&gt; — stable agent identifiers, not hostnames or IPs that change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt; — the message body (or a hash of it, if the payload is large or sensitive).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery outcome&lt;/strong&gt; — acked, delivered, failed, or unknown. "Unknown" is a legitimate value and often the most interesting one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation ID&lt;/strong&gt; — one identifier threaded through retries so a single logical message can be traced across multiple attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those five fields, "what was said" becomes a query instead of an argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option A: external storage — the kitchen-sink approach
&lt;/h2&gt;

&lt;p&gt;The most common pattern is to push every agent message into your own store: a Redis stream, a JSONL file, a Postgres table. A small sidecar subscribes to your agent traffic and appends one line per message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sidecar: append every agent message to an append-only log
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correlation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;correlation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-messages.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;External storage wins on control: you set retention, you can search, you can replay conversations into a fresh agent. It costs you infrastructure, a second failure mode, and the discipline to keep the sidecar running. If your agents already talk over a message bus, this is often the natural move — the bus is the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option B: use the transport's own record
&lt;/h2&gt;

&lt;p&gt;Before you build the sidecar, check what the transport already keeps. Some messaging systems persist messages as part of the protocol — and then "debugging what was said" is just reading the mailbox.&lt;/p&gt;

&lt;p&gt;This is where Pilot Protocol comes in. It's an open-source overlay network for AI agents, and its &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; is explicit about which of its four communication models are stored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stream&lt;/strong&gt; (&lt;code&gt;connect&lt;/code&gt; / &lt;code&gt;send&lt;/code&gt;) — synchronous request-response, not stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data exchange&lt;/strong&gt; (&lt;code&gt;send-message&lt;/code&gt; / &lt;code&gt;send-file&lt;/code&gt;) — async, &lt;strong&gt;stored on arrival&lt;/strong&gt; in &lt;code&gt;~/.pilot/inbox/&lt;/code&gt; and &lt;code&gt;~/.pilot/received/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pub/Sub&lt;/strong&gt; — real-time fan-out to active subscribers, not stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datagram&lt;/strong&gt; — fire-and-forget, not stored.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data-exchange model is the one designed for "delivery matters more than realtime response," and it gives you the message log for free. Every message lands on the recipient as a JSON file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JSON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0:0000.0000.0005"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;task&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;analyze&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"received_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-15T10:30:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file &lt;em&gt;is&lt;/em&gt; the he-said/she-said record: sender, payload, size, arrival time, captured by the transport on the receiving side. When an agent claims a message never arrived, you read the inbox instead of arguing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl send-message other-agent &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"task":"analyze","input":"data.csv"}'&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; json
&lt;span class="c"&gt;# ...later, on the recipient:&lt;/span&gt;
pilotctl inbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same model covers files: &lt;code&gt;send-file&lt;/code&gt; stores what arrived in &lt;code&gt;~/.pilot/received/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's a diagnostics side too. When the disagreement is about &lt;em&gt;connectivity&lt;/em&gt; rather than content — one agent swears the tunnel was up — the daemon reports connection state, bytes sent and received, retransmissions, and congestion stats per connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl connections   &lt;span class="c"&gt;# per-connection transport stats&lt;/span&gt;
pilotctl info          &lt;span class="c"&gt;# peers, encryption status, traffic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the built-in path gives you two complementary records: what was &lt;em&gt;delivered&lt;/em&gt; (the inbox) and what the &lt;em&gt;tunnel&lt;/em&gt; was doing (the diagnostics). Neither requires extra infrastructure, because the transport was designed to keep them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option C: hybrid — transport record plus external archive
&lt;/h2&gt;

&lt;p&gt;Honestly, most production systems end up with both. The transport's inbox is your ground truth for "did it arrive", and it's the fastest thing to check when an agent complains. External storage gives you long retention, full-text search, and replay across many agents and transports.&lt;/p&gt;

&lt;p&gt;A cheap hybrid: let the transport keep the live record, and have a scheduled job append the inbox to your archive. You get the zero-infra debugging experience day-to-day and the queryable history when you need to reconstruct a whole conversation weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick external storage&lt;/strong&gt; when your agents already talk over a bus, when you need full-text search or long retention, or when messages cross many different transports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick transport-native history&lt;/strong&gt; when you want the record to exist without building anything, when "did it arrive" is the question you debug most, and when you trust the transport's capture point more than your own logging code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick both&lt;/strong&gt; when the disagreement is expensive enough that you want receipts from two independent sources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying rule is the same in every case: log at the layer that actually moves the message, capture the five fields, and make sure the record survives the argument. Your future self, debugging at 2am, will thank you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pilot Protocol is an open-source overlay network that gives AI agents a permanent virtual address, encrypted tunnels, and built-in trust — with messaging models that decide up front whether a message is stored. The messaging and diagnostics docs cover the full model.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get started: &lt;code&gt;curl -fsSL https://pilotprotocol.network/install.sh | sh&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>debugging</category>
      <category>networking</category>
      <category>ai</category>
    </item>
    <item>
      <title>Agent to Human Notification Without Email or SMS: A Recipe That Skips SMTP and SMS Gateways</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Thu, 06 Aug 2026 23:13:04 +0000</pubDate>
      <link>https://dev.to/pstayet/agent-to-human-notification-without-email-or-sms-a-recipe-that-skips-smtp-and-sms-gateways-2afn</link>
      <guid>https://dev.to/pstayet/agent-to-human-notification-without-email-or-sms-a-recipe-that-skips-smtp-and-sms-gateways-2afn</guid>
      <description>&lt;p&gt;Your agent just spent two hours on a batch job. At 3 a.m. it hits something it can't resolve: an anomaly, a failed retry, a decision that needs a human. It has to alert you — and it has neither an email account nor a phone.&lt;/p&gt;

&lt;p&gt;Agent to human notification without email or sms sounds like a constraint you'd fight. In practice it's the better design. The alert channel becomes a capability your agent calls, not a credential your agent carries. This is a use-case recipe for exactly that: the options that actually exist, and a concrete discover → install → call flow you can run this afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent to Human Notification Without Email or SMS: Four Channels That Work
&lt;/h2&gt;

&lt;p&gt;You don't need SMTP and you don't need an SMS gateway. You need a delivery path from the agent to a screen a human actually watches. Four shapes cover the realistic options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;What it takes&lt;/th&gt;
&lt;th&gt;When it fits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Message to the operator's own node&lt;/td&gt;
&lt;td&gt;Operator runs a daemon; one &lt;code&gt;send-message&lt;/code&gt; call&lt;/td&gt;
&lt;td&gt;Zero third parties; delivery that survives disconnects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat-platform webhook&lt;/td&gt;
&lt;td&gt;Bot token or incoming webhook, outbound egress&lt;/td&gt;
&lt;td&gt;Your team already lives in Slack, Discord, or Telegram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notification API service&lt;/td&gt;
&lt;td&gt;One account and API key per service&lt;/td&gt;
&lt;td&gt;One-off pushes, retries handled for you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typed IPC call to an app&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;install&lt;/code&gt;, then one &lt;code&gt;call&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Agents that shouldn't own any channel credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is the interesting one, so let's start there, then build the recipe around the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Recipe: Make Notification a Typed IPC Call
&lt;/h2&gt;

&lt;p&gt;The pattern that keeps showing up in agent work: don't give the agent credentials, give it a capability. Pilot Protocol's &lt;a href="https://pilotprotocol.network/app-store" rel="noopener noreferrer"&gt;agent app store&lt;/a&gt; is built around that idea. Apps are installable capability services that run locally on your daemon as typed IPC endpoints — JSON in, JSON out — and they auto-spawn the moment you install them. The loop is discover → install → call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl appstore catalogue        &lt;span class="c"&gt;# find an app&lt;/span&gt;
pilotctl appstore view &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;        &lt;span class="c"&gt;# read what it does&lt;/span&gt;
pilotctl appstore &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;     &lt;span class="c"&gt;# one command, auto-spawned&lt;/span&gt;
pilotctl appstore call &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &amp;lt;app&amp;gt;.&amp;lt;method&amp;gt; &lt;span class="s1"&gt;'&amp;lt;json&amp;gt;'&lt;/span&gt;   &lt;span class="c"&gt;# do the work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every app is discoverable by 243k+ agents on the overlay, and installing is a single command. The security story matters here because your agent is unsupervised: installs are signature-verified (the manifest pins a sha256 and an ed25519 signature, re-checked on every spawn), permissions are grant-scoped and accepted at install time rather than ambient, and the daemon supervises the app's lifecycle. Runtime discovery is a convention: every app answers &lt;code&gt;&amp;lt;app&amp;gt;.help&lt;/code&gt; with its methods and parameters. No browser, no REST plumbing.&lt;/p&gt;

&lt;p&gt;Get started with the one-command install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Concrete Run: Agent Alerts Operator
&lt;/h2&gt;

&lt;p&gt;Say the agent is an overnight risk scan. It flags something it can't classify and needs a human before it does anything else. Two honest paths, depending on what "alert" means here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path A — the operator runs a node.&lt;/strong&gt; This is the purest version of "without email or sms": no third-party service in the path at all. The operator's node has a permanent virtual address, reachable even behind NAT via encrypted UDP tunnels. The agent sends a typed async message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl send-message operator-node &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"severity":"high","task":"nightly-risk-scan","detail":"spike in failed logins"}'&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data-exchange messages persist to the operator's &lt;code&gt;~/.pilot/inbox/&lt;/code&gt; and survive disconnections — the docs recommend them exactly when delivery matters more than real-time response. The operator reads them with &lt;code&gt;pilotctl inbox&lt;/code&gt;. If you want fan-out to a whole team, the same protocol has a pub/sub model: agents &lt;code&gt;publish&lt;/code&gt; events, subscribers receive them in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path B — SMS or email is genuinely the right channel, but the agent still doesn't own it.&lt;/strong&gt; The app store's communications apps hold the channel for you. AgentPhone gives the agent a real phone number — voice calls, SMS and iMessage, conversations over REST. Primitive provisions a managed email inbox in one call — send, receive, reply, search real mail over one REST API. In both cases the agent's side is identical: a typed IPC call with a JSON payload. The phone number or inbox lives in the app, granted at install, re-verified on every spawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Channel Shouldn't Live in the Agent
&lt;/h2&gt;

&lt;p&gt;Email means SMTP credentials, an account to provision, and outbound port decisions. SMS means a phone number and a provider account. Both are state your agent carries, rotates, and can leak — and both are exactly the kind of ambient authority that makes security reviews of autonomous agents painful.&lt;/p&gt;

&lt;p&gt;An app-store call inverts the dependency. The notification capability is installed once, scoped to what it needs, and invoked with JSON. If the app is updated or replaced, the agent's code doesn't change — the call contract stays the same. That's the same reason the publish side exists: bring an existing API, describe its methods in the guided submission, and Pilot builds, signs, and reviews the adapter. Once approved, install works everywhere on the overlay. Turn an API into an agent app, and every agent on the network gains that capability — notification included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Channel When
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Operator runs a node → &lt;code&gt;send-message&lt;/code&gt; to the inbox. No accounts, no egress, delivery that survives disconnects.&lt;/li&gt;
&lt;li&gt;Team lives in chat → webhook to the platform they already watch. You manage the token; it works well.&lt;/li&gt;
&lt;li&gt;SMS or email is the actual requirement → install the app that owns the channel (AgentPhone for a number, Primitive for an inbox) and call it over IPC.&lt;/li&gt;
&lt;li&gt;Multiple watchers, live updates → pub/sub events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these need the agent to hold an email account or a phone. The channel is a capability, the call is JSON, and the credentials stay where they're scoped and verified.&lt;/p&gt;

&lt;p&gt;If your agent has been silently failing at 3 a.m. because alerting meant provisioning mail infrastructure or buying SMS credits, this is the unblock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then browse the catalogue with &lt;code&gt;pilotctl appstore catalogue&lt;/code&gt;, install the app that fits your channel, and make the alert a one-line call.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Run an AI Agent That Calls Other Agents as Tools — What “Call” Actually Means on the Wire</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:26:37 +0000</pubDate>
      <link>https://dev.to/pstayet/run-an-ai-agent-that-calls-other-agents-as-tools-what-call-actually-means-on-the-wire-4d6c</link>
      <guid>https://dev.to/pstayet/run-an-ai-agent-that-calls-other-agents-as-tools-what-call-actually-means-on-the-wire-4d6c</guid>
      <description>&lt;p&gt;You want to run an AI agent that calls other agents as tools. An orchestrator that hands subtasks to a research agent, a coding agent, a human-approval agent. On paper, this is just tool calling — the same mechanism as a calculator or a web-search tool. In practice, "calling an agent" is two problems stacked together, and most tutorials only solve one of them.&lt;/p&gt;

&lt;p&gt;The first problem is &lt;strong&gt;framework-level tool calling&lt;/strong&gt;: the interface the model uses to discover and invoke a tool. The second is &lt;strong&gt;transport-level agent reachability&lt;/strong&gt;: whether one agent's process can actually open a connection to another agent's process. They are different layers, and confusing them is why multi-agent systems that demo beautifully on a laptop fall apart the moment the agents live on different machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool calling is an interface, not a connection
&lt;/h2&gt;

&lt;p&gt;Start with what the framework layer actually does. Function calling in an LLM API gives the model a set of typed functions with JSON schemas; the model returns a structured call, and your runtime executes it. MCP standardizes that further — tools become resources a client can discover and invoke, with the server running over stdio or HTTP.&lt;/p&gt;

&lt;p&gt;Both are genuinely useful. MCP in particular is a real step forward: one client talks to many servers, and the ecosystem is growing fast. But notice what both of them assume: &lt;strong&gt;that the tool's process is reachable&lt;/strong&gt;. A stdio MCP server lives in your process tree. An HTTP MCP server lives behind a URL that has to resolve, route, and accept your connection. Neither layer has any mechanism for making an agent on a laptop behind NAT reachable from an agent in a cloud VPC. That is not their job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part everyone skips: the other agent has to be reachable
&lt;/h2&gt;

&lt;p&gt;Here's the scenario that breaks the demo. Your orchestrator runs on a VM in the cloud. The agent you want to call as a tool — the one holding the private data, or the one that can trigger an action only it can perform — runs on a laptop at home, or inside a customer's private network. The framework layer is ready: the orchestrator knows the tool exists, knows its schema, and can emit a valid call. Then the call goes out and dies. No route. NAT. A firewall that only allows outbound. An IP that changed this morning.&lt;/p&gt;

&lt;p&gt;This is the transport problem, and it's the half of "calling an agent" that tool-calling frameworks explicitly don't solve. The usual workarounds each cost you something: expose a public endpoint (inbound port, static IP, DNS to babysit), poll a shared queue (latency and state), or run everything on one host (fine for a demo, wrong for production).&lt;/p&gt;

&lt;h2&gt;
  
  
  What transport-level agent reachability actually means
&lt;/h2&gt;

&lt;p&gt;For agent A to call agent B as a tool, A's process needs a stable, working path to B's process. That's a networking problem, and it has a networking answer: an overlay network built for agents. Same family as a mesh VPN, with the agent use case in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A permanent virtual address&lt;/strong&gt; per agent, surviving IP changes and moves between clouds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted UDP tunnels&lt;/strong&gt; — X25519 key exchange, AES-GCM — so agent traffic is not plaintext on the public internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT traversal&lt;/strong&gt; via STUN, hole punching, and relay fallback, so agents behind NAT are reachable without opening inbound ports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit trust&lt;/strong&gt;: per-peer handshake, mutually approved, so "connected" never silently means "trusted."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of this layer: reachability stops being an assumption. When an agent is on the network, it has an address other agents can reach, wherever it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run an AI agent that calls other agents as tools: the two-layer stack
&lt;/h2&gt;

&lt;p&gt;So the working shape of a multi-agent system has two layers, and you want both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer one — the framework:&lt;/strong&gt; the model discovers tools and emits structured calls. Function calling, or MCP with a client and servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer two — the transport:&lt;/strong&gt; the processes can reach each other. Stable addresses, encrypted tunnels, a trust relationship.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice. Keep MCP as your framework layer — it's good at that. Put an agent-native network underneath it. Pilot Protocol, for example, ships an MCP server (pilot-mcp) that bridges the two: your MCP client discovers other agents as tools, and each tool call travels over an encrypted tunnel to a peer behind NAT. The model sees an ordinary tool; the wire sees a reachable peer.&lt;/p&gt;

&lt;p&gt;The transport layer also gives you the plumbing that makes calls land:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# establish mutual trust with the agent you want to call&lt;/span&gt;
pilotctl handshake &amp;lt;peer&amp;gt; &lt;span class="s2"&gt;"orchestrator calling you as a tool"&lt;/span&gt;
pilotctl trust          &lt;span class="c"&gt;# confirm mutual trust&lt;/span&gt;

&lt;span class="c"&gt;# then call it — stable address, encrypted tunnel&lt;/span&gt;
pilotctl send-message &amp;lt;peer&amp;gt; &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"task": "summarize the incident", "return": "tool_result"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the tool layer and transport layer are separate, the tools themselves can be delivered the same way. Pilot's app store ships capability apps as typed IPC services — JSON in, JSON out, auto-spawned on install, discoverable by the 243k+ agents on the network. The loop is &lt;code&gt;pilotctl appstore catalogue&lt;/code&gt; → &lt;code&gt;install&lt;/code&gt; → &lt;code&gt;call&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl appstore catalogue
pilotctl appstore &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
pilotctl appstore call &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &amp;lt;method&amp;gt; &lt;span class="s1"&gt;'{"query": "..."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model's tool call becomes a JSON message to a local, signature-verified app. Framework on top, transport underneath — both explicit instead of assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you need both
&lt;/h2&gt;

&lt;p&gt;Skip the framework layer and you have reachable agents nothing can invoke: the processes can talk, but no model is driving them in a loop. Skip the transport layer and you have a beautiful tool-calling setup where every call to another agent dies with a connection error at deploy time. The first is a design gap. The second is the one that costs you a weekend.&lt;/p&gt;

&lt;p&gt;The honest takeaway: MCP and function calling solved a real problem — the interface. Don't bolt a custom tool-calling protocol onto an overlay; use the standard framework layer. And don't treat reachability as an implementation detail of tool calling; it's a network property. Solve the interface with the frameworks the ecosystem standardized, and solve reachability with an agent-native network underneath — permanent addresses, NAT traversal, and explicit trust.&lt;/p&gt;

&lt;p&gt;Run an AI agent that calls other agents as tools, and it will fail on the wire before it fails on the schema. Build both layers, and "calling an agent" becomes what it always looked like in the demo: one line, and it just works.&lt;/p&gt;




&lt;p&gt;Get started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read the &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol docs&lt;/a&gt; for the MCP bridge and app store commands. Source: github.com/pilot-protocol.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>networking</category>
    </item>
    <item>
      <title>Send Large Files Between AI Agents Without a Cloud Storage Bucket: Direct Transfers Over an Encrypted Tunnel</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:36:10 +0000</pubDate>
      <link>https://dev.to/pstayet/send-large-files-between-ai-agents-without-a-cloud-storage-bucket-direct-transfers-over-an-1g0f</link>
      <guid>https://dev.to/pstayet/send-large-files-between-ai-agents-without-a-cloud-storage-bucket-direct-transfers-over-an-1g0f</guid>
      <description>&lt;p&gt;Your agent finished training and the other agent needs the weights. Or a pipeline stage produced a dataset and the next stage lives on a different cloud. The reflex is to stand up an S3 bucket, hand credentials to both sides, and pay the egress bill while you're at it.&lt;/p&gt;

&lt;p&gt;But if you need to &lt;strong&gt;send large files between AI agents without a cloud storage bucket&lt;/strong&gt;, there's a more direct path: give the agents a real network channel between them and transfer the file point-to-point. No bucket policy, no shared NFS mount, no third party sitting in the middle of your data.&lt;/p&gt;

&lt;p&gt;This tutorial walks through what the direct approach actually requires, then shows a working agent-to-agent file transfer with Pilot Protocol — an open-source overlay network that gives every agent a permanent address and encrypted tunnels through NAT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why buckets became the default
&lt;/h2&gt;

&lt;p&gt;Let's be fair to object storage first. S3 (and GCS, and Azure Blob) became the standard way to move artifacts between machines for a good reason: &lt;strong&gt;reachability&lt;/strong&gt;. Two servers behind NAT can't open connections to each other, but both can reach a bucket. Upload from one side, download from the other. Problem solved — at the cost of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credentials on both sides (plus rotation, plus a bigger leakage surface)&lt;/li&gt;
&lt;li&gt;A third party holding your data, even briefly&lt;/li&gt;
&lt;li&gt;Egress and storage costs for data that was just passing through&lt;/li&gt;
&lt;li&gt;Bucket policies, lifecycle rules, and versioning you never wanted to configure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A shared filesystem (NFS and friends) only works inside one trust domain — the same VPC, the same cluster. The moment your agents live on different clouds, or one sits behind a corporate firewall, the shared mount is off the table.&lt;/p&gt;

&lt;p&gt;The underlying problem was never "we need storage." It was &lt;strong&gt;"these two machines can't reach each other."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The direct approach: agent-to-agent file transfer
&lt;/h2&gt;

&lt;p&gt;If two agents have a persistent, encrypted, NAT-traversing channel, moving a file becomes a point-to-point operation. You don't need a bucket to transfer data between two endpoints that can already talk to each other.&lt;/p&gt;

&lt;p&gt;That's the shape of an overlay network like Pilot Protocol. Each agent gets a permanent virtual address that survives restarts and IP changes. Traffic flows over encrypted UDP tunnels (X25519 key exchange + AES-GCM, with reliability handled in userspace), and STUN plus hole-punching with a relay fallback means agents behind NAT are reachable without any port forwarding. Trust is explicit: a handshake between the two peers, approved on both sides — joining the network doesn't make you trusted by anyone.&lt;/p&gt;

&lt;p&gt;File transfer is built into the daemon, not bolted on. No object-store client library, no SDK ceremony for the common case. One command sends, one command lists what arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tutorial: moving an artifact between two agents
&lt;/h2&gt;

&lt;p&gt;Here's the whole loop. Two hosts, &lt;code&gt;worker-a&lt;/code&gt; and &lt;code&gt;worker-b&lt;/code&gt;, on different networks.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Install on both hosts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same one-line install for the daemon and the &lt;code&gt;pilotctl&lt;/code&gt; CLI, on both machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Start each daemon
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl daemon start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent registers and gets its permanent virtual address. From here on, that address is how the other side reaches it — no public IP, no port forwarding, no DNS gymnastics.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Establish trust
&lt;/h3&gt;

&lt;p&gt;Private nodes require mutual trust before any data flows. On &lt;code&gt;worker-a&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl handshake worker-b &lt;span class="s2"&gt;"shipping model weights"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;code&gt;worker-b&lt;/code&gt;, approve the incoming request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl approve &amp;lt;node-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire trust model — membership and trust are decoupled. Being on the network doesn't grant anyone access to your agent; you explicitly approve who can talk to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Send the file
&lt;/h3&gt;

&lt;p&gt;On &lt;code&gt;worker-a&lt;/code&gt;, where the artifact lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl send-file worker-b ./model-weights.bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returns delivery metadata: &lt;code&gt;filename&lt;/code&gt;, &lt;code&gt;bytes&lt;/code&gt;, &lt;code&gt;destination&lt;/code&gt;, &lt;code&gt;sha256&lt;/code&gt;, &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;transport&lt;/code&gt;. Those last two fields are the integrity story — the receiving daemon verifies the transfer, so you're not shipping silent corruption risk across the wire.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Receive on the other side
&lt;/h3&gt;

&lt;p&gt;On &lt;code&gt;worker-b&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl received
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Files land in &lt;code&gt;~/.pilot/received/&lt;/code&gt; on the target. Delivery is asynchronous — the daemon stores files on arrival, so the recipient doesn't need an interactive session running at the exact moment the file lands. When you're done with them, &lt;code&gt;pilotctl received --clear&lt;/code&gt; wipes the directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sending large files without a cloud storage bucket actually requires
&lt;/h2&gt;

&lt;p&gt;For big artifacts, the default transfer is &lt;strong&gt;streamed in chunks&lt;/strong&gt; rather than sent as a single frame, so artifact size isn't what gates the transfer. What does matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both sides must be reachable on the overlay.&lt;/strong&gt; That's what the NAT traversal is for — it's handled for you, relay fallback included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk space on the receiving side.&lt;/strong&gt; The file lands in &lt;code&gt;~/.pilot/received/&lt;/code&gt;; the daemon doesn't garbage-collect for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutual trust before you send.&lt;/strong&gt; The handshake is a one-time setup and it persists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your agent isn't the CLI, there's an SDK.&lt;/strong&gt; Go, Python (&lt;code&gt;pilotprotocol&lt;/code&gt; on PyPI), Node, and Swift bindings cover the programmatic case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is open source (AGPL-3.0, Go, zero external dependencies), so the tunnel and the trust logic are inspectable rather than a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to keep the bucket anyway
&lt;/h2&gt;

&lt;p&gt;Direct transfer is a delivery mechanism, not a storage service. Keep the object store when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;durable archival&lt;/strong&gt; — the artifact must outlive both agents&lt;/li&gt;
&lt;li&gt;You're &lt;strong&gt;sharing with parties outside the overlay&lt;/strong&gt; — humans, other orgs, pre-signed URLs&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;CDN distribution&lt;/strong&gt; — many readers, one source&lt;/li&gt;
&lt;li&gt;You want an &lt;strong&gt;audit trail of who accessed what&lt;/strong&gt; — bucket logging gives you that&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the common case — one agent handing a large artifact to another agent it trusts, across clouds, through NAT — the direct channel removes a whole class of credential management and takes the middleman out of the data path. The network already carries 243k+ agents and users, so the addressing and trust layer is a solved problem rather than something you assemble from parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Install the daemon on the two hosts that need to trade files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then handshake, &lt;code&gt;send-file&lt;/code&gt;, and check &lt;code&gt;received&lt;/code&gt;. The full command reference for file transfer, the inbox, and the stream and pub/sub models for everything that isn't a file lives in the &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next time an agent needs to hand a large artifact to another agent, ask whether you actually need a bucket — or just a direct connection.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>AI Agent Credential Rotation Without Downtime: Keeping In-Flight Communication Alive</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Wed, 05 Aug 2026 18:11:19 +0000</pubDate>
      <link>https://dev.to/pstayet/ai-agent-credential-rotation-without-downtime-keeping-in-flight-communication-alive-2bol</link>
      <guid>https://dev.to/pstayet/ai-agent-credential-rotation-without-downtime-keeping-in-flight-communication-alive-2bol</guid>
      <description>&lt;p&gt;Your agent has been talking to a peer for six hours. There are in-flight requests, a signing key, an API token, and a long-lived tunnel that peers discovered through NAT. Security says: rotate everything by Friday. You swap the secret — and every in-flight message dies, every reconnect fails, and the peer on the other end has a cached identity that no longer matches anything you present. AI agent credential rotation without downtime is the problem nobody hands you a runbook for, so here's the one I wish I'd had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rotating credentials breaks a running agent
&lt;/h2&gt;

&lt;p&gt;Web services rotate keys all the time. The difference is that a stateless HTTP request lives for milliseconds: by the time a key rotates, every in-flight request has already completed. Agents are stateful. They hold sessions open for hours or days, and the messages in flight at the moment of rotation were &lt;em&gt;signed with the old key&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Three failure modes show up in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;In-flight requests get rejected.&lt;/strong&gt; A message signed with the old key arrives after rotation. Strict verification drops it. The peer retries, the retry is signed with the new key, and now the two sides disagree about which key is valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peers cache your identity.&lt;/strong&gt; Your peer remembered your public key (or token) at handshake time. A hard rotation invalidates that cached identity and forces a full re-handshake — mid-conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect storms.&lt;/strong&gt; The moment the old credential stops working, every peer that was silently connected tries to re-establish at once. That's the worst time to be doing key exchange.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is new — TLS and secrets infrastructure solved most of it decades ago. The trick is porting those playbooks to agents before you need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The classic playbooks: dual-key rotation and grace periods
&lt;/h2&gt;

&lt;p&gt;The core idea behind zero-downtime rotation is simple: &lt;strong&gt;never have a moment where the old key is invalid and the new key isn't trusted yet&lt;/strong&gt;. You keep both alive through an overlap window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versioned keys.&lt;/strong&gt; Give every key a &lt;code&gt;kid&lt;/code&gt; (key ID) plus an activation window. Sign with the newest active key; verify with &lt;em&gt;any&lt;/em&gt; key whose window hasn't closed. This is exactly how JWKS rotation works and how TLS cross-signed certificates ease a CA transition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RotatingKeyring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Old keys stay verifiable during the grace window; only signing moves forward.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# [{"kid", "key", "active_from", "expires_at"}]
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active_from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;KeyExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; outside grace window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Grace period sizing.&lt;/strong&gt; The overlap window must cover the worst case: the longest a message can be in flight, plus the longest a peer can reasonably take to notice the new key, plus reconnect backoff. A few multiples of your keepalive interval is a sane starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staggered rotation.&lt;/strong&gt; Rotate one node at a time, watch error rates, then continue. Fleet-wide rotation turns a single mistake into an outage with a thousand witnesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short-lived secrets.&lt;/strong&gt; If a token expires every hour instead of every year, rotation stops being a fire drill and becomes a background event. Dynamic secrets from a secrets manager (Vault-style leases are the classic example) give you this for free: agents fetch, use, and refresh without any human in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where agents make it worse
&lt;/h2&gt;

&lt;p&gt;The standard playbooks get you most of the way. Agents add three complications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-flight messages are signed, not just authorized.&lt;/strong&gt; A token check happens at request time; a signature check happens at &lt;em&gt;verify&lt;/em&gt; time, which can be after the sender already rotated. Your verification path must tolerate the old key for a while.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The identity is the credential.&lt;/strong&gt; If your agent's "key" is also its identity — the thing peers use to recognize it — rotating it is indistinguishable from a new node appearing. Peers will treat the post-rotation agent as a stranger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The transport is stateful.&lt;/strong&gt; Agents often sit behind NAT with a mapped endpoint. A daemon restart to "apply the new key" can drop that mapping, and the agent becomes unreachable until the next keepalive. Rotation that requires a restart &lt;em&gt;is&lt;/em&gt; downtime, just delayed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The through-line: &lt;strong&gt;decouple the identity from the credential.&lt;/strong&gt; Who you are should be stable; what you present to authenticate should be replaceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decouple identity from credential (the actual fix)
&lt;/h2&gt;

&lt;p&gt;TLS did this with long-term certificates and ephemeral session keys: the certificate is the identity, the per-connection key exchange is disposable. Agent infrastructure needs the same split, and it's exactly the design choice Pilot Protocol makes.&lt;/p&gt;

&lt;p&gt;Pilot Protocol is an open-source overlay network for agents — a permanent virtual address per agent, encrypted UDP tunnels, NAT traversal, and a per-peer trust model. Two properties make rotation boring there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Addressing is decoupled from the key.&lt;/strong&gt; Every agent gets a stable virtual address (like &lt;code&gt;0:0000.0000.0001&lt;/code&gt;) assigned at registration. It survives restarts, IP changes, and moving across clouds. Peers reach you by address, not by key — so rotating key material never changes how you're found.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunnel secrets are per-connection.&lt;/strong&gt; Each tunnel derives its own shared secret via X25519 key exchange, with AES-256-GCM for the traffic. Long-term identity material (an Ed25519 keypair in &lt;code&gt;~/.pilot/identity.json&lt;/code&gt;) is used for trust handshake signing, not for the data path. Rotating long-term material doesn't tear down active tunnels, because the active tunnels aren't encrypted with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split is what makes rotation without downtime structurally possible rather than a sequence of lucky timings. And the tooling treats it as a first-class operation: &lt;code&gt;pilotctl rotate-key&lt;/code&gt; generates a fresh identity keypair, and there's a documented recovery flow (&lt;code&gt;pilotctl recovery enroll / new-key / recover&lt;/code&gt;) so that even a &lt;em&gt;lost&lt;/em&gt; key doesn't cost you the address — you rotate to a fresh key and reclaim the same address with recovery material.&lt;/p&gt;

&lt;p&gt;Trust is handled the same way: explicit per-peer handshakes that persist across daemon restarts and can be revoked with &lt;code&gt;untrust&lt;/code&gt;. Membership and trust are separate — rotating your key doesn't reset your trust graph, and revoking a peer doesn't require re-keying the network. The &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol docs&lt;/a&gt; walk through the addressing, transport, and trust model in detail.&lt;/p&gt;

&lt;p&gt;That's not magic — it's the same identity/credential separation that made TLS rotation boring, applied to the agent's whole networking layer instead of just one connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rotation checklist for running agents
&lt;/h2&gt;

&lt;p&gt;Whatever transport you use, the same checklist applies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Version your keys.&lt;/strong&gt; &lt;code&gt;kid&lt;/code&gt; + activation window on everything you sign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate one node at a time.&lt;/strong&gt; Watch error rates before continuing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size the grace window honestly.&lt;/strong&gt; It must cover in-flight message lifetime + peer reconnect backoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate identity from credential.&lt;/strong&gt; The thing peers use to find you should never be the thing you rotate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer session-key separation.&lt;/strong&gt; If active sessions carry their own keys, long-term rotation is a background event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on a staging peer.&lt;/strong&gt; Rotate a throwaway node first, watch the handshake logs, then do production.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does "credential rotation without downtime" mean for agents?&lt;/strong&gt;&lt;br&gt;
Replacing an agent's API tokens, signing keys, or identity keypair while it keeps running and keeps talking to peers — no dropped in-flight messages, no forced reconnects, no window where the agent is unreachable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long should the overlap window be?&lt;/strong&gt;&lt;br&gt;
Long enough to cover the longest in-flight message plus the time a peer can take to notice the new key, plus reconnect backoff. A few multiples of your keepalive interval is a reasonable starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to restart an agent to rotate a key?&lt;/strong&gt;&lt;br&gt;
Only if your architecture couples the credential to the process. If identity and credential are decoupled and tunnels carry per-connection session keys, rotation is a config-level operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between rotating an API token and rotating an identity key?&lt;/strong&gt;&lt;br&gt;
A token authorizes requests and is cheap to rotate. An identity key is what peers use to recognize you; rotating it without an overlap window looks like a new node appearing. That's why the stable-address pattern matters.&lt;/p&gt;

&lt;p&gt;Rotation is a normal maintenance event, not an outage in waiting. The agents that survive it are the ones whose identity doesn't change when their keys do.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;Get started with Pilot Protocol:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;pilotctl rotate-key&lt;/code&gt; and &lt;code&gt;pilotctl recovery enroll&lt;/code&gt; are a command away. Docs: &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;pilotprotocol.network/docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>agents</category>
    </item>
    <item>
      <title>Test Agent-to-Agent Connectivity Locally Before Deploying: Two Daemons, One Host</title>
      <dc:creator>Philip Stayetski</dc:creator>
      <pubDate>Wed, 05 Aug 2026 13:14:23 +0000</pubDate>
      <link>https://dev.to/pstayet/test-agent-to-agent-connectivity-locally-before-deploying-two-daemons-one-host-12fk</link>
      <guid>https://dev.to/pstayet/test-agent-to-agent-connectivity-locally-before-deploying-two-daemons-one-host-12fk</guid>
      <description>&lt;p&gt;You wrote two agents. Unit tests pass, the tool-call mocks are beautiful, and the first integration test — both agents in one process — works too. Then you deploy, and agent A cannot reach agent B. Not a logic bug. A connectivity bug: the handshake times out, the address doesn't resolve, or the message lands on a channel the peer never approved.&lt;/p&gt;

&lt;p&gt;The fix is boring and effective: test agent-to-agent connectivity locally before deploying. Run two daemon processes on one host, give each a real identity, and exercise the same failure modes you'll hit at distance — restarts, address changes, trust decisions, lossy links. If the pair survives that loop, deploying is a formality.&lt;/p&gt;

&lt;p&gt;Here's the dev loop I use, and the exact commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "agent-to-agent connectivity" actually means
&lt;/h2&gt;

&lt;p&gt;Before you test anything, name the layers. Four things have to be true for agent A to talk to agent B:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Addressing&lt;/strong&gt; — A can name B. A name that survives restarts and IP changes, not a socket that dies with the process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport&lt;/strong&gt; — A's bytes actually reach B. Through NAT, through firewalls, without a static IP on either side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust&lt;/strong&gt; — B accepts messages from A. Explicit, per-peer, mutual — not "everything on the VPN is trusted."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery&lt;/strong&gt; — A can find out that B exists at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unit tests mock all four. Integration tests usually run both agents inside one process, which makes them impossible to break — localhost always answers. Neither tells you anything about production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-process dev loop
&lt;/h2&gt;

&lt;p&gt;The core move: stop testing agents and start testing the network between them. Two daemon processes on one host, each with its own identity and address, talking over the same transport they'll use in production.&lt;/p&gt;

&lt;p&gt;Pilot Protocol is an open-source overlay network built for exactly this shape, and its daemon &lt;em&gt;is&lt;/em&gt; the identity layer, so the harness is a two-minute setup. Start node one and node two with separate identity files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# node one&lt;/span&gt;
pilotctl daemon start &lt;span class="nt"&gt;--identity&lt;/span&gt; ~/.pilot/identity-a.json &lt;span class="nt"&gt;--hostname&lt;/span&gt; agent-a

&lt;span class="c"&gt;# node two&lt;/span&gt;
pilotctl daemon start &lt;span class="nt"&gt;--identity&lt;/span&gt; ~/.pilot/identity-b.json &lt;span class="nt"&gt;--hostname&lt;/span&gt; agent-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each daemon registers a permanent virtual address that survives restarts and IP changes. That's the property you're about to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulate what goes wrong at distance
&lt;/h2&gt;

&lt;p&gt;Localhost hides everything. Two daemons with real identities don't. Walk the failure modes in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Restart one daemon.&lt;/strong&gt; Kill node B, start it again. Does its address survive? If a daemon re-registers with a new address on every boot, peers can't find it, and your "distributed" system breaks on a deploy. With Pilot, the identity file persists, so the address should come back the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl info          &lt;span class="c"&gt;# note the address&lt;/span&gt;
&lt;span class="c"&gt;# restart the daemon...&lt;/span&gt;
pilotctl info          &lt;span class="c"&gt;# same address? good.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Replace the identity.&lt;/strong&gt; Start node B with a &lt;em&gt;new&lt;/em&gt; identity file. The peer should no longer recognize it — trust is tied to identity, not to the IP it came from. If your test harness happily accepts a stranger, your trust model is fake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Send before approval.&lt;/strong&gt; Try to message node B from A before any handshake. It should fail. &lt;code&gt;pilotctl pending&lt;/code&gt; should show the request waiting, and nothing should flow until both sides approve. Membership and trust are decoupled: "joined the network" must never mean "trusted by everyone."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl pending       &lt;span class="c"&gt;# incoming requests waiting on you&lt;/span&gt;
pilotctl approve &amp;lt;node-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Round-trip both directions.&lt;/strong&gt; Trust is mutual, so test it both ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pilotctl ping &amp;lt;peer-address&amp;gt;
pilotctl send-message &amp;lt;peer-address&amp;gt; &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'hello from agent-a'&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Move one node behind a different network.&lt;/strong&gt; A container with its own network namespace, a hotspot, a cloud VM — anything that puts real NAT in the path. This is the step localhost can't fake, and it's where most A2A setups die. Pilot handles it with STUN plus hole-punching and a relay fallback (the beacon), so the same commands keep working when the direct path doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete test script
&lt;/h2&gt;

&lt;p&gt;Here's the whole loop as a script you can run before every deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# 1. identity survives restart&lt;/span&gt;
&lt;span class="nv"&gt;ADDR_BEFORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pilotctl info &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .data.address&lt;span class="si"&gt;)&lt;/span&gt;
pilotctl daemon stop &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pilotctl daemon start
&lt;span class="nv"&gt;ADDR_AFTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pilotctl info &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .data.address&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ADDR_BEFORE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ADDR_AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: address changed"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# 2. unapproved peer cannot send&lt;/span&gt;
pilotctl pending | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"note: pending requests exist"&lt;/span&gt;

&lt;span class="c"&gt;# 3. message round-trips&lt;/span&gt;
pilotctl ping agent-b
pilotctl send-message agent-b &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'ping'&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: agent-to-agent connectivity OK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it, fix what breaks, run it again. That's the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test agent-to-agent connectivity locally before deploying — the checklist
&lt;/h2&gt;

&lt;p&gt;If you take nothing else from this post, use this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The address survives a daemon restart.&lt;/li&gt;
&lt;li&gt;[ ] A message round-trips over the tunnel in both directions.&lt;/li&gt;
&lt;li&gt;[ ] An unapproved peer cannot deliver messages.&lt;/li&gt;
&lt;li&gt;[ ] Approval flips delivery on, with no code changes.&lt;/li&gt;
&lt;li&gt;[ ] A fresh identity is rejected by the peer.&lt;/li&gt;
&lt;li&gt;[ ] Discovery works: A can find B by name, not by hardcoded IP.&lt;/li&gt;
&lt;li&gt;[ ] The same commands pass with real NAT in the path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every item is a thing that breaks in production and costs an hour to debug. Every item is testable on one machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the daemon shape matters
&lt;/h2&gt;

&lt;p&gt;The reason two daemons on one host is the right harness, rather than a mock, is that you're testing the actual artifacts you'll deploy: the process, its identity, its trust decisions, its transport. When the pair works locally — including a restart and a NAT hop — the remaining risk between you and production is configuration, not architecture.&lt;/p&gt;

&lt;p&gt;Pilot Protocol is one honestly-fitting option for this loop: it's open source (AGPL-3.0, Go, no external dependencies), gives every agent a permanent virtual address, encrypts tunnels with X25519 key exchange and AES-GCM, and makes trust an explicit per-peer handshake. The &lt;a href="https://pilotprotocol.network/docs" rel="noopener noreferrer"&gt;Pilot Protocol docs&lt;/a&gt; cover the addressing, transport, and trust model in full. It also has an app store of agent-native tools you can install with a single command once your nodes are talking — but that's a follow-up post.&lt;/p&gt;

&lt;p&gt;Get started and run the loop yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pilotprotocol.network/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two daemons, one host, a few minutes. Cheaper than the first production outage.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
