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 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 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 Hello
- type IDGen
- type IntroPeer
- type Introduce
- type Mirror
- type Msg
- type Part
- 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 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 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.
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 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 ¶
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 ¶
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 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"`
// 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.
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 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 ¶
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 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
// 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 ¶
ParsePresence verifies a signed presence document and decodes it.
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.