pkg

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: BSD-2-Clause Imports: 27 Imported by: 0

Documentation

Overview

Package pkg holds the controller's backend-independent control API: the request and response shapes a caller speaks over A2A, and the single interface every backend implements. Nothing in this package names a backend — that is the point of it.

Index

Constants

View Source
const (
	// DefaultDockerImage is the image every worker runs. ⚠️ It is untagged, so docker
	// resolves it to :latest — and no :latest is published for this image, upstream or
	// locally, so a create that used this value verbatim would fail with docker's own
	// resolution error rather than anything naming the real cause. The binary therefore
	// requires the image to be configured and overrides this field; it survives as the
	// package's own placeholder, not as a value that resolves.
	DefaultDockerImage = "bborbe/agent-claude"
	// DefaultAgentNameBase prefixes the name each worker's Agent Card advertises; the worker
	// id is appended so two workers never advertise the same name.
	DefaultAgentNameBase = "claude-interactive"
	// DefaultAnthropicModel is the model the worker's turns run against.
	DefaultAnthropicModel = "MiniMax-M2.7-highspeed"
	// DefaultAllowedTools is the tool allow-list the worker's agent runs with.
	DefaultAllowedTools = "Read,Bash,Grep,Glob,Write,Edit,WebSearch,WebFetch"
	// DefaultPortStart and DefaultPortEnd bound the host-port search. 9090 is commonly taken
	// on a laptop and 9091/9092 are held by hand-started containers that are out of bounds,
	// so the search starts above them.
	DefaultPortStart = 9093
	DefaultPortEnd   = 9199
)

The worker container's fixed settings. They mirror the local-docker-run recipe the agent image documents, which is the command create automates.

View Source
const (
	// DefaultKubernetesTemplateConfig is the Config every worker is cloned from. Cloning a
	// live Config keeps the image pin, resources and priority class in the one place the chart
	// already maintains them, so a worker never runs an image the shared pod does not.
	DefaultKubernetesTemplateConfig = "claude-interactive"
	// DefaultKubernetesIngressClassName is the ingress class every worker Ingress uses.
	DefaultKubernetesIngressClassName = "traefik"
)

The defaults for the k8s backend's settings.

View Source
const A2APath = "/a2a"

A2APath is the route the A2A JSON-RPC binding is served at.

View Source
const AgentCardPath = a2asrv.WellKnownAgentCardPath

AgentCardPath is the well-known path the Agent Card is served at, as the A2A spec defines it. It is the SDK's own constant, re-exported so the service's wiring reads both A2A routes from one place.

View Source
const (
	// AuthorizationHeader is the request header carrying the caller's bearer credential.
	AuthorizationHeader = "Authorization"
)

The bearer gate's header and scheme. A caller presents the controller's token as `Authorization: Bearer <token>`, the scheme the A2A binding and every sibling service use.

Variables

This section is empty.

Functions

func NewA2AExecutor added in v0.2.0

func NewA2AExecutor(provider BackendProvider, backend Backend) a2asrv.AgentExecutor

NewA2AExecutor creates the A2A executor for the control API.

provider resolves a backend name to the WorkerBackend that serves it; backend is the name the controller was configured with. Both are injected — the executor reads no global and no environment — so the same executor serves any backend, and a test drives it with a stub through the real selection wiring.

func NewA2AHandler added in v0.2.0

func NewA2AHandler(executor a2asrv.AgentExecutor) http.Handler

NewA2AHandler serves the A2A JSON-RPC binding over the given executor. The body is capped before the SDK handler sees it.

func NewAgentCard added in v0.2.0

func NewAgentCard(config AgentCardConfig) *a2a.AgentCard

NewAgentCard builds the Agent Card this service advertises. Its single supported interface is the JSON-RPC binding at config.PublicURL, which is the externally reachable address supplied as configuration — never the listen address and never a value derived from a request. The card carries no credential.

func NewAgentCardHandler added in v0.2.0

