wire

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Index

Constants

View Source
const (
	FrameOutput byte = 0x00 // daemon -> client
	FrameInput  byte = 0x01 // client -> daemon
	// FrameFile carries one chunk of a file being read, under the ref the
	// daemon minted for that read. Daemon -> client only: nothing here reads a
	// file the client sends.
	FrameFile byte = 0x02 // daemon -> client
)

Binary frame types. Layout is [1 byte type][4 bytes ref BE][payload].

Variables

View Source
var ErrShortFrame = errors.New("wire: frame shorter than header")

Functions

func DecodeBinary

func DecodeBinary(b []byte) (typ byte, ref uint32, payload []byte, err error)

DecodeBinary parses a binary data frame. The returned payload aliases b.

func DecodeControl

func DecodeControl(b []byte) (any, error)

DecodeControl parses a control message into its concrete type.

func EncodeBinary

func EncodeBinary(typ byte, ref uint32, payload []byte) []byte

EncodeBinary builds a binary data frame.

func EncodeControl

func EncodeControl(msg any) ([]byte, error)

EncodeControl marshals msg and injects its "type" discriminator.

Types

type AgentHits added in v0.8.0

type AgentHits struct {
	Building  bool             `json:"building"`
	Truncated bool             `json:"truncated"`
	Hits      []agentstore.Hit `json:"hits"`
	// ReqID echoes the reqId of the agentSearch this answers.
	ReqID uint64 `json:"reqId,omitempty"`
}

AgentHits answers agentSearch. Truncated says the answer is a prefix — the hit cap or the scan budget was reached with sessions unread — and Building says what it says on AgentIndex: a sweep is running, and asking again may find more.

func (AgentHits) MarshalJSON added in v0.8.0

func (a AgentHits) MarshalJSON() ([]byte, error)

MarshalJSON writes an empty list as [] rather than null; "nothing matched" is the ordinary answer to a search, and it is reached by the zero value.

type AgentIndex added in v0.8.0

type AgentIndex struct {
	Building bool                 `json:"building"`
	Sessions []agentstore.Summary `json:"sessions"`
	// History is per-day backfill from the tools' own aggregates — days the
	// transcripts no longer witness because the tool pruned them (see
	// agentstore.HistoryDay). It honours the request's Tools filter, and a
	// request scoped by Cwd gets none at all: the aggregates carry no
	// working directory, and global history merged into a per-project
	// answer would inflate it. Optional and additive: a daemon without it
	// simply sends sessions alone, which is what every daemon did before
	// the field existed.
	History []agentstore.HistoryDay `json:"history,omitempty"`
	// HistoryTotals is the same aggregates' lifetime per-model accounting,
	// in the summaries' own four buckets (see agentstore.HistoryTotals).
	// Lifetime only — it cannot be sliced by range — so a client folds it
	// into its all-time view alone. Filtered and omitted exactly as History
	// is, and additive on the same terms.
	HistoryTotals []agentstore.HistoryTotals `json:"historyTotals,omitempty"`
	// ReqID echoes the reqId of the agents this answers.
	ReqID uint64 `json:"reqId,omitempty"`
}

AgentIndex answers agents. Building says an index sweep is still running, so the list is what the daemon knows so far and asking again will know more — the client's cue to re-poll rather than to conclude the machine has no sessions.

func (AgentIndex) MarshalJSON added in v0.8.0

func (a AgentIndex) MarshalJSON() ([]byte, error)

MarshalJSON writes an empty list as [] rather than null, for the reason Sessions does: "no transcripts on this machine" is reached by building the zero value, the field is not optional, and the client declares it an array.

type AgentPage added in v0.8.0

type AgentPage struct {
	Tool     string               `json:"tool"`
	ID       string               `json:"id"`
	Messages []agentstore.Message `json:"messages"`
	Start    int64                `json:"start"`
	Next     int64                `json:"next"`
	Eof      bool                 `json:"eof"`
	FileSize int64                `json:"fileSize"`
	// ReqID echoes the reqId of the agentRead this answers.
	ReqID uint64 `json:"reqId,omitempty"`
}

