Documentation
¶
Overview ¶
Package managedagent renders and reconciles the Brain workload of a ManagedAgent.
Rendering is a pure function so the whole object graph a ManagedAgent produces can be diffed against a known-good baseline in a unit test. That matters more than it sounds: the Brain's behaviour is configured almost entirely through environment variables, and a variable that silently fails to render does not crash anything — it turns a capability off. A missing sandbox image leaves the agent with sandboxes whose command endpoint never answers; a missing credentials file variable leaves its CLI unauthenticated; a missing telemetry variable simply stops the traces. None of those surface as an error.
Index ¶
- Constants
- func BrainName(agent string) string
- func Endpoint(agent, namespace string, port int32, proxyService string) string
- func GatewayPort(ma *agentsv1alpha1.ManagedAgent) int32
- func HandsEnvName(agent string) string
- func OpenCodeConfigSecretName(agent string) string
- func PublicURL(ma *agentsv1alpha1.ManagedAgent, base string) string
- func RenderOpenCodeConfig(ma *agentsv1alpha1.ManagedAgent, apiKey string) ([]byte, error)
- func ScenarioNames(ma *agentsv1alpha1.ManagedAgent) []string
- func WorkspaceFSPort(ma *agentsv1alpha1.ManagedAgent) int32
- type ClusterEndpoint
- type ClusterResolver
- type DerivedEnv
- type DerivedMember
- type HandsProvisioner
- type RESTHandsProvisioner
- type Reconciler
- type Rendered
Constants ¶
const ( LabelManagedBy = "agentbox.navix.sh/managed-by" LabelAgent = "agentbox.navix.sh/managed-agent" // AnnotationOwnerAgent records the agent an env was derived for. Cross- // cluster ownerReferences do not exist, so this is the only link back, and // it is also how an orphan is found: an env carrying LabelAgent whose named // agent no longer exists has outlived its owner. // // A derived env is never deleted with its agent, and nothing marks it at // deletion time. Marking would need a finalizer, which would block deleting // an agent whenever its worker cluster is unreachable — trading a tidy // annotation for an object that cannot be removed during an outage. The env // also holds warm pods that someone may still be using. Reclaiming one is // therefore a human decision, made against this label. AnnotationOwnerAgent = "agentbox.navix.sh/owner-managed-agent" )
Labels and annotations stamped on a derived env, so an operator looking at a worker cluster can tell where the object came from and what happens to it.
const ( // NamePrefix prefixes every object a ManagedAgent owns, so a Brain is // never mistaken for a hand-rolled Deployment of the same agent name. NamePrefix = "agentbox-brain-" // DefaultGatewayPort is the agent gateway: the one port callers use. DefaultGatewayPort int32 = 4099 // DefaultWorkspaceFSPort serves attachment staging and the workspace file // browser. It is a separate process from the gateway, and the file panel // and attachment upload both stop working if it is not exposed. DefaultWorkspaceFSPort int32 = 8766 // DefaultOpenCodePort is the loopback port for `opencode serve`. DefaultOpenCodePort int32 = 4096 // StateRoot is where the Brain keeps everything a restart must not lose: // the thread map, the Claude Code transcripts and the OpenCode session DB. // // It is mounted at exactly one path, at the volume root. The runtime treats // a volume without its layout marker as one to convert, and converting // clears the root — a marker written under one subPath is invisible to the // others, so per-owner subPath mounts would make every restart look like a // fresh volume and discard the history. StateRoot = "/home/opencode/.local/share/opencode" // ClaudeConfigDir holds one JSONL transcript per session. ClaudeConfigDir = "/home/opencode/.local/share/claude" // ThreadStorePath is the gateway's thread map. Every harness builds its // history list from this file, so losing it loses the history even when the // transcripts survive. ThreadStorePath = ClaudeConfigDir + "/gateway/threads.json" // OpenCodeConfigPath is where the OpenCode harness reads its runtime // config. The filename is fixed by the harness. OpenCodeConfigPath = "/home/opencode/.config/opencode/opencode.json" // ManagedByValue marks every object this controller owns. The Hands // reconciler refuses to update a SandboxEnv that does not carry it, so a // user who takes manual ownership of an object keeps it. ManagedByValue = "agentbox-managedagent" // ConfigChecksumAnnotation carries a hash of everything that is NOT part of // the pod spec but still changes the Brain's behaviour — the referenced // Secrets and ConfigMaps. Without it a credential rotation leaves the old // value live in a running pod with nothing to indicate it. ConfigChecksumAnnotation = "agentbox.navix.sh/config-checksum" )
const DefaultProviderID = "platform"
DefaultProviderID is used when the agent does not name one. The provider id is part of every model's address ("<provider>/<model>"), so it is a spec field rather than a constant here.
Variables ¶
This section is empty.
Functions ¶
func Endpoint ¶
Endpoint is the in-cluster URL callers are given.
It points at the control-plane proxy, not at the Brain's own Service. Both are in-cluster, but only one of them checks anything: the Brain takes the caller's word for which end user is asking, so handing out its address makes every pod in the cluster able to read any tenant's threads. Reporting the proxied address instead means the published route and the in-cluster route differ only in hostname — same path, same key, same authorization.
proxyService is "<service>.<namespace>:<port>"; empty falls back to the Brain's own address, which is the only thing available on a deployment that has no proxy.
func GatewayPort ¶
func GatewayPort(ma *agentsv1alpha1.ManagedAgent) int32
GatewayPort is the port the Brain serves its agent API on. Exported because anything proxying to an agent has to address it by the same number the Deployment and Service were rendered with.
func HandsEnvName ¶
HandsEnvName is the env derived for an agent.
The worker caps env names at 24 characters because it appends the instance type to form each pool name. Deriving a name that the worker will reject would surface as a create failure on every reconcile, so the suffix is budgeted for here rather than discovered at the remote.
func OpenCodeConfigSecretName ¶
OpenCodeConfigSecretName is the Secret holding the generated opencode.json.
It is generated rather than asked for because the harness must be pinned to a single provider: left to its own defaults OpenCode also loads every provider reachable without credentials, including its vendor's hosted free models, and those become both pickable and reachable by model id.
func PublicURL ¶
func PublicURL(ma *agentsv1alpha1.ManagedAgent, base string) string
PublicURL is the address external callers use, or "" when the agent is not published. base is the shared route this deployment serves agents on.
func RenderOpenCodeConfig ¶
func RenderOpenCodeConfig(ma *agentsv1alpha1.ManagedAgent, apiKey string) ([]byte, error)
RenderOpenCodeConfig produces the opencode.json for one agent.
The result is the agent's overlay with the platform-owned keys stamped on top. apiKey is read from the agent's credential Secret by the caller; it is embedded here because the harness reads its provider credential from this file rather than from the environment.
func ScenarioNames ¶
func ScenarioNames(ma *agentsv1alpha1.ManagedAgent) []string
ScenarioNames lists the scenarios a Brain serves, sorted for a stable status.
func WorkspaceFSPort ¶
func WorkspaceFSPort(ma *agentsv1alpha1.ManagedAgent) int32
WorkspaceFSPort serves attachment staging and the workspace file browser. It is exported for the same reason as GatewayPort: a proxy in front of an agent must address it by the number the Service was rendered with.
Types ¶
type ClusterEndpoint ¶
type ClusterEndpoint struct {
BaseURL string
// HostHeader carries the virtual host when the base URL is an IP. The
// worker's ingress routes on Host, so omitting it lands on the default
// backend and every call 404s while the address looks correct.
HostHeader string
}
ClusterEndpoint is one worker cluster's native API, as the control plane addresses it.
type ClusterResolver ¶
type ClusterResolver func(clusterID string) (ClusterEndpoint, bool)
ClusterResolver hands out the endpoint for a cluster id.
type DerivedEnv ¶
type DerivedEnv struct {
Name string
TemplateRef string
Image string
Members []DerivedMember
Labels map[string]string
Annotations map[string]string
}
DerivedEnv is the env one agent needs, already named and sized.
func DeriveEnv ¶
func DeriveEnv(ma *agentsv1alpha1.ManagedAgent) (DerivedEnv, error)
DeriveEnv turns the agent's auto spec into the env to create.
type DerivedMember ¶
type DerivedMember struct {
InstanceType string
Resources *corev1.ResourceRequirements
Replicas int32
MinReplicas int32
MaxReplicas int32
}
DerivedMember is one member pool of a derived env.
Resources and InstanceType are alternatives, not a pair: a cluster with an instance-type catalog sizes members by catalog entry, one without it needs the size spelled out, and sending both is rejected.
type HandsProvisioner ¶
type HandsProvisioner interface {
// EnsureEnv makes the env and its member pools exist, and reports whether
// the env is serving. It must be safe to call on every reconcile.
EnsureEnv(ctx context.Context, clusterID string, spec DerivedEnv) (ready bool, detail string, err error)
}
HandsProvisioner derives an agent's sandbox supply on a worker cluster.
It is an interface rather than a concrete client because the object it creates does not live in the control plane's API server: SandboxEnv is a worker-cluster CRD, so the control plane reaches it over the worker's REST API. Keeping that behind an interface is also what lets the reconciler be tested without a worker.
type RESTHandsProvisioner ¶
type RESTHandsProvisioner struct {
Resolve ClusterResolver
// APIKey authenticates to every worker. The control plane acts as itself
// here, not on behalf of the agent's owner: the env is platform
// infrastructure and outlives any one caller's credential.
APIKey string
Client *http.Client
}
RESTHandsProvisioner provisions envs over a worker cluster's native REST API.
func NewRESTHandsProvisioner ¶
func NewRESTHandsProvisioner(resolve ClusterResolver, apiKey string) *RESTHandsProvisioner
NewRESTHandsProvisioner builds a provisioner with a bounded HTTP client.
func (*RESTHandsProvisioner) EnsureEnv ¶
func (p *RESTHandsProvisioner) EnsureEnv( ctx context.Context, clusterID string, spec DerivedEnv, ) (bool, string, error)
EnsureEnv creates the env and any missing member pool, then reports whether the remote considers it serving.
type Reconciler ¶
type Reconciler struct {
client.Client
Scheme *runtime.Scheme
// ProxyService is this process reached from inside the cluster, as
// "<service>.<namespace>:<port>". It is what status.endpoint points at so
// in-cluster callers go through authentication too.
ProxyService string
// PublicBaseURL is the shared route this deployment serves published agents
// on, e.g. "https://console.example.com/agentbox/api/managed-agents". Empty
// leaves status.publicURL unset: the agent still works in-cluster, it just
// has no address to hand out.
PublicBaseURL string
// Hands derives an agent's SandboxEnv on a worker cluster. It is nil on a
// control plane with no worker clusters registered, which is why
// spec.hands.auto reports "unavailable" instead of failing: the other two
// hands modes stay usable.
Hands HandsProvisioner
}
Reconciler drives a ManagedAgent's Brain workload.
It runs on the control plane only. The worker binary installs on every cluster, so a control-plane object reconciled there would get one reconciler per cluster all competing for the same resource.
func (*Reconciler) Reconcile ¶
Reconcile brings the Brain in line with the spec and reports what it found.
func (*Reconciler) SetupWithManager ¶
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error
SetupWithManager registers the controller.
type Rendered ¶
type Rendered struct {
Deployment *appsv1.Deployment
Service *corev1.Service
// PVC is nil when session persistence is disabled or supplied by an
// existing claim.
PVC *corev1.PersistentVolumeClaim
}
Rendered is the full object graph one ManagedAgent produces.
func Render ¶
func Render(ma *agentsv1alpha1.ManagedAgent, checksum string) (*Rendered, error)
Render turns a ManagedAgent into the objects that implement it.
It is pure: same spec in, same bytes out. `checksum` is the caller's hash of the referenced Secrets and ConfigMaps; passing "" omits the annotation.