func NewAgentCardHandler(card *a2a.AgentCard) http.Handler

NewAgentCardHandler serves the Agent Card. The card is public by design: it advertises the endpoint and skills and carries no credential.

Types

type AgentCardConfig added in v0.2.0

type AgentCardConfig struct {
	// Name names the deployed agent in the Agent Card.
	Name string
	// PublicURL is the externally reachable A2A endpoint the card advertises verbatim.
	// It is never derived from the listen address or a request header.
	PublicURL string
}

AgentCardConfig is what the A2A Agent Card advertises about this service.

type Auth added in v0.4.1

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

Auth is the controller's authentication decision: the bearer token every gated request must present.

The zero value is not a usable default — it refuses every gated request, so a controller built without a token serves only the exempt admin paths. That is the fail-closed outcome: a decision left unmade cannot silently open a surface that creates and destroys containers. There is deliberately no opt-out value. The reference gate carries one so a deliberately unexposed consumer can state its choice; this controller has no deployment in which serving /a2a unauthenticated is correct, so the escape hatch is omitted rather than left unused.

func NewAuthToken added in v0.4.1

func NewAuthToken(token string) Auth

NewAuthToken returns the Auth that requires token as the bearer credential on every gated request. An empty token is not a usable credential: it yields the same fail-closed value as the zero Auth.

func (Auth) Wrap added in v0.4.1

func (a Auth) Wrap(next http.Handler) http.Handler

Wrap returns the handler that serves next only to a request carrying this decision's bearer credential.

It wraps the whole router rather than individual routes, so /a2a and any route registered after this change are covered by construction rather than by remembering to add the gate. A refused request is answered before the wrapped handler runs, so it never reaches the executor.

type Backend

type Backend string

Backend names the implementation that serves workers. Its value space belongs to the backend implementations and to the selection wiring, never to this schema: a constant naming a backend here would put that backend's identity above the seam this package exists to hold, and a second backend would have to edit the shared schema to become selectable.

const (
	// BackendDocker runs each worker as a container on the local host.
	BackendDocker Backend = "docker"
	// BackendKubernetes runs each worker as a workload in the cluster.
	BackendKubernetes Backend = "k8s"
)

The backend names. They live here and nowhere else: this file is the one place above the seam that names a backend, so a backend becomes selectable by editing this selection wiring alone. The shared schema (types.go) names none of them, which is what keeps a backend's identity from becoming part of the control API.

type BackendProvider added in v0.2.0

type BackendProvider interface {
	// Get returns the backend serving the named backend, or an error when the name is
	// not one the controller knows or its implementation is not wired.
	Get(ctx context.Context, backend Backend) (WorkerBackend, error)
}

BackendProvider resolves a backend name to the WorkerBackend that serves it.

func NewBackendProvider added in v0.2.0

func NewBackendProvider(docker WorkerBackend, kubernetes WorkerBackend) BackendProvider

NewBackendProvider creates the backend selection wiring.

docker and kubernetes are the implementations of the two backends the controller supports, in the order their names are declared above. Either may be nil while its implementation is not yet wired; Get then refuses that backend rather than handing back a nil seam that would fail on first use.

type ControlRequest

type ControlRequest struct {
	Operation Operation `json:"operation"`
	// Create is set when Operation is OperationCreate.
	Create *CreateRequest `json:"create,omitempty"`
	// Stop is set when Operation is OperationStop.
	Stop *StopRequest `json:"stop,omitempty"`
}

ControlRequest is one control-API call. Exactly one operation-specific field is set and it matches Operation; a request that sets none, or more than one, is refused rather than guessed at.

type ControlResponse

type ControlResponse struct {
	Operation Operation       `json:"operation"`
	Create    *CreateResponse `json:"create,omitempty"`
	List      *ListResponse   `json:"list,omitempty"`
	Stop      *StopResponse   `json:"stop,omitempty"`
}

ControlResponse is the controller's reply to one ControlRequest. Exactly one operation-specific field is set and it matches Operation.

