harnessplane

package
v0.2.31 Latest Latest
Warning

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

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

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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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

func CachePath(agentHome, edgeName string) string

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 Detect

func Detect(account RunAsAccount) (Detection, Blocked)

Detect reports which harnesses are installed on this machine and launchable by the runner account.

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.

The search order, most specific to the runner account first:

  1. the "~/" entries of harness.WellKnownBinDirs against the runner account's home — a per-user install made AS that account;
  2. the agent's PATH;
  3. harness.WellKnownBinDirs in order, "~/" expanded against the agent's own home — which for a root systemd agent is /root.

Every candidate must be a regular executable file that the runner account can reach. The last step is where that matters: an operator who installs Claude Code as root gets /root/.local/bin/claude, behind a /root nobody else can traverse. The agent used to pick it, and the runner — running as the account precisely so it is not root — failed with "permission denied" from a path it never chose. Such an install is skipped and reported in Blocked, so the machine's harness status says what to move where.

func SeedCache

func SeedCache(path string, setting Setting) (bool, error)

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

func WriteCache(path string, setting Setting) error

WriteCache records the effective setting. It is called after every OBSERVED change, so the file always describes the hub's last word.

Types

type Blocked added in v0.2.15

type Blocked map[string]string

Blocked maps a harness name to an executable that exists on this machine but that the runner account cannot execute. It is only set for a harness that Detection lacks: a usable install anywhere else wins, and the blocked one is then of no interest.

type Detection

type Detection map[string]string

Detection is what the machine has installed: the harness name mapped to the executable the runner should launch. Only an executable the runner ACCOUNT can launch is in it; one the machine has but that account cannot run is reported through Blocked instead (see Detect).

func (Detection) Names

func (d Detection) Names() []string

Names returns the detected harness names in canonical order.

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

func NewManager(hub dynamic.Interface, opts Options) (*Manager, error)

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

func (m *Manager) AccountDescription() string

AccountDescription is a log-safe description of the runner account.

func (*Manager) Observe

func (m *Manager) Observe(ctx context.Context, setting Setting)

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

func (m *Manager) Reconcile(ctx context.Context) error

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

func (m *Manager) Run(ctx context.Context) error

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:

  1. Seed the cache from --harness, but ONLY when no cache exists.
  2. 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.
  3. 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

func (m *Manager) RunnerToken(port int) (string, bool)

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) Setting

func (m *Manager) Setting() Setting

Setting returns the effective setting.

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.

func (*Manager) StopAll

func (m *Manager) StopAll()

StopAll stops every supervised runner, keeping all durable state. It uses its own context because the agent's is already cancelled by the time shutdown runs, and a stop still has to be able to wait out the drain window.

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 the runner account can launch, and
	// the installs it cannot; nil uses Detect(Account). Tests replace it.
	Detect func() (Detection, Blocked)
	// 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

func LoadCache(path string) (Setting, bool, error)

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

func ParseSetting(raw string) (Setting, error)

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

func (s Setting) Resolve(detected []string) []string

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

func (s Setting) ResolvePermissionMode() string

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.

func (Setting) String

func (s Setting) String() string

String renders the setting the way the --harness flag accepts it, so a log line and a command line read the same.

func (Setting) Validate

func (s Setting) Validate() error

Validate rejects a setting this agent cannot act on. An unknown mode is an error rather than a silent fallback to auto: the CRD enum means the agent should never see one, and guessing would start harnesses nobody asked for.

Jump to

Keyboard shortcuts

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