api

package
v0.2.16 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package api defines the control-plane<->agent HTTP protocol.

Model B is keyless: the agent holds the GitHub App key, polls GitHub, and mints JIT configs itself. The control plane sees only metadata and never a credential. Agents dial out (NAT-friendly). Dispatch is two-phase:

  1. POST /v1/agents/{name}/poll with capacity, busy slots, labels, AND the agent's own queued jobs (derived from its GitHub polling). The control plane merges every agent's report, ranks by priority + aging, and returns the one job this agent should run as an Offer (200) or empty (204). No credential crosses the wire.
  2. POST /v1/agents/{name}/dispatches/{id}/claim to accept. The control plane records the dispatch in the ledger and returns 200; 410 means the offer expired or is unknown — drop it. The agent then mints the JIT locally (its own key) and provisions.

When the runner takes a job, the agent POSTs /v1/agents/{name}/slots/{handle}/started. GitHub may give the runner a different queued job, so the offered job is released for a new offer. When a dispatch finishes, the agent POSTs its terminal status to /v1/agents/{name}/slots/{handle}/done. The poll doubles as the agent's heartbeat: the control plane holds even a zero-capacity poll open, so a healthy agent is never silent for longer than the poll window.

Index

Constants

View Source
const PollTimeout = 25 * time.Second

PollTimeout is how long the server holds a poll connection open before returning 204 if no work surfaces. Agents immediately re-poll afterward, which makes the poll a heartbeat with a guaranteed cadence — a full-capacity poll is held open too, not bounced.

Variables

View Source
var ErrOfferGone = errors.New("offer gone")

ErrOfferGone reports a claim on an offer the server no longer holds (it expired or was resolved). The agent should drop the offer; the job has been re-enqueued server-side.

Functions

This section is empty.

Types

type Client

type Client struct {
	UpdateRequest string
	UpdateVersion string
	BaseURL       string
	Token         string
	HTTP          *http.Client
}

Client is the agent-side HTTP client to the controller.

func NewClient

func NewClient(baseURL, token string) *Client

NewClient constructs a Client. Default timeout is generous because /poll long-polls server-side.

func (*Client) Claim

func (c *Client) Claim(ctx context.Context, agent, id string) error

Claim accepts an offer: the control plane records the dispatch in its ledger and returns 200. No credential comes back — the agent mints the JIT locally. ErrOfferGone (410) means the offer expired or is unknown; drop it.

func (*Client) Poll

func (c *Client) Poll(ctx context.Context, agent string, req PollRequest) (*Offer, error)

Poll sends one poll iteration. Returns nil offer on long-poll timeout.

func (*Client) ReportDone

func (c *Client) ReportDone(ctx context.Context, agent, handle string, req DoneRequest) error

ReportDone notifies the controller that a dispatch has finished.

func (*Client) ReportStarted added in v0.2.13

func (c *Client) ReportStarted(ctx context.Context, agent, handle string) error

ReportStarted notifies the controller that a dispatch's runner has taken a job, which may not be the job it was offered.

type DoneRequest

type DoneRequest struct {
	Status string `json:"status"`
	Error  string `json:"error,omitempty"`
}

DoneRequest is what the agent posts when a dispatch finishes. Status is one of: "done" (clean exit), "failed" (provision error), "lost" (the slot was found orphaned after an agent restart and destroyed — its runner never reported, so its registration may linger).

type Offer

type Offer struct {
	ID     string   `json:"id"`     // dispatch id == slot name == runner name
	Org    string   `json:"org"`    // GitHub org the runner will register to
	JobID  int64    `json:"job_id"` // which job — the agent mints for this
	Labels []string `json:"labels"` // labels to advertise to GitHub
}

Offer names the job the agent should run. It carries no credential: the agent already holds the full job (it polled GitHub) and mints the JIT itself after claiming. JobID lets the agent match the decision to its local queue.

type OrgQueue

type OrgQueue struct {
	Org      string      `json:"org"`
	Priority int         `json:"priority"`
	Jobs     []QueuedJob `json:"jobs"`
}

OrgQueue is one org's pending jobs as reported by an agent. Priority is the agent's advisory org priority; the control plane owns the final ordering.

type PollRequest

