gateway

package module
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 8 Imported by: 0

README

squadron-gateway-sdk

Go SDK for building Squadron gateways: subprocess integrations that bridge a running squadron to an external system (Discord, Slack, Microsoft Teams, custom dashboards, …).

Concept

Gateways are squadron's pluggable integration surface. Squadron launches a gateway as a managed subprocess (same install/version lifecycle as plugins) and connects to it over a bidirectional gRPC channel:

  • Squadron → Gateway: pushes events (OnHumanInputRequested, OnHumanInputResolved, OnNotification, PostMessage, …) so the gateway can mirror state to its external system. OnNotification is a one-way mission-lifecycle post (mission_completed / mission_failed); PostMessage posts an agent-authored message (backs the builtins.gateway.post tool). The gateway owns the post-message contract: MessageToolSpec returns the tool description + a JSON Schema squadron shows the LLM, and PostMessage receives the raw JSON the agent produced for that schema, so each gateway defines exactly the rich-message shape it accepts (text, embeds/blocks, attachments).
  • Gateway → Squadron: pulls / mutates state (ListHumanInputs, ResolveHumanInput, …) so user actions in the external system flow back to squadron.

Hello-world

package main

import (
    "context"

    gateway "github.com/mlund01/squadron-gateway-sdk"
)

type myGateway struct{ api gateway.SquadronAPI }

func (g *myGateway) Configure(ctx context.Context, settings map[string]string, api gateway.SquadronAPI) error {
    g.api = api
    // catch up on anything that happened while we were down
    rows, _, err := api.ListHumanInputs(ctx, gateway.HumanInputFilter{OldestFirst: true})
    if err != nil { return err }
    for _, r := range rows { g.show(r) }
    return nil
}

func (g *myGateway) OnHumanInputRequested(ctx context.Context, rec gateway.HumanInputRecord) error {
    g.show(rec)
    return nil
}

func (g *myGateway) OnHumanInputResolved(ctx context.Context, rec gateway.HumanInputRecord) error {
    g.markAnswered(rec)
    return nil
}

func (g *myGateway) OnNotification(ctx context.Context, rec gateway.NotificationRecord) error {
    // one-way mission-lifecycle post; rec.Channel optionally overrides the
    // destination channel
    g.post(rec)
    return nil
}

func (g *myGateway) PostMessage(ctx context.Context, req gateway.PostMessageRequest) error {
    // req.Payload is the raw JSON the agent produced for MessageToolSpec's
    // schema — parse and render it however this gateway sees fit
    g.send(req.Payload)
    return nil
}

func (g *myGateway) MessageToolSpec(ctx context.Context) (gateway.MessageToolSpec, error) {
    // tell the LLM how to format a message for this gateway
    return gateway.MessageToolSpec{
        Description:  "Post a message. `text` supports markdown.",
        ParamsSchema: `{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}`,
    }, nil
}

func (g *myGateway) Shutdown(ctx context.Context) error { return nil }

func (g *myGateway) show(_ gateway.HumanInputRecord)         {}
func (g *myGateway) markAnswered(_ gateway.HumanInputRecord) {}
func (g *myGateway) post(_ gateway.NotificationRecord)       {}
func (g *myGateway) send(_ string)                           {}

func main() { gateway.Serve(&myGateway{}) }

Catch-up after disconnects

Squadron pushes events live, but a gateway subprocess can crash or restart. The pattern is:

  1. Persist the latest event timestamp the gateway has processed.
  2. On Configure, call ListHumanInputs with that timestamp as Since and replay anything you missed.
  3. Live OnHumanInput* events resume after Configure returns.

ResolveHumanInput is idempotent — calling it on an already-resolved request returns AlreadyResolved=true with the prior resolution intact.

Distribution

Squadron downloads a gateway's release archive from GitHub on first load (same lifecycle as native plugins). The contract:

  • Archive: <repo>_<GOOS>_<GOARCH>.tar.gz (or .zip on Windows).
  • Inside: one gateway binary at the root.
  • Sibling: checksums.txt with sha256 hashes.

For local development, set version = "local" in your squadron config and place the binary at .squadron/gateways/<platform>/<name>/local/gateway.

See also

Documentation

Overview

