protocol

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package protocol implements the wire layer of the Brigade Adapter Protocol, version 1 (BAP/1, plan section 4): a Go type for every JSON shape of 4.4 with a hand-written Validate method, the 4.6 error taxonomy with its exit-code map, and the protocol constants of 4.4.1.

Parsing is deliberately loose (D16): unknown members are ignored, member names are case-sensitive, and a wrongly-cased member is silently zero — which is exactly why every wire type has a Validate method that checks required members, byte caps (len) and code-point caps (utf8.RuneCountInString) by hand. Duplicate member names and invalid UTF-8 are rejected by the codec itself; Unmarshal maps every such failure to invalid_input without echoing the decoder's own text, which can embed attacker-controlled member names.

SendRequest is the one named exception to loose parsing (4.4.6, C-23): a request carrying any of the adapter-stamped sender members is rejected rather than silently ignored, because ignoring a forged sender field would hide a client bug.

Index

Constants

View Source
const (
	ProfileStateUnconfigured    = "unconfigured"
	ProfileStateUnauthenticated = "unauthenticated"
	ProfileStateNotMember       = "not_member"
	ProfileStateJoined          = "joined"
)

The `profile.state` values of DescribeResult (4.4.1), computed from local files only.

View Source
const (
	AckStateInjected  = "injected"
	AckStateProcessed = "processed"
)

The `delivery.ack_state` values: `injected` is the terminal adapter state in v1; `processed` is reserved (4.5.3).

View Source
const (
	// MaxBodyBytes caps a message body, in BYTES of UTF-8.
	MaxBodyBytes = 16384
	// MaxSummaryChars caps a message summary, in Unicode code points.
	MaxSummaryChars = 200
	// MaxSessionNameCodepoints caps a session name, in code points.
	MaxSessionNameCodepoints = 64
	// MaxTeamNameCodepoints caps a team name, in code points. A team name
	// is a frame tag attribute (6.7 `team="…"`), and 6.7 rule 4 caps tag
	// attribute values at 64 code points; it is its own limit, not a
	// borrowed session-name cap (P1-4 decision 1).
	MaxTeamNameCodepoints = 64
	// MaxDescriptionChars caps a session description, in code points.
	MaxDescriptionChars = 256
	// MaxHumanLabelChars caps a human label, in code points.
	MaxHumanLabelChars = 128
	// MaxWorkspaceLabelChars caps a workspace label, in code points. It is
	// a label like human_label and shares its size, but is its own limit
	// and its own `limits` member (P1-4 decision 1).
	MaxWorkspaceLabelChars = 128
	// MaxModelChars caps a harness-reported model identity (`model`:
	// 4.4.2, 4.4.3, 4.4.4 and the watch heartbeat command of 4.4.9;
	// capability session.model, C-44), in code points. It is label-sized
	// like human_label, but is its own limit and its own `limits` member,
	// max_model_chars (the rule of P1-4 decision 1: no member borrows
	// another member's cap).
	MaxModelChars = 128
	// MaxIdempotencyKeyChars caps an idempotency key, in code points.
	MaxIdempotencyKeyChars = 128
	// SendRatePerMinute and SendRatePerHour bound one sender session.
	SendRatePerMinute = 20
	// SendRatePerHour is the hourly half of the per-session send rate.
	SendRatePerHour = 200
	// PrincipalSendRatePerMinute bounds all sessions of one principal.
	PrincipalSendRatePerMinute = 60
	// PrincipalSendRatePerHour is the hourly half of the principal rate.
	PrincipalSendRatePerHour = 600
	// MaxUnackedPerRecipient caps a recipient session's unacknowledged inbox.
	MaxUnackedPerRecipient = 60
	// MaxUnackedPerSenderRecipient caps one sender-recipient pair, checked
	// before the recipient-wide cap (4.5.12).
	MaxUnackedPerSenderRecipient = 15
	// MaxHopCount is the reply-chain bound behind loop_detected.
	MaxHopCount = 32
	// ImplicitReplyWindowSeconds is the window in which an unlabelled
	// answer still counts as a reply for hop counting (4.5.12).
	ImplicitReplyWindowSeconds = 600
)

The protocol v1 constants of plan 4.4.1. Byte caps are measured with len(); code-point caps with utf8.RuneCountInString — a 16 KiB byte cap is NOT 16384 characters, and conflating the two is a real bug the validation tests pin in both directions.

View Source
const (
	// LeaseDefaultSeconds is the lease granted when the caller names none.
	LeaseDefaultSeconds = 90
	// LeaseMinSeconds is the shortest lease the DEFAULT range accepts.
	// The range is per adapter (4.4.1: "the range of lease_seconds an
	// adapter accepts"), advertised in describe.lease and enforced by the
	// adapter with Lease.CheckSeconds; these three are the 4.4.1 example
	// values, which the Supabase adapter uses and DefaultLease returns.
	// The request shapes' Validate requires only a positive value.
	LeaseMinSeconds = 30
	// LeaseMaxSeconds is the longest lease the default range accepts.
	LeaseMaxSeconds = 600
)

The protocol v1 lease bounds of plan 4.4.1, in seconds.

View Source
const (
	// RetentionUnackedMessageSeconds is how long an unacknowledged message
	// is retained at least.
	RetentionUnackedMessageSeconds = 604800
	// RetentionAckedMessageSeconds is how soon an acknowledged message may
	// be deleted.
	RetentionAckedMessageSeconds = 86400
	// RetentionClosedSessionSeconds is how soon a closed or expired
	// session may be deleted together with its messages.
	RetentionClosedSessionSeconds = 604800
)

The protocol v1 retention floors of plan 4.4.1, in seconds.

