api

package
v0.0.0-...-81d828d Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 40 Imported by: 0

Documentation

Overview

Package api is relayd's one HTTP surface: the phone's WebSocket, the console's REST and SSE, and nothing else.

One API for both deployments (DASHBOARD.md §5), so Relay Cloud is a proxy plus an auth layer rather than a second backend. One place for the authorization checks is the only way they stay consistent, and the vault sits behind them.

The phone contract is SYSTEM.md §6.1, exactly: one authenticated WebSocket, JSON envelopes, both directions.

{ v: 1, id: "<uuid>", type: "<name>", at: <unix_ms>, payload: {...} }

Chosen over gRPC or plain HTTP because the traffic is small, bidirectional and long-lived, and because a WebSocket survives a phone that sleeps and wakes far more gracefully than a stream that has to be re-established with state.

Index

Constants

View Source
const (
	// AssuranceSingleFactor is a password, and nothing else.
	AssuranceSingleFactor = "aal1"
	// AssuranceSecondFactor is a password plus a second factor.
	AssuranceSecondFactor = "aal2"
)

Assurance levels, named for Supabase's `aal` claim because that is where they come from and a translation layer would only be a second vocabulary to keep in step.

View Source
const (
	ConsoleCredential = "credential"
	ConsoleConnector  = "connector"
	ConsoleFact       = "fact"
	ConsoleAudit      = "audit"
	ConsoleProbe      = "probe"
)

Console event kinds.

View Source
const (
	SourceRegistry = "registry"
	SourceIndex    = "index"
	SourceBoth     = "both"
)

Session sources.

View Source
const (
	TranscriptWindow    = 64 * 1024
	MaxTranscriptWindow = 1 << 20
)

TranscriptWindow is the default read size, and MaxTranscriptWindow the ceiling. 64 KiB is a comfortable screenful of JSONL and four orders of magnitude below the smallest store we measured.

View Source
const (
	TypeUtterance       = "utterance"
	TypeTouch           = "touch"
	TypeWear            = "wear"
	TypeAudioChunk      = "audio.chunk"
	TypePhoto           = "photo"
	TypeSessionCommand  = "session.command"
	TypeConsentDecision = "consent.decision"
	TypeSyncOffer       = "sync.offer"
)

Phone → server. SYSTEM.md §6.1's list, complete.

View Source
const (
	TypeSpeak             = "speak"
	TypeUIRender          = "ui.render"
	TypeSessionList       = "session.list"
	TypeConfirmRequest    = "confirm.request"
	TypeConnectorProposal = "connector.proposal"
	TypeDigest            = "digest"

	// TypeAck and TypeError are transport frames rather than product ones: every
	// phone→server frame carries an id, and something has to say whether it
	// landed. Without them a phone cannot distinguish "delivered" from "the
	// socket is up but the daemon dropped it", which is the failure a
	// store-and-forward queue exists to handle.
	TypeAck   = "ack"
	TypeError = "error"

	// TypeAuth is phone → server, and only over a relayed socket. On the LAN
	// the credential rides the handshake as `Authorization: Bearer …`; through
	// the rendezvous relay that handshake terminates at the relay, which is a
	// pipe and not this daemon, so the header never arrives. The token becomes
	// the first frame instead. See [Server.ServeRelayedSocket].
	TypeAuth = "auth"

	// TypeNotify is the silent-but-present notification. ADAPTERS.md §7 requires
	// a channel that reaches the phone without speaking — quiet hours hold the
	// speech and keep the notification — and SYSTEM.md §4 already lists "push
	// notification {title, body, session_id?}" as an output. §6.1's list omitted
	// the frame that carries it.
	TypeNotify = "notify"

	// TypeConfirmResolved retracts a confirm.request that is no longer true: the
	// approval was answered in a terminal (Codex's serverRequest/resolved), or
	// the turn was cancelled. A ping that outlives its question wakes someone to
	// approve what is already approved.
	TypeConfirmResolved = "confirm.resolved"
)

Server → phone. SYSTEM.md §6.1's list, plus three frames named below.

View Source
const (
	CodeBadEnvelope    = "bad_envelope"
	CodeBadVersion     = "unsupported_version"
	CodeUnknownType    = "unknown_type"
	CodeBadPayload     = "bad_payload"
	CodeNotImplemented = "not_implemented"
	CodeNoSuchSession  = "no_such_session"
	CodeUnsupported    = "unsupported"
	CodeFailed         = "failed"

	// Console codes. The console renders each of these differently, so they are
	// distinct values rather than one generic refusal: "you may not" and "prove
	// it again" lead to opposite user actions.
	CodeUnauthorized = "unauthorized"
	CodeForbidden    = "forbidden"
	// CodeReauthenticate is DASHBOARD.md §4's cloud rule: every vault write is
	// re-authenticated regardless of session age.
	CodeReauthenticate = "reauthenticate"
	CodeNotFound       = "not_found"
	CodeConflict       = "conflict"
	// CodeUnavailable is a surface that exists but has nothing wired behind it
	// yet — no vault, no prober, no detection pass.
	CodeUnavailable = "unavailable"
	// CodeSelfHosted is a cloud-only route asked for on the free tier. It is not
	// an error the user caused, and the message says so.
	CodeSelfHosted = "self_hosted"
)

Error codes carried by TypeError.

View Source
const DefaultLeeway = 30 * time.Second

DefaultLeeway is the clock skew allowed on `exp`.

View Source
const GatewayPrefix = mcp.HTTPPrefix

GatewayPrefix is where the shared tool bus mounts. It is mcp.HTTPPrefix rather than a second copy of the string, because the installer writes this path into five runtimes' configs and a drift between the two would be discovered by every agent on the machine at once.

View Source
const KeepAlive = 25 * time.Second

KeepAlive is how often an idle SSE stream writes a comment, so a proxy that reaps quiet connections does not silently sever the console's live view.

View Source
const UnusedAfter = 30 * 24 * time.Hour

UnusedAfter is how long untouched access has to sit before the screen calls it out. DASHBOARD.md §3.4 says "a month" in as many words.

View Source
const Version = 1

Version is the envelope version. A frame with any other value is refused rather than best-guessed: a phone from a future release talking to an old daemon should be told so, once, instead of half-working.

Variables

View Source
var (
	// ErrWrongAccount is a token that is valid and belongs to somebody else.
	// Distinct because it is the one failure that is not a bug in the client and
	// not an attack in progress — it is what a user sees if they are signed into
	// the wrong account, and the console can say so.
	ErrWrongAccount = errors.New("api: this session belongs to a different account")
	// ErrNoKeys is a box that cannot reach Supabase's JWKS with a cold cache. It
	// fails closed: a degraded mode that widens access is not a degraded mode.
	ErrNoKeys = errors.New("api: cannot verify sessions right now")
)

Errors a caller may want to distinguish. Everything else collapses to ErrUnauthenticated on purpose: a verifier that explains precisely which check a token failed is a probe of what a valid one would look like.

View Source
var ErrBadVersion = errors.New("api: unsupported envelope version")

ErrBadVersion is a frame from a client that speaks a different envelope.

View Source
var ErrExposed = errors.New("api: refusing to bind to a non-loopback address without --lan")

ErrExposed is a non-loopback bind without the flag that admits to it.

View Source
var ErrNoScreen = errors.New("api: no phone is connected, so there is nowhere to draw")

ErrNoScreen is a view with no phone to draw it on.

View Source
var ErrNotProposed = errors.New("that connector has not been proposed on this machine")

ErrNotProposed is an accept for something that was never suggested.

It is exported because the implementation lives in the composition root and this is the door §4b rule 1 would otherwise leak through: without it, POST /v1/connectors/proposals/gmail/accept would be a general grant endpoint with a longer path. It maps to 404 rather than 500 — "there is no such offer" is the truth, not a fault.

View Source
var ErrUnauthenticated = errors.New("api: no valid credential")

ErrUnauthenticated is any request that did not prove who it is.

Functions

func Bind

func Bind[T any](e Envelope) (T, error)

Bind unmarshals a frame's payload.

func BindNotice

func BindNotice(listen string, lan bool, token string) string

BindNotice is what relayd prints on start, in both cases. It exists here rather than in the daemon so the sentence and the check that produces it stay in one file.

func CheckBind

func CheckBind(listen string, lan bool) error

CheckBind refuses to expose the API on a network by accident.

DASHBOARD.md §4: the console can write to the vault, which makes it the highest-value target in the system, above the glasses and above relayd's own API. So loopback is the default, exposure is a flag, and a config file that quietly says 0.0.0.0 does not count as consent.

func GenerateToken

func GenerateToken() (string, error)

GenerateToken makes a 256-bit bearer token.

func IsLoopback

func IsLoopback(host string) bool

IsLoopback reports whether a host part binds only to this machine.

func LANWarning

func LANWarning(listen string) string

LANWarning is the sentence relayd prints when exposure is chosen on purpose.

func WithIdentity

func WithIdentity(ctx context.Context, id Identity) context.Context

WithIdentity attaches an identity, for a cloud host that authenticates in its own middleware and hands the request on.

