Documentation
¶
Overview ¶
Package wire implements the wire format of the Agent Wire Protocol (AWP): NDJSON framing, the message envelope, the typed messages of SPEC.md sections 6 to 10, Ed25519 key encoding, canonical JSON and signed grants.
The package has no I/O policy of its own. It is shared by the daemon, the tests and anyone who wants to write an AWP peer in Go.
Index ¶
- Constants
- Variables
- func AuthPayload(myHello, peerHello []byte) []byte
- func Canonical(doc []byte) ([]byte, error)
- func CanonicalValue(v any) ([]byte, error)
- func Decode(line []byte, v any) error
- func DecodeB64(s string) ([]byte, error)
- func Encode(v any) ([]byte, error)
- func EncodeB64(b []byte) string
- func ErrCloses(code string) bool
- func ErrCodes() []string
- func Extensions() []string
- func Fingerprint(pub ed25519.PublicKey) string
- func FormatKey(pub ed25519.PublicKey) string
- func FormatTime(t time.Time) string
- func IDTime(id string) (time.Time, bool)
- func Nonce(n int) string
- func Now() string
- func ParseKey(s string) (ed25519.PublicKey, error)
- func ParseTime(s string) (time.Time, error)
- func ShortKey(key string) string
- func SignAuth(priv ed25519.PrivateKey, myHello, peerHello []byte) string
- func SignPresence(priv ed25519.PrivateKey, p *Presence) (json.RawMessage, error)
- func ThreadStates() []string
- func VerifyAuth(peer ed25519.PublicKey, sig string, peerHello, myHello []byte) error
- type Ack
- type Auth
- type Bye
- type Chunk
- type Envelope
- type Err
- type Grant
- type GrantMsg
- type GrantObject
- type Hello
- type IDGen
- type IntroPeer
- type Introduce
- type Message
- type Mirror
- type Msg
- type Part
- type PartKind
- type Ping
- type Pong
- type Presence
- type PresenceMsg
- type PresencePeer
- type PresenceThread
- type Private
- type Reader
- type Resume
- type State
- type Writer
Constants ¶
const ( // TMirror carries a copy of one line of a thread the sender is part // of. It is reliable like msg: queued, acked by id, replayed on resume // (its th is the mirrored thread's, for the seen map). TMirror = "mirror" // TPrivate asks the receiver not to mirror the thread th. It is // reliable like msg. TPrivate = "private" )
Conversation sharing is an extension: an agent may mirror its threads to hosts it names (typically a dashboard), so that they can show those conversations, not only their subjects and states.
The sharing agent announces it: Hello.Shares and Presence.Shares list the hosts it shares with, so the other party of every thread knows. That party can keep a thread out with a private message; the sharer then stops mirroring it and asks its hosts to forget what they have of it.
const ( // MaxPresenceBytes caps a signed presence document. MaxPresenceBytes = 64 << 10 // PresenceMaxHops bounds how far a document is forwarded. PresenceMaxHops = 8 )
Presence limits.
const ( // Version is the protocol major version sent in hello.v. Version = 0 // MaxLine is the largest line a peer accepts, excluding the newline. MaxLine = 1 << 20 // ChunkSize is the recommended chunk payload size before encoding. ChunkSize = 256 << 10 // AuthContext prefixes the byte string signed in auth. AuthContext = "awp-auth-v0" )
Protocol constants.
const ( THello = "hello" TAuth = "auth" TResume = "resume" TMsg = "msg" TState = "state" TAck = "ack" TChunk = "chunk" TPing = "ping" TPong = "pong" TBye = "bye" TErr = "err" TGrant = "grant" TIntroduce = "introduce" )
Message types.
const ( ErrBadFrame = "bad_frame" ErrVersion = "version" ErrAuth = "auth" ErrUnsupported = "unsupported" ErrForbidden = "forbidden" ErrBlobRefused = "blob_refused" ErrTooLarge = "too_large" ErrInternal = "internal" )
Error codes from section 9.7.
const ( PartText = "text" PartCode = "code" PartData = "data" PartBlob = "blob" )
Part kinds.
const ( StateOpen = "open" StateWorking = "working" StateWaiting = "waiting" StateDone = "done" StateFailed = "failed" StateClosed = "closed" )
Recommended thread states from section 8.1.
const ( KeyPattern = "^ed25519:[A-Za-z0-9_-]{43}$" B64URLPattern = "^[A-Za-z0-9_-]+$" B64Pattern = "^[A-Za-z0-9+/]*={0,2}$" )
Patterns for the encoded fields, as the JSON Schema states them: keys are "ed25519:" and 43 characters of unpadded base64url (section 7.1); nonces and signatures are unpadded base64url; chunk data is standard base64 (section 9.3). Receivers are more liberal (DecodeB64).
const KeyPrefix = "ed25519:"
KeyPrefix prefixes an encoded Ed25519 public key.
const TPresence = "presence"
TPresence is the presence extension: a signed summary of what an agent is doing, gossiped across the network so that any connected host can watch it (NOTES.md, "Presence gossip"). It is also the hello cap that asks for it: presence is only sent to peers that list it.
Variables ¶
var ( ErrGrantExpired = errors.New("grant expired") ErrGrantInvalid = errors.New("invalid grant") )
Errors from ParseGrant.
var ErrBadSignature = errors.New("signature does not verify")
ErrBadSignature means a signature did not verify.
var ErrLineTooLong = errors.New("line exceeds 1 MiB")
ErrLineTooLong means a line exceeded MaxLine (too_large).
var ErrNotObject = errors.New("line is not a JSON object")
ErrNotObject means a line is not a JSON object (bad_frame).
var ErrPresenceInvalid = errors.New("invalid presence document")
ErrPresenceInvalid means a presence document failed verification.
Functions ¶
func AuthPayload ¶
AuthPayload is the byte string signed in auth: "awp-auth-v0" || 0x00 || my_hello_line || 0x00 || peer_hello_line.
func Canonical ¶
Canonical re-encodes a JSON document the way section 10.2 defines canonical JSON: object keys sorted, no whitespace, UTF-8. Strings escape only what JSON requires (quote, backslash and control characters, using the short forms \b \f \n \r \t), so the output matches Python's json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False). Numbers keep their original text.
func CanonicalValue ¶
CanonicalValue canonicalizes an already decoded value (as produced by a json.Decoder with UseNumber).
func DecodeB64 ¶
DecodeB64 accepts base64url or standard base64, padded or not. The spec says base64url for keys, nonces and signatures and plain base64 for chunk data, but is silent on padding, so a receiver takes any of them.
func Encode ¶
Encode marshals a message to one line, without the trailing newline and without HTML escaping, so that text reads naturally under cat.
func ErrCloses ¶
ErrCloses reports whether an err with this code closes the connection. Section 9.7 lists forbidden, unsupported, blob_refused and internal as non-closing. Unknown codes are treated as non-closing too, so a newer peer cannot tear down a connection just by inventing a code.
func ErrCodes ¶ added in v0.6.0
func ErrCodes() []string
ErrCodes lists the error codes of section 9.7, in the spec's order.
func Extensions ¶ added in v0.6.0
func Extensions() []string
Extensions lists the message types that are not in SPEC.md: this implementation speaks them, peers that do not know them ignore them.
func Fingerprint ¶
Fingerprint is the SHA-256 fingerprint of a public key, the way section 13 suggests agents print it in their about text.
func FormatTime ¶
FormatTime formats t the way awp timestamps are written.
func Now ¶
func Now() string
Now returns the current time as an RFC 3339 UTC timestamp with millisecond precision.
func ShortKey ¶
ShortKey is a short, human-friendly form of an encoded key: the first ten characters of its base64url body.
func SignAuth ¶
func SignAuth(priv ed25519.PrivateKey, myHello, peerHello []byte) string
SignAuth signs the auth transcript from the signer's point of view.
func SignPresence ¶
func SignPresence(priv ed25519.PrivateKey, p *Presence) (json.RawMessage, error)
SignPresence fills in Origin and Sig and returns the document's wire form.
func ThreadStates ¶ added in v0.6.0
func ThreadStates() []string
ThreadStates lists the recommended states of section 8.1. Peers may use others.
Types ¶
type Ack ¶
type Ack struct {
Envelope
}
Ack says the msg or state named by re is durably received (section 9.2). Acks drive outbox pruning and are not acked themselves.
type Auth ¶
type Auth struct {
Envelope
// Sig is the Ed25519 signature, base64url, over "awp-auth-v0" || 0x00 ||
// my hello line || 0x00 || peer hello line, the lines as sent and
// received without the newline.
Sig string `json:"sig" jsonschema:"pattern=^[A-Za-z0-9_-]+$"`
// Grants are grant objects (section 10.2) the sender presents.
Grants []json.RawMessage `json:"grants,omitempty"`
}
Auth proves possession of the key sent in hello (section 7.2).
type Bye ¶
type Bye struct {
Envelope
// Reason says why, for the log.
Reason string `json:"reason,omitempty"`
}
Bye is a graceful close (section 9.6). After sending it a peer sends nothing else and closes after the other side's bye or after 5 seconds.
type Chunk ¶
type Chunk struct {
Envelope
// Ref names the blob, as the sender chose it.
Ref string `json:"ref"`
// N is the chunk index, from 0.
N int `json:"n" jsonschema:"minimum=0"`
// Last is true on the final chunk.
Last bool `json:"last"`
// Data is the chunk's bytes, standard base64 (B64Pattern; the tag
// syntax cannot carry the padding).
Data string `json:"data"`
}
Chunk carries part of a blob (section 9.3). Chunks of one blob arrive in order; chunks of different blobs may interleave. A chunk carries th when the blob belongs to a thread, so that resume replays it like any other threaded message.
type Envelope ¶
type Envelope struct {
// T is the message type.
T string `json:"t"`
// ID is unique per sender. A ULID or UUIDv7 is recommended, so that ids
// sort by time; resume compares ids as strings.
ID string `json:"id" jsonschema:"minLength=1"`
// TS is an RFC 3339 UTC timestamp.
TS string `json:"ts" jsonschema:"format=date-time"`
// Th is the thread id. Required for msg, state and ack.
Th string `json:"th,omitempty"`
// Re is the id of the message this one responds to.
Re string `json:"re,omitempty"`
}
Envelope holds the fields every line carries (section 6). The type-specific fields sit beside them at the top level.
func ParseEnvelope ¶
ParseEnvelope decodes the envelope of a line. It fails with ErrNotObject when the line is not a JSON object, which the caller answers with bad_frame.
type Err ¶
type Err struct {
Envelope
// Code is the error code.
Code string `json:"code"`
// Detail is human readable.
Detail string `json:"detail,omitempty"`
// Ref names the blob a blob_refused is about.
Ref string `json:"ref,omitempty"`
}
Err reports a problem (section 9.7). Whether it closes the connection depends on the code: bad_frame, version, auth and too_large do, unsupported, forbidden, blob_refused and internal do not.
type Grant ¶
type Grant struct {
Iss string
Sub string
Caps []string
Exp time.Time
Nonce string
Sig string
Aud string // see GrantObject.Aud
Raw json.RawMessage
}
Grant is a parsed, verified GrantObject. Raw holds the exact object as issued or received; it is what travels on the wire, so fields this implementation does not know about survive and stay covered by the signature.
func MintGrant ¶
func MintGrant(priv ed25519.PrivateKey, sub string, caps []string, exp time.Time, aud string) (*Grant, error)
MintGrant issues a grant from priv to sub. aud, if non-empty, restricts which peer should honor it (see Grant.Aud).
func ParseGrant ¶
ParseGrant decodes and verifies a grant: the signature must be iss's over the canonical JSON of the object without sig. If now is non-zero the grant must not have expired at that time.
type GrantMsg ¶
type GrantMsg struct {
Envelope
// Grant is the grant object (section 10.2).
Grant json.RawMessage `json:"grant"`
}
GrantMsg delivers a grant after the handshake (section 10.3).
type GrantObject ¶ added in v0.6.0
type GrantObject struct {
// Iss is the granting key.
Iss string `json:"iss" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// Sub is the receiving key.
Sub string `json:"sub" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// Caps are the capability strings granted (section 10.1).
Caps []string `json:"caps"`
// Exp is when the grant expires, RFC 3339.
Exp string `json:"exp" jsonschema:"format=date-time"`
// Nonce makes each grant distinct, base64url.
Nonce string `json:"nonce" jsonschema:"pattern=^[A-Za-z0-9_-]+$"`
// Sig is the issuer's signature, base64url.
Sig string `json:"sig" jsonschema:"pattern=^[A-Za-z0-9_-]+$"`
// Aud is an extension: when set, only the peer with this key should
// honor the grant. Introductions use it so that a grant meant for the
// introduced peer does not also give the recipient powers over the
// introducer. Peers that do not know the field ignore it, which can
// only make them honor less, never more, than the spec already says.
Aud string `json:"aud,omitempty" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
}
GrantObject is the wire form of a grant (section 10.2): a signed statement that sub may use caps until exp, honored by iss and by whoever trusts iss. sig is iss's Ed25519 signature over the canonical JSON of the object without sig: keys sorted, no whitespace, UTF-8.
type Hello ¶
type Hello struct {
Envelope
// V is the protocol major version. A peer that sees a version it does
// not speak sends err version and closes.
V int `json:"v" jsonschema:"minimum=0"`
// Key is the long-term Ed25519 public key. Its bytes are the peer's
// identity.
Key string `json:"key" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// Name is how the peer calls itself, "harness@host" by convention.
Name string `json:"name,omitempty"`
// Nonce is 32 random bytes, base64url. The auth signature covers the
// whole hello line, nonce included.
Nonce string `json:"nonce" jsonschema:"pattern=^[A-Za-z0-9_-]+$"`
// Caps lists the supported message families beyond the mandatory chat
// and resume: blob, grant, introduce, and any extension.
Caps []string `json:"caps,omitempty"`
// About is free text for the other agent's context. It is not
// authenticated until auth completes.
About string `json:"about,omitempty"`
// Addr is an extension: an address at which the sender can be reached,
// so that either side can reconnect (section 4.3). Peers that do not
// know it ignore it, as section 5 requires.
Addr string `json:"addr,omitempty"`
// mirrors its conversations to, so the other party knows.
Shares []string `json:"shares,omitempty"`
}
Hello is the first line each side sends, at once, without waiting for the other (section 7.1).
type IDGen ¶
type IDGen struct {
// contains filtered or unexported fields
}
IDGen generates ULIDs that strictly increase, even within one millisecond and even if the wall clock steps backwards (a sandbox restored from a snapshot can do that). Resume compares ids as strings, so monotonicity is what keeps "ids greater than the seen id" meaningful.
type IntroPeer ¶
type IntroPeer struct {
// Key is the introduced peer's public key.
Key string `json:"key" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// Name is what the introduced peer calls itself.
Name string `json:"name,omitempty"`
// Address is where the introduced peer listens, a hint: the key is the
// identity.
Address string `json:"address,omitempty"`
}
IntroPeer identifies the introduced peer.
type Introduce ¶
type Introduce struct {
Envelope
// Peer is the introduced peer.
Peer IntroPeer `json:"peer"`
// Grant is issued by the introducer to the recipient, for the
// introduced peer to honor.
Grant json.RawMessage `json:"grant,omitempty"`
}
Introduce hands the recipient another peer's identity and address plus a grant issued by the introducer (section 10.4). The introduced peer honors the grant only if it trusts the introducer with introduce.
type Mirror ¶
type Mirror struct {
Envelope
// Of is the other party of the mirrored thread (the sender is one side).
Of string `json:"of" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// OfName is the other party's name.
OfName string `json:"of_name,omitempty"`
// Subject is the mirrored thread's subject.
Subject string `json:"subject,omitempty"`
// Dir says who wrote Line: "out" the sender, "in" the other party.
Dir string `json:"dir,omitempty" jsonschema:"enum=out,enum=in"`
// Line is the original msg or state line, verbatim. A msg's blob parts
// keep their name, type and size; the file itself is not mirrored.
Line json.RawMessage `json:"line,omitempty"`
// Withdraw asks the receiver to delete everything it has of the thread
// th between the sender and Of.
Withdraw bool `json:"withdraw,omitempty"`
}
Mirror is an extension: one mirrored line of a thread the sender is part of, or, with Withdraw, a request to forget a mirrored thread. Its th is the mirrored thread's.
type Msg ¶
type Msg struct {
Envelope
// Subject is the thread's title, meaningful on its first message.
Subject string `json:"subject,omitempty"`
// Parts are the message's pieces, in order.
Parts []Part `json:"parts"`
}
Msg is one turn in a thread (section 9.1). The first msg with a new th creates the thread.
type Part ¶
type Part struct {
// K is the part kind: text, code, data or blob.
K string `json:"k"`
// Text is the text of a text or code part.
Text string `json:"text,omitempty"`
// Lang names a code part's language, as a fenced block would.
Lang string `json:"lang,omitempty"`
// Data is a data part's inline JSON.
Data json.RawMessage `json:"data,omitempty"`
// Mime is the media type of a data or blob part.
Mime string `json:"mime,omitempty"`
// Ref names the blob a blob part refers to, as the chunks carry it.
Ref string `json:"ref,omitempty"`
// Name is a blob part's file name.
Name string `json:"name,omitempty"`
// Size is a blob part's size in bytes, before encoding.
Size int64 `json:"size,omitempty" jsonschema:"minimum=0"`
}
Part is one piece of a message. Which fields are meaningful depends on K (PartKinds).
func (Part) MarshalJSON ¶
MarshalJSON emits exactly the fields that belong to the part's kind, so a zero-length blob still carries "size":0 and a text part never grows a stray mime.
type PartKind ¶ added in v0.6.0
PartKind says which fields a part of one kind carries (section 9.1): Required are always present, Optional only when set. Part.MarshalJSON follows this table, and the JSON Schema is generated from it.
type Ping ¶
type Ping struct {
Envelope
}
Ping is a liveness probe (section 9.4): sent when idle for 30 seconds by convention; two missed pongs mean the connection is dead.
type Presence ¶
type Presence struct {
// Origin is the key of the agent the document describes and is signed by.
Origin string `json:"origin" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// Name is the agent's name, as in hello.
Name string `json:"name,omitempty"`
// About is the agent's about text, as in hello.
About string `json:"about,omitempty"`
// Version is the agent's implementation version.
Version string `json:"version,omitempty"`
// Harness is the agent harness: claude, codex, cursor, ...
Harness string `json:"harness,omitempty"`
// Model is the model the agent runs on, as it reports it.
Model string `json:"model,omitempty"`
// Host is the hostname of the machine the agent runs on.
Host string `json:"host,omitempty"`
// conversations to.
Shares []string `json:"shares,omitempty"`
// Active is when the agent last did something through awp, to 30
// seconds, RFC 3339.
Active string `json:"active,omitempty" jsonschema:"format=date-time"`
// Waiting says the agent is blocked waiting for a message.
Waiting bool `json:"waiting,omitempty"`
// Seq increases with every document the origin signs; a relay keeps the
// highest it has seen.
Seq int64 `json:"seq" jsonschema:"minimum=0"`
// TS is when the document was signed, RFC 3339.
TS string `json:"ts" jsonschema:"format=date-time"`
// Peers are the origin's peers.
Peers []PresencePeer `json:"peers,omitempty"`
// Threads are the origin's threads.
Threads []PresenceThread `json:"threads,omitempty"`
// Outbox counts the origin's queued, unacked messages.
Outbox int `json:"outbox,omitempty" jsonschema:"minimum=0"`
// Unread counts the messages the origin has not read.
Unread int `json:"unread,omitempty" jsonschema:"minimum=0"`
// Sig is the origin's signature, base64url.
Sig string `json:"sig,omitempty" jsonschema:"pattern=^[A-Za-z0-9_-]+$"`
}
Presence is what an agent says about itself. It is signed by Origin over the canonical JSON of the document without "sig", like a grant.
func ParsePresence ¶
ParsePresence verifies a signed presence document and decodes it.
type PresenceMsg ¶
type PresenceMsg struct {
Envelope
// Doc is the signed presence document (Presence), verbatim.
Doc json.RawMessage `json:"doc"`
// Hops counts the relays the document has been through; it is not
// forwarded past PresenceMaxHops.
Hops int `json:"hops" jsonschema:"minimum=0"`
}
PresenceMsg is an extension: it carries one signed presence document, gossiped to every peer that lists presence in its hello caps. Doc travels unchanged from relay to relay; the envelope and Hops are per hop.
type PresencePeer ¶
type PresencePeer struct {
// Key is the peer's key.
Key string `json:"key" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// Name is the peer's name.
Name string `json:"name,omitempty"`
// Up says the peer is connected right now.
Up bool `json:"up,omitempty"`
// RTT is the connection's last round trip time in milliseconds, to show
// latency across the network; 0 if unknown.
RTT int64 `json:"rtt,omitempty" jsonschema:"minimum=0"`
}
PresencePeer is one of the origin's peers.
type PresenceThread ¶
type PresenceThread struct {
// Th is the thread id.
Th string `json:"th"`
// Peer is the key of the other party.
Peer string `json:"peer" jsonschema:"pattern=^ed25519:[A-Za-z0-9_-]{43}$"`
// Subject is the thread's subject.
Subject string `json:"subject,omitempty"`
// Mine is the origin's state in the thread.
Mine string `json:"mine,omitempty"`
// Theirs is the peer's state, as the origin last heard it.
Theirs string `json:"theirs,omitempty"`
// Updated is when the thread last changed, RFC 3339.
Updated string `json:"updated,omitempty" jsonschema:"format=date-time"`
// Unread counts the thread's messages the origin has not read.
Unread int `json:"unread,omitempty" jsonschema:"minimum=0"`
}
PresenceThread is one of the origin's threads.
type Private ¶
type Private struct {
Envelope
}
Private is an extension: it asks the receiver to keep the thread th out of what it shares with its hosts, and those hosts to forget it.
type Reader ¶
type Reader struct {
// contains filtered or unexported fields
}
Reader reads newline-terminated lines of at most MaxLine bytes.
type Resume ¶
type Resume struct {
Envelope
// Seen maps each thread id to the last message id the sender has
// durably received in it. Empty on a first connection.
Seen map[string]string `json:"seen"`
}
Resume is sent by both sides after every handshake (section 9.5). The receiver replays its outbox messages the sender has not seen.
type State ¶
type State struct {
Envelope
// State is the thread state: open, working, waiting, done, failed or
// closed by convention; peers may use others.
State string `json:"state" jsonschema:"example=open,example=working,example=waiting,example=done,example=failed,example=closed"`
// Note says more, such as why a thread failed.
Note string `json:"note,omitempty"`
}
State is the sender's view of a thread's soft state (section 8.1). The two sides can disagree.