adapterclient

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 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) 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