AgentPage answers agentRead: a window of messages and where the window sits. Start is the offset of the first returned message's line; Next is where a forward parse would continue; Eof says the parse reached the end of the file as it stood — not that the transcript is finished, since the session may still be running and appending. FileSize is the size at read time, which is also the offset that jumps a backward read to the end.

func (AgentPage) MarshalJSON added in v0.8.0

func (a AgentPage) MarshalJSON() ([]byte, error)

MarshalJSON writes an empty page as [] rather than null — an empty transcript is reached by the ordinary path, and the client maps over the list.

type AgentRead added in v0.8.0

type AgentRead struct {
	Tool   string `json:"tool"`
	ID     string `json:"id"`
	Offset int64  `json:"offset"`
	Dir    string `json:"dir,omitempty"`
	Limit  int    `json:"limit,omitempty"`
	// ReqID correlates this request with the agentPage or error answering it.
	ReqID uint64 `json:"reqId,omitempty"`
}

AgentRead asks for one page of one transcript, addressed by (tool, id) rather than by path: the daemon's index is the only thing that knows where a session lives, and a path on the wire would be an invitation to name one. Answered by agentPage.

Offset is a byte offset into the transcript file — 0, a fileSize, or a start or next a previous page reported. Dir is "forward" or "backward", forward when absent; backward is how "load earlier" and "jump to the end" (offset = fileSize) both work. Limit caps the page's messages, clamped to the daemon's ceiling rather than refused, the way peek's bytes is.

type AgentSearch added in v0.8.0

type AgentSearch struct {
	Query string   `json:"query"`
	Tools []string `json:"tools,omitempty"`
	Cwd   string   `json:"cwd,omitempty"`
	Limit int      `json:"limit,omitempty"`
	// ReqID correlates this request with the agentHits or error answering it.
	ReqID uint64 `json:"reqId,omitempty"`
}

AgentSearch asks for the messages matching a query, case-insensitive, newest sessions first. Tools and Cwd filter as on Agents; Limit caps the hits. Answered by agentHits.

type Agents added in v0.8.0

type Agents struct {
	Tools []string `json:"tools,omitempty"`
	Cwd   string   `json:"cwd,omitempty"`
	// ReqID correlates this request with the agentIndex or error answering it.
	ReqID uint64 `json:"reqId,omitempty"`
}

Agents asks for the machine's agent transcript sessions — the conversations Claude Code, Codex and Pi keep on disk — and is answered by agentIndex. Tools and Cwd are filters: a list screen scoped to one tool or one project sends them, and an empty filter means everything.

type Attach

type Attach struct {
	ID      string `json:"id"`
	LastSeq uint64 `json:"lastSeq"`
	// ReqID correlates this request with the attached or error answering it.
	ReqID uint64 `json:"reqId,omitempty"`
}

type Attached

type Attached struct {
	Ref       uint32 `json:"ref"`
	ID        string `json:"id"`
	Cols      uint16 `json:"cols"`
	Rows      uint16 `json:"rows"`
	Title     string `json:"title"`
	Seq       uint64 `json:"seq"`
	Truncated bool   `json:"truncated"`
	// Head is the offset one past the replayed backlog: bytes below Head are
	// history, bytes at or after it are live. Head == Seq means no backlog.
	Head    uint64 `json:"head"`
	Primary bool   `json:"primary"`
	// ReqID echoes the reqId of the attach or spawn this answers.
	ReqID uint64 `json:"reqId,omitempty"`
}

type Cancel added in v0.5.0

type Cancel struct {
	Ref uint32 `json:"ref"`
}

Cancel abandons a read in flight — a viewer closed before its file finished arriving.

Addressed by ref rather than by reqId, because the ref is what the daemon holds state under. A client that closed before its file arrived has no ref yet; it remembers the reqId as abandoned and cancels once the ref lands, exactly as it already does for an attach whose view went away mid-flight.

type CloseSession

type CloseSession struct {
	Ref uint32 `json:"ref,omitempty"`
	ID  string `json:"id,omitempty"`
}

CloseSession ends a session, addressed one of two ways.

Ref is the original spelling: an attachment handle, for a view that is looking at the session it is closing. ID arrived with the all-machines sessions list, which closes rows it never attached to — attaching first just to earn a ref would cost a subscribe, a backlog replay and a detach, all to deliver one verb. A message carries one address or the other; when both appear, the ref wins and the id is ignored, so the ref semantics are exactly what they always were.