type CreateRequest

type CreateRequest struct {
	Task TaskName `json:"task"`
}

CreateRequest asks for a worker serving one task.

type CreateResponse

type CreateResponse struct {
	Worker Worker `json:"worker"`
	// Token is the worker's own bearer credential. It must never be logged, echoed into a
	// transcript, or written to a file.
	Token string `json:"token"`
}

CreateResponse is the reply to a create. It is the only place a worker's token is ever returned: the controller keeps no copy it depends on, so a caller that loses this reply stops the worker and creates another rather than asking the controller to repeat it.

type DockerClient added in v0.3.0

type DockerClient interface {
	// Run runs docker with args and the extra environment, returning its standard output.
	// The extra environment is layered on the caller's own and is where a credential
	// travels, so the value never reaches argv.
	Run(ctx context.Context, args []string, extraEnv []string) (string, error)
}

DockerClient runs one docker CLI invocation. It is the backend's only door to the daemon and it is injected, so the backend reads no global and a test drives it with a fake that records the argv and the environment it was handed and answers with canned output — no daemon required.

func NewDockerCLIClient added in v0.3.0

func NewDockerCLIClient() DockerClient

NewDockerCLIClient creates the DockerClient that runs the docker CLI.

type DockerConfig added in v0.3.0

type DockerConfig struct {
	// Image is the image every worker container runs.
	Image string
	// AgentNameBase prefixes the Agent Card name each worker advertises.
	AgentNameBase string
	// Model is the model the worker's turns run against.
	Model string
	// AllowedTools is the tool allow-list the worker's agent runs with.
	AllowedTools string
	// PortStart and PortEnd bound the host ports the backend will publish on.
	PortStart int
	PortEnd   int
}

DockerConfig holds the docker backend's settings. It carries no credential: the token a worker is created with is minted per worker and returned once.

func DefaultDockerConfig added in v0.3.0

func DefaultDockerConfig() DockerConfig

DefaultDockerConfig is the configuration the controller uses when none is supplied. Its values are the local-docker-run recipe's own.

type KubernetesClient added in v0.5.0

type KubernetesClient interface {
	// GetConfig returns the named Config.
	GetConfig(ctx context.Context, name string) (*agentv1.Config, error)
	// ListConfigs returns every Config matching labelSelector.
	ListConfigs(ctx context.Context, labelSelector string) ([]agentv1.Config, error)
	// CreateConfig creates config and returns it as stored, carrying its uid.
	CreateConfig(ctx context.Context, config *agentv1.Config) (*agentv1.Config, error)
	// DeleteConfig deletes the named Config. Its owned objects are garbage-collected.
	DeleteConfig(ctx context.Context, name string) error
	// GetSecret returns the named Secret.
	GetSecret(ctx context.Context, name string) (*corev1.Secret, error)
	// CreateSecret creates secret.
	CreateSecret(ctx context.Context, secret *corev1.Secret) error
	// CreateService creates service.
	CreateService(ctx context.Context, service *corev1.Service) error
	// CreateIngress creates ingress.
	CreateIngress(ctx context.Context, ingress *networkingv1.Ingress) error
}

KubernetesClient is the k8s backend's only door to the cluster. It is injected, so the backend reads no global and a test drives it with a mock — no cluster required. Every call is scoped to the one namespace the client was built for.

func NewInClusterKubernetesClient added in v0.5.0

func NewInClusterKubernetesClient(ctx context.Context, namespace string) (KubernetesClient, error)

NewInClusterKubernetesClient creates the KubernetesClient from the pod's own service account. It is the only constructor production uses: the controller runs in the cluster it manages, so it needs no kubeconfig and holds no credential beyond its service account.

type KubernetesConfig added in v0.5.0

