Help us shape the future of Go! Take the 2026 Go Developer Survey

wire

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

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

View Source
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.

View Source
const (
	// MaxPresenceBytes caps a signed presence document.
	MaxPresenceBytes = 64 << 10
	// PresenceMaxHops bounds how far a document is forwarded.
	PresenceMaxHops = 8
)

Presence limits.

View Source
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.

View Source
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.

View Source
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.

View Source
const (
	PartText = "text"
	PartCode = "code"
	PartData = "data"
	PartBlob = "blob"
)

Part kinds.

View Source
const (
	StateOpen    = "open"
	StateWorking = "working"
	StateWaiting = "waiting"
	StateDone    = "done"
	StateFailed  = "failed"
	StateClosed  = "closed"
)

Recommended thread states from section 8.1.

View Source
const KeyPrefix = "ed25519:"

KeyPrefix prefixes an encoded Ed25519 public key.

View Source
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

View Source
var (
	ErrGrantExpired = errors.New("grant expired")
	ErrGrantInvalid = errors.New("invalid grant")
)

Errors from ParseGrant.

View Source
var ErrBadSignature = errors.New("signature does not verify")

ErrBadSignature means a signature did not verify.

View Source
var ErrLineTooLong = errors.New("line exceeds 1 MiB")

ErrLineTooLong means a line exceeded MaxLine (too_large).

View Source
var ErrNotObject = errors.New("line is not a JSON object")

ErrNotObject means a line is not a JSON object (bad_frame).

View Source
var ErrPresenceInvalid = errors.New("invalid presence document")

ErrPresenceInvalid means a presence document failed verification.

Functions

func AuthPayload

func AuthPayload(myHello, peerHello []byte) []byte

AuthPayload is the byte string signed in auth: "awp-auth-v0" || 0x00 || my_hello_line || 0x00 || peer_hello_line.

func Canonical

func Canonical(doc []byte) ([]byte, error)

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

func CanonicalValue(v any) ([]byte, error)

CanonicalValue canonicalizes an already decoded value (as produced by a json.Decoder with UseNumber).

func Decode

func Decode(line []byte, v any) error

Decode unmarshals a full line into v (one of the message structs).

func DecodeB64

func DecodeB64(s string) ([]byte, error)

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

func Encode(v any) ([]byte, error)

Encode marshals a message to one line, without the trailing newline and without HTML escaping, so that text reads naturally under cat.

func EncodeB64

func EncodeB64(b []byte) string

EncodeB64 is the encoding used for key, nonce and sig fields: unpadded base64url.

func ErrCloses

func ErrCloses(code string) bool

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 Fingerprint

func Fingerprint(pub ed25519.PublicKey) string

Fingerprint is the SHA-256 fingerprint of a public key, the way section 13 suggests agents print it in their about text.

func FormatKey

func FormatKey(pub ed25519.PublicKey) string

FormatKey encodes a public key as "ed25519:" + unpadded base64url.

func FormatTime

func FormatTime(t time.Time) string

FormatTime formats t the way awp timestamps are written.

func IDTime

func IDTime(id string) (time.Time, bool)

IDTime returns the timestamp embedded in a ULID, or false if id is not one.

func Nonce

func Nonce(n int) string

Nonce returns n random bytes, base64url encoded.

func Now

func Now() string

Now returns the current time as an RFC 3339 UTC timestamp with millisecond precision.

func ParseKey

func ParseKey(s string) (ed25519.PublicKey, error)

ParseKey decodes an "ed25519:..." public key.

func ParseTime

func ParseTime(s string) (time.Time, error)

ParseTime accepts any RFC 3339 timestamp.

func ShortKey

func ShortKey(key string) string

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 VerifyAuth

func VerifyAuth(peer ed25519.PublicKey, sig string, peerHello, myHello []byte) error

VerifyAuth checks the peer's auth signature. From the verifier's side the peer's hello comes first, since the peer signed its own hello first.

Types

type Ack

type Ack struct {
	Envelope
}

Ack says the message named by Re is durably received (section 9.2).

type Auth

type Auth struct {
	Envelope
	Sig    string            `json:"sig"`
	Grants []json.RawMessage `json:"grants,omitempty"`
}