Both fields are omitempty because each is absent in the other's message. Zero is no loss to either: refs are numbered from 1, so ref 0 never names an attachment, and an empty id never names a session.

No reply on success, by either address. The session's end announces itself: attached views get exit, and the list sees state "exited" on its next poll. An id the daemon does not hold is answered with error{not_found}, exactly as update answers it, and a ref this connection does not hold with error{bad_ref}.

type Detach

type Detach struct {
	Ref uint32 `json:"ref"`
}

type DeviceInfo

type DeviceInfo struct {
	ID       string `json:"id"`
	Label    string `json:"label"`
	PairedAt int64  `json:"pairedAt"`
	LastSeen int64  `json:"lastSeen"`
	// PairedOn is the machine that ran the ceremony this device came from —
	// the `pairedOn` of its fleet certificate, read from the signed blob and
	// not from anything the device said about itself.
	//
	// It is here so a Devices screen can tell its two kinds of row apart. A
	// machine on a fleet lists every device it admitted, and it admits two
	// ways: the ones it paired itself, and the ones it took on the fleet's word
	// when they turned up over the relay holding a certificate (rule 2,
	// spec/fleet-trust.md). Those read identically without this — a phone
	// paired on the laptop appears on the desktop looking exactly like a phone
	// paired on the desktop — and they are not identical at all: revoking the
	// second cuts it off every machine in the fleet, permanently.
	//
	// Empty for a device with no certificate (paired before the fleet key
	// existed) and for one whose certificate does not verify under the fleet
	// key this machine holds now. Both mean the same thing to a reader: nothing
	// signed says where this row came from, so it is treated as this machine's
	// own — the conservative direction, since it is the one that does not offer
	// a fleet-wide revoke on the strength of a blob that proved nothing.
	PairedOn string `json:"pairedOn,omitempty"`
	// Origin is where the device first reached this machine from — the Origin
	// header on its enrolment POST, recorded on the local row and never minted
	// into the certificate (crypto.Device). It exists because several loopback
	// tabs enrol as several devices, one per browser origin, and their rows
	// are otherwise identical. A hint, not an identity: absent is the ordinary
	// state of a device paired by ceremony, admitted on a fleet cert, or
	// enrolled before origins were recorded, and a screen must render absent
	// as unremarkable rather than suspicious.
	Origin string `json:"origin,omitempty"`
}

DeviceInfo is one paired device as the wire reports it. Timestamps are unix seconds rather than the registry's time.Time, so a client reads them without parsing RFC 3339.

type DeviceList

type DeviceList struct {
	Devices []DeviceInfo `json:"devices"`
}

DeviceList answers devices, and follows a revoke that succeeded.

func (DeviceList) MarshalJSON

func (d DeviceList) MarshalJSON() ([]byte, error)

MarshalJSON writes an empty list as [] rather than null.

A nil slice marshals to null by default, and "no devices are paired" is the one state a caller reaches by building the zero value — precisely the path that would ship null. The field is not optional and the client declares it `DeviceInfo[]`, so null would throw in every consumer that ranges over it. Normalising here rather than at each call site means no producer can get it wrong.

type Devices

type Devices struct{}

Devices asks for the paired-device list.

type Eof added in v0.5.0

type Eof struct {
	Ref uint32 `json:"ref"`
}

Eof says every byte of a read has been sent. It is the only way a client knows a stream ended rather than stalled.

type Error

type Error struct {
	Code string `json:"code"`
	Msg  string `json:"msg"`
	// ReqID echoes the reqId of the request this error answers, when it
	// answers one — not_found and spawn_failed do; a lagged stream does not.
	ReqID uint64 `json:"reqId,omitempty"`
}

type Exit

type Exit struct {
	Ref  uint32 `json:"ref"`
	Code int    `json:"code"`
}

type File added in v0.5.0