Types

type Ack

type Ack struct {
	Re string `json:"re"`
	OK bool   `json:"ok"`
}

Ack says a frame landed.

type AudioChunk

type AudioChunk struct {
	Seq   int64  `json:"seq"`
	Codec string `json:"codec"`
	Data  []byte `json:"data"`
}

AudioChunk is Opus from the mic. M4.

type AuditEntry

type AuditEntry struct {
	ID     string `json:"id"`
	At     int64  `json:"at"`
	Seq    int64  `json:"seq"`
	Action string `json:"action"`
	// Who, and from where.
	Actor   string `json:"actor"`
	ActorID string `json:"actor_id,omitempty"`
	From    string `json:"from,omitempty"`
	Agent   string `json:"agent,omitempty"`

	Target  string `json:"target,omitempty"`
	Service string `json:"service,omitempty"`

	Outcome string            `json:"outcome"`
	Reason  string            `json:"reason,omitempty"`
	Detail  map[string]string `json:"detail,omitempty"`
	// Attempt links an outcome back to its attempt so the console can pair them
	// into one row and show how long the write took.
	Attempt string `json:"attempt,omitempty"`
}

AuditEntry is one line, on the wire.

type AuditHealth

type AuditHealth struct {
	Durable bool   `json:"durable"`
	Path    string `json:"path,omitempty"`
	Note    string `json:"note,omitempty"`
}

AuditHealth is whether the evidence survives a restart.

type AuditList

type AuditList struct {
	Entries []AuditEntry `json:"entries"`
	// Durable is false on a machine with no writable data directory. The console
	// shows it, because an empty list from a memory log after a restart looks
	// exactly like "nothing has happened".
	Durable bool   `json:"durable"`
	Path    string `json:"path,omitempty"`
	// Intact is the hash chain verifying. A false here means a line was edited
	// or removed, which is worth more than every entry in the list.
	Intact bool   `json:"intact"`
	Broken string `json:"broken,omitempty"`
	// Pending is the attempts with no outcome — a mutation that started and did
	// not finish, which is the shape of an interrupted or crashed write.
	Pending int   `json:"pending"`
	At      int64 `json:"at"`
}

AuditList is the audit screen.

type Authenticator

type Authenticator interface {
	Authenticate(r *http.Request) (Identity, error)
}

Authenticator turns a request into an Identity. It answers "who is this", never "may they do this" — that decision stays in [Server.guard] so both deployments make it in the same place.

func NewSupabaseAuthenticator

func NewSupabaseAuthenticator(o SupabaseOptions) (Authenticator, error)

NewSupabaseAuthenticator builds the cloud tier's Authenticator.

type AuthenticatorFunc

type AuthenticatorFunc func(r *http.Request) (Identity, error)

AuthenticatorFunc adapts a function to Authenticator.

func (AuthenticatorFunc) Authenticate

func (f AuthenticatorFunc) Authenticate(r *http.Request) (Identity, error)

Authenticate implements Authenticator.

type BillingLink struct {
	URL       string `json:"url"`
	ExpiresAt int64  `json:"expires_at,omitempty"`
	// Provider is named so the console can say where it is sending you before it
	// sends you there.
	Provider string `json:"provider"`
}

BillingLink is the whole billing API.

type BillingPortal

type BillingPortal interface {
	// PortalURL returns a short-lived URL and its expiry. The identity is passed
	// because the portal session is per-customer and this is the one place the
	// console's identity has to become a Stripe customer.
	PortalURL(ctx context.Context, id Identity) (string, time.Time, error)
}

BillingPortal mints a Stripe customer portal session for one account.

Cloud-only by construction: on the self-hosted tier nothing implements it, and the route says so in plain words rather than 404-ing as though the console were broken.

type BillingPortalFunc

type BillingPortalFunc func(ctx context.Context, id Identity) (string, time.Time, error)

BillingPortalFunc adapts a function to BillingPortal.

func (BillingPortalFunc) PortalURL

func (f BillingPortalFunc) PortalURL(ctx context.Context, id Identity) (string, time.Time, error)

PortalURL implements BillingPortal.

type BusStats

type BusStats struct {
	Published   uint64 `json:"published"`
	Subscribers int    `json:"subscribers"`
}

BusStats is the event bus, for the health page.

type ConfirmOption

type ConfirmOption struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Kind string `json:"kind"`
	// Standing means choosing it grants something beyond the action in front of
	// us — "allow always". ORCHESTRATOR.md §4b: the orchestrator must never
	// select one on the user's behalf, so the phone marks it and a human picks.
	Standing bool `json:"standing"`
}

ConfirmOption is one answer the agent will accept, spoken by name.

type ConfirmRequest

type ConfirmRequest struct {
	ActionID string          `json:"action_id"`
	Session  string          `json:"session"`
	Runtime  string          `json:"runtime"`
	Ask      string          `json:"ask"` // permission | tool_value | elicitation
	Prompt   string          `json:"prompt"`
	Options  []ConfirmOption `json:"options,omitempty"`
	Tool     string          `json:"tool,omitempty"`
	Target   string          `json:"target,omitempty"`
	// Consequential is an action with effects outside the machine. Not
	// suppressible by batching or quiet hours.
	Consequential bool  `json:"consequential"`
	Deadline      int64 `json:"deadline,omitempty"`
	Repeat        int   `json:"repeat,omitempty"`
}

ConfirmRequest is a session blocked on a human.

type ConfirmResolved

type ConfirmResolved struct {
	ActionID string `json:"action_id"`
	Reason   string `json:"reason"`
}

ConfirmResolved retracts a confirm.request.

type ConnectorGrantResult

type ConnectorGrantResult struct {
	ID        string   `json:"id"`
	Connector string   `json:"connector"`
	Scopes    []string `json:"scopes"`
	GrantedAt int64    `json:"granted_at"`

	// Sessions and Note are §4b's catch, carried rather than hidden: some
	// runtimes enumerate their tools once per session, so a grant made now may
	// not reach an agent that is already running. Saying which sessions were
	// told beats leaving the user wondering why the thing they just connected
	// is invisible.
	Sessions []string `json:"sessions,omitempty"`
	Note     string   `json:"note,omitempty"`
}

ConnectorGrantResult is what saying yes actually did.

type ConnectorList

type ConnectorList struct {
	Connectors []ConnectorView `json:"connectors"`
	MCP        MCPView         `json:"mcp"`
	Available  bool            `json:"available"`
	Note       string          `json:"note,omitempty"`
	At         int64           `json:"at"`
}

ConnectorList is the connectors screen.

type ConnectorProposal

type ConnectorProposal struct {
	Connector string `json:"connector"`
	// Title is what the user is shown: "Prusa 3D printer".
	Title  string `json:"title,omitempty"`
	Access string `json:"access"`

	// Evidence is the counted sentence — "You have mentioned your Prusa four
	// times this week." — and Opens is what granting it would let the agent do
	// that it cannot now, in the connector's own words. Line is the two of them
	// as §4b writes it, which is what the glasses would speak.
	//
	// All three are built from counts and from the connector's own descriptor.
	// None of them quotes anything the user said: the proposer discards the
	// text once it has matched, so there is no path from an utterance into this
	// struct.
	Evidence string   `json:"evidence"`
	Opens    string   `json:"opens"`
	Line     string   `json:"line"`
	Scopes   []string `json:"scopes,omitempty"`

	// Episodes is separate conversations and Mentions is total sentences. Both,
	// because they are different claims and the sentence above counts episodes.
	Episodes int   `json:"episodes,omitempty"`
	Mentions int   `json:"mentions,omitempty"`
	FirstAt  int64 `json:"first_at,omitempty"`
	LastAt   int64 `json:"last_at,omitempty"`
}

ConnectorProposal is ORCHESTRATOR.md §4b's evidence-grounded suggestion.

You have mentioned your Prusa four times this week. Want me to connect it?
I could queue prints and tell you when they finish.

One shape for both surfaces: this is the console row on the connectors screen and the payload of the TypeConnectorProposal frame the phone receives. DASHBOARD.md §5's "one API behind both deployments" is only true if the two carry the same thing, and a second near-identical struct for the console is how that stops being true.

Access is always "read". §4b rule 2 makes the write half a second decision, so there is no field here for it to arrive in.

type ConnectorProposalList

type ConnectorProposalList struct {
	Proposals []ConnectorProposal `json:"proposals"`
	// Available is false when nothing on this machine can propose anything —
	// no connectors are configured, or the daemon has no proposer. Note then
	// says which, because an empty list with no explanation reads as "you have
	// nothing to connect" when it means "Relay cannot tell".
	Available bool   `json:"available"`
	Note      string `json:"note,omitempty"`
	At        int64  `json:"at"`
}

ConnectorProposalList is the proposals half of the connectors screen.

type ConnectorProposals

type ConnectorProposals interface {
	Proposals(ctx context.Context) ([]ConnectorProposal, error)
	// Accept grants the READ half and nothing else. The implementation, not the
	// caller, chooses the half — see [Server.handleAcceptConnectorProposal].
	Accept(ctx context.Context, connector, by string) (ConnectorGrantResult, error)
	Dismiss(ctx context.Context, connector, reason string) error
}

