adapterclient

package
v0.31.0 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package adapterclient is the one place the harness spawns an adapter child (plan 3.4, 4.1, 6.6; brief section 2.2). Every request/response command goes through Client.Call and adapterkit.Spawn; the one long-running `message watch` child goes through Client.StartWatch and the single exec.CommandContext in spawn.go. No other harness package spawns an adapter.

The child environment is built FROM SCRATCH with adapterkit.ChildEnv: inherited BRIGADE_* and CLAUDE_CODE_MESSAGING_* never reach the child, only the allow-listed variables (PATH, HOME, TMPDIR, the proxy variables, SSL_CERT_*, XDG_*, CLAUDE_CONFIG_DIR) plus the four computed BRIGADE_PROFILE/CONFIG_DIR/STATE_DIR/LOG_LEVEL do. The child's stderr is captured to the adapter log, never surfaced, so a returned error carries only the adapter's own safe envelope message or adapterkit.Spawn's fixed classification — never raw adapter stderr (U-24).

A failing envelope the adapter PRODUCED is returned as a *protocol.Error alongside the envelope; an error the adapter did NOT produce (a timeout, a signal death, a missing executable, a runaway or unparseable stdout) is returned by Spawn with a nil envelope, so a caller can tell "the adapter said" from "the adapter broke" (4.6).

Index

Constants

View Source
const (
	// DefaultTimeout is the 20 s budget for an ordinary request/response
	// command.
	DefaultTimeout = 20 * time.Second
	// RegisterTimeout is the 8 s budget for `session register` at
	// SessionStart.
	RegisterTimeout = 8 * time.Second
	// CloseTimeout is the 1 s budget for `session close` at SessionEnd.
	CloseTimeout = 1 * time.Second
	// WatchRequestTimeout is the 3 s budget for a request the watcher
	// makes.
	WatchRequestTimeout = 3 * time.Second
	// DescribeTimeout is the 3 s cap on the cached describe probe.
	DescribeTimeout = 3 * time.Second
)

The 4.1 timeout budgets, exposed so callers wrap ctx with them; Call itself honours whatever deadline ctx already carries.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// Adapter is the resolved argv prefix (config.ResolveAdapter's result).
	Adapter config.Adapter
	// Profile, ConfigDir and StateDir are the computed BRIGADE_* values the
	// child's environment carries.
	Profile   string
	ConfigDir string
	StateDir  string
	// LogLevel is the child's BRIGADE_LOG_LEVEL; "" lets the adapter
	// default it.
	LogLevel string
	// Environ is the parent environment ChildEnv filters. Inherited
	// BRIGADE_* and CLAUDE_CODE_MESSAGING_* are dropped from it.
	Environ []string
	// Logger receives this package's scalar diagnostics; nil discards them.
	Logger *slog.Logger
	// Spawn is the request/response spawn seam: nil means
	// [adapterkit.Spawn], and production never sets it. A caller's test
	// injects a recorder here to assert exactly what every child would
	// have received — argv, environment, stdin — or that no child was
	// spawned at all (U-25), without starting a process. StartWatch is not
	// routed through it: the watch child is a real process by nature and
	// the fake adapter's dump file is its recorder.
	Spawn SpawnFunc
}

A Client spawns one adapter's children for one profile. The zero value is not usable; set Adapter, Profile, ConfigDir and StateDir at least.

func (*Client) Ack

func (c *Client) Ack(ctx context.Context, sessionID string, req *protocol.AckRequest) (*protocol.AckResult, error)

Ack runs `message ack --session <id>`.

func (*Client) Call

func (c *Client) Call(ctx context.Context, group, verb string, flags []string, stdin any) (*protocol.Envelope, error)

Call runs one request/response adapter command and returns its envelope.

  • group/verb name the command (verb is "" for `describe`); flags are the argv flags (e.g. --session, --include-offline); stdin is the JSON input document, or nil for a command that takes none.
  • On a successful spawn that produced an OK envelope: (envelope, nil).
  • On a failing envelope the adapter PRODUCED: (envelope, *protocol.Error) — the code, message, retry_after_ms and details from the wire, with an unrecognised code normalised to `internal`; retryability is the caller's to recompute from the code (4.3).
  • On an error the adapter did NOT produce (a timeout, a signal death, a missing executable, a runaway or unparseable stdout): (nil, err) as adapterkit.Spawn returns it.

The deadline is ctx's: wrap it with one of the package timeout constants.