type File struct {
	Ref  uint32 `json:"ref"`
	Path string `json:"path"`
	// Size is the file's real size, which is not always how much is sent: see
	// Truncated.
	Size int64 `json:"size"`
	// Mime is sniffed from the content, never from the extension.
	Mime string `json:"mime"`
	// Kind is "text" or "image". Anything else is refused rather than sent,
	// because a client has nothing useful to do with it.
	Kind string `json:"kind"`
	// Truncated says the file is longer than this daemon will send and only
	// its head is coming. Reported rather than hidden: a viewer that showed
	// 8 MiB of a 40 MiB file in silence would be lying about the file.
	Truncated bool `json:"truncated,omitempty"`
	// ReqID echoes the reqId of the read this answers.
	ReqID uint64 `json:"reqId,omitempty"`
}

File answers read: the stream is open and these are its terms.

Ref is the handle every chunk carries, minted from the same counter attachments use — a read is structurally an attachment, and sharing the counter means the connection gains a kind of entry rather than a second numbering scheme.

Path is the *resolved* path, unlike PathEntry.Path. A symlink means the file you get is not always the path you clicked, and the reader should be told what it actually opened.

type Hello

type Hello struct {
	Ver  string   `json:"ver"`
	Caps []string `json:"caps,omitempty"`
}

type List

type List struct{}

type PairCancel

type PairCancel struct{}

PairCancel leaves pairing mode, invalidating any outstanding token.

type PairStart

type PairStart struct{}

PairStart enters pairing mode and is answered by pairing.

type Pairing

type Pairing struct {
	Token string `json:"token"`
	// URL is absolute: the /pair page on this origin, carrying the token.
	URL string `json:"url"`
	// DaemonPub is the daemon's static public key, base64.
	DaemonPub string `json:"daemonPub"`
	// ExpiresAt is unix seconds. The token is single-use and short-lived.
	ExpiresAt int64 `json:"expiresAt"`
}

Pairing answers pairStart with the credentials the second device needs.

type PathEntry added in v0.5.0

type PathEntry struct {
	Path   string `json:"path"`
	Exists bool   `json:"exists"`
	// Kind is "file", "dir" or "other". Empty when Exists is false.
	Kind string `json:"kind,omitempty"`
	// Size is bytes, and Mtime unix seconds — the same unit deviceList uses,
	// rather than the RFC 3339 strings sessions[] carries.
	Size  int64 `json:"size,omitempty"`
	Mtime int64 `json:"mtime,omitempty"`
}

PathEntry is what one path turned out to be.

Path is what was asked, not what it resolved to. Resolution is the daemon's (a leading ~, a relative path against the session's cwd, symlinks), and the client has no way to reproduce it — but it does need to know which of the candidates it sent this answers.

A path that does not exist, or cannot be resolved at all, is Exists false with the rest left at zero. There is deliberately no error for it: "no" is the ordinary answer here, not a failure.

type Peek

type Peek struct {
	ID    string `json:"id"`
	Bytes int    `json:"bytes,omitempty"`
	// ReqID correlates this request with the preview or error answering it.
	ReqID uint64 `json:"reqId,omitempty"`
}

Peek asks for the tail of a session's scrollback without attaching to it.

It exists for the sessions list, which wants to show what a row is doing without becoming a subscriber to it. An attach would work and is the wrong shape: it costs a ref, a backlog replay, a delivery channel and a detach per row, and a list hovering over twenty sessions would leave twenty subscriptions behind it. This is a read and nothing else — no ref is minted, no stream starts, and the session's LastActive is not touched, because looking at a preview is not activity inside the session.

Bytes caps how much of the tail is returned. Zero means the daemon's own default; anything above PeekMaxBytes is clamped to it rather than refused, since a client asking for too much wants as much as it can have.

Answered by preview, or by error{not_found} for an id the daemon does not hold. ReqID correlates the two, because a list peeks at many rows at once and the answers arrive in whatever order the daemon reaches them.

type Preview

type Preview struct {
	ID string `json:"id"`
	// Data is the raw tail of the session's scrollback.
	Data []byte `json:"data"`
	// Cols and Rows are the dimensions the bytes were drawn at, so a consumer
	// that replays them into an emulator can size it the way the session is.
	Cols uint16 `json:"cols"`
	Rows uint16 `json:"rows"`
	// ReqID echoes the reqId of the peek this answers.
	ReqID uint64 `json:"reqId,omitempty"`
}

Preview answers peek with raw terminal output — escape sequences and all, exactly as the ring holds them.