ConnectorProposals is ORCHESTRATOR.md §4b's suggestion queue.

It is NOT ProposalStore, which is MEMORY.md §6's credential queue. The two read alike and are entirely different things: that one asks whether a key found in a transcript should be saved, this one asks whether a service the user keeps talking about should be connected.

Three methods and no fourth, and the missing one is the point. There is no "propose" here: a proposal comes from observed evidence inside the daemon, and an HTTP surface that could manufacture one would make §4b's "grounded in something observed, not guessed" a convention rather than a mechanism.

type ConnectorRevoker

type ConnectorRevoker interface {
	Revoke(ctx context.Context, connector string) (RevokeResult, error)
}

ConnectorRevoker turns a connector off across all five runtimes.

It returns what it reached rather than an error alone, because ORCHESTRATOR.md §4b's catch is that agents differ in how they re-read tool lists: a running session may not notice, and the orchestrator has to say which it did rather than leaving the user wondering why the thing they just revoked is still there.

type ConnectorView

type ConnectorView struct {
	ID        string `json:"id"`
	Connector string `json:"connector"`

	// Scopes are the raw scope strings and Opens is ORCHESTRATOR.md §4b's "what
	// it opens" — scope in the user's words, plus what it lets the agent do that
	// it could not before. A reason that restates the permission is not a reason.
	Scopes []string `json:"scopes"`
	Opens  []string `json:"opens"`

	GrantedAt  int64 `json:"granted_at"`
	LastUsedAt int64 `json:"last_used_at,omitempty"`
	// LastUsedFor is the most recent tool call attributable to this connector.
	// Derived by matching tool names against the connector, which is a heuristic
	// and is labelled as one rather than presented as an audit trail.
	LastUsedFor string `json:"last_used_for,omitempty"`

	Revoked   bool  `json:"revoked"`
	RevokedAt int64 `json:"revoked_at,omitempty"`

	// Unused is access nobody has touched. DASHBOARD.md §3.4: that is the kind
	// that gets forgotten and then exploited, so the row says so itself rather
	// than leaving the user to compare dates.
	Unused     bool   `json:"unused"`
	UnusedFor  string `json:"unused_for,omitempty"`
	UnusedDays int    `json:"unused_days,omitempty"`

	// Runtimes are the runtimes that can currently reach this connector, from
	// the MCP union.
	Runtimes []string `json:"runtimes,omitempty"`
}

ConnectorView is one connector on the revocation screen.

type ConsentDecision

type ConsentDecision struct {
	ActionID  string `json:"action_id"`
	Approved  bool   `json:"approved"`
	Option    string `json:"option,omitempty"`
	Interrupt bool   `json:"interrupt,omitempty"`
	Message   string `json:"message,omitempty"`
}

ConsentDecision answers a confirm.request. ActionID is the ping id, which is stable across the two-minute re-ping so an answer to either lands on the same question.

type ConsoleEvent

type ConsoleEvent struct {
	// Kind is the SSE event name: credential | connector | fact | audit | probe.
	Kind string `json:"kind"`
	// Action is what happened, in the audit log's vocabulary where there is one.
	Action string `json:"action,omitempty"`
	ID     string `json:"id,omitempty"`
	// Outcome distinguishes "this landed" from "this was refused", so an
	// optimistic row can be rolled back rather than left looking applied.
	Outcome string `json:"outcome,omitempty"`
	Reason  string `json:"reason,omitempty"`
	At      int64  `json:"at"`
}

ConsoleEvent is a change to something the console holds a list of.

DASHBOARD.md §5 wants optimistic UI on the credential flows: the row appears the instant you press the button and reconciles against what actually landed. That needs a channel that says what landed, and §7.1 already settled the channel — SSE, one direction, no subscription to get wrong.

The payload is deliberately thin. A credential event carries an id and never a credential; the console re-reads the list, which is the same listing every other reader gets and therefore the same one that cannot contain a secret.

type CredentialList

type CredentialList struct {
	Credentials []CredentialView `json:"credentials"`
	// Vault says where the secrets live and whether the keychain was available,
	// so the console can show honest degradation rather than implying protection
	// that is not there.
	Vault VaultStatus `json:"vault"`
	// Available is false when no vault is wired. The screen still renders — an
	// empty list with a reason beats a 404.
	Available bool   `json:"available"`
	Note      string `json:"note,omitempty"`
	At        int64  `json:"at"`
}

CredentialList is the credential screen.

type CredentialStore

type CredentialStore interface {
	Put(ctx context.Context, in vault.Input) (vault.Entry, error)
	Get(ctx context.Context, id string) (vault.Entry, error)
	List(ctx context.Context) ([]vault.Entry, error)
	RecordValidation(ctx context.Context, id, reason string, at time.Time) error
	Revoke(ctx context.Context, id string) error
	Status() vault.Status
}

CredentialStore is the vault as the console is allowed to see it: everything except the one method that returns a plaintext secret.

vault.Vault satisfies this. The narrowing is the mechanism — an interface with no Reveal cannot be talked into revealing.

type CredentialValidator

type CredentialValidator interface {
	Validate(ctx context.Context, e vault.Entry) (Validation, error)
}

CredentialValidator makes MEMORY.md §6's one real call.

It takes an entry rather than a secret, and resolves the secret itself out of the vault it owns, so that even the validation path does not hand plaintext through this package.

type CredentialView

type CredentialView struct {
	ID      string `json:"id"`
	Service string `json:"service"`
	Label   string `json:"label,omitempty"`

	// LastFour is the display form, and the only form. It is empty for secrets
	// short enough that four characters would be most of them.
	LastFour string `json:"last_four"`

	// Backend is where the secret material actually is — the OS keychain, or
	// AES-GCM in the vault database. DASHBOARD.md §3.5 says it out loud rather
	// than implying a keychain that is not there.
	Backend string `json:"backend"`

	// Where it came from (MEMORY.md §6's three ways a key arrives).
	Source        string `json:"source"`
	SourceRuntime string `json:"source_runtime,omitempty"`
	SourceSession string `json:"source_session,omitempty"`
	SourcePath    string `json:"source_path,omitempty"`
	SourceAt      int64  `json:"source_at,omitempty"`
	// SharedSession marks a key found in a session that had another participant.
	// A key in your transcript may not be yours.
	SharedSession bool `json:"shared_session,omitempty"`

	CreatedAt int64 `json:"created_at"`
	// When it was last used, and by which runtime. DASHBOARD.md §3.4: access
	// nobody has touched in a month is the kind that gets forgotten and then
	// exploited.
	LastUsedAt int64  `json:"last_used_at,omitempty"`
	LastUsedBy string `json:"last_used_by,omitempty"`

	LastValidatedAt int64  `json:"last_validated_at,omitempty"`
	Validation      string `json:"validation,omitempty"`

	Revoked   bool  `json:"revoked"`
	RevokedAt int64 `json:"revoked_at,omitempty"`
}

CredentialView is one row of the credential screen.

There is no secret here and there is nowhere to put one. The test in this package that builds a view from an entry whose secret is known, marshals it, and searches the JSON for that secret is what fails if that ever changes.

type DeviceHandler

type DeviceHandler interface {
	Touch(ctx context.Context, t Touch) error
	Wear(ctx context.Context, w Wear) error
}

DeviceHandler receives the glasses' non-speech inputs.

type Digest

type Digest struct {
	Notes       []string `json:"notes"`
	Commitments []string `json:"commitments"`
	Decisions   []string `json:"decisions"`
}

Digest is the daily summary.

type EmbeddingSetup

type EmbeddingSetup struct {
	Provider string `json:"provider,omitempty"`
	Model    string `json:"model,omitempty"`
	Dims     int    `json:"dims,omitempty"`
	Local    bool   `json:"local"`
}

EmbeddingSetup is the third peer. Local by default, and the console says so, because on the self-hosted tier that is the whole argument.

type Envelope

type Envelope struct {
	V       int             `json:"v"`
	ID      string          `json:"id"`
	Type    string          `json:"type"`
	At      int64           `json:"at"` // unix milliseconds
	Payload json.RawMessage `json:"payload,omitempty"`
}

Envelope is SYSTEM.md §6.1's frame, in both directions.

func Decode

func Decode(b []byte) (Envelope, error)

Decode parses a frame and checks the envelope contract.

func Frame

func Frame(id, typ string, at time.Time, payload any) (Envelope, error)

Frame builds an outbound envelope.

type ErrorPayload

type ErrorPayload struct {
	Re      string `json:"re,omitempty"`
	Code    string `json:"code"`
	Message string `json:"message"`
	// Milestone names where the unimplemented half lives, so "not implemented"
	// is a schedule rather than a shrug.
	Milestone string `json:"milestone,omitempty"`
}

ErrorPayload says a frame did not land, and why.

type FactCounts

