librelay

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package librelay defines the wire contract between a contenox runtime and a relay: the Frame envelope, its NDJSON codec (Reader, Writer), and the relay-level control messages. A runtime dials out and holds the connection; nothing here listens. Control and tunnelled traffic share Frame.Type, the protocol version is negotiated once in Hello / Welcome, and unknown types and fields are never fatal.

Index

Constants

View Source
const (
	NonceSize     = 32
	MaxNonceBytes = 64
)

Nonce sizes. NonceSize is what a connector generates; MaxNonceBytes is what a relay will sign over.

View Source
const (
	MaxFrameBytes = 16 << 20
	MaxTypeBytes  = 128
	MaxIDBytes    = 256
)

Envelope limits bound what a decoder will allocate for a single frame before it gives up.

View Source
const (
	// TypeHello is the connector's opening frame, payload [Hello]; a request answered with TypeWelcome or TypeError.
	TypeHello = ControlPrefix + "hello"
	// TypeWelcome is the relay's answer to TypeHello, payload [Welcome].
	TypeWelcome = ControlPrefix + "welcome"
	// TypeHeartbeat is a liveness probe with no payload, sent by either end; a request answered with TypeAck.
	TypeHeartbeat = ControlPrefix + "heartbeat"
	// TypeAck answers TypeHeartbeat, carrying no payload.
	TypeAck = ControlPrefix + "ack"
	// TypeError reports a failure, payload [Error]; as a response it never induces a response.
	TypeError = ControlPrefix + "error"
)

Control message types.

View Source
const (
	// TypeChainTrigger asks the machine behind [Frame.Instance] to run a named task chain, payload [ChainTrigger]; relay→machine only.
	TypeChainTrigger       = "chain_trigger"
	TypeChainTriggerResult = "chain_trigger_result"
)

Chain-trigger types are cargo, not control traffic.

View Source
const (
	// ChainSessionNew runs the chain in a fresh session.
	ChainSessionNew = "new"
	// ChainSessionReused asks the machine to reuse a session across triggers; unsupported machines refuse.
	ChainSessionReused = "reused"
)

ChainTrigger.SessionMode values.

View Source
const (
	// ChainTriggerStatusOK: the chain ran to completion.
	ChainTriggerStatusOK = "ok"
	// ChainTriggerStatusError: the chain started and failed.
	ChainTriggerStatusError = "error"
	// ChainTriggerStatusRefused: the machine declined before any chain ran.
	ChainTriggerStatusRefused       = "refused"
	ChainTriggerStatusAwaitingHuman = "awaiting_human"
)

ChainTriggerResult.Status values.

View Source
const (
	// TypeAskPublished announces a durable ask the machine has recorded; payload is [AskPublished].
	TypeAskPublished = "ask.published"
	// TypeAskResolved announces that an ask is no longer open; payload is [AskResolved].
	TypeAskResolved = "ask.resolved"
	// TypeAskVerdict delivers one settled verdict to the machine; payload is [AskVerdict].
	TypeAskVerdict = "ask.verdict"
)

Ask types carry a durable approval outward and its verdict back; all three are notifications.

View Source
const (
	// AskResolvedAnswered reports a verdict was recorded against the row.
	AskResolvedAnswered = "answered"
	// AskResolvedExpired reports the row reached its deadline and its on-timeout action applied.
	AskResolvedExpired = "expired"
	// AskResolvedSuperseded reports the row was closed without a verdict, its run having gone.
	AskResolvedSuperseded = "superseded"
)

Reasons carried by AskResolved.Reason.

View Source
const (
	// AskDecisionAllow permits the gated call.
	AskDecisionAllow = "allow"
	// AskDecisionDeny refuses the gated call.
	AskDecisionDeny = "deny"
	// AskDecisionAnswer carries text answering a question rather than gating a call.
	AskDecisionAnswer = "answer"
)

Decisions carried by AskVerdict.Decision.

View Source
const (
	// TypeResume asks a session's producer to continue after a cursor; always a request, answered exactly once.
	TypeResume = "session.resume"
	// TypeResumed answers [TypeResume] and precedes the replayed frames.
	TypeResumed = "session.resumed"
)

Resumption types are not control traffic; see Resume.

View Source
const (
	CodeUnsupportedType = "unsupported_type"
	CodeMalformedFrame  = "malformed_frame"
	CodeUnknownInstance = "unknown_instance"
	CodeUnauthorized    = "unauthorized"
	CodeVersion         = "unsupported_version"
	// CodeCursorEvicted answers a [Resume] whose cursor the producer no longer retains.
	CodeCursorEvicted = "cursor_evicted"
)