Raw rather than rendered, because rendering is the client's job and it already owns a terminal emulator. A daemon that flattened this to text would have to make every decision an emulator makes — wrapping at which width, what a cursor move means, which of two overwrites won — and would make them differently from the emulator the same client uses to draw the session for real.

Data is base64 on the wire, as encoding/json carries every []byte. It is the *tail*, so it will usually begin mid-escape-sequence; a consumer must expect to discard a partial sequence at the front rather than treat it as content.

func (Preview) MarshalJSON

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

MarshalJSON writes an empty tail as "" rather than null.

encoding/json carries a nil []byte as null, and a session that has produced no output at all is reached by the ordinary path rather than an exotic one. The client declares the field a string it base64-decodes, so null would throw on exactly the sessions that have least to show.

type Read added in v0.5.0

type Read struct {
	ID   string `json:"id"`
	Path string `json:"path"`
	// ReqID correlates this request with the file or error answering it.
	ReqID uint64 `json:"reqId,omitempty"`
}

Read starts reading one file, resolved against a session's working directory the way Stat resolves one.

Answered by file, which mints the ref the content arrives under, or by an error naming why not. The reply is not the content: a file is streamed as FrameFile chunks under that ref and terminated by eof, because a WebSocket message is capped at 1 MiB by the relay and because a multi-megabyte frame would sit in front of the next keystroke.

type RelayInfo

type RelayInfo struct {
	// Status is "connecting" while the daemon is dialling and "connected" once
	// the socket is up. "off" is a daemon with no relay: on a welcome it is
	// never sent — the whole field is omitted instead — but on a RelayState it
	// is spelled out, because a push that said nothing would say nothing.
	Status string `json:"status"`
	// Origin is the https origin the relay serves browsers on — the address a
	// pairing URL names while the relay is up. Empty unless Status is
	// "connected", because a socket that is not up carries nothing.
	Origin string `json:"origin,omitempty"`
	// MachineID is the slot this daemon holds on the relay — the <id> in the
	// /client/<id> URL a browser opens to reach this machine. It comes from
	// relay.json rather than from the socket, so it is present whenever the
	// relay is configured, connecting and connected alike.
	MachineID string `json:"machineId,omitempty"`
	// MachineName is the machine's human label, free text from the same file.
	// For lists and titles, never for URLs — that is what MachineID is for.
	MachineName string `json:"machineName,omitempty"`
	// NoFleetKey says this daemon is on a relay and cannot sign for its fleet:
	// relay.json carries no fleet key, or one this daemon cannot parse, or no
	// machine id for a certificate to name. A device paired while it is true
	// gets no fleet key pinned and no certificate minted — it reaches this
	// machine and nothing else, for the life of the pairing, because both of
	// those records are written exactly once by the ceremony. So the screen
	// that draws the QR refuses to draw one (routes/devices.tsx), the way it
	// already refuses a QR that could only name loopback.
	//
	// Stated as the fault rather than as a capability, deliberately: absent
	// means "this daemon said nothing about it", which is what an older build
	// on the other end of a relay-served tab says, and that must not read as a
	// refusal. It is also the reason this is a field on the welcome rather than
	// something the client infers — whether a *process* can sign is not
	// visible from any file the browser can see.
	NoFleetKey bool `json:"noFleetKey,omitempty"`
}

RelayInfo is the state of the daemon's relay leg.

It rides the welcome because a client needs it to decide what to render the moment it is greeted — a QR that names an address a phone can reach, an honest "remote access is down" — and the connection's own opening frame is when it needs it. It also rides RelayState, which is how a daemon whose relay leg changed under a tab that was already open says so.

A pointer on Welcome, so "no relay configured" is an absent field rather than an object saying "off". The client's type is a union of the three statuses, and a fourth state — present but empty — is one neither side has a meaning for. RelayState is a value, because a push has no field to omit and "off" is exactly what it exists to be able to say.

type RelayState added in v0.3.2

type RelayState struct {
	Relay RelayInfo `json:"relay"`
}

RelayState is the relay leg's state pushed to every live connection, sent whenever it stops matching what was pushed last.