type FactCounts struct {
	Live       int `json:"live"`
	Superseded int `json:"superseded"`
	// NoEvidence is the number that cannot point at where they came from.
	// MEMORY.md §5 says those are deleted rather than kept, so anything above
	// zero is a bug in the extractor and the console should say so.
	NoEvidence int `json:"no_evidence"`
}

FactCounts summarises the tier.

type FactEvidence

type FactEvidence struct {
	Runtime    string `json:"runtime"`
	Session    string `json:"session"`
	Path       string `json:"path,omitempty"`
	ByteOffset int64  `json:"byte_offset,omitempty"`
	Quote      string `json:"quote,omitempty"`
	At         int64  `json:"at"`
}

FactEvidence is a pointer into a transcript, the same shape as the index's: where it came from, never a copy of it.

type FactList

type FactList struct {
	Facts []FactView `json:"facts"`
	// Superseded is the history half, returned only when asked for, because
	// DASHBOARD.md §3.3 puts it under a toggle.
	Superseded []FactView `json:"superseded,omitempty"`
	Counts     FactCounts `json:"counts"`
	// Available is false when there is no store to read. The screen still
	// renders — an empty list beats a 404 the console has to special-case.
	Available bool   `json:"available"`
	Note      string `json:"note,omitempty"`
	At        int64  `json:"at"`
}

FactList is the facts screen.

type FactView

type FactView struct {
	ID        string `json:"id"`
	Subject   string `json:"subject"`
	Predicate string `json:"predicate"`
	Object    string `json:"object"`
	// Text is the sentence a human reads, and the field the edit form writes.
	Text       string  `json:"text"`
	Confidence float64 `json:"confidence"`

	FirstSeen int64 `json:"first_seen"`
	// LastSeen is what decay runs on, not creation: a long-held habit that still
	// shows up stays strong.
	LastSeen int64 `json:"last_seen"`

	SupersededBy string `json:"superseded_by,omitempty"`
	SupersededAt int64  `json:"superseded_at,omitempty"`
	Superseded   bool   `json:"superseded"`
	// Edited marks a fact a human corrected. The console shows it, because an
	// edited fact should not be quietly re-derived over the top.
	EditedAt int64 `json:"edited_at,omitempty"`

	Evidence []FactEvidence `json:"evidence"`
}

FactView is one inferred fact.

type Health

type Health struct {
	OK        bool                `json:"ok"`
	Version   string              `json:"version,omitempty"`
	StartedAt int64               `json:"started_at"`
	UptimeSec int64               `json:"uptime_sec"`
	Listen    string              `json:"listen"`
	LAN       bool                `json:"lan"`
	Sessions  SessionCounts       `json:"sessions"`
	Runtimes  []RuntimeState      `json:"runtimes"`
	Bus       BusStats            `json:"bus"`
	Pings     PingStats           `json:"pings"`
	Clients   int                 `json:"clients"`
	Incidents []registry.Incident `json:"incidents"`

	// Vault says where the secrets actually live and whether the OS keychain was
	// available, so the console shows honest degradation rather than implying
	// protection that is not there. Nil when no vault is open.
	Vault *VaultStatus `json:"vault,omitempty"`

	// Audit is DASHBOARD.md §4's trail, and specifically whether it is durable.
	Audit AuditHealth `json:"audit"`

	// Subsystems is what this daemon actually wired, keyed by name, valued
	// either "on" or the reason it is not.
	//
	// It exists because the failure this codebase keeps producing is a
	// subsystem that is fully built, fully tested and constructed by nothing:
	// the suite stays green and the product does nothing, and there is no
	// symptom until somebody notices the feature never happens. A daemon that
	// reports what it wired turns that into a line on the health screen —
	// "compaction: no work model configured" is a fact a user can act on, and
	// its absence is a bug a test can catch.
	Subsystems map[string]string `json:"subsystems,omitempty"`

	// Setup is the installer's choices: the two orchestrator models, the voice,
	// the embedder. References, never secrets.
	Setup *Setup `json:"setup,omitempty"`

	// Probe is the last credential probe, cached so the health page already says
	// which credential is stale rather than the button being the only way to
	// find out. Nil until something has probed.
	Probe *ProbeReport `json:"probe,omitempty"`

	// Machine is cloud-only: uptime, disk, last backup.
	Machine *HostHealth `json:"machine,omitempty"`

	// Note carries anything that made this report less complete than it looks —
	// chiefly "no detection pass has run", which is why every runtime says
	// detected:false.
	Note string `json:"note,omitempty"`
}

Health is what DASHBOARD.md §3.5 renders: which runtimes are installed and running, what is degraded, and what has recently gone wrong.

"My glasses stopped talking" is almost always an expired credential or a runtime that will not start, and the fastest support answer is a page that already says which one.

type HostHealth

type HostHealth struct {
	UptimeSec  int64  `json:"uptime_sec,omitempty"`
	DiskFree   int64  `json:"disk_free_bytes,omitempty"`
	DiskTotal  int64  `json:"disk_total_bytes,omitempty"`
	LastBackup int64  `json:"last_backup,omitempty"`
	Note       string `json:"note,omitempty"`
}

HostHealth is the cloud box.

type HostHealthSource

type HostHealthSource func(ctx context.Context) (HostHealth, error)

HostHealthSource is cloud-only machine health. DASHBOARD.md §3.5: "Cloud tier adds machine health: uptime, disk, last backup."

type Identity

type Identity struct {
	// Kind is token | account | proxy — the door, not the person. SYSTEM.md §5
	// has no user table: one box, one person.
	Kind string
	// Subject is the account on the cloud tier and "local" behind the token.
	Subject string
	Scopes  []Scope

	// AuthAt is when this identity last proved itself. For a cloud session that
	// is when the account last authenticated, and DASHBOARD.md §4 requires every
	// vault write to be re-authenticated regardless of session age.
	AuthAt time.Time

	// PerRequest means the credential was presented with *this* request, so the
	// identity is fresh by construction and the re-authentication window does
	// not apply. The printed token is the case: it is sent on every call, so
	// there is no session to age. A cookie or a bearer session token is not,
	// however recently it was issued — that is the distinction the field exists
	// to make explicit rather than inferring it from Kind.
	PerRequest bool

	// Cloud marks the hosted deployment, which is what turns on session expiry,
	// vault re-authentication and the billing route.
	Cloud bool

	// Assurance is how strongly this identity was proved: [AssuranceSecondFactor]
	// when a second factor was presented, [AssuranceSingleFactor] when only a
	// password was.
	//
	// CONTROL-PLANE.md §3 item 5 requires the strong one for vault writes, and it
	// is a separate fact from AuthAt on purpose: a password typed one second ago
	// is fresh and is still one factor. Empty on a per-request credential, where
	// the question does not arise — see the guard.
	Assurance string

	// From is the origin as the server saw it, and Agent the trimmed
	// user-agent. Both end up in the audit log's "from where".
	From  string
	Agent string
}

Identity is who is making a request, after authentication.

func IdentityFrom

func IdentityFrom(ctx context.Context) (Identity, bool)

IdentityFrom returns the authenticated identity for a request.

func (Identity) Actor

func (i Identity) Actor() audit.Actor

Actor is this identity as the audit log records it.

func (Identity) Can

func (i Identity) Can(s Scope) bool

Can reports whether this identity holds a scope.

type KeySource

type KeySource interface {
	Key(kid string) (crypto.PublicKey, error)
}

KeySource returns the public key a token was signed with.

An interface because the production implementation fetches and caches Supabase's JWKS over the network, and every test in this file needs to mint tokens against a key it holds. It takes the `kid` because a project rotates keys and both are live during a rotation.

type KeySourceFunc

type KeySourceFunc func(kid string) (crypto.PublicKey, error)

KeySourceFunc adapts a function to KeySource.

func (KeySourceFunc) Key

func (f KeySourceFunc) Key(kid string) (crypto.PublicKey, error)

Key implements KeySource.

type MCPOriginRow

type MCPOriginRow struct {
	Runtime  string `json:"runtime"`
	Origin   string `json:"origin,omitempty"`
	File     string `json:"file,omitempty"`
	Readable bool   `json:"readable"`
	Reason   string `json:"reason,omitempty"`
	Servers  int    `json:"servers"`
}

MCPOriginRow is one runtime's source.

type MCPServerRow

type MCPServerRow struct {
	Name      string   `json:"name"`
	Display   string   `json:"display"`
	Transport string   `json:"transport,omitempty"`
	URL       string   `json:"url,omitempty"`
	Runtimes  []string `json:"runtimes"`
	// Names is what each runtime calls it. The same server is frequently named
	// three different things and the console has to show all three, or the user
	// cannot tell which row is theirs.
	Names  map[string]string `json:"names,omitempty"`
	Shared bool              `json:"shared"`
}

MCPServerRow is one server in the union.

type MCPSource

type MCPSource func(ctx context.Context) (detect.MCPInventory, error)

MCPSource returns MEMORY.md §7's reconciled union.

A function, not a value, because reading it shells out to five runtimes and must not happen on every request. The caller decides how often to refresh and what to cache.

type MCPView

