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
- Variables
- func FormatPublicKey(pub libcipher.SigningPublicKey) string
- func IsControl(msgType string) bool
- func NewNonce() ([]byte, error)
- func NewTraceID() string
- func ParsePublicKey(s string) (libcipher.SigningPublicKey, error)
- func SignWelcome(priv libcipher.SigningPrivateKey, nonce []byte, negotiatedVersion int, ...) ([]byte, error)
- func SigningInput(nonce []byte, negotiatedVersion int, instance string) ([]byte, error)
- func ValidTraceID(s string) bool
- func VerifyWelcome(pub libcipher.SigningPublicKey, nonce []byte, negotiatedVersion int, ...) error
- type AskPublished
- type AskResolved
- type AskVerdict
- type ChainTrigger
- type ChainTriggerResult
- type Error
- type Frame
- type Hello
- type Reader
- type Resume
- type Resumed
- type Welcome
- type Writer
Constants ¶
const ( NonceSize = 32 MaxNonceBytes = 64 )
Nonce sizes. NonceSize is what a connector generates; MaxNonceBytes is what a relay will sign over.
const ( MaxFrameBytes = 16 << 20 MaxTypeBytes = 128 MaxIDBytes = 256 )
Envelope limits bound what a decoder will allocate for a single frame before it gives up.
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.
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.
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.
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.
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.
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.
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.
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.
const ( CodeUnsupportedType = "unsupported_type" CodeMalformedFrame = "malformed_frame" CodeUnknownInstance = "unknown_instance" CodeVersion = "unsupported_version" // CodeCursorEvicted answers a [Resume] whose cursor the producer no longer retains. CodeCursorEvicted = "cursor_evicted" )
Error codes carried by Error.
const ControlPrefix = "relay."
ControlPrefix marks a type as relay-level control traffic; a relay handles frames with this prefix and forwards everything else.
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.
const ProtocolVersion = 1
ProtocolVersion is the envelope version this build speaks; exchanged once in Hello/Welcome, never on a frame.
const SigningDomain = "contenox-relay/welcome/v1"
SigningDomain is the domain-separation tag every Welcome signature starts with.
const TraceAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
TraceAlphabet is the complete set of bytes Frame.Trace may contain: unreserved URL characters and nothing else.
const TypeACPDetach = "acp.detach"
TypeACPDetach reports that the client behind one attachment is gone; it carries no payload.
const TypeACPMessage = "acp.message"
TypeACPMessage tunnels one ACP JSON-RPC message, byte for byte.
Variables ¶
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.
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.
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.
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 NewNonce ¶
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 ¶
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 ¶
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
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 ¶
NewError builds a response carrying code and message, correlated to req when req is a request.
func Unsupported ¶
Unsupported returns the reply owed to a frame this build cannot handle, and whether one is owed at all.
func (Frame) DecodePayload ¶
DecodePayload unmarshals f's payload into v; an absent payload is not an error.
func (Frame) IsRequest ¶
IsRequest reports whether f obliges the receiver to send exactly one response.
func (Frame) IsResponse ¶
IsResponse reports whether f answers an earlier request.
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 (*Reader) ReadFrame ¶
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 (*Writer) WriteFrame ¶
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.