The welcome alone was not enough, and the gap was not academic. A tab is greeted once and then holds that snapshot for as long as its socket lives, which on loopback is until it is closed; every relay a daemon gains, loses or re-dials underneath it happened after the only frame that ever mentioned one. Screens papered over the upgrade half by polling /api/relay/info, and that poll knows the transport's status and origin and nothing else — so a tab open across a `flue relay setup` learned it had a relay but never which machine it was on, and handed out pairing links that named no machine at all.

Wrapped in a struct rather than being the message, so the payload is `relay` and reads the same as the field it replaces on the welcome. A client applies it over its stored welcome and re-announces that; nothing downstream has to know which of the two frames a fact arrived on.

type Resize

type Resize struct {
	Ref     uint32 `json:"ref"`
	Cols    uint16 `json:"cols"`
	Rows    uint16 `json:"rows"`
	Primary bool   `json:"primary"`
}

type Revoke

type Revoke struct {
	DeviceID string `json:"deviceId"`
}

Revoke removes a paired device. The daemon answers the requester with a fresh deviceList, and the revoked device's own connections with revoked.

type Revoked

type Revoked struct {
	Reason string `json:"reason"`
}

Revoked goes to the revoked device's own connections just before the daemon closes them, so the tab can say why rather than showing a bare disconnect.

type Sessions

type Sessions struct {
	Sessions []session.Info `json:"sessions"`
}

func (Sessions) MarshalJSON

func (s Sessions) MarshalJSON() ([]byte, error)

MarshalJSON writes an empty list as [] rather than null, for the reason DeviceList does below.

"The daemon is running nothing" is reached by building the zero value — the one path a nil slice takes to the wire. The field is not optional and the client declares it `SessionInfo[]`, so null would throw in every consumer that ranges over the list, and would do it on a fresh machine, which is the first list anyone sees.

type Signal

type Signal struct {
	Ref uint32 `json:"ref"`
	Sig string `json:"sig"`
}

type SizeChanged

type SizeChanged struct {
	Ref     uint32 `json:"ref"`
	Cols    uint16 `json:"cols"`
	Rows    uint16 `json:"rows"`
	Primary bool   `json:"primary"`
}

type Spawn

type Spawn struct {
	Cwd  string   `json:"cwd,omitempty"`
	Cmd  []string `json:"cmd,omitempty"`
	Cols uint16   `json:"cols"`
	Rows uint16   `json:"rows"`
	// ReqID correlates this request with the attached or error answering it.
	// Client-chosen; zero means the client asked for no correlation.
	ReqID uint64 `json:"reqId,omitempty"`
	// Group links the new session under an anchor session — a split pane, or a
	// tab in the anchor's group. Optional and additive: a daemon from before
	// the field ignores it, and the session simply spawns ungrouped.
	Group string `json:"group,omitempty"`
	// Ephemeral marks a scratch terminal: hidden by clients, reaped fast once
	// exited, and closed by the daemon when the Group parent ends. Optional
	// and additive like Group.
	Ephemeral bool `json:"ephemeral,omitempty"`
}

type Stat added in v0.5.0

type Stat struct {
	ID    string   `json:"id"`
	Paths []string `json:"paths"`
	// ReqID correlates this request with the stats or error answering it.
	ReqID uint64 `json:"reqId,omitempty"`
}

Stat asks whether paths exist, resolved against a session's working directory.

Plural because of who asks. The terminal underlines a path only once it is known to be real, and a hovered line carries several candidates: a message per candidate would be a round trip per candidate, on a link where the round trip is the cost. One message per hovered line is the shape the caller actually has.

Paths are echoed back in Stats.Entries in the order they were asked about, so a client matches answers to the text it matched them from.

type Stats added in v0.5.0

type Stats struct {
	Entries []PathEntry `json:"entries"`
	// ReqID echoes the reqId of the stat this answers.
	ReqID uint64 `json:"reqId,omitempty"`
}

Stats answers stat, one entry per path asked about, in order.

func (Stats) MarshalJSON added in v0.5.0

func (s Stats) MarshalJSON() ([]byte, error)

MarshalJSON writes an empty list as [] rather than null, for the reason Sessions and DeviceList do: the client declares the field an array, and the zero value is exactly the path that would otherwise ship null.

type Update