type MCPView struct {
	// Headline is §7 step 3, verbatim in shape: "you have 7 MCP servers across
	// 3 tools. Manage them in one place?"
	Headline string         `json:"headline"`
	Servers  []MCPServerRow `json:"servers"`
	// Origins says where each runtime's list came from, and Unreadable is the
	// distinction §7 insists on: "no MCP servers" and "we could not read this
	// runtime's config" lead to opposite decisions and only one is recoverable.
	Origins    []MCPOriginRow `json:"origins"`
	Unreadable []string       `json:"unreadable,omitempty"`
	// Probed is false when no reconciliation has run. The screen then says so
	// rather than rendering an empty registry as "you have none".
	Probed bool `json:"probed"`
}

MCPView is MEMORY.md §7's union, on the wire.

type MachineSource

type MachineSource func(ctx context.Context) (detect.Report, error)

MachineSource is the detection pass behind "installed, authenticated and running". It is a function for the same reason MCPSource is: detection shells out to five runtimes and walks their stores, and must not happen on every page load.

type ModelProbe

type ModelProbe struct {
	// Role is "small" or "big" — the voice and the work (ORCHESTRATOR.md §3b).
	Role   string `json:"role"`
	Vendor string `json:"vendor,omitempty"`
	Model  string `json:"model,omitempty"`
	// Reason is llm.Reason: ok | missing_credential | expired | unresolved_ref |
	// unavailable. "Expired" is reserved for 401/403/402 — calling a 404 expired
	// would send somebody to rotate a working key.
	Reason string `json:"reason"`
	// Detail is the provider's own error, verbatim. Empirical beats maintained.
	Detail    string `json:"detail,omitempty"`
	LatencyMS int64  `json:"latency_ms,omitempty"`
	Ref       string `json:"ref,omitempty"`
	At        int64  `json:"at"`
	OK        bool   `json:"ok"`
}

ModelProbe is one orchestrator model's answer.

type ModelSetup

type ModelSetup struct {
	Vendor string `json:"vendor,omitempty"`
	Model  string `json:"model,omitempty"`
	API    string `json:"api,omitempty"`
	// Credential is the reference — "env:OPENROUTER_API_KEY", "vault:<id>" —
	// never the secret behind it.
	Credential string `json:"credential,omitempty"`
}

ModelSetup is one configured model.

type Notify

type Notify struct {
	Title    string   `json:"title"`
	Body     string   `json:"body"`
	Sessions []string `json:"sessions,omitempty"`
	Silent   bool     `json:"silent,omitempty"`
	Ping     string   `json:"ping,omitempty"`
}

Notify is a phone notification. Silent is quiet hours: present, soundless.

type Options

type Options struct {
	Registry *registry.Registry
	Pinger   *bus.Pinger
	Gate     *bus.SpeechGate

	// DB is the main database, for the index tier the console reads: historical
	// sessions, facts, connector grants, secret markers. Nil serves those
	// screens empty rather than 404 — DASHBOARD.md's screens have to render on a
	// box where backfill has not run.
	DB *store.DB

	// Token authenticates every request. Empty generates one, which is what
	// relayd prints on start — the same pattern as the pairing code.
	Token string

	// Authenticator overrides the token check. This is the seam Relay Cloud
	// uses: it authenticates accounts and hands on an [Identity], and every
	// authorization decision still happens in this package. Nil uses the
	// printed token.
	Authenticator Authenticator

	// VaultReauth is DASHBOARD.md §4's cloud rule — every vault write
	// re-authenticated regardless of session age. Zero switches the check off,
	// which is correct for the token authenticator: the token is presented on
	// every request, so it is always fresh.
	VaultReauth time.Duration

	// TrustForwardedFor reads X-Forwarded-For for the audit log's "from where".
	// Off by default: on a loopback bind anyone who can reach the port can set
	// that header, and a log that records an attacker's chosen address is worse
	// than one that records none.
	TrustForwardedFor bool

	// Cloud marks the hosted deployment. It turns on the billing route and
	// nothing else — the screens are identical by design (DASHBOARD.md §2).
	Cloud bool

	// Listen is the bind address, for the health endpoint to report and for
	// [CheckBind] to refuse.
	Listen string

	// LAN is the deliberate flag that allows a non-loopback bind. DASHBOARD.md
	// §4: "Exposing it on a LAN is a deliberate flag with a warning, not a
	// config default someone flips without reading."
	LAN bool

	// Audit records every credential and connector mutation. Nil gets an
	// in-memory log rather than none, because a mutation path with nowhere to
	// record it is refused, and refusing every credential write on a box with no
	// writable data directory would be worse than saying the trail is not
	// durable. Health reports which it is.
	Audit audit.Log

	// Credentials is the vault, minus any path to a plaintext secret. See
	// [CredentialStore]: the narrowing is the mechanism, not a convention.
	Credentials CredentialStore
	// Validator makes MEMORY.md §6's one real call. Nil reports "not validated
	// here" rather than pretending.
	Validator CredentialValidator
	// Proposals is MEMORY.md §6's queue of "I found what looks like a Twilio
	// token". Nil falls back to the index's secret markers for the listing.
	Proposals ProposalStore

	// Connectors revokes a grant across all five runtimes (ORCHESTRATOR.md §4b).
	// Nil records the grant as revoked and says plainly which runtimes were not
	// reached, rather than claiming a revoke that did not happen.
	Connectors ConnectorRevoker
	// ConnectorProposals is §4b's evidence-grounded suggestion queue — a
	// different thing from [Options.Proposals], which is MEMORY.md §6's
	// credential queue. Nil serves the list with Available false and a sentence
	// saying nothing on this machine can propose anything.
	ConnectorProposals ConnectorProposals
	// MCP is MEMORY.md §7's reconciled union. It is a function rather than a
	// value because detection shells out to five runtimes and must not run on
	// every request.
	MCP MCPSource

	// Gateway is the shared MCP tool bus. Nil leaves /mcp/ unmounted, which is
	// what a daemon with no gateway should do rather than answering 404 from a
	// path five runtimes have been told is real.
	Gateway http.Handler

	// Machine is the detection pass behind DASHBOARD.md §3.5's "installed" and
	// "running".
	Machine MachineSource
	// RuntimeAuth answers §3.5's "authenticated". Nil leaves every runtime's
	// login state reported as unknown, which is the honest answer until
	// something can observe it.
	RuntimeAuth RuntimeAuthSource
	// Prober is the re-probe button. Nil answers 503 with the reason.
	Prober Prober
	// Setup is the configured voice, orchestrator models and embedding, for the
	// health screen to name.
	Setup *Setup
	// Host reports cloud machine health: uptime, disk, last backup.
	Host HostHealthSource

	// Billing returns a Stripe customer portal URL. Cloud only, one endpoint,
	// and nothing about billing is rebuilt here (DASHBOARD.md §3.6).
	Billing BillingPortal

	Utterances UtteranceHandler
	Devices    DeviceHandler

	Version   string
	StartedAt time.Time
	Now       func() time.Time
	NewID     func() string
	Log       *slog.Logger
}

Options configures the server.

type Photo

type Photo struct {
	ID   string `json:"id"`
	MIME string `json:"mime"`
	Data []byte `json:"data,omitempty"`
	// Stored means the photo is still on the glasses. SYSTEM.md §3: photos stay
	// on the device and transfer on demand, not on capture.
	Stored bool `json:"stored"`
}

Photo is an image from the glasses. M4.

type Ping

type Ping struct {
	Ping    bus.Ping
	Confirm *ConfirmRequest
	Speak   *Speak
	Notify  *Notify
	// Render is a mini-app's view. It rides this topic because the topic is the
	// fan-out to every transport, not because a view is a ping: [Ping.Ping] is
	// zero for one, it is never batched, and it is never held for quiet hours.
	// An app draws in reply to something the user just did, and holding that for
	// a gap in the conversation would be the turn-taking policy applied to
	// something it was not written about.
	Render *UIRender
	// Resolved is set instead of the rest when a ping is retracted.
	Resolved *ConfirmResolved
}

Ping is a delivered ping in the shape the transports send it.

type PingStats

type PingStats struct {
	Blocking      uint64 `json:"blocking"`
	Informational uint64 `json:"informational"`
	Repings       uint64 `json:"repings"`
	Retracted     uint64 `json:"retracted"`
	Withdrawn     uint64 `json:"withdrawn"`
	Batched       uint64 `json:"batched"`
	Failed        uint64 `json:"failed"`
}

PingStats mirrors bus.PingStats over the wire.

type ProbeReport

type ProbeReport struct {
	At     int64        `json:"at"`
	Models []ModelProbe `json:"models"`
	Voice  []VoiceProbe `json:"voice"`
	// OK is false when anything the orchestrator needs is not working. It is the
	// one field a support answer starts from.
	OK bool `json:"ok"`
}

ProbeReport is Probes on the wire.

type Prober

type Prober interface {
	Probe(ctx context.Context) Probes
}

Prober re-runs ORCHESTRATOR.md §2's credential probes: one real call each.

type ProberFunc

type ProberFunc func(ctx context.Context) Probes

ProberFunc adapts a function to Prober.

func (ProberFunc) Probe