type PollRequest struct {
	UpdateProtocol int        `json:"update_protocol,omitempty"`
	Version        string     `json:"version,omitempty"`
	UpdateState    string     `json:"update_state,omitempty"`
	UpdateError    string     `json:"update_error,omitempty"`
	FailedRequest  string     `json:"failed_request,omitempty"`
	FailedVersion  string     `json:"failed_version,omitempty"`
	Capacity       int        `json:"capacity"` // total concurrent slots this agent supports
	Busy           []string   `json:"busy"`     // slot handles currently in use
	Labels         []string   `json:"labels"`   // labels this agent's runners advertise
	Queues         []OrgQueue `json:"queues"`   // per-org queued jobs this agent can serve

	// Disk* describe the filesystem backing the agent's image store, zero when
	// it can't be measured. Blocked means the agent is refusing work because
	// that store is under its free-space floor; it keeps polling so its
	// heartbeat, queue report and buffered done-reports still flow.
	DiskFreeBytes  uint64 `json:"disk_free_bytes,omitempty"`
	DiskTotalBytes uint64 `json:"disk_total_bytes,omitempty"`
	Blocked        bool   `json:"blocked,omitempty"`
}

PollRequest is what the agent sends on each long-poll iteration. Queues is the agent's own view of pending work, polled from GitHub with its own key — the control plane schedules on this metadata, never touching a credential.

func (PollRequest) FreeCapacity

func (r PollRequest) FreeCapacity() int

FreeCapacity returns how many more slots the agent can accept. A blocked agent has none: that is what makes the scheduler route the work to a host that can actually run it, instead of one that would fail every dispatch.

type PriorityPolicy added in v0.2.11

type PriorityPolicy interface {
	// Priorities returns the policy priority for each given scope that has
	// one, keyed by the lower-cased scope.
	Priorities(ctx context.Context, scopes []string) (map[string]int, error)
}

PriorityPolicy is the controller-side priority for a tenant scope (a GitHub org login, or "owner/repo" for a repo-level scope). A scope with a policy scores at that priority whatever the agent reports; a scope without one keeps the agent-reported value.

type QueuedJob

type QueuedJob struct {
	JobID       int64    `json:"job_id"`
	Labels      []string `json:"labels"`
	WaitingSecs int      `json:"waiting_secs"`
}

QueuedJob is one pending job's scheduling metadata. No repo, no secrets.

type Server

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

Server is the keyless control plane. It schedules on the queue metadata each agent reports (the agent holds the GitHub key and mints JIT itself) and never sees a credential.

func NewServer

func NewServer(boostPerMinute int, token string, led dispatch.Store, enr enroll.Store) *Server

NewServer wires the dependencies. boostPerMinute is the priority aging rate. token is the static shared secret (OSS single-host and the current trial); enr, when non-nil, adds Neon-backed per-agent enrollment tokens and the CLI login handshake. If both are empty/nil, RequireAuthOrLoopback refuses to bind beyond loopback.

func (*Server) RequireAuthOrLoopback

func (s *Server) RequireAuthOrLoopback(listenAddr string) error

RequireAuthOrLoopback returns an error if listenAddr binds beyond loopback while the server has an empty auth token. Call before binding to fail loudly.

"loopback" means literally `localhost` or an IP that satisfies net.IP.IsLoopback. Other hostnames are rejected because they may resolve to public addresses and we can't tell at config-load time.

func (*Server) RequireScopedAuth

func (s *Server) RequireScopedAuth() error

RequireScopedAuth refuses a config that enables enrollment while also setting a static token. The static token is unscoped — it bypasses queue scoping and the per-tenant claim/resolve guard — so on a multi-tenant deploy it is a cross-tenant master key. Enroll every agent instead; its token carries an org and name. OSS single-host (no enrollment) keeps the static token.

func (*Server) Routes

func (s *Server) Routes() http.Handler

Routes returns an http.Handler with all endpoints mounted.

func (*Server) WithPriorities added in v0.2.11

func (s *Server) WithPriorities(p PriorityPolicy)

WithPriorities sets the central priority policy. Call before serving; the default (nil) schedules on agent-reported priority alone.

func (*Server) WithTelemetry

func (s *Server) WithTelemetry(t telemetry.Recorder)

WithTelemetry swaps in a live Recorder (hosted control plane). Call before serving; the default is telemetry.Noop.

type StaticPriorities added in v0.2.11

type StaticPriorities map[string]int

StaticPriorities is the OSS policy, read from controller config.

func NewStaticPriorities added in v0.2.11

func NewStaticPriorities(m map[string]int) StaticPriorities

NewStaticPriorities lower-cases the keys: GitHub logins are case-insensitive and agents report operator-typed casing.

func (StaticPriorities) Priorities added in v0.2.11

func (p StaticPriorities) Priorities(_ context.Context, scopes []string) (map[string]int, error)

Jump to

Keyboard shortcuts

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