Error codes carried by Error.

View Source
const ControlPrefix = "relay."

ControlPrefix marks a type as relay-level control traffic; a relay handles frames with this prefix and forwards everything else.

View Source
const MaxTraceBytes = 128

MaxTraceBytes bounds Frame.Trace. It is far below MaxIDBytes because a trace is not addressable and is only ever spent on a log field.

View Source
const ProtocolVersion = 1

ProtocolVersion is the envelope version this build speaks; exchanged once in Hello/Welcome, never on a frame.

View Source
const SigningDomain = "contenox-relay/welcome/v1"

SigningDomain is the domain-separation tag every Welcome signature starts with.

View Source
const TraceAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

TraceAlphabet is the complete set of bytes Frame.Trace may contain: unreserved URL characters and nothing else.

View Source
const TypeACPDetach = "acp.detach"

TypeACPDetach reports that the client behind one attachment is gone; it carries no payload.

View Source
const TypeACPMessage = "acp.message"

TypeACPMessage tunnels one ACP JSON-RPC message, byte for byte.

Variables

View Source
var (
	ErrNonceSize    = errors.New("librelay: nonce is missing or larger than MaxNonceBytes")
	ErrNoSignature  = errors.New("librelay: welcome carries no signature")
	ErrBadSignature = errors.New("librelay: welcome signature does not verify")
	ErrBadPublicKey = errors.New("librelay: not an ed25519 public key")
	ErrNulByte      = errors.New("librelay: signing input contains a NUL byte")
)

Relay-authentication failures; a connector treats all of them as fatal.

View Source
var (
	ErrFrameTooLarge = errors.New("librelay: frame exceeds MaxFrameBytes")
	ErrReaderClosed  = errors.New("librelay: reader is unusable after a framing error")
)

Codec-level failures. ErrFrameTooLarge is the only one that ends a connection; every other error is per-frame and the Reader resumes at the next line.

View Source
var (
	ErrEmptyType    = errors.New("librelay: frame type is empty")
	ErrTypeTooLong  = errors.New("librelay: frame type exceeds MaxTypeBytes")
	ErrIDTooLong    = errors.New("librelay: identifier exceeds MaxIDBytes")
	ErrControlChar  = errors.New("librelay: identifier contains a control character")
	ErrBothIDs      = errors.New("librelay: frame sets both id and re")
	ErrSessionAlone = errors.New("librelay: frame sets session without instance")
	ErrNotUTF8      = errors.New("librelay: identifier is not valid UTF-8")
)

Validation failures: the frame is not addressable.

View Source
var (
	ErrTraceTooLong = errors.New("librelay: trace exceeds MaxTraceBytes")
	ErrTraceCharset = errors.New("librelay: trace contains a byte outside TraceAlphabet")
)

Trace validation failures: the value is unsafe to log.

Functions

func FormatPublicKey

func FormatPublicKey(pub libcipher.SigningPublicKey) string

FormatPublicKey renders a relay's key for storage and for an enrolment payload, delegating to libcipher.FormatPublicKey.

func IsControl

func IsControl(msgType string) bool

IsControl reports whether a type is relay-level control traffic.

func NewNonce

func NewNonce() ([]byte, error)

NewNonce returns a fresh challenge for Hello.Nonce. It must be generated per connection and never reused.

func NewTraceID

func NewTraceID() string

NewTraceID mints a fresh trace for one action — one browser request, one prompt — never once per connection. It is a correlation key only: do not reuse one as a token, a nonce or an idempotency key.

func ParsePublicKey

func ParsePublicKey(s string) (libcipher.SigningPublicKey, error)

ParsePublicKey reads a key produced by FormatPublicKey, delegating to libcipher.ParsePublicKey. The error matches both ErrBadPublicKey and the libcipher sentinel underneath it.

func SignWelcome

func SignWelcome(priv libcipher.SigningPrivateKey, nonce []byte, negotiatedVersion int, instance string) ([]byte, error)

SignWelcome produces the Welcome.Signature a relay answers hello with. The version is the one the relay selected, not the one the connector offered.

func SigningInput

func SigningInput(nonce []byte, negotiatedVersion int, instance string) ([]byte, error)

SigningInput is the exact byte string a Welcome signature covers, and the single definition of that layout:

SigningDomain ‖ 0x00 ‖ base64url(nonce) ‖ 0x00 ‖ decimal(version) ‖ 0x00 ‖ instance

func ValidTraceID

func ValidTraceID(s string) bool

ValidTraceID reports whether s may be carried in Frame.Trace. The empty string is valid.

func VerifyWelcome

func VerifyWelcome(pub libcipher.SigningPublicKey, nonce []byte, negotiatedVersion int, instance string, sig []byte) error

VerifyWelcome checks a relay's answer against the public key pinned at pairing time. Every failure is one of the sentinels above and none is retryable.

Types

type AskPublished added in v0.41.0

type AskPublished struct {
	AskID       string    `json:"ask_id"`
	SessionID   string    `json:"session_id,omitempty"`
	MissionID   string    `json:"mission_id,omitempty"`
	AgentName   string    `json:"agent_name,omitempty"`
	ToolsName   string    `json:"tools_name,omitempty"`
	ToolName    string    `json:"tool_name,omitempty"`
	PolicyName  string    `json:"policy_name,omitempty"`
	MatchedRule *int      `json:"matched_rule,omitempty"`
	ArgsSummary string    `json:"args_summary,omitempty"`
	ExpiresAt   time.Time `json:"expires_at,omitzero"`
}

AskPublished is the TypeAskPublished payload: what a human needs to decide, and never the call's arguments.

type AskResolved added in v0.41.0

type AskResolved struct {
	AskID  string `json:"ask_id"`
	Reason string `json:"reason"`
}

AskResolved is the TypeAskResolved payload: an ask that is no longer open, and why.

type AskVerdict added in v0.41.0

type AskVerdict struct {
	AskID     string    `json:"ask_id"`
	Decision  string    `json:"decision"`
	Answer    string    `json:"answer,omitempty"`
	Guidance  string    `json:"guidance,omitempty"`
	DecidedBy string    `json:"decided_by,omitempty"`
	DecidedAt time.Time `json:"decided_at,omitzero"`
}

AskVerdict is the TypeAskVerdict payload: one settled decision for an ask id, carrying no notion of who was entitled to make it.

type ChainTrigger added in v0.40.0

type ChainTrigger struct {
	// RequestID correlates the [ChainTriggerResult] with this trigger.
	RequestID string `json:"request_id"`
	// Chain names the chain file on the machine's own configuration path.
	Chain     string `json:"chain"`
	AgentName string `json:"agent_name,omitempty"`
	// SessionMode is [ChainSessionNew] or [ChainSessionReused].
	SessionMode string `json:"session_mode"`
	SessionName string `json:"session_name,omitempty"`
	// Input is the event envelope the chain receives, raw.
	Input json.RawMessage `json:"input"`
	// Policy optionally names the HITL policy envelope for the run; empty applies the machine's own default.
	Policy string `json:"policy,omitempty"`
}

ChainTrigger is the TypeChainTrigger payload: run this chain with this input.

type ChainTriggerResult added in v0.40.0

type ChainTriggerResult struct {
	// RequestID is [ChainTrigger.RequestID], echoed verbatim.
	RequestID string `json:"request_id"`
	// Status is one of the ChainTriggerStatus constants.
	Status string `json:"status"`
	// Error says why, for [ChainTriggerStatusError] and [ChainTriggerStatusRefused]; empty on ok.
	Error string `json:"error,omitempty"`
}

ChainTriggerResult is the TypeChainTriggerResult payload.

type Error

type Error struct {
	// Code is machine-readable; see the Code* constants.
	Code string `json:"code"`
	// Message is for humans and may change freely between versions.
	Message string `json:"message,omitempty"`
}

Error is the TypeError payload.

type Frame

type Frame struct {
	// Type discriminates the frame; control types carry ControlPrefix, everything else is routed opaquely.
	Type string `json:"type"`
	// Instance is the runtime instance this frame concerns; empty only on frames that precede identification.
	Instance string `json:"instance,omitempty"`
	// Session names the stream within Instance that this frame concerns; empty on control traffic and anything instance-scoped.
	Session string `json:"session,omitempty"`
	// ID marks this frame as a request and correlates the reply; its presence obliges exactly one answer.
	ID string `json:"id,omitempty"`
	// ReplyTo carries the ID of the request being answered and marks this frame as a response.
	ReplyTo string `json:"re,omitempty"`
	// Seq is the producer's per-(Instance, Session) cursor for this frame, monotonically increasing and gap-free within a session; zero means unsequenced.
	Seq uint64 `json:"seq,omitempty"`
	// Trace is the correlation key for the one human action this frame belongs to; peer-supplied text that must never be authorized on. Empty means untraced.
	Trace string `json:"trace,omitempty"`
	// Payload is the message body, left as raw JSON; empty is legal.
	Payload json.RawMessage `json:"payload,omitempty"`
}