func (f ProberFunc) Probe(ctx context.Context) Probes

Probe implements Prober.

type Probes

type Probes struct {
	Models map[string]llm.ProbeResult
	Voice  []voice.Check
}

Probes is one pass over every credential the orchestrator needs.

The two maps are the packages' own result types rather than a re-declaration, so a reason code cannot drift between the installer's output and the console's: llm.Pair.Probe returns exactly this map, and voice.ProbePlan exactly this slice.

type Proposal

type Proposal struct {
	ID      string `json:"id"`
	Service string `json:"service"`
	// Detector names the rule and its tier. MEMORY.md §12.2 measured a 26%
	// false-positive rate on tier 2, so the console shows which tier found it
	// rather than presenting every hit as equally likely.
	Detector string `json:"detector,omitempty"`

	Runtime    string `json:"runtime,omitempty"`
	Session    string `json:"session,omitempty"`
	Path       string `json:"path,omitempty"`
	ByteOffset int64  `json:"byte_offset,omitempty"`

	// LastFour is as much of the candidate as anything ever shows.
	LastFour string `json:"last_four,omitempty"`
	// SharedSession says the session had another participant, so the key may not
	// be the user's to keep. The proposal has to say so.
	SharedSession bool  `json:"shared_session,omitempty"`
	FoundAt       int64 `json:"found_at,omitempty"`
}

Proposal is MEMORY.md §6's "I found what looks like a Twilio auth token in a session from March. Save it as your Twilio credential?"

It lives on the credential screen because that flow needs somewhere to be accepted or dismissed that is not a voice prompt at 2 a.m. (DASHBOARD.md §3.2).

type ProposalStore

type ProposalStore interface {
	List(ctx context.Context) ([]Proposal, error)
	Accept(ctx context.Context, id, label string) (vault.Entry, error)
	Dismiss(ctx context.Context, id, reason string) error
}

ProposalStore is the queue behind the proposal list. Accepting one has to re-read the transcript at its byte offset to recover the candidate, which is the index's job and not this package's.

type RevokeResult

type RevokeResult struct {
	// Runtimes is the per-runtime outcome, in adapter.Runtimes() order.
	Runtimes []RuntimeRevoke `json:"runtimes"`
	// Sessions is the live sessions that were restarted or re-announced, because
	// a grant change mid-session may not reach one that already enumerated its
	// tools.
	Sessions []string `json:"sessions,omitempty"`
	Note     string   `json:"note,omitempty"`
}

RevokeResult is what a revoke actually reached.

type RuntimeAuth

type RuntimeAuth struct {
	OK        bool   `json:"ok"`
	Note      string `json:"note,omitempty"`
	CheckedAt int64  `json:"checked_at,omitempty"`
}

RuntimeAuth is whether one runtime can currently reach its provider.

It is a separate seam from MachineSource because it is a different kind of question: detection reads the filesystem, and this needs a real call or a token inspection per runtime. Nothing implements it yet, and until something does the console shows "unknown" rather than a guess.

type RuntimeAuthSource

type RuntimeAuthSource func(ctx context.Context) (map[string]RuntimeAuth, error)

RuntimeAuthSource reports the login state of each runtime, keyed by runtime id. A runtime absent from the map stays unknown.

type RuntimeRevoke

type RuntimeRevoke struct {
	Runtime string `json:"runtime"`
	// Reached is false when this runtime's config could not be written. A revoke
	// that silently missed a runtime is worse than one that failed loudly.
	Reached bool   `json:"reached"`
	Reason  string `json:"reason,omitempty"`
}

RuntimeRevoke is one runtime's answer.

type RuntimeState

type RuntimeState struct {
	Runtime  string `json:"runtime"`
	Protocol string `json:"protocol"`
	// Adapter is whether relayd can drive this runtime at all right now.
	Adapter bool `json:"adapter"`
	// Missing is what this runtime cannot be observed to do. It is rendered as
	// fact, not as a bug: ACP has no cost field anywhere in its protocol, and a
	// console that shows a zero there is lying.
	Missing         []string          `json:"missing,omitempty"`
	CapabilityNotes map[string]string `json:"capability_notes,omitempty"`
	Sessions        int               `json:"sessions"`

	// Model is the model this runtime last actually ran on. There is no
	// per-runtime model setting — a session is started with one — so the honest
	// answer is the most recent, and empty where it has never run.
	Model string `json:"model,omitempty"`

	// Detected is false when no detection pass has run. Everything below it is
	// then unknown rather than false, which is a distinction MEMORY.md §1 is
	// emphatic about: "installed but never used" and "not installed" are both
	// normal, and "we could not tell" is neither.
	Detected bool `json:"detected"`
	// Installed is a binary on PATH; Status is absent | never_run | in_use |
	// history_only, and StatusLine is the clause the console prints.
	Installed  bool   `json:"installed"`
	Status     string `json:"status,omitempty"`
	StatusLine string `json:"status_line,omitempty"`

	Version     string `json:"version,omitempty"`
	VersionNote string `json:"version_note,omitempty"`
	BinaryPath  string `json:"binary_path,omitempty"`

	StateDir string `json:"state_dir,omitempty"`
	// StateDirSource is env | asked | config | profile | default, and Trusted is
	// whether that counts as authoritative. Never hardcode ~/.openclaw: a reader
	// that assumes the default silently reports an empty history as success.
	StateDirSource  string `json:"state_dir_source,omitempty"`
	StateDirTrusted bool   `json:"state_dir_trusted,omitempty"`

	// Authenticated is DASHBOARD.md §3.5's third word, and it is a tri-state on
	// purpose. Nothing in internal/detect observes whether a runtime is logged
	// in — the five keep their credentials in five different places and three of
	// them are OAuth — so nil means *unknown* and AuthNote says why. A false here
	// would be a claim, and "your Claude Code is logged out" is exactly the kind
	// of claim that sends somebody to re-run a login that was fine.
	Authenticated *bool  `json:"authenticated"`
	AuthNote      string `json:"auth_note,omitempty"`

	// Running is how many processes on this machine look like this runtime.
	Running int `json:"running"`
	// Stored is the session count in the runtime's own store, nil where nobody
	// counted. Rendering "0 sessions" for a store we never opened is the same
	// class of lie as an adapter emitting an event it did not see.
	Stored     *int   `json:"stored_sessions"`
	StoreBytes *int64 `json:"store_bytes,omitempty"`

	// Notes are everything detection had to derive or could not observe.
	Notes []string `json:"notes,omitempty"`
}

RuntimeState is one of the five, on DASHBOARD.md §3.5's screen.

Three sources meet in this struct and the field names keep them apart, because conflating them is how a support page starts lying: what the adapter layer can *drive*, what detection found on *disk*, and what the registry has actually *run*.

type Scope

type Scope string

Scope is what a request is allowed to do. Three, because the console has three levels of consequence and collapsing them would mean a read-only dashboard token could rotate a Stripe key.

const (
	// ScopeRead is every listing and every stream.
	ScopeRead Scope = "read"
	// ScopeWrite drives sessions and edits facts: consequential, reversible,
	// and confined to this machine's own state.
	ScopeWrite Scope = "write"
	// ScopeVault is credential and connector mutation. DASHBOARD.md §4: the
	// console can write to the vault, and that makes it the highest-value target
	// in the system, above the glasses and above relayd's own API.
	ScopeVault Scope = "vault"
)

func AllScopes

func AllScopes() []Scope

AllScopes is what the printed token carries: on the self-hosted tier the person holding it is the person who ran the installer.

type Server

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

Server is relayd's HTTP and WebSocket surface. It is also a bus.Delivery: a ping becomes a confirm.request or a speak on every connected phone.

func New

func New(o Options) (*Server, error)

New builds the server.

func (*Server) Clients

func (s *Server) Clients() int

Clients is how many phone sockets are attached.

func (*Server) Deliver

func (s *Server) Deliver(_ context.Context, p bus.Ping) error

Deliver turns a ping into frames and fans them out to every transport.

Nothing about ADAPTERS.md §7's policy is re-decided here — the Pinger already applied it and this function only renders. Notify is always set because a ping that reaches nobody is not a ping, Speak is set when [speaks] — the voice backend's rule, not the policy's — says this one is said out loud, and Confirm carries the options so the answer can come back by voice.

func (*Server) Draw

func (s *Server) Draw(_ context.Context, r UIRender) error

Draw sends a mini-app's view to the phone.

It returns when the frame has been published, not when a human has looked at it. There is no signal for the latter and inventing one would be an event we cannot observe.

A box with no phone connected is ErrNoScreen rather than a silent success: the app is entitled to know its card went nowhere, because the alternative is an app that reports having shown you something it did not.

func (*Server) DrawAndAsk

func (s *Server) DrawAndAsk(ctx context.Context, r UIRender, deadline time.Time) (bool, error)

DrawAndAsk sends a view containing a question and waits for the answer.

The correlation lives here rather than in the caller because this is where [Server.answer] already looks: the phone replies with the same `consent.decision` frame it uses for a runtime's approval, naming the action id this hands it, and ws.go needs no case for mini-apps at all. An app's question participates in the same bookkeeping as every other question, including being cleared from the pending map when it is answered.