type KubernetesConfig struct {
	// Namespace is where every worker Config, Secret, Service and Ingress is written.
	Namespace string
	// TemplateConfig names the Config every worker is cloned from.
	TemplateConfig string
	// AgentNameBase prefixes the name each worker's Agent Card advertises.
	AgentNameBase string
	// IngressDomain is the parent domain of every worker host, so a worker named
	// a2a-worker-<id> is served at https://a2a-worker-<id>.<IngressDomain>/a2a.
	IngressDomain string
	// IngressClassName is the ingress class every worker Ingress uses.
	IngressClassName string
	// TLSSecretName names the wildcard certificate Secret covering IngressDomain.
	TLSSecretName string
}

KubernetesConfig holds the k8s backend's settings. It carries no credential: a worker's shared credentials are copied from its template's Secret inside the cluster, and its own token is minted per worker and returned once.

func DefaultKubernetesConfig added in v0.5.0

func DefaultKubernetesConfig() KubernetesConfig

DefaultKubernetesConfig is the k8s backend's configuration before the deployment-specific values (namespace, domain, certificate) are set.

type ListResponse

type ListResponse struct {
	Workers []Worker `json:"workers"`
}

ListResponse is the reply to a list.

type Operation

type Operation string

Operation names one control-API operation. It travels as a plain string so a caller written against this schema needs no shared Go type to address the controller.

const (
	// OperationCreate asks the controller for a worker serving one task.
	OperationCreate Operation = "create"
	// OperationList asks the controller for every worker it owns.
	OperationList Operation = "list"
	// OperationStop asks the controller to stop and remove one worker.
	OperationStop Operation = "stop"
)

type PortProber added in v0.3.0

type PortProber interface {
	// IsFree reports whether port can be bound on the host.
	IsFree(ctx context.Context, port int) bool
}

PortProber reports whether a host port is free to bind. It is injected so the port search is testable without opening a socket.

func NewTCPPortProber added in v0.3.0

func NewTCPPortProber() PortProber

NewTCPPortProber creates the PortProber that bind-checks a port on the host.

type StopRequest

type StopRequest struct {
	WorkerID WorkerID `json:"worker_id"`
}

StopRequest names the worker to stop and remove.

type StopResponse

type StopResponse struct {
	Worker Worker `json:"worker"`
}

StopResponse is the reply to a stop. It carries the stopped worker so a caller can confirm which one the controller acted on.

type StubWorkerBackend added in v0.2.0

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

StubWorkerBackend is a throwaway in-memory WorkerBackend. It exists for two reasons.

It makes the executor testable with no container, no cluster and no credential: a test drives the real executor and the real selection wiring against it. And it is the second implementation of WorkerBackend, which is what proves the seam — the executor drives it through the same interface it drives a real backend through, without knowing which one it holds.

It is absent from the selection wiring in backend-provider.go: no Backend value resolves to it. It is therefore reachable only by tests and by explicit construction, and can never serve a production request.

func NewStubWorkerBackend added in v0.2.0

func NewStubWorkerBackend() *StubWorkerBackend

NewStubWorkerBackend creates an empty in-memory backend.

func (*StubWorkerBackend) Create added in v0.2.0

Create records a worker for the requested task and returns it with a token derived from its id. A task that already has a worker returns the existing one, matching the contract the real backends keep, so a repeated create never starts a second worker.

func (*StubWorkerBackend) List added in v0.2.0

List returns every worker the stub holds, in no particular order.

func (*StubWorkerBackend) Stop added in v0.2.0

Stop removes the named worker and returns it, reporting it as stopped. A worker the stub does not hold is refused rather than silently ignored.

type TaskName

type TaskName string

TaskName is the vault task a worker serves. It names the work, not the worker — a task has at most one worker, and a second create for a task that already has one returns the existing worker rather than starting another.

type Worker

type Worker struct {
	WorkerID WorkerID    `json:"worker_id"`
	Task     TaskName    `json:"task"`
	A2AURL   string      `json:"a2a_url"`
	Backend  Backend     `json:"backend"`
	State    WorkerState `json:"state"`
}