Frame is the transport envelope. Instance and Session form the routing key.

func NewError

func NewError(req Frame, code, message string) Frame

NewError builds a response carrying code and message, correlated to req when req is a request.

func Unsupported

func Unsupported(f Frame) (Frame, bool)

Unsupported returns the reply owed to a frame this build cannot handle, and whether one is owed at all.

func (Frame) DecodePayload

func (f Frame) DecodePayload(v any) error

DecodePayload unmarshals f's payload into v; an absent payload is not an error.

func (Frame) IsRequest

func (f Frame) IsRequest() bool

IsRequest reports whether f obliges the receiver to send exactly one response.

func (Frame) IsResponse

func (f Frame) IsResponse() bool

IsResponse reports whether f answers an earlier request.

func (Frame) Validate

func (f Frame) Validate() error

Validate reports whether f is structurally addressable. It never rejects an unknown Type.

func (Frame) WithPayload

func (f Frame) WithPayload(v any) (Frame, error)

WithPayload returns f carrying v as its payload, encoded as compact JSON.

type Hello

type Hello struct {
	// ProtocolVersion is the highest envelope version the connector speaks.
	ProtocolVersion int `json:"protocol_version"`
	// Instance identifies the runtime; also set on the frame.
	Instance string `json:"instance"`
	// Agent names the implementation and version, for operator diagnosis only.
	Agent string `json:"agent,omitempty"`
	// Nonce is a fresh random challenge the relay signs into [Welcome.Signature].
	Nonce []byte `json:"nonce,omitempty"`
}

Hello is the TypeHello payload: what the connector claims to be.

type Reader

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

Reader decodes NDJSON frames from a stream. It is not safe for concurrent use; one goroutine owns the read side of a connection.

func NewReader

func NewReader(r io.Reader) *Reader

NewReader returns a Reader over r.

func (*Reader) ReadFrame

func (r *Reader) ReadFrame() (Frame, error)

ReadFrame returns the next frame. A malformed or invalid frame is reported with the line already consumed, so calling ReadFrame again reads the next one; after ErrFrameTooLarge the Reader is dead and every later call returns ErrReaderClosed.

type Resume

type Resume struct {
	// AfterSeq is the last [Frame.Seq] the requester saw; zero asks for the whole retained stream.
	AfterSeq uint64 `json:"after_seq"`
}

Resume is the TypeResume payload: continue a session's stream after a cursor.

type Resumed

type Resumed struct {
	// FromSeq is the first [Frame.Seq] that follows; greater than Resume.AfterSeq+1 only when Evicted is set.
	FromSeq uint64 `json:"from_seq"`
	// Evicted reports the requested cursor is older than what the producer retains.
	Evicted bool `json:"evicted,omitempty"`
}

Resumed is the TypeResumed payload: what the producer is about to send.

type Welcome

type Welcome struct {
	// ProtocolVersion is the version the relay selected, min(peer, self); the connector must close if it's lower than it can speak.
	ProtocolVersion int `json:"protocol_version"`
	// Relay names the relay implementation and version, diagnostics only.
	Relay string `json:"relay,omitempty"`
	// Signature is Ed25519 over [SigningInput] of Hello.Nonce, ProtocolVersion and instance.
	Signature []byte `json:"sig,omitempty"`
	// RetryAfterSeconds hints how long to wait before redialling. Zero means the connector's own backoff applies.
	RetryAfterSeconds int `json:"retry_after,omitempty"`
}

Welcome is the TypeWelcome payload: the relay's acceptance.

type Writer

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

Writer encodes frames as NDJSON. It is safe for concurrent use.

func NewWriter

func NewWriter(w io.Writer) *Writer

NewWriter returns a Writer over w.

func (*Writer) WriteFrame

func (wr *Writer) WriteFrame(f Frame) error

WriteFrame validates and writes f followed by a newline. An invalid frame is never partially written; the line reaches the connection as a single Write.

Jump to

Keyboard shortcuts

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