func (*Client) Close

func (c *Client) Close(ctx context.Context, sessionID string) (*CloseResult, error)

Close runs `session close --session <id>` (SessionEnd; CloseTimeout).

func (*Client) Describe

func (c *Client) Describe(ctx context.Context) (*protocol.DescribeResult, error)

Describe returns the adapter's describe result, cached per adapter argv, and performs the protocol check: a describe whose protocol_version is not this harness's is protocol_mismatch (exit 10), carrying the adapter's own name and version in details for `whoami`. The cached result serves the capability checks P3-3/P3-5 make.

The deadline is ctx's, like Client.Call: a caller wraps it with DescribeTimeout (the 3 s describe cap of 4.1). Deferring the cap to the caller keeps the one describe per command bounded in production without making a loaded test's spawn race a 3 s stopwatch.

func (*Client) Heartbeat

func (c *Client) Heartbeat(ctx context.Context, sessionID string, hb *protocol.HeartbeatRequest) (*protocol.HeartbeatResult, error)

Heartbeat runs `session heartbeat --session <id>`.

func (*Client) ListSessions

func (c *Client) ListSessions(ctx context.Context, includeOffline bool) (*ListResult, error)

ListSessions runs `session list [--include-offline]`.

func (*Client) OwnDescription added in v0.7.0

func (c *Client) OwnDescription(ctx context.Context, sessionID string) (text string, listed bool, err error)