Worker is one worker as the control API reports it. It deliberately carries no token: a worker's token is returned once by create and never again, so no listing, no log and no restart can leak one.

type WorkerBackend added in v0.2.0

type WorkerBackend interface {
	// Create starts one worker serving the requested task and returns it together with
	// its own bearer token. A task that already has a worker returns the existing one
	// rather than starting a second.
	Create(ctx context.Context, req CreateRequest) (*CreateResponse, error)

	// List returns every worker the backend owns. It is rebuilt from the backend's own
	// resource state on every call, never from controller memory, so a controller
	// restart loses nothing.
	List(ctx context.Context) (*ListResponse, error)

	// Stop stops and removes the named worker and returns it.
	Stop(ctx context.Context, req StopRequest) (*StopResponse, error)
}

WorkerBackend is the single seam every backend implements. It is expressible with the control-API schema alone: it takes the schema's request shapes and returns its response shapes, so nothing above the seam names a backend and a new backend needs no change to the schema, to the executor or to the HTTP surface.

func NewDockerBackend added in v0.3.0

func NewDockerBackend(client DockerClient, prober PortProber, config DockerConfig) WorkerBackend

NewDockerBackend creates the WorkerBackend that runs each worker as a docker container.

client is the backend's only door to the daemon and prober decides which host port is free; both are injected, so the backend reads no global and a test drives it without a daemon. config carries the image, the Agent Card name prefix, the model and the host-port range.

func NewKubernetesBackend added in v0.5.0

func NewKubernetesBackend(
	client KubernetesClient,
	builder WorkerResourceBuilder,
	templateConfig string,
) WorkerBackend

NewKubernetesBackend creates the WorkerBackend that runs each worker as a per-task pod.

client is the backend's only door to the cluster and builder renders every object a worker is made of; both are injected, so a test drives the backend without a cluster. templateConfig names the Config every worker is cloned from.

type WorkerID

type WorkerID string

WorkerID identifies one worker the controller created. It is the value a caller passes back to stop a worker and the value list reports, so it stays stable across a controller restart: it is read back from the worker's own labels rather than minted per request and held in memory.

type WorkerResourceBuilder added in v0.5.0

type WorkerResourceBuilder interface {
	// BuildConfig clones template into the Config for one worker serving task.
	BuildConfig(template agentv1.Config, workerID WorkerID, task TaskName) *agentv1.Config
	// BuildSecret renders the worker's credential Secret, owned by owner: every key of
	// template, with the bearer replaced by token.
	BuildSecret(owner agentv1.Config, template corev1.Secret, token string) *corev1.Secret
	// BuildService renders the ClusterIP Service in front of owner's pod.
	BuildService(owner agentv1.Config) *corev1.Service
	// BuildIngress renders the TLS Ingress that exposes owner's Service on its own host.
	BuildIngress(owner agentv1.Config) *networkingv1.Ingress
	// A2AURL is the address the worker named name advertises.
	A2AURL(name string) string
}

WorkerResourceBuilder renders every Kubernetes object a k8s worker is made of. It decides what a worker looks like; it never talks to the cluster, so every method is a pure function of its arguments and the backend's configuration.

func NewWorkerResourceBuilder added in v0.5.0

func NewWorkerResourceBuilder(config KubernetesConfig) WorkerResourceBuilder

NewWorkerResourceBuilder creates the WorkerResourceBuilder that renders workers for config's namespace, ingress domain and certificate.

type WorkerState

type WorkerState string

WorkerState is what list reports about a worker's lifecycle.

const (
	// WorkerStateRunning means the worker exists and answers on its own A2A URL.
	WorkerStateRunning WorkerState = "running"
	// WorkerStateStopped means the worker no longer exists. It appears only in the reply to
	// the stop that removed it; list never returns it.
	WorkerStateStopped WorkerState = "stopped"
	// WorkerStateUnknown means the worker's backing resource exists but the controller
	// cannot classify it, so a caller must not read it as running.
	WorkerStateUnknown WorkerState = "unknown"
)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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