Package gateway is the SDK for building Squadron gateways: subprocess integrations that bridge Squadron to external systems (Discord, Slack, PagerDuty, custom dashboards, …).

Squadron loads a gateway the same way it loads a plugin — declared in HCL with `gateway "name" { source = "github.com/..."; version = "..." }`, downloaded from the GitHub release on first load, and run as a managed subprocess for the lifetime of the squadron process.

The protocol is bidirectional gRPC:

  • Squadron pushes events to the gateway (OnHumanInputRequested, OnHumanInputResolved, …) so the gateway can mirror Squadron state into its external system.
  • The gateway pulls / mutates state by calling Squadron's API (ListHumanInputs, ResolveHumanInput, …) so user actions in the external system flow back to Squadron.

Catch-up after disconnects is the gateway's responsibility: persist a checkpoint timestamp locally, and on startup call ListHumanInputs with `since=<checkpoint>` to backfill anything missed before live pushes resume.

Index

Constants

View Source
const (
	HandshakeProtocolVersion  uint = 1
	HandshakeMagicCookieKey        = "SQUADRON_GATEWAY"
	HandshakeMagicCookieValue      = "squadron-gateway-v1"
)

Handshake config — must match between squadron's loader and any gateway binary that wants squadron to consume it.

Squadron runs hashicorp/go-plugin's handshake against this; mismatched magic cookies cause the subprocess to abort cleanly with a helpful error rather than running detached.

View Source
const MaxGRPCMessageBytes = 32 << 20 // 32 MiB

MaxGRPCMessageBytes is the gRPC message-size limit for the gateway<->squadron channel. It is raised well above gRPC's 4 MB default so file attachments (shipped as raw bytes in PostMessageRequest) fit. Squadron caps individual attachments below this.

View Source
const PluginName = "gateway"

PluginName is the well-known plugin name registered with go-plugin. Both sides reference the same string when looking up the gRPC plugin.

Variables

This section is empty.

Functions

func HostHandshake

func HostHandshake() plugin.HandshakeConfig

HostHandshake returns the handshake config the host should use when constructing a plugin.Client. Symmetric with the value the gateway uses on Serve — mismatched values prevent the connection.

func Serve

func Serve(impl Gateway)

Serve is the entry point for a gateway binary. Call it from main() after constructing the gateway implementation. Serve blocks until squadron tears down the subprocess.

Gateway authors do not need to know anything about hashicorp/go-plugin or gRPC — the SDK handles handshake, broker setup, and the bidirectional connection to squadron's SquadronAPI service.

Types

type FileAttachment added in v0.0.3

type FileAttachment struct {
	Filename string
	MimeType string
	Content  []byte
}

FileAttachment is a squadron-local file (memory/scratchpad/packet) that squadron has already read and is shipping as raw bytes for the gateway to upload to its external system.

type GRPCGatewayClient

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

GRPCGatewayClient is the host-side handle on a running gateway. It is returned from plugin.NewClient(...).Client().Dispense(...) and satisfies the GatewayClient interface that squadron uses.

func (*GRPCGatewayClient) Configure

func (g *GRPCGatewayClient) Configure(ctx context.Context, settings map[string]string) error

Configure pushes settings to the gateway and tells it the broker stream id where SquadronService is being served. After this returns successfully the gateway has a working SquadronAPI client and is ready to receive event pushes.

func (*GRPCGatewayClient) MessageToolSpec added in v0.0.2

func (g *GRPCGatewayClient) MessageToolSpec(ctx context.Context) (MessageToolSpec, error)

MessageToolSpec fetches the post-tool spec from the gateway.

func (*GRPCGatewayClient) OnHumanInputRequested

func (g *GRPCGatewayClient) OnHumanInputRequested(ctx context.Context, rec HumanInputRecord) error

OnHumanInputRequested forwards a new request event to the gateway.

func (*GRPCGatewayClient) OnHumanInputResolved

func (g *GRPCGatewayClient) OnHumanInputResolved(ctx context.Context, rec HumanInputRecord) error

OnHumanInputResolved forwards a resolution event to the gateway.

func (*GRPCGatewayClient) OnNotification added in v0.0.2

func (g *GRPCGatewayClient) OnNotification(ctx context.Context, rec NotificationRecord) error

OnNotification forwards a mission-lifecycle notification to the gateway.

