Documentation
¶
Overview ¶
Package harnessplane makes a machine a harness host for its workspace.
A harness is a coding agent (Claude Code, Codex) the agent supervises as a local `runner/v1` service on loopback. There is exactly ONE setting: `spec.harness` on the edge object. The agent watches its own edge, resolves that setting against what is actually installed on the machine, supervises one runner child per enabled harness, and advertises each running child over the discovery channel the edges provider already pulls.
Three rules shape everything here:
- The hub wins over flags, always. `--harness` only SEEDS the on-disk cache and only when no cache exists; the first observed spec overwrites it, and no code path feeds a flag value back in afterwards. See Manager.Run.
- No harness credential ever lands on this host. The caller sends its own credential with each runner/v1 attempt (see pkg/runner/harness.Launch), so this package writes a bearer token, an enrollment and harness homes — and nothing else.
- A stop keeps state. Disabling a harness, or shutting the agent down, signals the child's process group and lets it drain; the token, the enrollment, the harness home and the runner's own clones stay on disk so re-enabling resumes the same sessions.
The edge API lives in the standalone edges provider module, so — exactly like pkg/agent/reconciler — this package decodes the object as unstructured into local mirror structs rather than importing the provider.
The package is deliberately NOT called `runner`: it would then read `runner.Config` for pkg/runner's types inside a package of the same name.
Index ¶
- Constants
- Variables
- func CachePath(agentHome, edgeName string) string
- func SeedCache(path string, setting Setting) (bool, error)
- func WriteCache(path string, setting Setting) error
- type Detection
- type HarnessStatus
- type Manager
- func (m *Manager) AccountDescription() string
- func (m *Manager) Observe(ctx context.Context, setting Setting)
- func (m *Manager) Reconcile(ctx context.Context) error
- func (m *Manager) Run(ctx context.Context) error
- func (m *Manager) RunnerToken(port int) (string, bool)
- func (m *Manager) Services() []discovery.DiscoveredService
- func (m *Manager) Setting() Setting
- func (m *Manager) Statuses() []HarnessStatus
- func (m *Manager) StopAll()
- type Mode
- type Options
- type RunAsAccount
- type Setting
Constants ¶
const ( // HarnessClaude is headless Claude Code. HarnessClaude = "claude" // HarnessCodex is the Codex app-server. HarnessCodex = "codex" )
Harness names. Mirrors v1alpha1.Harness* in the edges provider and runnercli.Harnesses in pkg/runner.
const ( // PermissionAcceptEdits is the default: file edits inside the turn's own // working directory, and anything else that would prompt is denied. PermissionAcceptEdits = "acceptEdits" // PermissionBypass approves every tool call. The runner account and the // working directory are then the only isolation left. PermissionBypass = "bypassPermissions" )
Permission modes, mirroring v1alpha1.HarnessPermissionMode.
const DefaultBasePort = 8787
DefaultBasePort is the first loopback port a runner is offered. Allocation walks up from here so two harnesses on one machine never collide and nobody has to assign ports by hand.
const DefaultResync = 60 * time.Second
DefaultResync is how often the plane reconciles without a watch event. It is short because readiness is a live probe of a supervised process, not a cached fact, and because detection re-runs with it: a harness installed after onboarding becomes usable within a minute without touching the hub.
const InheritUID = supervisor.InheritUID
InheritUID marks an account as "the agent's own", for a non-root agent that launches the child as itself. Re-exported so callers do not have to import the supervisor package just to say so.
Variables ¶
var Names = []string{HarnessClaude, HarnessCodex} //nolint:gochecknoglobals
Names are every harness this agent build can supervise, in the order ports are allocated. The order is fixed rather than sorted-on-the-fly so two machines with the same harnesses land on the same ports, and so a reconcile never renumbers a harness that is already running.
Functions ¶
func CachePath ¶
CachePath is <agentHome>/.railgrid/agent-<edge>.harness.json. It lives under the AGENT's home, not the runner account's: it is the agent's memory of a hub decision, and the account that executes untrusted code has no business being able to rewrite it.
func SeedCache ¶
SeedCache writes setting ONLY when no cache exists, and reports whether it wrote. This is the whole of what an install flag may do: it cannot override a setting the agent has already observed on its edge, because the observation is what wrote the file it refuses to touch.
func WriteCache ¶
WriteCache records the effective setting. It is called after every OBSERVED change, so the file always describes the hub's last word.
Types ¶
type Detection ¶
Detection is what the machine has installed: the harness name mapped to the executable the runner should launch.
func Detect ¶
Detect reports which harnesses are installed on this machine.
It is re-run on every reconcile rather than once at onboarding, because a harness installed after a machine joined must become usable without anyone touching the hub. runnerHome is the home of the account the runner child runs as: harness.ResolveBinary expands its "~/" search paths against the CALLING process's home, which for a root systemd agent is /root — not where the runner account's per-user install of Claude Code lives. The second pass closes exactly that gap.
type HarnessStatus ¶
type HarnessStatus struct {
Name string `json:"name"`
Detected bool `json:"detected"`
Enabled bool `json:"enabled"`
Ready bool `json:"ready"`
Version string `json:"version,omitempty"`
Port int32 `json:"port,omitempty"`
Reasons []string `json:"reasons,omitempty"`
}
HarnessStatus is one harness as this machine sees it. The JSON tags match edgeapi.HarnessStatus in the edges provider, which is what the heartbeat publishes on the edge's status.harnesses.
Detected and Enabled are separate facts on purpose: detected says the executable is installed, enabled says spec.harness asks for it.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager keeps the machine's supervised runners in step with spec.harness.
It is the only writer of the harness cache file and the only owner of the runner children, so "what is this machine offering" has exactly one answer at any moment, whatever order the hub and the local clock deliver events in.
func NewManager ¶
NewManager builds a manager. hub may be nil, in which case no edge is watched and the cached setting is all there is; that is the shape tests use.
func (*Manager) AccountDescription ¶
AccountDescription is a log-safe description of the runner account.
func (*Manager) Observe ¶
Observe applies a setting seen on the edge object and records it on disk.
This is the ONLY way the setting changes after start, and it always persists: the cache exists to remember the hub's last word, so an observation that disagrees with the flag that seeded it must win, permanently.
func (*Manager) Reconcile ¶
Reconcile converges the machine on the current setting: it re-runs detection, starts what is enabled and installed, stops what is not, and probes what is running. It is idempotent and safe to call on every event and every tick.
func (*Manager) Run ¶
Run starts the plane and blocks until ctx is cancelled, at which point every supervised runner is stopped with its full drain window.
The order here is the cache-versus-spec precedence, and it is the only place the seed is read:
- Seed the cache from --harness, but ONLY when no cache exists.
- Apply whatever the cache says, before the first watch event, so a machine that boots while the hub is unreachable runs what it was last told.
- Watch the edge. The first observed spec.harness overwrites the cache, and from then on nothing but an observation can change it.
func (*Manager) RunnerToken ¶
RunnerToken returns the bearer for a loopback port this manager supervises. The /svc proxy calls it to inject Authorization on the way to a runner, which is why the published Service carries no credential: a token the hub never holds is a token the hub cannot leak.
A port that is not one of this manager's RUNNING runners gets nothing, so a Service pointed at some other local port can never borrow a runner's identity.
func (*Manager) Services ¶
func (m *Manager) Services() []discovery.DiscoveredService
Services advertises every RUNNING runner over the discovery channel the edges provider already pulls, so the runner becomes an ordinary Service with no new machinery. auth is none on that Service because the bearer never leaves this host: the agent's own /svc proxy injects it (see RunnerToken).
func (*Manager) Statuses ¶
func (m *Manager) Statuses() []HarnessStatus
Statuses is what the heartbeat publishes on the edge's status.harnesses: one entry per harness this build knows, whether or not it is installed or enabled. Reporting all of them is the point — "installed but switched off" and "asked for but not installed" are both answers an operator needs.
type Mode ¶
type Mode string
Mode mirrors v1alpha1.HarnessMode.
const ( // ModeAuto offers every harness installed on the machine. It is the // default, and it is what makes a joined machine a harness host with no // second step. ModeAuto Mode = "auto" // ModeNone offers none. This is the opt-out. ModeNone Mode = "none" // ModeExplicit offers exactly Setting.Enabled, installed or not. ModeExplicit Mode = "explicit" )
type Options ¶
type Options struct {
// EdgeName is this edge's name and half of each runner's identity.
EdgeName string
// EdgeGVR addresses the agent's own edge object, which it watches for
// spec.harness changes. Zero disables the watch (used by tests).
EdgeGVR schema.GroupVersionResource
// Executable is the agent's own binary, supervised as `<exe> runner run`.
Executable string
// Account is the non-root account every runner child runs as.
Account RunAsAccount
// CachePath is where the last OBSERVED setting is remembered, under the
// agent's own home. See CachePath.
CachePath string
// Seed is the --harness flag value. It is written to CachePath only when no
// cache exists yet and is never consulted again, which is the whole of the
// "config wins over flags" rule.
Seed Setting
// Resync, BasePort, probe and backoff knobs; zero means the default.
Resync time.Duration
BasePort int
ProbeDeadline time.Duration
ProbeTimeout time.Duration
InitialBackoff time.Duration
// HTTPClient probes the loopback runners; nil builds one.
HTTPClient *http.Client
// Detect reports the installed harnesses; nil uses Detect(account home).
// Tests replace it.
Detect func() Detection
// PortFree reports whether a loopback port can be bound; nil dials to find
// out. Tests replace it.
PortFree func(port int) bool
}
Options configures a Manager.
type RunAsAccount ¶
type RunAsAccount struct {
// Home is the account's home directory; runner state lives under it.
Home string
// UID/GID are the account's ids, or InheritUID when the agent is already
// non-root and the child runs as the agent's own account.
UID int
GID int
}
RunAsAccount is the local account the supervised runner runs as.
func (RunAsAccount) Inherited ¶
func (a RunAsAccount) Inherited() bool
Inherited reports whether the child runs as the agent's own account.
type Setting ¶
type Setting struct {
Mode Mode `json:"mode,omitempty"`
Enabled []string `json:"enabled,omitempty"`
// PermissionMode and AllowedTools are the MACHINE's ceiling on what a turn
// may do without asking. They are the machine owner's to set, which is why
// they live on the edge: a tenant asking for more cannot grant themselves
// more. Mirrors v1alpha1.EdgeHarnessSpec.
PermissionMode string `json:"permissionMode,omitempty"`
AllowedTools []string `json:"allowedTools,omitempty"`
}
Setting is the effective harness selection: the mirror of the edge object's spec.harness, and the shape written to the on-disk cache.
func DefaultSetting ¶
func DefaultSetting() Setting
DefaultSetting is what an edge with no spec.harness means, and what a machine with no cache and no flag runs: every harness it has installed.
func LoadCache ¶
LoadCache reads the cached setting. The second return is false when no cache exists yet, which is the only case in which a flag may seed one.
func ParseSetting ¶
ParseSetting parses a --harness value: "auto", "none", or a comma-separated list of harness names (which becomes mode explicit). A typo is an error, not an empty list: an operator who typed --harness=cluade must find out at startup rather than wonder why no runner ever appears.
func (Setting) Resolve ¶
Resolve returns the harnesses this setting asks for given what is installed. It is the same decision as v1alpha1.EdgeHarnessSpec.ResolveHarnesses; the two are deliberately identical so the hub can predict what the agent will do.
A name that is enabled but not detected is RETURNED anyway: the caller reports it as detected=false with a reason instead of silently dropping it, because "I asked for claude and nothing happened" is the worst outcome.
func (Setting) ResolvePermissionMode ¶
ResolvePermissionMode is the mode this machine's runners are started with. Anything unrecognised is the default rather than a refusal: an agent that refused to start a runner over a spec value it did not know would turn a newer hub into a dead machine.