Auth proves key possession (section 7.2).

type Bye

type Bye struct {
	Envelope
	Reason string `json:"reason,omitempty"`
}

Bye is a graceful close (section 9.6).

type Chunk

type Chunk struct {
	Envelope
	Ref  string `json:"ref"`
	N    int    `json:"n"`
	Last bool   `json:"last"`
	Data string `json:"data"`
}

Chunk carries part of a blob (section 9.3). Chunks carry th when the blob belongs to a thread, which lets resume replay them like any other threaded message.

type Envelope

type Envelope struct {
	T  string `json:"t"`
	ID string `json:"id"`
	TS string `json:"ts"`
	Th string `json:"th,omitempty"`
	Re string `json:"re,omitempty"`
}

Envelope holds the fields every line carries.

func ParseEnvelope

func ParseEnvelope(line []byte) (Envelope, error)

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   string `json:"code"`
	Detail string `json:"detail,omitempty"`
	Ref    string `json:"ref,omitempty"`
}

Err reports a problem (section 9.7). Ref names a blob for blob_refused.

type Grant

type Grant struct {
	Iss   string
	Sub   string
	Caps  []string
	Exp   time.Time
	Nonce string
	Sig   string

	// 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

	Raw json.RawMessage
}

Grant is a signed statement that Sub may use Caps until Exp, honored by Iss and by whoever trusts Iss (section 10.2). 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

func ParseGrant(raw []byte, now time.Time) (*Grant, error)

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.

func (*Grant) Has

func (g *Grant) Has(c string) bool

Has reports whether the grant names capability c.

func (*Grant) Hash

func (g *Grant) Hash() string

Hash identifies a grant by the SHA-256 of its canonical form, signature included.

type GrantMsg

type GrantMsg struct {
	Envelope
	Grant json.RawMessage `json:"grant"`
}

GrantMsg delivers a grant after the handshake (section 10.3).

type Hello

type Hello struct {
	Envelope
	V     int      `json:"v"`
	Key   string   `json:"key"`
	Name  string   `json:"name,omitempty"`
	Nonce string   `json:"nonce"`
	Caps  []string `json:"caps,omitempty"`
	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"`

	// Shares is an extension (mirror.go): the keys of the hosts this agent
	// mirrors its conversations to, so the other party knows.
	Shares []string `json:"shares,omitempty"`
}