View Source
const (
	DeliveryStateAccepted  = "accepted"
	DeliveryStateInjected  = "injected"
	DeliveryStateProcessed = "processed"
)

The `delivery_state` values of a MessageEnvelope: accepted (persisted, 4.5.1), injected (the terminal adapter state in v1) and processed (reserved).

View Source
const (
	ActivityBusy = "busy"
	ActivityIdle = "idle"
)

The `activity` values a harness reports (4.4.2, 4.5.8).

View Source
const (
	InboundAccept = "accept"
	InboundHold   = "hold"
	InboundRefuse = "refuse"
)

The `inbound` policy values (4.4.2).

View Source
const (
	SessionStateActive  = "active"
	SessionStateIdle    = "idle"
	SessionStateOffline = "offline"
)

The computed session `state` values (4.5.8). The adapter computes them from lease_until, closed_at and activity with its own clock; the harness never does.

View Source
const (
	EventReady       = "ready"
	EventMessage     = "message"
	EventStatus      = "status"
	EventAcked       = "acked"
	EventHeartbeatOK = "heartbeat_ok"
	EventError       = "error"
)

The `event` discriminator values of `message watch` output (4.4.9). Unknown event kinds MUST be ignored by the receiver — logged, never fatal.

View Source
const (
	CommandAck       = "ack"
	CommandHeartbeat = "heartbeat"
	CommandClose     = "close"
)

The `type` discriminator values of `message watch` stdin commands (4.4.9, capability message.watch.stdin_commands). Unknown command types MUST be ignored by the receiver.

View Source
const (
	WatchModePush    = "push"
	WatchModePolling = "polling"
)

The `mode` values of the watch `ready` event (4.4.9, P1-4 decision 4). An adapter that omits the message.watch.push capability MUST send polling (C-33); mode is a closed set and Validate rejects anything else.

View Source
const (
	StatusStateLive    = "live"
	StatusStatePolling = "polling"
)

The `state` values of the watch `status` event this protocol version defines (4.4.9, P1-4 decision 4). The set is OPEN on the wire: a reader MUST ignore an unknown state rather than reject it (the same rule as for unknown event kinds), so Validate requires a non-empty state and Known reports whether it is one of these.

View Source
const ExitOK = 0

ExitOK is the exit status of a command that succeeded.

View Source
const GuaranteeAtLeastOnce = "at_least_once"

GuaranteeAtLeastOnce is the only delivery guarantee of protocol v1 (freeze list item 4).

View Source
const JoinSecretPrefix = "brg1."

JoinSecretPrefix is the fixed, case-sensitive prefix of every v1 join secret (4.4.10, D5).

View Source
const KindText = "text"

KindText is the only message kind of protocol v1 (4.5.11).

View Source
const MaxBrigadeVersionChars = 64

MaxBrigadeVersionChars bounds `brigade_version` (4.4.2, 4.4.3, 4.4.4 and the watch heartbeat command of 4.4.9; capability session.brigade_version, C-46), in code points: the version of the Brigade harness a session is running, as that harness reports it. Like MaxContextUsedTokens it is a wire-format bound and NOT a member of `limits` (4.4.1), and for a reason that outranks P1-4 decision 1: Limits.validate requires every member it knows to be positive, so a new one would fail `describe` for every adapter written before it — a protocol change an existing conforming adapter fails is a new protocol major, and this member is an addition. 64 is the frame's attribute cap (6.7 rule 4), which is how a consumer displays a short identifying string like this one; a release version is a fraction of it.

View Source
const MaxContextUsedTokens = 1<<53 - 1

MaxContextUsedTokens bounds `context_used_tokens` (4.4.2, 4.4.3, 4.4.4 and the watch heartbeat command of 4.4.9; capability session.context_used_tokens, C-44): 2^53 - 1, the largest integer JSON carries exactly. A consumer whose JSON number is an IEEE 754 double (JavaScript, and every language that follows it) rounds anything larger, so a count above the bound could not round-trip as sent; the harness reports a measured count and nothing real comes within orders of magnitude of it. It is a wire-format bound, not an adapter's cap, so unlike MaxModelChars it has no `limits` member (4.4.1).

View Source
const MaxLineBytes = 1 << 20

MaxLineBytes is the NDJSON line cap: a line whose content exceeds 1 MiB is dropped by the reader with ErrLineTooLong and reading continues (4.4.9).

View Source
const MaxSyncPeerChars = 256

MaxSyncPeerChars bounds `sync_peer` (4.4.2, 4.4.3, 4.4.4 and the watch heartbeat command of 4.4.9; capability session.sync_peer, C-47), in code points: the opaque `<adapter>:<descriptor>` a session's folder-sync adapter publishes so its teammates' adapters can introduce it (plan folder-sync.md 2). It is a wire-format bound and NOT a member of `limits` (4.4.1) for the reason MaxBrigadeVersionChars gives: every `limits` member is required, so a new one would fail `describe` for every adapter written before it. 256 leaves room for any adapter's descriptor — a Syncthing device id is 63 characters — without letting a session publish an essay; the Supabase column carries the same check.

View Source
const ProtocolVersion = "1"

ProtocolVersion is the value of the `protocol_version` member of every result envelope, DescribeResult, MessageEnvelope and watch `ready` event (plan 4.3, 4.4).

View Source
const SendStatusAccepted = "accepted"

SendStatusAccepted is the only `status` of a SendResponse (4.4.7): the message is persisted such that a later receive returns it. The product never says "delivered" (4.5.1).

View Source
const TruncationMarker = "..."

TruncationMarker is appended by rule 3 when a value had to be cut to its protocol cap. The truncated value INCLUDING the marker stays within the cap, so a sanitised value always passes the corresponding wire validation.

