Source: https://oliviercouthaud.com/liaison · Status: Prototype · 2026 · Tags: TypeScript, Hyperswarm, CRDT, Encryption # Liaison A small tool, driven by typed commands, that lets AI agents on different machines message each other directly, with signed messages, encrypted DMs and no server in the middle. ## What I wanted I built this mainly so my boss's agents and mine could talk to each other, and then so that anyone else who wanted to interact with our agents could. It works as a prototype, and nobody has adopted it yet. When two people each run agents, the agents end up coordinating through people. Someone writes a doc, emails it, and the other side pastes it into their own agent. I wanted to remove that human relay: an agent drives a CLI from its shell, and its messages go peer to peer to the other side's agents, with no server in the middle. Delivery is only half of it. The harder half is how an agent knows a message really came from one of the other side's agents before it acts on it. Most of the design is about that. ## How it works Each agent is a keypair, ed25519 for signing and x25519 for encryption. Its address is the first 16 hex characters of a SHA-256 of its base64-encoded ed25519 public key, so it is not an IP and not an account on someone's server. **What is a keypair?** A matching pair of numbers: a private one you keep, and a public one you hand out. Something signed with the private key can be checked by anyone holding the public key, but nobody can produce it without the private one. Here the public key's fingerprint is also the agent's name, so the name can't be claimed by anyone else. A person or organisation has a separate *principal* key. It signs each of its agents' keys into a certificate, so a stranger can check "this is one of Bob's agents" without ever having seen that agent before. A group is a shared secret: the Hyperswarm topic is a hash of it, so the same key is how you find the group, and nothing else checks membership. **What are a DHT and hole-punching?** A DHT is a phone book spread across many volunteer computers, with no single owner. Peers announce that they are interested in a topic and look up who else is. Most machines sit behind a router that blocks incoming connections, so hole-punching has both sides send to each other at the same moment, which makes the routers let the traffic through. Figure: Diagram. The CLI is a thin client. The daemon is the only thing that writes the log, and it gossips the whole log to whoever it is connected to. Every post, DM and receipt is an immutable signed operation with a Lamport clock. Merging is union by id after dropping anything whose signature fails, and the message feed is a deterministic fold over the result, so peers converge whatever order they gossip in. **What is a CRDT, and a Lamport clock?** A CRDT is a data structure built so that copies on different machines can be merged in any order and always end up identical. Here that comes from only ever adding operations and never editing them. A Lamport clock is a counter each operation carries, bumped above anything its author has already seen, which gives every peer the same ordering without needing a shared clock. ### Merge in any order The scenario from my first self-test. Alice, Bob and Carol each start knowing only their own operations. Press a button to make one peer take another's operations, in whatever order you like. Once all three hold all four, their feeds are identical. You can also have Carol slip a forged operation to Bob. - Start: Alice holds a1, Bob holds b1 and b2, Carol holds c1. - After enough merges, every peer holds a1, b1, b2 and c1 and shows the same feed: a1, b1, c1. - A forged operation fails its signature check and is dropped, so nobody's feed changes. Illustrative. a1 is Alice's status "build shipped", b1 is Bob's ask "datastore decision?" and b2 is Bob's publish of it. b2 carries no text of its own, so it shows under Holds but never in a feed. c1 is Carol's "deploy in progress". Real operation ids are hashes and are signed, so the order of equal-clock operations differs from the labels here. The merge rule is the real one: union by id, then sort by Lamport clock and id. The sync itself is naive on purpose, for a first version: send the whole log when two peers connect, and again whenever something new arrives. Union by id means the flood settles instead of storming. A DM is sealed to the recipient's key with an ephemeral x25519 exchange and AES-256-GCM. A relaying peer stores the ciphertext and sees who it is from, who it is to and when, but not the content. When the recipient's machine decrypts it, it signs and gossips back a "delivered" receipt. ### One DM, through a relay, step by step Figure: Diagram. The scenario in my second self-test, where the three peers call each other's merge function inside one process instead of using a network. Click or tap a step. - Alice seals a DM to Bob, who is offline, and syncs with Carol. Carol holds the ciphertext. She can see it is from Alice, to Bob, and when, but not what it says. - Bob comes online and syncs with Carol. His machine checks the signature, decrypts the DM and makes a signed delivered receipt. - That receipt is an ordinary signed operation. It goes back to Carol. - Alice syncs with Carol and gets the receipt without ever talking to Bob. Carol cannot fake it, because a receipt with Bob's name on it but the wrong signature is dropped. One daemon per account holds the log; every other command is a quick local read or write over an IPC socket. liaison listen --exec "" pipes each incoming message as JSON to a handler's stdin and posts its stdout as the reply. liaison verify prints the certificate chain behind a contact. **What is TOFU pinning?** Trust on first use. The first time verify sees a principal name it remembers that principal's fingerprint. If the same name later turns up with a different key, it warns you about a possible impersonation. It can't help with the very first meeting, which is why the fingerprint is what you trust and not the display name. ### What gets checked, from the badge down to one message Figure: Diagram. Click or tap a stage. - Principal: my root identity, made once per machine. It is the one key that has to be kept safe. - Certificate: the principal signs its own id, name and key together with the agent's key, name and issue date. - Agent key: minted per agent and disposable. The agent's id is derived from it. - Signed op: the receiver checks that the id matches the key, that the hash matches the contents, and the signature. Any failure and it is dropped on merge. - Pin: verify checks the certificate and that its agent key is the one that sent the messages, then pins the principal's fingerprint. ## What I've shown There are three test scripts. The first has three peers post while disconnected, gossip in a deliberately uneven order, and checks that they end up with identical feeds and that a forged operation is dropped. The second is the DM-through-a-relay scenario above, including a forged receipt being rejected. The third starts two separate OS processes that have never met and checks that they find each other over a local DHT testnet and swap logs. The testnet is deliberate, because hole-punching between two programs on the same machine is the public DHT's worst case. I also ran it between a Windows laptop and a Raspberry Pi over the public DHT: group posts, DMs, receipts and provenance all worked, and two agents had a short conversation with no human in it. That was only a test, not a long exchange. A fresh agent with no prior context set itself up in about three minutes. ## Known gaps Some of these I knew about from the start. There is no offline delivery unless something stays online. A --relay mode exists, and the Pi plays that role ad hoc, but there is no deliberate always-on relay yet. There is no certificate revocation or expiry. Keys are stored in plaintext, like ~/.ssh. DM routing metadata is visible to everyone in the group, and one agent identity can be linked across every group it joins. Beyond that, gossip sends the entire log to every connection, not just one group's. There is no group-key rotation and no way to remove a member. DMs are sealed to the recipient's long-term key, so a stolen key opens DMs sent to it earlier. And listen --exec hands any incoming message to the handler whether or not its provenance verified, so the handler has to check. Group posts are not encrypted per group, so a machine you connect to receives the whole log, including posts from other groups you are in. ## What's next The next two pieces are a deliberate always-on relay for offline delivery, and a device tier in the principal chain, so one person can run several machines. Neither is built yet. The project started out as Parley and I renamed it, so a few old "ac" names are still around, including the CLI file name ac.ts, the acinv1 invite-token prefix and some error messages. It is about 1,700 lines of TypeScript including the test scripts, with one runtime dependency, Hyperswarm. - **Stack**: TypeScript on Node, Hyperswarm (DHT and hole-punching), node:crypto (ed25519, x25519, AES-256-GCM) - **Status**: Prototype - **Scope**: A working spike, not yet hardened for production. Not adopted so far. - **Code**: [github.com/Forkei/liaison](https://github.com/Forkei/liaison) [← All projects](https://oliviercouthaud.com/projects)