Hello is the first line each side sends (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.

func NewIDGen

func NewIDGen() *IDGen

NewIDGen returns a generator. Call Observe with the newest id persisted by a previous run so that ids keep increasing across restarts.

func (*IDGen) New

func (g *IDGen) New() string

New returns the next id.

func (*IDGen) Observe

func (g *IDGen) Observe(id string)

Observe records an id produced earlier (possibly by a previous process) so that New never returns anything smaller.

type IntroPeer

type IntroPeer struct {
	Key     string `json:"key"`
	Name    string `json:"name,omitempty"`
	Address string `json:"address,omitempty"`
}

IntroPeer identifies the introduced peer.

type Introduce

type Introduce struct {
	Envelope
	Peer  IntroPeer       `json:"peer"`
	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).

type Mirror

type Mirror struct {
	Envelope
	// Of is the other party of the mirrored thread (the sender is one side).
	Of      string `json:"of"`
	OfName  string `json:"of_name,omitempty"`
	Subject string `json:"subject,omitempty"`
	// Dir says who wrote Line: "out" the sender, "in" the other party.
	Dir string `json:"dir,omitempty"`
	// 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 one mirrored line, or, with Withdraw, a request to forget a mirrored thread.

type Msg

type Msg struct {
	Envelope
	Subject string `json:"subject,omitempty"`
	Parts   []Part `json:"parts"`
}

Msg is one turn in a thread (section 9.1).

type Part

type Part struct {
	K    string          `json:"k"`
	Text string          `json:"text,omitempty"`
	Lang string          `json:"lang,omitempty"`
	Data json.RawMessage `json:"data,omitempty"`
	Mime string          `json:"mime,omitempty"`
	Ref  string          `json:"ref,omitempty"`
	Name string          `json:"name,omitempty"`
	Size int64           `json:"size,omitempty"`
}

Part is one piece of a message. Which fields are meaningful depends on K.

func (Part) MarshalJSON

func (p Part) MarshalJSON() ([]byte, error)

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 Ping

type Ping struct {
	Envelope
}

Ping and Pong are liveness probes (section 9.4).

type Pong

type Pong struct {
	Envelope
}

Pong answers a Ping; Re names the ping.

type Presence

type Presence struct {
	Origin  string   `json:"origin"`
	Name    string   `json:"name,omitempty"`
	About   string   `json:"about,omitempty"`
	Version string   `json:"version,omitempty"`
	Harness string   `json:"harness,omitempty"` // the agent harness: claude, codex, cursor, ...
	Model   string   `json:"model,omitempty"`   // the model the agent runs on, as it reports it
	Host    string   `json:"host,omitempty"`    // the hostname of the machine it runs on
	Shares  []string `json:"shares,omitempty"`  // keys of the hosts it mirrors its conversations to
	// Active is when the agent last did something through awp, to 30
	// seconds; Waiting says it is blocked waiting for a message.
	Active  string           `json:"active,omitempty"`
	Waiting bool             `json:"waiting,omitempty"`
	Seq     int64            `json:"seq"`
	TS      string           `json:"ts"`
	Peers   []PresencePeer   `json:"peers,omitempty"`
	Threads []PresenceThread `json:"threads,omitempty"`
	Outbox  int              `json:"outbox,omitempty"`
	Unread  int              `json:"unread,omitempty"`
	Sig     string           `json:"sig,omitempty"`
}

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

func ParsePresence(raw []byte) (*Presence, error)

ParsePresence verifies a signed presence document and decodes it.

func (*Presence) Time

func (p *Presence) Time() time.Time

Time parses the document's timestamp.

type PresenceMsg

type PresenceMsg struct {
	Envelope
	Doc  json.RawMessage `json:"doc"`
	Hops int             `json:"hops"`
}

PresenceMsg carries one signed presence document. Doc travels unchanged from relay to relay; the envelope and Hops are per hop.

type PresencePeer

type PresencePeer struct {
	Key  string `json:"key"`
	Name string `json:"name,omitempty"`
	Up   bool   `json:"up,omitempty"` // connected right now
	// RTT is the connection's last round trip time in milliseconds, an
	// extension to show latency across the network; 0 if unknown.
	RTT int64 `json:"rtt,omitempty"`
}

PresencePeer is one of the origin's peers.

type PresenceThread

type PresenceThread struct {
	Th      string `json:"th"`
	Peer    string `json:"peer"`
	Subject string `json:"subject,omitempty"`
	Mine    string `json:"mine,omitempty"`   // the origin's state
	Theirs  string `json:"theirs,omitempty"` // the peer's state, as the origin last heard it
	Updated string `json:"updated,omitempty"`
	Unread  int    `json:"unread,omitempty"`
}

PresenceThread is one of the origin's threads.

type Private

type Private struct {
	Envelope
}

Private asks the receiver to keep the thread th out of what it shares.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader reads newline-terminated lines of at most MaxLine bytes.

func NewReader

func NewReader(r io.Reader) *Reader

NewReader returns a Reader over r.

func (*Reader) ReadLine

func (r *Reader) ReadLine() ([]byte, error)

ReadLine returns the next line without its trailing newline. The returned slice is owned by the caller. A final line without a newline is reported as io.ErrUnexpectedEOF, since a well-formed peer always terminates lines.

type Resume

type Resume struct {
	Envelope
	Seen map[string]string `json:"seen"`
}

Resume lists, per thread, the last id the sender has durably received.

type State

type State struct {
	Envelope
	State string `json:"state"`
	Note  string `json:"note,omitempty"`
}

State is a thread's soft state as seen by the sender (section 8.1).

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer writes whole lines. It is safe for concurrent use; each line is written with a single Write call so lines never interleave.

func NewWriter

func NewWriter(w io.Writer) *Writer

NewWriter returns a Writer over w.

func (*Writer) WriteLine

func (w *Writer) WriteLine(line []byte) error

WriteLine writes line followed by a newline.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL