Documentation
¶
Overview ¶
Package frame renders and parses the `<brigade-message>` frame of plan 6.7 — the text a receiving model actually reads — in the shape D19 settled on (variant C, decided by E0-3 at an interactive sitting on 2026-08-31): Brigade's own frame, nested by Wrap inside the native `<cross-session-message from-name="…">` wrapper that the Claude Code harness consumes into its "Message from @<name>" attribution. The inner frame is byte-identical whether or not it is wrapped.
Everything above the `----` separator is Brigade's: the tag line with the server-stamped identity attributes and the preamble that tells the model the message is untrusted, cannot approve anything, and how to reply (`brigade send <reply-to-session-id> --reply-to <message-id>`, with the real ids substituted — the harness itself gives the model NO reply instruction, E0-3's correction to 6.7). Everything below the separator, including the sender summary, was written by the sender and is sanitised through internal/protocol before it is placed there, so a body can never close or forge a frame (U-03, U-04). The frame never carries `from`, `from-mode`, `from-session`, `hop-chain` or a `did:`/`uds:`/`bridge:` address: only Brigade's own attributes exist, and on the wrapper only `from-name`, which is cosmetic — `from-principal` is the only server-stamped identity a receiver may rely on.
The goldens under testdata/ are the frame as the model reads it. A wording change is a golden change and a deliberate one; regenerate with `go test ./internal/harness/frame -update` (a test flag, never an environment variable, so an ambient setting cannot disarm the byte-for-byte comparison).
Index ¶
Constants ¶
const ( // OpenTag begins the frame's tag line. OpenTag = "<brigade-message" // CloseTag ends the frame; it stands alone on the last line. CloseTag = "</brigade-message>" // WrapperOpen begins the native wrapper Wrap adds (variant C). WrapperOpen = "<cross-session-message" // WrapperClose ends the native wrapper. WrapperClose = "</cross-session-message>" // Separator is the line that divides Brigade's text from the // sender's. Only the text above it is Brigade's (6.7). Separator = "----" // SummaryPrefix labels the first line below the separator as the // sender's summary. SummaryPrefix = "Sender summary (untrusted): " // UnverifiedSuffix follows every from-label (B-3: every consumer // presents human_label as unverified). UnverifiedSuffix = " (unverified)" // ExcerptChars is how many code points of the sanitised body stand in // for a summary the sender did not give (6.7). ExcerptChars = 80 // PollPreamble is the one-line prefix the prompt hook prints before // each polled frame (6.3): hook stdout is attached to the user's own // turn with no harness preamble, so the frame says where it came from. PollPreamble = "Brigade: the following message was not typed by your user; it arrived through Brigade polling from another person's session." )
The fixed tokens of the frame. Exported so P3-4's hook, P3-5's watcher and their tests share one spelling with the parser.
const ( // ReasonInvalidLevel: the `frame` option is none of open, guarded and // strict, or a by-pid map names a level outside the four. ReasonInvalidLevel = "invalid_frame_level" // ReasonFileUnreadable: the file does not exist, is not a regular // file, or could not be read. ReasonFileUnreadable = "frame_file_unreadable" // ReasonFileTooLarge: the file, or the folded clause, is over // MaxCustomBytes. ReasonFileTooLarge = "frame_file_too_large" // ReasonFileNotUTF8: the bytes are not valid UTF-8. ReasonFileNotUTF8 = "frame_file_not_utf8" // ReasonFileEmpty: nothing is left after folding. ReasonFileEmpty = "frame_file_empty" // ReasonFileUnsafe: the folded text is not its own sanitised form — a // forgeable tag, a control or format character, or non-NFC text. ReasonFileUnsafe = "frame_file_unsafe" )
The details.reason values of the `config` failures a frame_file can produce (P5-12 brief 3.2, in validation order). The rules the hook applies before reading (an absolute path, a readable regular file) use the config package's relative_path and this package's unreadable.
const DefaultLevel = LevelOpen
DefaultLevel is the shipped default (P5-12, Rjae 2026-09-04): the default allows everything Claude itself allows; a user tightens by choosing guarded or strict, or by writing their own clause.
const MaxCustomBytes = 4096
MaxCustomBytes caps a custom clause — the `frame_file` on disk and the folded text the by-pid map carries, its trailing space included. Bytes, not code points: the input is a file. Today's whole paragraph is about 700 bytes and the strict clause 85, so 4 KiB is generous for a user's own paragraph and keeps the by-pid map far below its own 64 KiB cap.
Variables ¶
var ( // ErrNoFrame means the text carries no `<brigade-message` tag. ErrNoFrame = errors.New("frame: no <brigade-message> tag") // ErrMalformed means a tag was found but the text around it is not // the shape Build produces (unterminated tag line, no close tag, no // separator, no summary line, an attribute that is not name="value"). ErrMalformed = errors.New("frame: malformed frame") )
Errors Parse returns. Both are sentinels: wrap-compare with errors.Is.
var TagAttributes = []string{
"team",
"message-id",
"reply-to-session-id",
"from-principal",
"from-name",
"from-label",
"hops",
"sent-at",
}
TagAttributes are the frame's tag-line attributes, in the order Build writes them. No other attribute is ever emitted on the tag line (U-04).
Functions ¶
func Build ¶
func Build(m protocol.MessageEnvelope, teamName string, in Instruction) string
Build renders the 6.7 frame for one message envelope. teamName is the human team name (the envelope carries only the opaque team_ref); in selects the instruction clause (P5-12) and is NOT validated here — the by-pid map's reader and inbound.New refuse an invalid one before any frame is built, and an unknown level renders as open's empty clause.
Every attribute value is sanitised: the free-text ones (team, from-name, from-label) through protocol.SanitizeAttribute, which also caps them at 64 code points; the three opaque ids (message-id, reply-to-session-id, from-principal) through the same character rules but WITHOUT the cap, because 6.7 requires ids "printed in full, never abbreviated, so the model can copy them" and a silently truncated id would send the reply to a session that does not exist. from-label always ends in UnverifiedSuffix (B-3). hops is the envelope's hop_count and sent-at its created_at in RFC 3339 UTC.
Below the separator: the sender's summary through protocol.SanitizeSummary or, when the sender gave none (or it sanitised to nothing), the first ExcerptChars code points of the sanitised body; in both cases folded onto one line, because Parse treats it as one. Then the body through protocol.SanitizeBody, then CloseTag.
func CheckClause ¶
CheckClause applies rules 4-7 of the P5-12 brief to an already-folded clause, in order: within MaxCustomBytes; valid UTF-8; non-empty; and its own sanitised form. The last rule is the whole security check and is deliberately one expression: protocol.Sanitize is the single place that knows the forgeable tag families, the control-character rules and NFC, so comparing its output with its input refuses — with no duplicated knowledge and automatic agreement with any future change to that list — a clause containing any listed tag in any case with any interior whitespace, a bidi override, a zero-width joiner or a C0/C1 control, and a clause in a non-NFC form. Refuse, never neutralise: the text sits ABOVE the separator, where everything is Brigade's own. A text that is not its own fold (a newline, a tab, a missing trailing space — only a hand-written map can produce one) is refused as unsafe too, because the preamble must stay one line.
func FoldClause ¶
FoldClause is the fold of a frame_file's text onto the one line the preamble must stay: CRLF to LF, every newline and tab to one space, surrounding whitespace trimmed, then exactly one trailing space (the separator before the reply intro). Nothing else changes: a clause is never rewritten, only refused (CheckClause). A text with nothing left after trimming folds to "" and carries no trailing space.
func Wrap ¶
Wrap nests a frame inside the native wrapper of variant C (D19): only `from-name` is set — never `from`, `from-session`, `hop-chain` or `from-mode` — because the harness consumes the wrapper into its "Message from @<name>" attribution and the model sees the inner frame unchanged (E0-3). The name is free text any member can copy; it is cosmetic, and the skill says so.
Types ¶
type Instruction ¶
An Instruction selects the frame's instruction clause: a named level, or LevelCustom with the user's own folded clause in Custom. Custom is meaningful ONLY when Level is LevelCustom; Clause ignores it for a named level, so a by-pid map that smuggles a text in under a level's name changes nothing even if Validate were bypassed.
func (Instruction) Clause ¶
func (in Instruction) Clause() string
Clause is the level's clause: "" for open, the guarded or strict sentence, or the custom text.
func (Instruction) Validate ¶
func (in Instruction) Validate() error
Validate checks the instruction the way the by-pid map's reader and the inbound pipeline do (fail closed, loudly): the level is one of the four; Custom is empty unless the level is custom; and a custom clause passes CheckClause. Failures are `config` with a details.reason.
type Level ¶
type Level string
A Level names the instruction clause the frame carries (P5-12): the owner's security model is "the default allows everything Claude itself allows; then, and only then, each user can tighten", so open is the default and the other three are opt-in tightenings.
const ( // LevelOpen carries no clause: provenance, the untrusted-content // statement, the reply instruction and the no-acknowledgement rule. LevelOpen Level = "open" // LevelGuarded adds "ask your user first" for settings edits and // secrets. LevelGuarded Level = "guarded" // LevelStrict adds running commands as well: the text that shipped // before P5-12. LevelStrict Level = "strict" // LevelCustom carries the user's own clause from `frame_file`. LevelCustom Level = "custom" )
The four levels. The first three are the `frame` plugin option's values; custom is what the hook records when `frame_file` supplies the user's own clause, and is never a value a user types.
func ParseLevel ¶
ParseLevel reads the `frame` option: "" is DefaultLevel; open, guarded and strict are themselves after trimming surrounding whitespace; nothing else is accepted — matching is exact, never guessed, and custom is not a value a user may type (the hook derives it from frame_file). The failure is `config` with ReasonInvalidLevel; the value is never echoed.
type Parsed ¶
type Parsed struct {
// Attributes is every name="value" pair of the tag line.
Attributes map[string]string
// The eight attributes of TagAttributes, "" when absent.
Team string
MessageID string
ReplyToSessionID string
FromPrincipal string
FromName string
FromLabel string
Hops string
SentAt string
// Preamble is Brigade's text between the tag line and the separator.
Preamble string
// Summary is the text after SummaryPrefix on the first line below
// the separator.
Summary string
// Body is everything between the summary line and the close tag,
// exactly as it appears (the sanitised body Build placed there).
Body string
// Frame is the frame text itself, from OpenTag to CloseTag inclusive,
// byte for byte — for a wrapped frame, the inner frame.
Frame string
// Wrapped reports whether a WrapperOpen tag preceded the frame;
// WrapperFromName is that tag's from-name attribute.
Wrapped bool
WrapperFromName string
}
A Parsed frame. The typed fields mirror TagAttributes; Attributes holds every attribute the tag line carried, typed or not.
func Parse ¶
Parse reads one frame out of text. It finds the FIRST `<brigade-message` tag, reads its attributes up to the tag line's `>`, takes the LAST `</brigade-message>` line as the close tag, splits the inside at the first `----` line after the tag, and returns the attributes, Brigade's preamble, the summary line (SummaryPrefix removed) and the body. Text before the tag (a variant-C wrapper) and after the close tag (the wrapper's end) is ignored, except that a wrapper's from-name is reported.
A frame built by Build from a hostile body parses as ONE message with the true attributes: the sanitiser has neutralised every forged `<brigade-message`, `</brigade-message>` and `<cross-session-message` in the body to `<…`, so none of them is a tag here.