type Update struct {
	ID     string    `json:"id"`
	Name   *string   `json:"name,omitempty"`
	Tags   *[]string `json:"tags,omitempty"`
	Pinned *bool     `json:"pinned,omitempty"`
	// Ephemeral exists for one edit: clearing it keeps a scratch terminal,
	// promoting it to an ordinary member of its group. A pointer for the same
	// reason the others are — absent means this edit is not about it.
	Ephemeral *bool `json:"ephemeral,omitempty"`
}

Update edits the metadata a human owns on a session — its name, its tags, whether it is pinned. Nothing the program inside the session says can reach these fields, and nothing here touches what that program says.

Partial, and partial by construction: a field this message does not carry is a field the edit leaves alone. Two views on one session is the ordinary case, so a message that had to restate every field would undo whatever the other view changed since this one last read.

Tags is a pointer to a slice for the one distinction a plain []string cannot keep: "the user removed the last tag" arrives as `[]` and "this edit is not about tags" arrives as nothing, and both decode to a nil slice. The shape mirrors session.MetaPatch field for field, so the daemon can hand the patch straight to the registry rather than rebuild it — a translation being exactly where that distinction would go missing.

Answered by a fresh sessions to the connection that asked, or by error{not_found}. Only that connection: nothing broadcasts the edit, so a second browser sees it on its next list rather than the instant it lands.

type Welcome

type Welcome struct {
	DaemonID string   `json:"daemonId"`
	Host     string   `json:"host"`
	Ver      string   `json:"ver"`
	Caps     []string `json:"caps,omitempty"`
	// Relay is how this daemon is reachable from outside the machine, or nil
	// when it is not configured for a relay at all. See RelayInfo.
	Relay *RelayInfo `json:"relay,omitempty"`

	// FleetCert is this device's own fleet certificate — the signed blob it
	// presents to every *other* machine in the fleet to be admitted without a
	// second ceremony (spec/fleet-trust.md, rule 2) — or empty when there is
	// none to give.
	//
	// It rides the welcome because this connection is the one place the cert
	// can be handed over privately and to the right device at once: the socket
	// is inside Noise, so the relay carries ciphertext, and the far end has
	// already proved it holds the key the cert names. The alternative this
	// replaced was publishing every device cert to the credential-less
	// `GET /directory` and letting the browser find its own — which worked, and
	// put every device's public key and human label in a document anybody on
	// the internet could read, and spent one of the directory's 512 permanent
	// entries on every pairing ceremony ever performed.
	//
	// Empty for a loopback connection (no device identity to hand one to), for
	// a daemon with no fleet key, and for a device paired before the fleet key
	// existed. A client that gets none keeps whatever it already had.
	FleetCert []byte `json:"fleetCert,omitempty"`

	// FleetPub is the fleet's Ed25519 *public* key — what every certificate in
	// the fleet verifies under — or empty on a daemon that holds none.
	//
	// It rides the welcome for the reason FleetCert does, one level up. A
	// browser pins this key from the QR at pairing time and from nowhere else,
	// which was right and was also final: a browser that paired while its
	// machine held no fleet key pinned nothing, and nothing afterwards could
	// give it one, so it saw the single machine it paired with until somebody
	// ran the ceremony again. The certificate had the same problem and this
	// field is the same answer.
	//
	// Handing it over here is not trust-on-first-use, because of who is
	// speaking. A relayed connection is a Noise IK session the browser opened
	// against a static key it pinned out of band, so a key that arrives on it
	// is an authenticated statement from a party the browser already trusts,
	// not an assertion from an unknown peer: a hostile relay cannot forge the
	// session, and a daemon that could lie about this already holds the fleet
	// seed (it is in relay.json on every machine) and can therefore already
	// mint a certificate for any key it likes. The condition is enforced on the
	// receiving side, which is the only side that knows how it learned the
	// static key it is talking to: web/src/fleet/fleet.ts, adoptFleetKey, keeps
	// this only from a machine whose daemon key this browser pinned itself.
	//
	// The public half only, and never the seed. The seed rides the join line
	// between machines; a browser holding it could mint certificates for the
	// fleet, which is the one thing the layering in spec/fleet-trust.md exists
	// to keep out of a tab's reach.
	FleetPub []byte `json:"fleetPub,omitempty"`
}

Jump to

Keyboard shortcuts

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