func (*GRPCGatewayClient) PostMessage added in v0.0.2

func (g *GRPCGatewayClient) PostMessage(ctx context.Context, req PostMessageRequest) error

PostMessage forwards a post request to the gateway.

func (*GRPCGatewayClient) Shutdown

func (g *GRPCGatewayClient) Shutdown(ctx context.Context) error

Shutdown asks the gateway to clean up before squadron kills the subprocess. Best-effort — squadron should tear down the subprocess regardless of return value.

type Gateway

type Gateway interface {
	// Configure runs once at gateway startup. Settings come from the
	// HCL `settings = { ... }` map. The api handle is squadron's
	// SquadronAPI implementation reachable over the broker stream.
	//
	// Implementations should perform their startup catch-up here
	// (ListHumanInputs with a `Since` derived from local checkpoint).
	Configure(ctx context.Context, settings map[string]string, api SquadronAPI) error

	// OnHumanInputRequested is invoked once per new request that
	// squadron observes. Gateways post the question to their external
	// system and arrange for the user's reply to flow back via
	// SquadronAPI.ResolveHumanInput.
	OnHumanInputRequested(ctx context.Context, rec HumanInputRecord) error

	// OnHumanInputResolved is invoked once per resolution event,
	// regardless of who originated the resolution (this gateway, a
	// commander operator, another gateway, the agent timing out).
	// Gateways update their external surface to reflect the answer.
	OnHumanInputResolved(ctx context.Context, rec HumanInputRecord) error

	// OnNotification is invoked when a mission reaches a terminal state
	// (completed, failed, stopped) and the mission opted into gateway
	// notifications. Gateways post an informational message to their
	// external system; there is nothing for the user to act on.
	OnNotification(ctx context.Context, rec NotificationRecord) error

	// PostMessage posts a message to the gateway's external system. The
	// payload is the raw, gateway-schema-shaped JSON the agent produced for
	// the builtins.gateway.post tool; the gateway parses it itself.
	PostMessage(ctx context.Context, req PostMessageRequest) error

	// MessageToolSpec returns the description + optional JSON Schema squadron
	// uses to present the builtins.gateway.post tool to the LLM. Return a zero
	// MessageToolSpec to accept squadron's default { message } shape.
	MessageToolSpec(ctx context.Context) (MessageToolSpec, error)

	// Shutdown is invoked once when squadron is tearing the subprocess
	// down. Release external resources here (close the Discord
	// session, flush queues, persist checkpoint).
	Shutdown(ctx context.Context) error
}

Gateway is the contract every gateway implementation must satisfy. Squadron calls these methods over the gRPC subprocess channel.

The squadron-supplied SquadronAPI is delivered to the gateway via Configure. Implementations should hold onto it for the lifetime of the gateway and use it to pull state on startup (catch-up) and to write resolutions when the external system's user answers a request.

type HostPlugin

type HostPlugin struct {
	plugin.Plugin
	// API is squadron's implementation of the SquadronAPI surface. The
	// host plugin registers it on a broker stream and hands the stream
	// id to the gateway during Configure so the gateway can dial back.
	API SquadronAPI
}

HostPlugin is the variant of the gRPC plugin used by the host (squadron) to talk to a launched gateway subprocess. Construct it with the SquadronAPI implementation you want the gateway to call back into; the host side automatically registers that on a broker stream and passes the stream id to the gateway in Configure.

func (*HostPlugin) GRPCClient

func (h *HostPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error)

func (*HostPlugin) GRPCServer