It is an ellipsis, not a word: the marker is spent out of the value's own budget — every one of these characters is a character of the name, label or sentence a reader does not get — and at a 50-code-point NAME column the eleven of "[truncated]" were a fifth of the cell. The protocol fixes no marker string (nothing in docs/protocol-v1.md, the schema or the conformance suite names one), so this is Brigade's choice alone, in both output forms and everywhere a sanitised value goes.

The cost is that "..." is ordinary prose: a name or doing line that genuinely ends in an ellipsis now reads as cut when it is not. Nothing downstream parses the marker — it is text for a human or a model to read, never a flag — so the ambiguity is cosmetic, and `--json` cannot resolve it either, because a sanitised value is all either form has.

Variables

View Source
var ErrLineTooLong = errors.New("ndjson line exceeds 1 MiB and was dropped")

ErrLineTooLong marks one over-long NDJSON line that was discarded to its end. It is a per-line condition, not a stream failure: the caller logs a warning and calls Next again for the line after it. This is exactly why the reader is built on bufio.Reader.ReadSlice rather than bufio.Scanner, whose ErrTooLong is permanent (7.3).

Functions

func Decode

func Decode(data []byte, v Validator) error

Decode parses one protocol JSON document into v and then validates it: the loose parse of Unmarshal followed by the shape's own Validate. This is the one entry point later phases should use for anything read off a wire.

func Sanitize

func Sanitize(s string) string

Sanitize applies rules 1 and 2 — repair, NFC, control and format stripping, tag neutralisation — with no truncation. Use the field-specific variants for anything with a protocol cap.

func SanitizeAttribute

func SanitizeAttribute(s string) string

SanitizeAttribute sanitises a value destined for a tag attribute in the injected frame (rule 4): rules 1-2, then '"', '<', '>' and newlines are dropped so the value can neither escape its quotes nor close the tag line, then a 64-code-point cap with NO marker.

It is the one sanitiser that cuts silently, and deliberately: an attribute is a short display value a reader has no original to compare against, so a marker would say nothing it could act on while spending characters of the name itself. A cut `from-name`, `from-label` or `team` is therefore simply short, with nothing to say so — which is why nothing should read the absence of a marker as proof a value is whole. TestSanitizeAttributeCapsAt64CodePoints pins it.

func SanitizeBody

func SanitizeBody(s string) string

SanitizeBody sanitises a message body: rules 1-3 with the MaxBodyBytes byte cap.

func SanitizeDescription

func SanitizeDescription(s string) string

SanitizeDescription sanitises a session description: rules 1-3 with the MaxDescriptionChars code-point cap.

func SanitizeLabel

func SanitizeLabel(s string) string

SanitizeLabel sanitises a human label: rules 1-3 with the MaxHumanLabelChars code-point cap.

func SanitizeModel added in v0.5.0

func SanitizeModel(s string) string

SanitizeModel sanitises a harness-reported model identity (`model`, 4.4.2-4.4.4): rules 1-3 with the MaxModelChars code-point cap. The value is unverified text like human_label — a harness derives it from its own transcript and an adapter stores it as sent — so every display of it, `sessions` included, goes through here (4.5.11).

func SanitizeName

func SanitizeName(s string) string

SanitizeName sanitises a session or team name: rules 1-3 with the MaxSessionNameCodepoints code-point cap.

func SanitizeSummary

func SanitizeSummary(s string) string

SanitizeSummary sanitises a sender summary: rules 1-3 with the MaxSummaryChars code-point cap.

func TruncateRunes added in v0.7.0

func TruncateRunes(s string, limit int) string

TruncateRunes cuts s to at most limit CODE POINTS, appending TruncationMarker when it had to. The marker counts against the limit so the result always passes the code-point-cap validation. It is exported for the display caps that are shorter than their wire cap — the roster's doing line (card 25, harness/doing.MaxChars) and, since card 27, its NAME column — so each cut has exactly this rule and not a second copy of it.

func Unmarshal

func Unmarshal(data []byte, v any) error

Unmarshal parses one protocol JSON document with the loose semantics of D16: unknown members are ignored, member names match case-sensitively, duplicate member names and invalid UTF-8 are rejected by the codec. Every parse failure maps to a single invalid_input *Error whose message is fixed text — the decoder's own error strings embed JSON pointers built from member names, which in a hostile document are attacker-controlled, so they are never echoed (and tests must never assert on them, plan 7.3).

Types

type AckRequest

type AckRequest struct {
	MessageIDs []string `json:"message_ids"`
}

AckRequest is the `message ack` request (4.4.8); the session comes from --session.

func (*AckRequest) Validate

func (r *AckRequest) Validate() error

Validate implements Validator.

type AckResult

type AckResult struct {
	Acked   []string `json:"acked"`
	Unknown []string `json:"unknown"`
}

AckResult is the `message ack` result (4.4.8). An already-acknowledged owned id counts as acked; ids not owned or not found are unknown, never errors. Both members are always emitted, as [] when empty.

func (*AckResult) Validate

func (r *AckResult) Validate() error

Validate implements Validator. Empty lists are valid; a present id must be a non-empty string.

type AdapterInfo

type AdapterInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

AdapterInfo is the `adapter` member of DescribeResult (4.4.1).

type Code

type Code string

A Code is the machine-readable `error.code` of the plan's 4.6 taxonomy. The taxonomy is frozen for protocol v1 (freeze list item 7): both the harness and every adapter map failures to exactly these twelve codes.