OwnDescription reads back the session_description the backend holds for sessionID (card 25, plan 5.3): one `session list --include-offline` — offline included, because the watcher asks right after its session was closed under it — and the raw value of the record whose session_id matches, or "" when that record carries none or an empty one. listed says whether the record was in the list at all: a truncated list (a team past the adapter's cap, plan 8) can lack an old closed record, and the caller must tell "nothing held" from "nothing seen", because a resume that omits the member writes that absence over whatever the backend holds. Only the list call's own failure is an error. The value is returned as the backend gave it; the caller cleans it (doing.Clean) before it travels anywhere.

func (*Client) PassThrough

func (c *Client) PassThrough(ctx context.Context, group, verb string, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error)

PassThrough runs one adapter command with the CALLER's streams instead of captured ones: the human terminal commands of 6.4 (`brigade team create|join|leave`, `brigade profile …`) hand their stdin, stdout and stderr straight to the adapter, so `--prompt` can read a join secret from the TTY without echo and the adapter's own envelope reaches the human unchanged. Nothing is parsed here: the argv is `<adapter prefix> --profile <p> <group> <verb> <args…>` and the child's environment is the same from-scratch one every other child gets (inherited BRIGADE_* and CLAUDE_CODE_MESSAGING_* never cross). stdin may be nil for a command that takes none; an *os.File is handed to the child as a descriptor, which is what keeps a terminal a terminal.

It returns the child's own exit status, forwarded verbatim (4.6 gives it meaning) with a nil error. The error is non-nil only when the adapter did NOT run to an exit of its own — the executable was not found ( `unavailable`, details.reason adapter_not_found), it died on a signal or ctx ended (`unavailable`), or the start failed (`internal`) — and the status is then -1. It is the one other exec.CommandContext of the harness beside StartWatch, which is why it lives in this file.

func (*Client) Receive

func (c *Client) Receive(ctx context.Context, sessionID string, limit int) (*ReceiveResult, error)

Receive runs `message receive --session <id> [--limit <n>]`.

func (*Client) Register

Register runs `session register` (SessionStart). The caller sets ctx's deadline (RegisterTimeout).

func (*Client) Send

Send runs `message send` (the sender is a member of the request).

func (*Client) StartWatch

func (c *Client) StartWatch(ctx context.Context, sessionID string) (*Watch, error)

StartWatch spawns `message watch --session <id>` with stdin and stdout pipes and the same from-scratch environment as Client.Call. It returns once the child has started; events arrive on Events.

func (*Client) TeamMembers

func (c *Client) TeamMembers(ctx context.Context) (*MembersResult, error)

TeamMembers runs `team members` (capability team.roster).

type CloseResult

type CloseResult struct {
	SessionID string `json:"session_id"`
	State     string `json:"state"`
}

CloseResult is the `session close` result (4.4.3).

type Event

type Event struct {
	Kind        EventKind
	Ready       *protocol.WatchReady
	Message     *protocol.MessageEnvelope
	Status      *protocol.WatchStatus
	Acked       *protocol.WatchAcked
	HeartbeatOK *protocol.WatchHeartbeatOK
	Error       *protocol.ErrorObject
}

An Event is one decoded, validated `message watch` event (4.4.9). Exactly one typed member is set, selected by Kind. The supervision, backoff and restart policy of 6.6 is P3-5's; this seam only decodes and delivers.

type EventKind

type EventKind int

An EventKind names which watch event a decoded Event carries.

const (
	KindReady EventKind = iota + 1
	KindMessage
	KindStatus
	KindAcked
	KindHeartbeatOK
	KindError
)

The event kinds a Watch delivers. Unknown wire kinds are never delivered — they are logged and skipped (B-5).

type ListResult

type ListResult struct {
	TeamRef    string                   `json:"team_ref"`
	TeamName   string                   `json:"team_name"`
	ServerTime time.Time                `json:"server_time"`
	Sessions   []protocol.SessionRecord `json:"sessions"`
	Truncated  bool                     `json:"truncated"`
}

ListResult is the `session list` result (4.4.3).

type Member

type Member struct {
	PrincipalRef string     `json:"principal_ref"`
	HumanLabel   string     `json:"human_label,omitzero"`
	Status       string     `json:"status"`
	JoinedAt     time.Time  `json:"joined_at"`
	LastSeenAt   *time.Time `json:"last_seen_at"`
	SessionCount int        `json:"session_count"`
}

Member is one roster entry of the `team members` result (4.4.10).

type MembersResult

type MembersResult struct {
	TeamRef    string    `json:"team_ref"`
	TeamName   string    `json:"team_name"`
	ServerTime time.Time `json:"server_time"`
	Members    []Member  `json:"members"`
}

MembersResult is the `team members` result (4.4.10, C-43).

type ReceiveResult

type ReceiveResult struct {
	Messages []protocol.MessageEnvelope `json:"messages"`
}

ReceiveResult is the `message receive` result (4.4.5).

type RegisterResult

type RegisterResult struct {
	protocol.SessionRecord
	Resumed      bool      `json:"resumed"`
	LeaseSeconds int       `json:"lease_seconds"`
	ServerTime   time.Time `json:"server_time"`
}

RegisterResult is the `session register` result (4.4.2): a SessionRecord with the three registration-only members.

type SpawnFunc

A SpawnFunc has adapterkit.Spawn's shape.

type Watch

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

A Watch is one running `message watch` child. Events are delivered on Watch.Events until the stream ends; commands are written with Watch.Ack, Watch.Heartbeat and Watch.Close; Watch.Wait reaps the child and reports its exit status. Cancel the ctx passed to StartWatch to stop the child (SIGTERM, then SIGKILL after five seconds).

Delivery is unbuffered: the reader blocks until the caller takes each event. Once ctx is cancelled the reader stops delivering and drains the stream to EOF instead, so the stop sequence — cancel, then Wait — never deadlocks on events the caller no longer reads; a message that was not delivered is not acknowledged and the server redelivers it. A caller that has NOT cancelled ctx must consume Events until it closes before Wait can return, exactly as with an exec.Cmd stdout pipe.

func (*Watch) Ack

func (w *Watch) Ack(ids []string) error

Ack writes an `ack` command on the child's stdin.

func (*Watch) Close

func (w *Watch) Close() error

Close writes a `close` command, which ends the watch (the child exits 0).

func (*Watch) Events

func (w *Watch) Events() <-chan Event

Events delivers each decoded event. The channel closes when the child's stdout reaches EOF or fails.

func (*Watch) Heartbeat

func (w *Watch) Heartbeat(cmd protocol.WatchCommand) error

Heartbeat writes a `heartbeat` command; Type is set for the caller.

func (*Watch) Wait

func (w *Watch) Wait() (int, error)

Wait waits for the event stream to end (the caller has consumed it, or ctx was cancelled and the reader drained it), reaps the child and returns its exit status. A child that exited by itself — 0 or a 4.6 status, including a 0 after the SIGTERM a cancel sends — reports that status with a nil error. (-1, err) means the child did NOT exit by itself: it was killed by a signal (the SIGKILL that follows a cancel by WaitDelay, or a death of its own) or the spawn failed at wait level. Wait is idempotent, so a test may both call it and register it in a cleanup; it must be called, since it is what reaps the child and closes the adapter log.

Jump to

Keyboard shortcuts

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