False and nil is "no". False and context.DeadlineExceeded is nobody answering, which the caller converts to a no — this returns the difference because the *transport* did observe it, and flattening it here would leave the audit trail unable to say whether a question was declined or ignored.

func (*Server) Retract

func (s *Server) Retract(_ context.Context, id, reason string) error

Retract withdraws a confirm.request whose question is gone.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, ln net.Listener) error

Serve runs the HTTP server until ctx is cancelled, then shuts it down.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP makes the server an http.Handler.

func (*Server) ServeHTTPSocket

func (s *Server) ServeHTTPSocket(parent context.Context, c *websocket.Conn)

ServeHTTPSocket runs the console's HTTP API over an already-established socket, and returns when the socket ends.

The counterpart to Server.ServeSocket: that one speaks the phone's protocol, this one speaks HTTP. Both exist because a socket does not have to have arrived as an inbound request, and both hand the work to the same code the inbound path uses. The caller owns closing the connection.

func (*Server) ServeRelayedSocket

func (s *Server) ServeRelayedSocket(parent context.Context, c *websocket.Conn)

ServeRelayedSocket runs the phone protocol over a socket that arrived through the rendezvous relay, after authenticating it.

Server.ServeSocket deliberately does no authentication: on the inbound path the router has already done it, and duplicating the check there would be a second implementation to keep in step. A relayed socket has had no such check — the handshake it came from terminated at the relay — so this is where the credential is demanded, and the whole of the difference between the two paths is the frame below.

The BoxID comment in relaylink says anyone who learns a box's id "can open a socket to this daemon, and get exactly as far as a stranger on the LAN — which is nowhere, because the API authenticates." That sentence is only true if this function exists; wiring ServeSocket straight onto the relay would have handed the write scope to anybody who knew a public identifier.

func (*Server) ServeSocket

func (s *Server) ServeSocket(parent context.Context, c *websocket.Conn)

ServeSocket runs the phone protocol over an already-established socket.

Split out of [Server.handleWS] because a socket does not have to have arrived as an inbound request. SYSTEM.md §7's rendezvous relay works by both sides *dialling out*, so when a phone on cellular reaches a machine behind NAT, this daemon is the one that opened the TCP connection — and the protocol it then speaks is identical. Everything below this line is unchanged from the inbound path, which is the property that matters: there is no second implementation to keep in step, and no relay-only branch where an authorization check could be forgotten.

It returns when the socket ends. The caller owns closing it.

func (*Server) SetPinger

func (s *Server) SetPinger(p *bus.Pinger)

func (*Server) SetSubsystem

func (s *Server) SetSubsystem(name, status string)

SetPinger attaches the ping policy after construction.

The two are mutually dependent by design and neither dependency is accidental: the Pinger needs the Server as its bus.Delivery, and the Server needs the Pinger so that answering a question counts as having heard the ping and cancels the two-minute repeat. One of the two edges has to be set after the fact, and this is the one that is optional — a Server with no Pinger still serves, it simply never marks a ping heard. SetSubsystem records that a subsystem was wired, or why it was not.

Called from the composition root as each one is built. status is "on" or a sentence a user would understand.

func (*Server) Speak

func (s *Server) Speak(text string, sessionID string)

Speak pushes a line to every attached phone outside the ping path — the routing announcement, which SYSTEM.md §7b calls the acknowledgement: "adding that to the payments refactor", spoken the moment routing decides.

func (*Server) Token

func (s *Server) Token() string

Token is the bearer token every request needs.

type SessionCommand

type SessionCommand struct {
	// Command is list | send | cancel | steer | close | answer.
	Command string `json:"command"`
	Session string `json:"session,omitempty"`
	Turn    string `json:"turn,omitempty"`
	Text    string `json:"text,omitempty"`

	// Question, Option, Decision and Interrupt answer a blocked session.
	Question  string `json:"question,omitempty"`
	Option    string `json:"option,omitempty"`
	Decision  string `json:"decision,omitempty"` // allow | deny | cancelled
	Interrupt bool   `json:"interrupt,omitempty"`
}

SessionCommand is every session-directed action the phone can take.

type SessionCounts

type SessionCounts struct {
	Total    int `json:"total"`
	Running  int `json:"running"`
	Awaiting int `json:"awaiting"`
	Idle     int `json:"idle"`
	Closed   int `json:"closed"`
	Live     int `json:"live"`
}

SessionCounts is the list, summarised.

type SessionDetail

type SessionDetail struct {
	Session      SessionSummary      `json:"session"`
	Turns        []TurnView          `json:"turns"`
	Tools        []ToolCallView      `json:"tools"`
	Live         bool                `json:"live"`
	Capabilities map[string]string   `json:"capabilities,omitempty"`
	Missing      []string            `json:"missing,omitempty"`
	Questions    []registry.Question `json:"questions,omitempty"`
	// Transcript is a POINTER into the runtime's own file, never a copy
	// (MEMORY.md §3). It is filled in by the index once backfill has run, and it
	// is what GET /v1/sessions/{id}/transcript range-reads. Nil means backfill
	// has not seen this session, which is different from "there is no
	// transcript".
	Transcript *TranscriptRef `json:"transcript,omitempty"`
}

SessionDetail is one session, with what the console shows beside it.

type SessionList

type SessionList struct {
	Sessions []SessionSummary `json:"sessions"`
	At       int64            `json:"at"`
}

SessionList is every session across every runtime.

type SessionQuery

type SessionQuery struct {
	Runtime   string
	State     string
	Workspace string
	// Source is all | live | index. Default all.
	Source string
	// BlockedOnly narrows to sessions waiting on a human.
	BlockedOnly bool
	// Text matches the subject or title, case-insensitively.
	Text  string
	Limit int
}

SessionQuery is the console's filter.

type SessionSummary

type SessionSummary struct {
	ID string `json:"id"`
	// NativeID is the runtime's own id, which is what the index keys on. It is
	// the join between the live tier and the historical one.
	NativeID   string   `json:"native_id,omitempty"`
	Runtime    string   `json:"runtime"`
	Subject    string   `json:"subject"`
	Workspace  string   `json:"workspace"`
	State      string   `json:"state"`
	LastActive int64    `json:"last_active"`
	CreatedAt  int64    `json:"created_at"`
	CostUSD    *float64 `json:"cost_usd"` // nil, never 0, where the runtime cannot report it
	Tokens     *int64   `json:"tokens"`
	// Blocked hoists a session waiting on a human. DASHBOARD.md §3.1 puts these
	// at the top, unmissable, because a blocked session is the one failure mode
	// that silently stops all work.
	Blocked   bool `json:"blocked"`
	Questions int  `json:"questions,omitempty"`
	Live      bool `json:"live"`

	// Source is registry | index | both.
	Source string `json:"source,omitempty"`
	// Title is the runtime's own title where it wrote one (MEMORY.md §4: Claude
	// Code and Hermes both do). Subject is what the user called it.
	Title     string `json:"title,omitempty"`
	Model     string `json:"model,omitempty"`
	Messages  int64  `json:"messages,omitempty"`
	ToolCalls int64  `json:"tool_calls,omitempty"`
	// Transcript is a POINTER into the runtime's own file, never a copy
	// (MEMORY.md §3).
	Transcript *TranscriptRef `json:"transcript,omitempty"`
}

SessionSummary is one row of the list, the same shape the console renders.

type Setup

type Setup struct {
	// Small is ORCHESTRATOR.md §2b's voice model, Big the one that does the work
	// and holds the MCP registry and a shell.
	Small ModelSetup `json:"small"`
	Big   ModelSetup `json:"big"`

	Voice     VoiceSetup     `json:"voice"`
	Embedding EmbeddingSetup `json:"embedding"`
}

Setup is what the installer chose: the two orchestrator models, the voice, the embedder. Names and credential *references* only — never a secret, the same rule config.toml itself follows.

type Speak

type Speak struct {
	Text string `json:"text"`
	// Interrupt is the hard stop: a blocked session may speak over narration.
	Interrupt bool   `json:"interrupt,omitempty"`
	Session   string `json:"session,omitempty"`
	Ping      string `json:"ping,omitempty"`
}

Speak is something to say out loud.

type SupabaseOptions

type SupabaseOptions struct {
	// Issuer is the project's token issuer, `https://<ref>.supabase.co/auth/v1`.
	// Required: a valid signature over the wrong project is still the wrong
	// project.
	Issuer string
	// Audience is Supabase's, which is "authenticated" for a signed-in user.
	// Empty defaults to that.
	Audience string
	// AccountID is the Supabase user id this box belongs to. Required. See the
	// file comment.
	AccountID string
	// Keys resolves signing keys.
	Keys KeySource
	// Leeway forgives clock skew on expiry. Small on purpose: DASHBOARD.md §4
	// says cloud sessions expire and means it.
	Leeway time.Duration
	Now    func() time.Time
}

SupabaseOptions configures the cloud authenticator.

type SyncOffer