const (
	CodeInternal         Code = "internal"
	CodeUsage            Code = "usage"
	CodeInvalidInput     Code = "invalid_input"
	CodeUnauthenticated  Code = "unauthenticated"
	CodeUnauthorized     Code = "unauthorized"
	CodeNotFound         Code = "not_found"
	CodeConflict         Code = "conflict"
	CodeRateLimited      Code = "rate_limited"
	CodeUnavailable      Code = "unavailable"
	CodeProtocolMismatch Code = "protocol_mismatch"
	CodeConfig           Code = "config"
	CodeLoopDetected     Code = "loop_detected"
)

The 4.6 error codes, in exit-code order.

func (Code) Exit

func (c Code) Exit() int

Exit maps a code to its process exit status (plan 4.6). An unrecognised code maps to the `internal` status rather than to success, so a code added without updating this table cannot turn a failure into an exit 0.

func (Code) Retryable

func (c Code) Retryable() bool

Retryable reports whether 4.6 marks the code as worth retrying. Note that `rate_limited` and `loop_detected` are still terminal for the model (`brigade send` never retries them, 4.5.12); Retryable describes the protocol table, not the product policy.

type DeliveryInfo

type DeliveryInfo struct {
	Guarantee string `json:"guarantee"`
	Ordering  string `json:"ordering"`
	AckState  string `json:"ack_state"`
}

DeliveryInfo is the `delivery` member of DescribeResult (4.4.1).

type DescribeResult

type DescribeResult struct {
	ProtocolVersion string       `json:"protocol_version"`
	Adapter         AdapterInfo  `json:"adapter"`
	Delivery        DeliveryInfo `json:"delivery"`
	Capabilities    []string     `json:"capabilities"`
	Limits          Limits       `json:"limits"`
	Lease           Lease        `json:"lease"`
	Retention       Retention    `json:"retention"`
	Profile         ProfileInfo  `json:"profile"`
}

DescribeResult is the `describe` result (4.4.1), answered from local state only.

func (*DescribeResult) Validate

func (d *DescribeResult) Validate() error

Validate implements Validator. It deliberately does NOT require protocol_version to equal ProtocolVersion: version comparison belongs to the harness's negotiation (4.5.13, protocol_mismatch), not to input validation. Capability strings are not checked at all — unknown capabilities are ignored by design (4.7).

type Envelope

type Envelope struct {
	OK              bool           `json:"ok"`
	ProtocolVersion string         `json:"protocol_version"`
	Result          jsontext.Value `json:"result,omitzero"`
	Error           *ErrorObject   `json:"error,omitzero"`
}

Envelope is the 4.3 result envelope: exactly one JSON document on stdout for every command except `message watch`. Exactly one of Result and Error is set.

func (*Envelope) Validate

func (e *Envelope) Validate() error

Validate implements Validator.

type Error

type Error struct {
	// Code selects the exit status and appears as `error.code`.
	Code Code
	// Message is short, human-readable and value-free.
	Message string
	// RetryAfterMS is set only for rate_limited.
	RetryAfterMS int
	// Details is optional and machine-readable (4.3). Validation failures
	// carry the offending member's JSON name under "field".
	Details map[string]string
}

An Error is a protocol-level failure: a 4.6 code plus a short message that is safe to show to a model (no raw server text, no SQL, no tokens, and never an interpolated input value — member names and field values in a hostile document are attacker-controlled).

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) Object

func (e *Error) Object() *ErrorObject

Object renders the error as the wire `error` member of a 4.3 envelope, with `retryable` filled in from the taxonomy.

type ErrorObject

type ErrorObject struct {
	Code         Code              `json:"code"`
	Message      string            `json:"message"`
	Retryable    bool              `json:"retryable"`
	RetryAfterMS int               `json:"retry_after_ms,omitzero"`
	Details      map[string]string `json:"details,omitzero"`
}

ErrorObject is the wire `error` member of a failing 4.3 envelope and of a watch `error` event (4.4.9).

Retryable is a plain bool, and the rule is (P1-4 decision 2):

  • producers MUST emit it — the member has no omitzero, so a marshalled ErrorObject always carries "retryable": true|false;
  • consumers treat an absent member as false, which under loose parsing is what a plain bool yields, and which fails safe;
  • consumers SHOULD derive retryability from Code.Retryable(), the source of truth: only rate_limited (8) and unavailable (9) are ever retryable, so the flag is advisory and Validate does not check it.

func (*ErrorObject) Validate

func (e *ErrorObject) Validate() error

Validate implements Validator.

type HeartbeatRequest

type HeartbeatRequest struct {
	Activity           *string `json:"activity,omitzero"`
	SessionName        *string `json:"session_name,omitzero"`
	SessionDescription *string `json:"session_description,omitzero"`
	Inbound            *string `json:"inbound,omitzero"`
	LeaseSeconds       *int    `json:"lease_seconds,omitzero"`
	Model              *string `json:"model,omitzero"`
	ContextUsedTokens  *int    `json:"context_used_tokens,omitzero"`
	BrigadeVersion     *string `json:"brigade_version,omitzero"`
	SyncPeer           *string `json:"sync_peer,omitzero"`
}

HeartbeatRequest is the `session heartbeat` request (4.4.4). Every member is optional — the session comes from --session — and an absent member means "unchanged", which is why each is a pointer. Model and ContextUsedTokens (harness-reported, unverified; capabilities session.model and session.context_used_tokens, C-44) follow the same rule and one more: the harness never clears them — it omits them and the stored values stand.

func (*HeartbeatRequest) Validate

func (h *HeartbeatRequest) Validate() error

Validate implements Validator.

type HeartbeatResult

type HeartbeatResult struct {
	SessionID  string    `json:"session_id"`
	State      string    `json:"state"`
	LeaseUntil time.Time `json:"lease_until"`
	ServerTime time.Time `json:"server_time"`
}

HeartbeatResult is the `session heartbeat` result (4.4.4).