func (h *HostPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error

func (*HostPlugin) PluginMap

func (h *HostPlugin) PluginMap() map[string]plugin.Plugin

PluginMap returns the plugin map for plugin.NewClient. Always pairs PluginName with this HostPlugin so squadron and the gateway agree on the registration key.

type HumanInputFilter

type HumanInputFilter struct {
	// State narrows by lifecycle state. Empty means both open + resolved.
	State HumanInputState
	// MissionID restricts to a specific mission run.
	MissionID string
	// Since is the catch-up cursor: rows whose requested_at OR
	// resolved_at is at or after this time. Use for reconnect / startup
	// backfill — pass the latest event timestamp the gateway has
	// already processed locally.
	Since time.Time
	// OldestFirst flips the default newest-first ordering. Use this
	// when replaying for catch-up so events are processed in the order
	// they happened.
	OldestFirst bool
	// Limit caps the page size. 0 means "use server default".
	Limit int
	// Offset skips that many results from the start of the page.
	Offset int
}

HumanInputFilter narrows a ListHumanInputs call. Zero-valued fields are not applied (so a fresh HumanInputFilter{} returns everything).

type HumanInputRecord

type HumanInputRecord struct {
	ID                string
	MissionID         string
	MissionName       string
	TaskID            string
	TaskName          string
	ToolCallID        string
	Question          string
	ShortSummary      string
	AdditionalContext string
	Choices           []string
	// MultiSelect is true when the human may pick 1+ choices rather
	// than exactly one. When true, the resolved Response is a JSON-
	// encoded string array (e.g. `["A","C"]`); when false, Response is
	// the single chosen string. Always false for free-text questions.
	MultiSelect     bool
	State           HumanInputState
	RequestedAt     time.Time
	ResolvedAt      time.Time // zero when State is open
	Response        string
	ResponderUserID string
}

HumanInputRecord is the canonical view of a single ask_human request, mirrored from squadron's store.

Timestamps are RFC3339Nano strings on the wire so the proto schema stays language-neutral, but the SDK exposes them as time.Time on the Go interface for convenience. Empty timestamps decode to the zero time.Time.

type HumanInputState

type HumanInputState string

HumanInputState is the lifecycle state of an ask_human request.

const (
	HumanInputStateOpen     HumanInputState = "open"
	HumanInputStateResolved HumanInputState = "resolved"
)

type MessageToolSpec added in v0.0.2

type MessageToolSpec struct {
	// Description is appended to the tool description so the LLM knows how to
	// format messages for this gateway.
	Description string
	// ParamsSchema is an optional JSON Schema (object) for the tool's
	// parameters. Empty → squadron's default { message } shape.
	ParamsSchema string
}

MessageToolSpec describes the builtins.gateway.post tool for one gateway.

type NotificationRecord added in v0.0.2

type NotificationRecord struct {
	MissionID   string
	MissionName string
	// Event is one of "mission_completed" or "mission_failed".
	Event      string
	Title      string
	Message    string
	OccurredAt time.Time
	// Error is set when Event is "mission_failed", empty otherwise.
	Error string
	// Channel is an optional per-mission destination override. When
	// empty the gateway posts to its globally configured default channel.
	Channel string
}

NotificationRecord describes a single mission-lifecycle notification pushed to the gateway. Notifications are one-way and informational — unlike human-input requests there is nothing for the user to resolve.

type PostMessageRequest added in v0.0.2

type PostMessageRequest struct {
	Payload     string
	Attachments []FileAttachment
}

PostMessageRequest carries the raw, gateway-schema-shaped JSON the agent produced for the builtins.gateway.post tool (text, channel override, rich layout) plus any squadron-resolved file attachments. The gateway parses Payload and uploads each attachment's bytes directly — it never fetches a URL.

type ResolveResult

type ResolveResult struct {
	// Record is the canonical row after the call. For not-found this
	// is the zero value.
	Record HumanInputRecord
	// AlreadyResolved is true when the request was resolved by some
	// other actor before this call landed. Display "already answered
	// by X" rather than confirming the gateway's user's submission.
	AlreadyResolved bool
	// NotFound is true when the tool_call_id is unknown to squadron.
	// Most commonly: the row was trimmed from the store, or a stale
	// event made it into the gateway's queue. Treat as a no-op.
	NotFound bool
}

ResolveResult describes the outcome of a ResolveHumanInput call.

The two boolean flags exist because squadron treats both already-resolved and not-found as non-error outcomes — they're distinguishable expected results, not exceptions. Wrapping in an error type would force gateways to deal with errors.Is / errors.As for the common case of "I just want to know if my click landed".

type SquadronAPI

type SquadronAPI interface {
	ListHumanInputs(ctx context.Context, filter HumanInputFilter) ([]HumanInputRecord, int, error)
	ResolveHumanInput(ctx context.Context, toolCallID, response, responderUserID string) (ResolveResult, error)
}

SquadronAPI is the squadron-side surface that gateways call into. The list deliberately starts small; new capabilities (mission management, dataset queries, cost reporting, …) are added here over time without breaking existing gateways.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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