type SyncOffer struct {
	Files int   `json:"files"`
	Bytes int64 `json:"bytes"`
	// OnLAN is whether the phone and the machine share a network. If they do
	// not, bulk sync waits rather than silently burning a data plan (§7).
	OnLAN bool `json:"on_lan"`
}

SyncOffer is the phone offering a night's audio. M4.

type ToolCallView

type ToolCallView struct {
	ID         string `json:"id"`
	Tool       string `json:"tool"`
	Target     string `json:"target,omitempty"`
	ArgsDigest string `json:"args_digest,omitempty"`
	At         int64  `json:"at"`
	Status     string `json:"status,omitempty"`
}

ToolCallView is one tool call. ArgsDigest is a digest and never the arguments — tool arguments routinely carry paths, tokens and payloads.

type Touch

type Touch struct {
	Gesture string `json:"gesture"` // tap1 | tap2 | tap3 | long | swipe+ | swipe-
}

Touch is a gesture on the glasses.

type TranscriptChunk

type TranscriptChunk struct {
	Runtime string `json:"runtime"`
	Session string `json:"session"`
	Path    string `json:"path"`

	// Offset is relative to the session's start in the file, not the file's
	// start. Hermes and OpenClaw put many sessions in one store, and an offset
	// that meant "into the file" would have the console paging through other
	// people's conversations to reach this one.
	Offset int64 `json:"offset"`
	Length int64 `json:"length"`
	// Size is the whole file right now, and SessionOffset where this session
	// begins in it. Both are shown because "you are 4 KB into a 2.5 GB store" is
	// the fact that stops somebody expecting a scrollbar.
	Size          int64 `json:"size"`
	SessionOffset int64 `json:"session_offset"`
	// NextOffset is where to ask for the following window, or -1 at the end.
	NextOffset int64 `json:"next_offset"`
	EOF        bool  `json:"eof"`

	Text string `json:"text"`
	// Truncated is set when the window ended mid-rune and the tail byte was
	// dropped rather than emitted as a replacement character.
	Truncated bool `json:"truncated,omitempty"`

	// Markers are the secrets detection found in this session. They are carried
	// with the text because the console is about to display a file that is known
	// to have contained a credential, and MEMORY.md §6's whole ordering argument
	// is that the user should be told before rather than after.
	Markers []TranscriptMarker `json:"markers,omitempty"`
}

TranscriptChunk is one window into a transcript.

type TranscriptMarker

type TranscriptMarker struct {
	ID         string `json:"id"`
	Detector   string `json:"detector"`
	Service    string `json:"service,omitempty"`
	ByteOffset int64  `json:"byte_offset"`
	// Captured is true once the credential was moved into the vault, which is
	// what MEMORY.md §6's proposal flow does on accept.
	Captured bool  `json:"captured"`
	At       int64 `json:"at"`
}

TranscriptMarker is one detection in this session.

type TranscriptRef

type TranscriptRef struct {
	Runtime string `json:"runtime"`
	Session string `json:"session"`
	Path    string `json:"path"`
	// ByteOffset is where this session starts in the file. Hermes and OpenClaw
	// keep every session in one store, so it is frequently not zero.
	ByteOffset int64 `json:"byte_offset"`
	// Size is the file's size as backfill last saw it.
	Size int64 `json:"size,omitempty"`
}

TranscriptRef locates a session's transcript where it already lives.

MEMORY.md §3 keeps the measured 3.6 GB on disk, in place, unmoved: the index holds a pointer, not a copy. So does this — the console opens the file through a bounded range read (GET /v1/sessions/{id}/transcript) rather than ever receiving it whole.

type TurnView

type TurnView struct {
	ID         string   `json:"id"`
	Role       string   `json:"role"`
	Text       string   `json:"text"`
	At         int64    `json:"at"`
	OK         bool     `json:"ok"`
	StopReason string   `json:"stop_reason,omitempty"`
	DurationMS int64    `json:"duration_ms,omitempty"`
	CostUSD    *float64 `json:"cost_usd"`
	Tokens     *int64   `json:"tokens"`
}

TurnView is one exchange.

type UIRender

type UIRender struct {
	// ActionID is set only when an answer is expected, and it is what the phone
	// puts in the consent.decision it sends back — the same field, the same
	// route and the same server bookkeeping a confirm.request uses. An app's
	// question is a question like any other.
	ActionID string `json:"action_id,omitempty"`
	// App and AppName say who drew it. "Which of my apps is asking me this" is
	// the first question a confirmation raises and the view cannot answer it.
	App     string `json:"app"`
	AppName string `json:"appName,omitempty"`
	// Deadline is when the question stops standing, in unix milliseconds. The
	// phone dismisses it rather than leaving a button that no longer does
	// anything — an app that stopped waiting has already treated it as a no.
	Deadline int64           `json:"deadline,omitempty"`
	View     json.RawMessage `json:"view"`
}

UIRender is a mini-app's view on its way to the phone.

ORCHESTRATOR.md §5: app code runs on the server, sandboxed; app UI renders in the phone app through a small declarative vocabulary the host draws natively. This is the frame between those two halves.

View is json.RawMessage on purpose, and it is the boundary that keeps this package honest: `internal/api` is transport and does not know the vocabulary. `internal/apps` owns it, validates every view against it before one gets here, and is the only place the caps and the block kinds live on this side. If this struct grew a `Blocks []Block` field, the vocabulary would be defined in two packages in one binary and the transport would start having opinions about what an app may draw.

type Utterance

type Utterance struct {
	Text       string  `json:"text"`
	Confidence float64 `json:"confidence,omitempty"`
	Source     string  `json:"source,omitempty"` // glasses | phone
	// Final distinguishes a streaming partial from the finished utterance.
	// Streaming ASR is the point (§7b): the prompt is ready the moment they
	// stop, rather than starting a 400 ms job at that point.
	Final bool `json:"final"`
}

Utterance is recognised speech. The glasses have no recogniser (SYSTEM.md §7b) — the phone does the recognition and sends the text.

type UtteranceFunc

type UtteranceFunc func(ctx context.Context, u Utterance) error

UtteranceFunc adapts a function to UtteranceHandler.

func (UtteranceFunc) Utterance

func (f UtteranceFunc) Utterance(ctx context.Context, u Utterance) error

type UtteranceHandler

type UtteranceHandler interface {
	Utterance(ctx context.Context, u Utterance) error
}

UtteranceHandler is where a recognised sentence goes.

Routing is M1 step 3 and lives in internal/routing, deliberately outside this package: the API's job is to carry the utterance, not to decide what it means. Until a router is wired the phone gets an explicit "not implemented" naming the milestone rather than silence, because a device that hears you and says nothing is indistinguishable from a broken one.

type Validation

type Validation struct {
	// Probed is false when nothing was tested. Reporting an untested credential
	// as ok is the same mistake as an adapter emitting an event it did not see.
	Probed bool   `json:"probed"`
	Reason string `json:"reason,omitempty"`
	Detail string `json:"detail,omitempty"`
	At     int64  `json:"at"`
}

Validation is what one real call found out. Reason reuses llm.Reason's vocabulary — ok, missing_credential, expired, unresolved_ref, unavailable — so the console, the installer and the health screen speak one language.

type ValidatorFunc

type ValidatorFunc func(ctx context.Context, e vault.Entry) (Validation, error)

ValidatorFunc adapts a function to CredentialValidator.

func (ValidatorFunc) Validate

func (f ValidatorFunc) Validate(ctx context.Context, e vault.Entry) (Validation, error)

Validate implements CredentialValidator.

type VaultStatus

type VaultStatus struct {
	Backend   string `json:"backend,omitempty"`
	KeySource string `json:"key_source,omitempty"`
	Degraded  bool   `json:"degraded"`
	Reason    string `json:"reason,omitempty"`
}

VaultStatus mirrors vault.Status over the wire.

type VoiceProbe

type VoiceProbe struct {
	Option string `json:"option"`
	Label  string `json:"label,omitempty"`
	// Probed is false where a row cannot be tested from this machine at all —
	// phone-native synthesis happens on the handset. Reporting that as ok would
	// claim a verification that never happened.
	Probed    bool   `json:"probed"`
	Reason    string `json:"reason,omitempty"`
	Detail    string `json:"detail,omitempty"`
	Bytes     int    `json:"bytes,omitempty"`
	LatencyMS int64  `json:"latency_ms,omitempty"`
	At        int64  `json:"at"`
	OK        bool   `json:"ok"`
}

VoiceProbe is one voice option's answer.

type VoiceSetup

type VoiceSetup struct {
	Provider   string `json:"provider,omitempty"`
	Model      string `json:"model,omitempty"`
	Credential string `json:"credential,omitempty"`
	Fallback   string `json:"fallback,omitempty"`
}

VoiceSetup is ORCHESTRATOR.md §2a's choice plus its keyless fallback, which is never empty because "mute out of the box" is the worst possible first hour for a voice product.

type Wear

type Wear struct {
	Worn bool `json:"worn"`
}

Wear is the glasses going on or off a face.

Jump to

Keyboard shortcuts

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