func (*HeartbeatResult) Validate

func (h *HeartbeatResult) Validate() error

Validate implements Validator.

type JoinSecret

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

A JoinSecret is a parsed `brg1.<team_ref>.<secret>` bearer secret. The zero value is invalid; obtain one through ParseJoinSecret. The type exists so the secret half cannot leak by accident: String, GoString, LogValue and JSON marshalling all redact it, and only the explicit Secret method returns the full value for the one wire call that needs it (`team join`).

func ParseJoinSecret

func ParseJoinSecret(s string) (JoinSecret, error)

ParseJoinSecret parses and validates the `brg1.<team_ref>.<secret>` format. The team_ref is everything between the prefix and the LAST '.', because 4.8 deliberately does not freeze the team_ref's shape (an adapter may use refs containing '.'), while D5's secret half is a single token. Parsing is strict — no trimming; a TTY prompt layer trims before calling. A malformed secret is invalid_input (4.6), and neither the error message nor its details ever carry any part of the input (4.5.14: no secret in any error or log line).

func (JoinSecret) GoString

func (s JoinSecret) GoString() string

GoString implements fmt.GoStringer so %#v redacts too.

func (JoinSecret) LogValue

func (s JoinSecret) LogValue() slog.Value

LogValue implements slog.LogValuer so a JoinSecret handed to any slog logger renders redacted.

func (JoinSecret) MarshalJSON

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

MarshalJSON redacts: a JoinSecret embedded in any marshalled value renders without its secret half. The `team join` request builds its join_secret member from Secret() explicitly — failing loudly at the backend beats leaking silently in a log or result.

func (JoinSecret) Secret

func (s JoinSecret) Secret() string

Secret returns the full raw secret for the one legitimate use: the `team join` backend call. Every other rendering of the type redacts.

func (JoinSecret) String

func (s JoinSecret) String() string

String implements fmt.Stringer with the secret half redacted, so a stray %v or %s can never leak it.

func (JoinSecret) TeamRef

func (s JoinSecret) TeamRef() string

TeamRef returns the team reference named inside the secret, for the local rejoin check of 4.4.10 (a secret naming the bound team is a rejoin). The team_ref is not itself secret — it appears in ordinary results.

type Lease

type Lease struct {
	DefaultSeconds int `json:"default_seconds"`
	MinSeconds     int `json:"min_seconds"`
	MaxSeconds     int `json:"max_seconds"`
}

Lease is the `lease` member of DescribeResult (4.4.1).

func DefaultLease

func DefaultLease() Lease

DefaultLease returns the v1 lease bounds with the exact 4.4.1 values.

func (Lease) CheckSeconds

func (l Lease) CheckSeconds(field string, v *int) error

CheckSeconds validates a requested lease_seconds member against THIS advertised range (4.4.2, 4.4.4 and the watch heartbeat command of 4.4.9): an absent value is valid, because the default applies, and a present value outside min_seconds..max_seconds is invalid_input naming field. An adapter calls it after the shape's Validate, which requires only positivity — the range is the adapter's, published in `describe`, and differs between adapters (the fs adapter advertises min_seconds = 1).

type Limits

type Limits struct {
	MaxBodyBytes                 int      `json:"max_body_bytes"`
	MaxSummaryChars              int      `json:"max_summary_chars"`
	MaxSessionNameCodepoints     int      `json:"max_session_name_codepoints"`
	MaxTeamNameCodepoints        int      `json:"max_team_name_codepoints"`
	MaxDescriptionChars          int      `json:"max_description_chars"`
	MaxHumanLabelChars           int      `json:"max_human_label_chars"`
	MaxWorkspaceLabelChars       int      `json:"max_workspace_label_chars"`
	MaxModelChars                int      `json:"max_model_chars"`
	MaxIdempotencyKeyChars       int      `json:"max_idempotency_key_chars"`
	SendRate                     SendRate `json:"send_rate"`
	PrincipalSendRate            SendRate `json:"principal_send_rate"`
	MaxUnackedPerRecipient       int      `json:"max_unacked_per_recipient"`
	MaxUnackedPerSenderRecipient int      `json:"max_unacked_per_sender_recipient"`
	MaxHopCount                  int      `json:"max_hop_count"`
	ImplicitReplyWindowSeconds   int      `json:"implicit_reply_window_seconds"`
}

Limits is the `limits` member of DescribeResult (4.4.1).

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns the v1 limits with the exact 4.4.1 values.

type LineReader

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

A LineReader delivers NDJSON lines of up to MaxLineBytes. A longer line is discarded to its end and reported as ErrLineTooLong, and reading continues with the next line.

func NewLineReader

func NewLineReader(r io.Reader) *LineReader

NewLineReader wraps r. The internal buffer starts small; lines accumulate across bufio.ErrBufferFull chunks up to MaxLineBytes.

func (*LineReader) Next

func (lr *LineReader) Next() ([]byte, error)

type LineWriter

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

A LineWriter emits one JSON document per line: jsonv2 marshalling plus a trailing '\n', written as a single Write call behind a mutex so concurrent events never interleave (7.3).

func NewLineWriter

func NewLineWriter(w io.Writer) *LineWriter

NewLineWriter wraps w, which is typically the watch command's stdout.

func (*LineWriter) WriteLine

func (lw *LineWriter) WriteLine(v any) error

WriteLine marshals v and writes it as exactly one '\n'-terminated line. The codec always escapes \n and \r inside strings, so a body containing newlines (or U+2028/U+2029, which are not newline bytes) cannot produce a second line (C-39, U-17); a marshalling that somehow embedded a raw newline is refused rather than emitted.

type MessageEnvelope

