Olivier Couthaud

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.

I built Liaison mainly so my boss's AI agents and mine could talk to each other, and after that so anyone else who wanted to interact with our agents could join in. An agent is a program that does tasks for you. Without something like this, agents end up coordinating through people: someone writes a document, emails it, and the other side pastes it into their own agent. It works, but a human carries every message, and I wanted to take that person out of the loop. Nobody adopted it, so it stayed a prototype. I'm still proud of it.

Without it Agent, team A Person writes it up Emailed across Person pastes it in Agent, team B With Liaison Agent, team A signed message, goes straight across Agent, team B
Diagram. The usual route, with people in the middle, and the route Liaison gives the agents.

Liaison is a small program an agent runs by typing commands. The messages go straight from one machine to the other, without passing through a server of mine. Machines find each other through a big shared directory held by many volunteer computers, then connect directly.

The part I spent the most time on is trust. If an agent gets a message that says "approved, ship it", how does it know who sent it? Every agent has its own secret key, and every message it sends is stamped with something only that key can produce. It works like a wax seal: anyone can check it, nobody can fake it. On top of that, a person or company can vouch for its agents, like an employee badge, so a stranger can check "this is one of Bob's agents".

A group is just a shared secret phrase. If you know it you can find the group, and if you don't you can't even see that it exists. Private messages are locked so that only the person they are for can open them. They can pass through someone else's machine on the way, like a sealed letter handed to a neighbour who can see who it is from and who it is for, but not what it says. When it is finally opened, the recipient's machine sends back a signed "delivered" note.

Alice's agentseals the message Carol's machinecarries it, cannot open it Bob's agentopens it A signed "delivered" note comes back the same way, so Alice knows it arrived.
Diagram. A private message to someone who is offline, carried by a third machine.

I tried it between a Windows laptop and a Raspberry Pi over the public internet. Group posts, private messages, delivery notes and the "is this really one of Bob's agents" check all worked, and two agents had a short conversation with no person involved. That was only a test, not a long exchange. A fresh agent set itself up in about three minutes. There are three self-tests: one checks that three machines sharing messages in scrambled order end up with identical lists and that forged messages are thrown out, one checks that a private message can pass through a machine that cannot read it, and one checks that two separate programs find each other and swap messages.

It is a prototype and it has real holes. If the sender and the recipient are never online at the same time, nothing arrives, unless some third machine stays on to carry it. There is no way to cancel a badge once it is issued. The secret keys sit on disk unprotected. And there is a bigger one: a machine you connect to gets a copy of everything in your message log, including posts from other groups you belong to. None of these are fixed yet.

What's next. It was not adopted, so none of this has been built. The two things it still needs are a proper always-on relay, so messages can wait for someone who is offline, and a way for one person to run several machines under the same badge.

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 was not adopted, and it is still a prototype, but I'm proud of it.

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.

Agent liaison CLIac.ts IPC daemon.ts, one per account Peer (p2p.ts)keys, signed op-log, merge log.jsonlops persisted on disk wireGossipfull op-log on connect,re-sent when anything is new Hyperswarm, public DHTtopic = sha256("liaison:"+key) Local TCP, 127.0.0.1same-machine agents Other daemons, or --relaystore-and-forward node
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.

  1. Start: Alice holds a1, Bob holds b1 and b2, Carol holds c1.
  2. After enough merges, every peer holds a1, b1, b2 and c1 and shows the same feed: a1, b1, c1.
  3. 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

  1. 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.
  2. Bob comes online and syncs with Carol. His machine checks the signature, decrypts the DM and makes a signed delivered receipt.
  3. That receipt is an ordinary signed operation. It goes back to Carol.
  4. 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 "<cmd>" pipes each incoming message as JSON to a handler's stdin and posts its stdout as the reply. liaison verify <id> 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

  1. Principal: my root identity, made once per machine. It is the one key that has to be kept safe.
  2. Certificate: the principal signs its own id, name and key together with the agent's key, name and issue date.
  3. Agent key: minted per agent and disposable. The agent's id is derived from it.
  4. 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.
  5. 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.

What isn't solved

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. Group posts aren't encrypted individually, so a peer met through one group would receive posts from the other groups you are in. 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

It was not adopted, so none of this has been built. The two things it still needs are a deliberate always-on relay for offline delivery, and a device tier in the principal chain, so one person can run several machines.

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 hardened for production. Not adopted.
Code
github.com/Forkei/liaison

← All projects