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
- Variables
- func Decode(data []byte, v Validator) error
- func Sanitize(s string) string
- func SanitizeAttribute(s string) string
- func SanitizeBody(s string) string
- func SanitizeDescription(s string) string
- func SanitizeLabel(s string) string
- func SanitizeModel(s string) string
- func SanitizeName(s string) string
- func SanitizeSummary(s string) string
- func TruncateRunes(s string, limit int) string
- func Unmarshal(data []byte, v any) error
- type AckRequest
- type AckResult
- type AdapterInfo
- type Code
- type DeliveryInfo
- type DescribeResult
- type Envelope
- type Error
- type ErrorObject
- type HeartbeatRequest
- type HeartbeatResult
- type JoinSecret
- type Lease
- type Limits
- type LineReader
- type LineWriter
- type MessageEnvelope
- type ProfileInfo
- type ResumeRef
- type Retention
- type SendRate
- type SendRequest
- type SendResponse
- type Sender
- type SessionRecord
- type SessionRegistration
- type TeamCreateRequest
- type TeamCreateResult
- type TeamJoinRequest
- type TeamJoinResult
- type TeamLeaveResult
- type Validator
- type WatchAcked
- type WatchCommand
- type WatchError
- type WatchHeartbeatOK
- type WatchMessage
- type WatchReady
- type WatchStatus
Constants ¶
const ( ProfileStateUnconfigured = "unconfigured" ProfileStateUnauthenticated = "unauthenticated" ProfileStateNotMember = "not_member" ProfileStateJoined = "joined" )
The `profile.state` values of DescribeResult (4.4.1), computed from local files only.
const ( AckStateInjected = "injected" AckStateProcessed = "processed" )
The `delivery.ack_state` values: `injected` is the terminal adapter state in v1; `processed` is reserved (4.5.3).
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.
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.
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.
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).
const ( ActivityBusy = "busy" ActivityIdle = "idle" )
The `activity` values a harness reports (4.4.2, 4.5.8).
const ( InboundAccept = "accept" InboundHold = "hold" InboundRefuse = "refuse" )
The `inbound` policy values (4.4.2).
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.
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.
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.
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.
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.
const ExitOK = 0
ExitOK is the exit status of a command that succeeded.
const GuaranteeAtLeastOnce = "at_least_once"
GuaranteeAtLeastOnce is the only delivery guarantee of protocol v1 (freeze list item 4).
const JoinSecretPrefix = "brg1."
JoinSecretPrefix is the fixed, case-sensitive prefix of every v1 join secret (4.4.10, D5).
const KindText = "text"
KindText is the only message kind of protocol v1 (4.5.11).
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.
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).
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).
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.
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).
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).
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
SanitizeBody sanitises a message body: rules 1-3 with the MaxBodyBytes byte cap.
func SanitizeDescription ¶
SanitizeDescription sanitises a session description: rules 1-3 with the MaxDescriptionChars code-point cap.
func SanitizeLabel ¶
SanitizeLabel sanitises a human label: rules 1-3 with the MaxHumanLabelChars code-point cap.
func SanitizeModel ¶ added in v0.5.0
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 ¶
SanitizeName sanitises a session or team name: rules 1-3 with the MaxSessionNameCodepoints code-point cap.
func SanitizeSummary ¶
SanitizeSummary sanitises a sender summary: rules 1-3 with the MaxSummaryChars code-point cap.
func TruncateRunes ¶ added in v0.7.0
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 ¶
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.
type AckResult ¶
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.
type AdapterInfo ¶
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" CodeNotFound Code = "not_found" CodeConflict Code = "conflict" CodeRateLimited Code = "rate_limited" CodeProtocolMismatch Code = "protocol_mismatch" CodeConfig Code = "config" CodeLoopDetected Code = "loop_detected" )
The 4.6 error codes, in exit-code order.
func (Code) Exit ¶
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.
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.
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) 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 ¶
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 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.
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.
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).
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.
Source Files
¶
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. |