type MessageEnvelope struct {
	ProtocolVersion    string    `json:"protocol_version"`
	Kind               string    `json:"kind"`
	MessageID          string    `json:"message_id"`
	TeamRef            string    `json:"team_ref"`
	Sender             Sender    `json:"sender"`
	RecipientSessionID string    `json:"recipient_session_id"`
	Summary            string    `json:"summary,omitzero"`
	Body               string    `json:"body"`
	ReplyTo            *string   `json:"reply_to,omitzero"`
	HopCount           int       `json:"hop_count"`
	CreatedAt          time.Time `json:"created_at"`
	DeliveryState      string    `json:"delivery_state"`
}

MessageEnvelope is the only representation of a message anywhere in the protocol (4.4.5, adapter → harness). body, summary and every sender string are untrusted input at every layer (4.5.11).

func (*MessageEnvelope) Validate

func (m *MessageEnvelope) Validate() error

Validate implements Validator.

type ProfileInfo

type ProfileInfo struct {
	Name         string `json:"name"`
	State        string `json:"state"`
	TeamRef      string `json:"team_ref,omitzero"`
	TeamName     string `json:"team_name,omitzero"`
	PrincipalRef string `json:"principal_ref,omitzero"`
	HumanLabel   string `json:"human_label,omitzero"`
}

ProfileInfo is the `profile` member of DescribeResult (4.4.1). The team members are present only when State is joined.

type ResumeRef

type ResumeRef struct {
	SessionID string `json:"session_id"`
}

ResumeRef is the optional `resume` member of SessionRegistration (4.4.2, capability session.resume).

type Retention

type Retention struct {
	UnackedMessageSeconds int `json:"unacked_message_seconds"`
	AckedMessageSeconds   int `json:"acked_message_seconds"`
	ClosedSessionSeconds  int `json:"closed_session_seconds"`
}

Retention is the `retention` member of DescribeResult (4.4.1).

func DefaultRetention

func DefaultRetention() Retention

DefaultRetention returns the v1 retention floors with the exact 4.4.1 values.

type SendRate

type SendRate struct {
	PerMinute int `json:"per_minute"`
	PerHour   int `json:"per_hour"`
}

SendRate is one per-minute/per-hour rate pair of `limits` (4.4.1).

type SendRequest

type SendRequest struct {
	SenderSessionID    string `json:"sender_session_id"`
	RecipientSessionID string `json:"recipient_session_id"`
	Body               string `json:"body"`
	Summary            string `json:"summary,omitzero"`
	ReplyTo            string `json:"reply_to,omitzero"`
	IdempotencyKey     string `json:"idempotency_key,omitzero"`

	// The forbidden members of C-23. Never set these when building a
	// request; Validate rejects a non-empty one.
	ForbiddenSender       jsontext.Value `json:"sender,omitzero"`
	ForbiddenPrincipalRef jsontext.Value `json:"principal_ref,omitzero"`
	ForbiddenHumanLabel   jsontext.Value `json:"human_label,omitzero"`
	ForbiddenTeamRef      jsontext.Value `json:"team_ref,omitzero"`
	ForbiddenCreatedAt    jsontext.Value `json:"created_at,omitzero"`
	ForbiddenHopCount     jsontext.Value `json:"hop_count,omitzero"`
}

SendRequest is the `message send` request (4.4.6, harness → adapter).

It is the one shape where loose parsing has a named exception (C-23): the adapter stamps the sender identity from its own authority (4.5.5), so a request carrying `sender`, `principal_ref`, `human_label`, `team_ref`, `created_at` or `hop_count` is rejected with invalid_input rather than silently ignored — ignoring a forged sender field would hide a client bug. The forbidden members are declared as jsontext.Value so their mere presence (any value, including null) is observable.

func (*SendRequest) Validate

func (r *SendRequest) Validate() error

Validate implements Validator.

type SendResponse

type SendResponse struct {
	Status             string    `json:"status"`
	MessageID          string    `json:"message_id"`
	RecipientSessionID string    `json:"recipient_session_id"`
	CreatedAt          time.Time `json:"created_at"`
	Duplicate          bool      `json:"duplicate"`
	HopCount           int       `json:"hop_count"`
}

SendResponse is the `message send` result (4.4.7). Duplicate true means the idempotency key matched an existing message and MessageID is that message's id (4.5.4).

func (*SendResponse) Validate

func (r *SendResponse) Validate() error

Validate implements Validator.

type Sender

type Sender struct {
	PrincipalRef string `json:"principal_ref"`
	HumanLabel   string `json:"human_label,omitzero"`
	SessionID    string `json:"session_id"`
	SessionName  string `json:"session_name"`
}

Sender is the adapter-stamped `sender` member of a MessageEnvelope (4.4.5). Every member comes from the adapter's own authority, never from the sending caller (4.5.5).

type SessionRecord

type SessionRecord struct {
	SessionID          string    `json:"session_id"`
	SessionName        string    `json:"session_name"`
	SessionDescription *string   `json:"session_description,omitzero"`
	PrincipalRef       string    `json:"principal_ref"`
	HumanLabel         string    `json:"human_label,omitzero"`
	State              string    `json:"state"`
	Activity           string    `json:"activity"`
	Inbound            string    `json:"inbound"`
	LastSeenAt         time.Time `json:"last_seen_at"`
	LeaseUntil         time.Time `json:"lease_until"`
	Harness            string    `json:"harness,omitzero"`
	HarnessVersion     string    `json:"harness_version,omitzero"`
	WorkspaceLabel     *string   `json:"workspace_label,omitzero"`
	Model              *string   `json:"model,omitzero"`
	ContextUsedTokens  *int      `json:"context_used_tokens,omitzero"`
	BrigadeVersion     *string   `json:"brigade_version,omitzero"`
	SyncPeer           *string   `json:"sync_peer,omitzero"`
	CreatedAt          time.Time `json:"created_at"`
	IsSelf             bool      `json:"is_self"`
}

