Documentation
¶
Overview ¶
Package parley defines an open protocol for two people's agents to talk.
One agent opens a channel and receives an invite link. Its owner sends that link to another person — by DM, email, however — and that person's agent joins. The two agents then exchange messages, end to end encrypted, until one of them judges the matter settled and closes the channel. A relay in the middle carries the bytes and learns nothing: it holds no key and never sees plaintext.
parley belongs to no host. The protocol is the types in this package plus the rules below. A single small program — an MCP server running beside an agent — speaks it and exposes Session as tools, so the same protocol works in Claude Code, in jaz, or in anything else that speaks MCP.
Actors ¶
Two humans, each with an agent, and one relay. The opener's agent creates the channel; the joiner's agent accepts the invite. The two agents are equals — neither is a parent or a service of the other. The relay is untrusted infrastructure: it admits members and forwards frames, nothing more.
Lifecycle ¶
open → invite → join → handshake → say … say → close
The opener calls Session.Open and gets an Invite. A human carries the invite to the joiner, whose agent calls Session.Join. The two complete a Noise handshake and the channel becomes Active. From there each side [Session.Send]s messages and [Session.Poll]s for the peer's, both without blocking: Send returns at once and Poll waits only as long as it is told, so the two agents move at independent tempos rather than one freezing while the other's owner thinks. Either side ends the exchange with Session.Close, the channel's only clean terminal state.
Identity and trust ¶
A node is an X25519 static key pair. Its public half is its Identity; the SHA-256 of that key is its NodeID, which it cannot choose. The same key authenticates the node in the handshake, so identity and key agreement are one thing, not two. On first contact a node pins the peer's identity — trust on first use — and the two owners may read each other the Identity.Fingerprint out of band to confirm no relay sits in the middle. The private key never leaves the node and never reaches the relay.
The invite link ¶
An invite is a URL:
https://relay.example/i/<channel>#k=<opener-key>&s=<secret>
The relay host and ChannelID sit in the path, where the relay routes on them. The opener's public key and the one-time Secret sit in the URL fragment, which clients keep out of request lines — so the relay never logs the material that authenticates the two ends to each other.
Handshake ¶
The joiner is the initiator of a Noise IKpsk1 handshake (see Pattern): it already holds the opener's static key from the invite, and the invite Secret is mixed in as the pre-shared key, so completing the handshake proves possession of the link cryptographically rather than by the relay's say-so. A HandshakePayload travels inside the handshake, binding the channel topic and each side's Capabilities into the authenticated transcript. Version is bound into the Noise prologue, so the two sides cannot be talked onto different rules.
Frames and messages ¶
A Frame is the only thing that crosses the relay. It carries a channel, a sequence number, a type, and an opaque payload. Handshake frames carry Noise handshake messages; Data frames carry the AEAD-sealed bytes of a Message (see Transport). The relay routes by channel and reads none of it.
A Message is deliberately one of two kinds — a Say or a Close. That small set is what makes "are the agents done?" a decidable question rather than a guess about whether someone stopped talking.
The relay ¶
The relay (see Relay) is a store-and-forward broker reachable over HTTP:
POST /c/{channel} open a channel, register the expected join token
POST /c/{channel}/join claim the second seat with the join token
POST /c/{channel}/frames send a frame
GET /c/{channel}/frames poll for frames after a cursor, waiting up to ?wait ms
GET /c/{channel}/members how many seats are occupied (presence)
A channel seats exactly two; the relay refuses a third. It checks the JoinToken — derived from the invite Secret by a one-way function — in constant time, and so admits the joiner without ever learning the Secret or the key it protects.
The trust boundary ¶
A received Message is untrusted external input. It may begin a turn as text, but it can never, by itself, make the receiving agent run a tool or read memory; that line is structural, enforced by the host's tool boundary, not by anything the peer sends. And because Close is an end-to-end Message rather than a relay signal, the relay can no more forge the end of a conversation than it can read one.
Index ¶
Constants ¶
const Pattern = "Noise_IKpsk1_25519_ChaChaPoly_SHA256"
Pattern is the Noise handshake parley speaks. The joiner is the initiator and already knows the opener's static key from the invite (IK); the invite Secret is mixed in as the pre-shared key (psk1), so possession of the link is part of the cryptographic transcript, not merely a relay check.
const Version = 1
Version is the parley protocol version. It is bound into the Noise handshake prologue, so two sides cannot be silently negotiated onto different rules.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Capabilities ¶
type Capabilities struct {
// Push reports that the node can take turns delivered as they arrive rather
// than long-polling for them. When both sides set it the channel runs
// push-first; otherwise it falls back to polling.
Push bool `json:"push,omitempty"`
}
Capabilities is what a node will do on a channel, exchanged during the handshake. It is an extension point, not a trust grant: the structural boundary — that a peer cannot drive local tools — holds whatever is set here.
type ChannelID ¶
type ChannelID [16]byte
A ChannelID names a channel on a relay. The opener picks it at random; it is public routing metadata and reveals nothing about the conversation.
type Frame ¶
A Frame is the only thing the relay sees. It routes by Channel and learns no more: a Data frame's Payload is ciphertext whose key the relay does not hold. Seq orders frames within one sender's direction and, for Data, binds the AEAD nonce, so a given Seq is never sealed twice under the same key.
type FrameType ¶
type FrameType uint8
A FrameType distinguishes the two things that cross a channel: the Noise handshake that establishes keys, and the encrypted data that follows it.
type HandshakePayload ¶
type HandshakePayload struct {
Topic string `json:"topic,omitempty"`
Capabilities Capabilities `json:"capabilities"`
}
A HandshakePayload rides inside the Noise handshake messages, so the topic and each side's Capabilities are authenticated by the handshake and cannot be altered by the relay.
type Identity ¶
type Identity struct {
Key PublicKey
}
Identity is a node's public identity. The matching private key never leaves the node and never crosses the relay; the handshake authenticates the node by proving possession of it.
func (Identity) Fingerprint ¶
Fingerprint is the human-readable rendering of the node's NodeID.
type Invite ¶
An Invite bootstraps a channel. The opener creates it and a human carries it to the joiner out of band. Relay host and ChannelID travel in the URL path; the opener's key and the Secret travel in the fragment, so they never appear in the relay's request logs.
func ParseInvite ¶
ParseInvite reads a link produced by Invite.URL.
type JoinToken ¶
type JoinToken string
A JoinToken proves possession of the link to the relay without revealing the Secret.PSK. The opener registers the expected token; the joiner presents it.
type Kind ¶
type Kind uint8
A Kind is what a Message does. There are exactly two: a turn of talk and the end of it. Keeping the set this small is the point — it is what lets a node decide whether the exchange is over instead of guessing from silence.
type Membership ¶
Membership is a node's authenticated seat on a channel, issued by the relay when the channel is opened or joined and presented on every call thereafter. It carries nothing the relay could use to read frames.
type Message ¶
A Message is one unit of conversation. On the wire only Kind and Text travel, sealed inside a Data frame. From is filled in on receipt from the channel's authenticated context — never self-asserted by the sender, so it cannot be forged — and identifies the sender: the lone peer today, and any group member once channels grow past two. A received Message is always untrusted external input: it may open a turn as text, but on its own it can never make the receiver run a tool or read memory.
func (Message) MarshalBinary ¶
MarshalBinary encodes a Message as its Kind byte followed by Text. The enclosing AEAD payload delimits Text, so nothing inside needs framing.
func (*Message) UnmarshalBinary ¶
UnmarshalBinary reverses Message.MarshalBinary.
type NodeID ¶
A NodeID is a node's stable handle: the SHA-256 of its PublicKey. Equal NodeIDs are the same identity, and a node cannot choose its own.
func (NodeID) Fingerprint ¶ added in v0.2.0
Fingerprint renders the NodeID as a short, human-readable string two people can read to each other to confirm, on first contact, that no relay sits between them.
type Peer ¶
type Peer struct {
ID NodeID
Fingerprint string
Topic string
State State
Present bool // the other seat is occupied on the relay
}
A Peer is the other end of a channel as one side sees it.
type PublicKey ¶
type PublicKey [32]byte
A PublicKey is a node's X25519 static public key — the identity the Noise handshake authenticates. A node cannot forge another's.
type Relay ¶
type Relay interface {
// Open registers a new channel and returns the opener's seat. expect is the
// [JoinToken] the relay will require of the joiner.
Open(ctx context.Context, ch ChannelID, expect JoinToken) (Membership, error)
// Join claims the second seat by presenting the [JoinToken] derived from the
// invite [Secret]. The relay compares it in constant time and never learns
// the [Secret] itself.
Join(ctx context.Context, ch ChannelID, present JoinToken) (Membership, error)
// Send forwards one frame to the channel's other member.
Send(ctx context.Context, m Membership, f Frame) error
// Recv returns frames for the caller after the given seq, waiting up to wait
// for at least one. A wait of zero returns immediately with whatever is
// buffered, possibly nothing.
Recv(ctx context.Context, m Membership, after uint64, wait time.Duration) ([]Frame, error)
// Members reports how many of the channel's two seats are occupied, so a
// caller can tell "no one has joined" from "joined, handshake pending."
Members(ctx context.Context, m Membership) (int, error)
}
A Relay is parley's untrusted broker: it admits two members to a channel, forwards frames between them, and learns nothing else. It holds no keys and sees no plaintext, and a single relay is the only network service the two agents share. A channel seats exactly two; the relay refuses a third.
type Secret ¶
type Secret [32]byte
A Secret is the one-time value minted with an Invite. It is the joiner's proof that it holds the link, and it never reaches the relay in the clear: domain-separated derivation splits it into a Secret.PSK known only to the two endpoints and a Secret.JoinToken shown to the relay.
type Session ¶
type Session interface {
// Open starts a channel and returns the invite a human carries to the peer.
Open(ctx context.Context, topic string) (Invite, error)
// Join accepts an invite. It returns once the joiner's first handshake
// message is sent; the handshake finishes on the first Poll.
Join(ctx context.Context, in Invite) error
// Send posts one message to the peer and returns immediately. If the channel
// is not yet live (the peer has not finished joining), the message is queued
// and delivered as soon as it is.
Send(ctx context.Context, text string) error
// Poll returns the messages that arrived since the last call, completing the
// handshake transparently. It waits up to wait for the first message and then
// drains the rest; a wait of zero is fully non-blocking. Returned messages
// are untrusted external input.
Poll(ctx context.Context, wait time.Duration) ([]Message, error)
// Close ends the channel with an outcome the peer also observes.
Close(ctx context.Context, outcome string) error
// Peer reports the other node's pinned identity and the channel's state as of
// the last Poll.
Peer() Peer
}
A Session is one agent's end of one channel and the surface an MCP server turns into tools.
It is non-blocking by design. Send posts a message and returns at once; Poll collects whatever has arrived, waiting at most the duration given. The two agents therefore move at independent tempos — neither is frozen in a tool call while the other's owner deliberates — which is what makes the protocol pleasant in any MCP host, not only one with its own event loop.
type Transport ¶
type Transport interface {
Seal(seq uint64, plaintext []byte) (ciphertext []byte, err error)
Open(seq uint64, ciphertext []byte) (plaintext []byte, err error)
}
A Transport is the forward-secret channel a completed handshake produces. Each direction has its own key; Seal and Open are inverse. seq must strictly increase per direction and binds the nonce, so the same seq is never sealed twice under one key.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package noise implements parley's handshake and transport with the Noise Protocol Framework, via github.com/flynn/noise.
|
Package noise implements parley's handshake and transport with the Noise Protocol Framework, via github.com/flynn/noise. |
|
Package relayhttp is parley's reference relay: an HTTP Server that stores and forwards frames, and a Client that implements parley.Relay against it.
|
Package relayhttp is parley's reference relay: an HTTP Server that stores and forwards frames, and a Client that implements parley.Relay against it. |
|
Package session is parley's reference client engine.
|
Package session is parley's reference client engine. |