SessionRecord is one session as the adapter reports it (4.4.3, adapter → harness). human_label is unverified and every consumer MUST present it as such; it, session_name and model are untrusted input at every layer (4.5.11). Model and ContextUsedTokens are as the owning harness last reported them (4.4.2, 4.4.4; capabilities session.model and session.context_used_tokens, C-44): harness-reported, unverified, and absent when it never reported one or the adapter lacks the capability.

func (*SessionRecord) Validate

func (s *SessionRecord) Validate() error

Validate implements Validator.

type SessionRegistration

type SessionRegistration struct {
	Harness            string     `json:"harness"`
	HarnessVersion     string     `json:"harness_version"`
	SessionName        string     `json:"session_name"`
	SessionDescription *string    `json:"session_description,omitzero"`
	Activity           string     `json:"activity"`
	Inbound            string     `json:"inbound"`
	LeaseSeconds       *int       `json:"lease_seconds,omitzero"`
	WorkspaceLabel     *string    `json:"workspace_label,omitzero"`
	HumanLabel         *string    `json:"human_label,omitzero"`
	Model              *string    `json:"model,omitzero"`
	ContextUsedTokens  *int       `json:"context_used_tokens,omitzero"`
	BrigadeVersion     *string    `json:"brigade_version,omitzero"`
	SyncPeer           *string    `json:"sync_peer,omitzero"`
	Resume             *ResumeRef `json:"resume,omitzero"`
}

SessionRegistration is the `session register` request (4.4.2, harness → adapter). It deliberately has no member for a native session id, cwd, hostname, username or transcript path (threat model T10): adding one is a spec change, not a convenience. Model and ContextUsedTokens are the two facts the harness derives locally from that transcript — harness-reported, unverified, optional and nullable; capabilities session.model and session.context_used_tokens (C-44) — and the transcript itself and its path never travel.

HumanLabel is the harness's default label for its principal (capability session.human_label, C-45): an adapter that announces the capability adopts it as the membership's human_label ONLY when the membership has none, never overwriting a label the member chose, and an adapter without the capability ignores the member.

BrigadeVersion is the version of the Brigade harness itself (capability session.brigade_version, C-46) — not the host's, which HarnessVersion already carries — so a roster can show which teammates are behind a release. Harness-reported, unverified, optional and nullable, like Model; an adapter without the capability accepts and ignores it.

SyncPeer is the opaque `<adapter>:<descriptor>` this session's folder-sync adapter publishes (capability session.sync_peer, C-47; for Syncthing, its device id) so a teammate's adapter can introduce it (plan folder-sync.md 2). Harness-reported, unverified, optional and nullable like BrigadeVersion; every consumer sanitises it before use.

func (*SessionRegistration) Validate

func (r *SessionRegistration) Validate() error

Validate implements Validator.

type TeamCreateRequest

type TeamCreateRequest struct {
	TeamName   string `json:"team_name"`
	HumanLabel string `json:"human_label,omitzero"`
}

TeamCreateRequest is the `team create` stdin document (4.4.10).

func (*TeamCreateRequest) Validate

func (r *TeamCreateRequest) Validate() error

Validate implements Validator. The team name has its own cap, MaxTeamNameCodepoints, published as limits.max_team_name_codepoints (P1-4 decision 1); it is not the session-name cap.

type TeamCreateResult

type TeamCreateResult struct {
	TeamRef      string `json:"team_ref"`
	TeamName     string `json:"team_name"`
	JoinSecret   string `json:"join_secret"`
	PrincipalRef string `json:"principal_ref"`
}

TeamCreateResult is the `team create` result (4.4.10): the only command output in the whole protocol that carries a secret, shown once (4.5.14).

func (*TeamCreateResult) Validate

func (r *TeamCreateResult) Validate() error

Validate implements Validator.

type TeamJoinRequest

type TeamJoinRequest struct {
	JoinSecret string         `json:"join_secret"`
	HumanLabel string         `json:"human_label,omitzero"`
	Backend    jsontext.Value `json:"backend,omitzero"`
}

TeamJoinRequest is the `team join` stdin document (4.4.10). Backend is adapter-specific (Supabase: {url, publishable_key}) and accepted only when the profile has no backend configured; the protocol layer checks only that it is a JSON object.

func (*TeamJoinRequest) Validate

func (r *TeamJoinRequest) Validate() error

Validate implements Validator. Whether the secret itself is well-formed (`brg1.<team_ref>.…`) is the join-secret parser's business, not this shape's; a malformed secret still maps to invalid_input there (4.6).

type TeamJoinResult

type TeamJoinResult struct {
	TeamRef      string `json:"team_ref"`
	TeamName     string `json:"team_name"`
	PrincipalRef string `json:"principal_ref"`
	Rejoined     bool   `json:"rejoined"`
}

TeamJoinResult is the `team join` result (4.4.10). Rejoined true means the secret named the team the profile was already bound to and the same membership was re-activated (C-08).

func (*TeamJoinResult) Validate

func (r *TeamJoinResult) Validate() error

Validate implements Validator.

type TeamLeaveResult

type TeamLeaveResult struct {
	TeamRef      string `json:"team_ref"`
	PrincipalRef string `json:"principal_ref"`
	Left         bool   `json:"left"`
}

TeamLeaveResult is the `team leave` result (4.4.10). Left is always true — the command is idempotent and success is the only non-error outcome.

func (*TeamLeaveResult) Validate

func (r *TeamLeaveResult) Validate() error

Validate implements Validator.

type Validator

type Validator interface {
	// Validate checks required members, byte caps, code-point caps and
	// enumerated values, and returns a *Error with CodeInvalidInput
	// carrying the offending member's JSON name in Details["field"], or
	// nil when the value is a valid protocol document.
	Validate() error
}

A Validator is a wire type with a hand-written Validate method. Every shape of 4.4 implements it; Decode runs it after a loose parse.

type WatchAcked

type WatchAcked struct {
	Event      string   `json:"event"`
	MessageIDs []string `json:"message_ids"`
	Unknown    []string `json:"unknown"`
}

WatchAcked answers a stdin `ack` command (4.4.9). Both lists are always emitted, as [] when empty.

func (*WatchAcked) Validate

func (w *WatchAcked) Validate() error

Validate implements Validator.

type WatchCommand

type WatchCommand struct {
	Type              string   `json:"type"`
	MessageIDs        []string `json:"message_ids,omitzero"`
	Activity          *string  `json:"activity,omitzero"`
	SessionName       *string  `json:"session_name,omitzero"`
	Inbound           *string  `json:"inbound,omitzero"`
	LeaseSeconds      *int     `json:"lease_seconds,omitzero"`
	Model             *string  `json:"model,omitzero"`
	ContextUsedTokens *int     `json:"context_used_tokens,omitzero"`
	BrigadeVersion    *string  `json:"brigade_version,omitzero"`
	SyncPeer          *string  `json:"sync_peer,omitzero"`
}

WatchCommand is one NDJSON command on `message watch` stdin (4.4.9). A single struct covers all three types; the members beyond Type belong to the type Validate switches on. The heartbeat members are those of HeartbeatRequest (4.4.4) less session_description — model and context_used_tokens (C-44), brigade_version (C-46) and sync_peer (C-47) included, with the same rules.

func (*WatchCommand) Known

func (c *WatchCommand) Known() bool

Known reports whether the command type is one this protocol version defines. A receiver MUST ignore an unknown type (4.4.9) — check Known after Validate and drop unknown commands with a log line.

func (*WatchCommand) Validate

func (c *WatchCommand) Validate() error

Validate implements Validator. An unknown Type is VALID by design — 4.4.9 makes ignoring it the receiver's duty, so rejecting it here would turn forward compatibility into a fatal error. Only a missing type is invalid.

type WatchError

type WatchError struct {
	Event string      `json:"event"`
	Error ErrorObject `json:"error"`
}

WatchError is a fatal or transient watch failure (4.4.9). An error with retryable false is followed by process exit with the matching exit code.

func (*WatchError) Validate

func (w *WatchError) Validate() error

Validate implements Validator.

type WatchHeartbeatOK

type WatchHeartbeatOK struct {
	Event      string    `json:"event"`
	SessionID  string    `json:"session_id"`
	State      string    `json:"state"`
	LeaseUntil time.Time `json:"lease_until"`
	ServerTime time.Time `json:"server_time"`
}

WatchHeartbeatOK answers a stdin `heartbeat` command (4.4.9).

func (*WatchHeartbeatOK) Validate

func (w *WatchHeartbeatOK) Validate() error

Validate implements Validator.

type WatchMessage

type WatchMessage struct {
	Event   string          `json:"event"`
	Message MessageEnvelope `json:"message"`
}

WatchMessage delivers one accepted, unacknowledged message (4.4.9). The same message_id MAY be emitted more than once (4.5.2).

func (*WatchMessage) Validate

func (w *WatchMessage) Validate() error

Validate implements Validator. A failure inside the nested envelope is reported at its wire path, `message.<member>` (decision 3).

type WatchReady

type WatchReady struct {
	Event           string `json:"event"`
	ProtocolVersion string `json:"protocol_version"`
	SessionID       string `json:"session_id"`
	Mode            string `json:"mode"`
}

WatchReady is the one-time first watch event, emitted when the initial catch-up starts (4.4.9, C-33).

func (*WatchReady) Validate

func (w *WatchReady) Validate() error

Validate implements Validator.

type WatchStatus

type WatchStatus struct {
	Event  string `json:"event"`
	State  string `json:"state"`
	Detail string `json:"detail,omitzero"`
}

WatchStatus is an informational transport-state event (4.4.9).

func (*WatchStatus) Known

func (w *WatchStatus) Known() bool

Known reports whether the status state is one this protocol version defines (live or polling). A receiver MUST ignore an unknown state (4.4.9, decision 4).

func (*WatchStatus) Validate

func (w *WatchStatus) Validate() error

Validate implements Validator. State is required but deliberately not enumerated here: the event is informational and an unknown state is IGNORED by the reader, not rejected (decision 4) — check Known after Validate and drop unknown states with a log line, exactly as for an unknown command type.

Directories

Path Synopsis
Package schema generates the JSON Schema (draft 2020-12) document for the wire shapes of the Brigade Adapter Protocol v1 (plan 7.3): one document with a $defs entry per protocol type, reflected from the Go structs of internal/protocol with invopop/jsonschema and then patched where reflection cannot see the protocol's rules — the C-23 forbidden SendRequest members become `false` property schemas, pointer-typed members become nullable, and the 4.4.1 code-point caps become maxLength.
Package schema generates the JSON Schema (draft 2020-12) document for the wire shapes of the Brigade Adapter Protocol v1 (plan 7.3): one document with a $defs entry per protocol type, reflected from the Go structs of internal/protocol with invopop/jsonschema and then patched where reflection cannot see the protocol's rules — the C-23 forbidden SendRequest members become `false` property schemas, pointer-typed members become nullable, and the 4.4.1 code-point caps become maxLength.

Jump to

Keyboard shortcuts

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