Help us shape the future of Go! Take the 2026 Go Developer Survey

hostpoller

package
v0.28.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package hostpoller is the host side of Hadron's host operations (hadron-cli#831, cor:agt:070): a poller that registers the workers this host can launch, collects the operations issued to it, carries out only the closed set of five, and reports every outcome.

It talks to the server through a narrow Server interface and to the host through a Host interface, so the loop and every operation's rules are tested without a server, git or a real agent.

Index

Constants

View Source
const (
	OpWorktreeCreate = "worktree.create"
	OpWorkerStart    = "worker.start"
	OpWorkerDeliver  = "worker.deliver"
	OpWorkerEnd      = "worker.end"
	OpWorktreeRemove = "worktree.remove"
)

The closed set of operation types a host carries out (cor:agt:070:03). Anything else is refused here as well as by the server, and there is no shell passthrough of any kind.

View Source
const (
	Succeeded = "SUCCEEDED"
	Failed    = "FAILED"
)

Outcomes a host reports.

View Source
const (

	// HostKeyEnv is never read as a key source (a same-user agent could
	// read it from the poller's environment); it is scrubbed from launched
	// agents in case the operator's shell carries one.
	HostKeyEnv = "HADRON_HOST_KEY"
)
View Source
const LaunchGrantTTL = 10 * time.Minute

LaunchGrantTTL is how long a collected start's grant lives (#1883, Proposed). Waiting for the bind longer than this cannot succeed.

Variables

View Source
var ErrDeliveryScope = errors.New("the host key lacks attention:deliver: delivery is off for this run")

ErrDeliveryScope is a host key without attention:deliver (APP_KEY_SCOPE_REQUIRED). It does not heal by retrying, so delivery is switched off for the rest of the run; host operations carry on.

View Source
var ErrFinal = errors.New("the operation is already finished")

ErrFinal is a Report refusal meaning the operation is already finished server-side (HOST_OPERATION_ALREADY_REPORTED): asking again cannot change it.

View Source
var ErrNotDone = errors.New("no session bound with this start's grant yet")

ErrNotDone is a Report refusal meaning a worker.start's session is not bound yet (HOST_OPERATION_NOT_DONE); the poller asks again.

Functions

func CanonicalFamily added in v0.27.0

func CanonicalFamily(s string) string

CanonicalFamily is the server's identity for an EXPLICIT family: trimmed, lowercased, and a provider alias folded to its family. Every other token is itself, so a custom family compares only to itself. It never infers a family from a title or a model and does not validate: an empty or malformed result is not a launch identity and callers must refuse it (familyRE).

func SameFamily added in v0.27.0

func SameFamily(a, b string) bool

SameFamily reports whether two explicit families are one launch identity. An empty or malformed family is never one, even against itself.

func ValidNameToken

func ValidNameToken(s string) bool

ValidNameToken reports whether s is a host name token the server accepts.

Types

type Agent

type Agent interface {
	// Done is closed when the process exits.
	Done() <-chan struct{}
	// ExitDetail describes how it exited, once Done is closed.
	ExitDetail() string
	// PID is its process id (0 when unknown).
	PID() int
}

Agent is a launched agent process.

type AgentRecord

type AgentRecord struct {
	Worktree string `json:"worktree"`
	PID      int    `json:"pid"`
}

AgentRecord is one launched agent.

type BatchSource added in v0.25.0

type BatchSource interface {
	Batches(ctx context.Context) ([]delivery.Batch, error)
}

BatchSource reads this host's due batches with the host key.

type Config

type Config struct {
	// Server is the Hadron server URL; empty means the CLI's usual resolution.
	Server string `yaml:"server"`
	// App is the team App the host serves (hrn:app:<root>:<slug>, or an id).
	App string `yaml:"app"`
	// Label names the host to people; empty means the hostname.
	Label string `yaml:"label"`
	// PollIntervalSeconds is how often the host checks in (1 to 86400).
	PollIntervalSeconds int `yaml:"poll_interval_seconds"`
	// KeyFile holds the App key carrying host:operate for this host
	// (required; mode 0600 or tighter). Relative paths resolve against the
	// configuration's directory.
	KeyFile string `yaml:"key_file"`
	// WorktreesRoot is the directory every worktree name token is relative to.
	WorktreesRoot string `yaml:"worktrees_root"`
	// StateFile records the worktrees this poller created and any it must
	// keep. Empty means hadron-host-state.json beside the config.
	StateFile string `yaml:"state_file"`
	// Repos maps a repo token (as worktree.create names it, e.g.
	// hadron-memory/hadron-cli) to the local clone worktrees are added from.
	Repos map[string]string `yaml:"repos"`
	// Workers maps a worker reference (its name in the App, URN or id) to
	// how this host runs it.
	Workers map[string]WorkerConfig `yaml:"workers"`
	// Delivery turns on chat delivery into this host's Orca terminals
	// (cli#907). Absent, the poller carries out host operations only.
	Delivery DeliveryConfig `yaml:"delivery"`
}

The host-local configuration. Hadron never sees it: the launch command and any AI-plan login are the host's own (Holger #10777, cor:agt:070 Out of Scope), and the host key is read from a file named here — never from an environment variable, which a same-user agent could read from the poller's /proc entry.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads and validates a host configuration file.

func (*Config) CheckWorktreeAliases added in v0.25.0

func (c *Config) CheckWorktreeAliases(s *State) error

CheckWorktreeAliases refuses two workers whose worktrees, configured or created by this poller, name one directory under different tokens (cli#906). validate already refuses one token listed twice; a symlink gives one directory a second token. A tree that does not exist yet is compared by its cleaned path only. One worker's own aliases are allowed.

func (*Config) DeliveryStateDir added in v0.25.0

func (c *Config) DeliveryStateDir() string

DeliveryStateDir holds delivery's records and per-worker locks: beside the state file, where C2's --orca-host-config finds the same locks.

func (*Config) ReadHostKey

func (c *Config) ReadHostKey() (string, error)

ReadHostKey returns the host:operate key from key_file. Never from HADRON_TOKEN or the token store (the holder's own credential, which the launched agents bind on), and never from an environment variable.

The file must not be readable by group or others. That is the part of key custody the poller CAN check. The part it cannot: an agent running as the same OS user as the poller can read any file the poller can. Keeping the key from a launched agent's environment, argv and files is the poller's; keeping it from the agent's OS user is the deployment's — run the poller as its own user, with key_file readable by that user only.

func (*Config) Recipient added in v0.25.0

func (c *Config) Recipient(s *State, ref, workerID string) delivery.Recipient

Recipient is the configured worker ref, resolved to workerID, as delivery needs it: its expected interface and its configured and created worktrees as absolute paths. The poller and C2 build it the same way.

func (*Config) RefFor added in v0.25.0

func (c *Config) RefFor(id, urn, name string) string

RefFor is the configured reference for a worker, matched by id, URN or name (names are unique per App case-insensitively, cor:agt:020:02), or "" when none matches. It is the poller's registration rule, for a caller that delivers on this host without a live registration (C2, cli#908).

func (*Config) WorkerRefs

func (c *Config) WorkerRefs() []string

WorkerRefs lists the configured worker references in a stable order.

func (*Config) WorktreePath

func (c *Config) WorktreePath(token string) string

WorktreePath is the absolute path of a worktree name token.

type Deliverer added in v0.25.0

type Deliverer interface {
	Deliver(ctx context.Context, b delivery.Batch, r delivery.Recipient) delivery.Outcome
}

Deliverer carries one batch to its recipient; *delivery.Deliverer is one.

type DeliveryConfig added in v0.25.0

type DeliveryConfig struct {
	// Enabled turns delivery on. The host key then also needs
	// attention:deliver; without it delivery switches off for the run.
	Enabled bool `yaml:"enabled"`
	// OrcaBin is the absolute path of the orca CLI.
	OrcaBin string `yaml:"orca_bin"`
}

DeliveryConfig is chat delivery's part of a host configuration.

type Event

type Event struct {
	Time      time.Time
	Kind      string
	Operation string
	Type      string
	Outcome   string
	Detail    string
}

Event is one line of what the poller did, for the operator. The --json shape is the command package's DTO, not this.

type ExecHost

type ExecHost struct {
	// PromptTimeout bounds worker.end's prompt command.
	PromptTimeout time.Duration
}

ExecHost is the real Host: git for worktrees, exec for agents. Every command is a fixed argv — git's own, or the one in host configuration — never a shell string, and nothing in it comes from an operation's arguments except git ref and path names, which git receives as separate arguments after the option they belong to.

func (ExecHost) Alive

func (ExecHost) Alive(pid int) bool

Alive reports whether pid is a running process.

func (ExecHost) CreateWorktree

func (ExecHost) CreateWorktree(ctx context.Context, repoPath, path, branch, base string) error

CreateWorktree fetches the base from origin, then adds the worktree on a new branch from origin/<base> (or the local <base> when origin has none).

func (ExecHost) Inspect

func (ExecHost) Inspect(ctx context.Context, path string) (WorktreeState, error)

Inspect reads a worktree's branch, cleanliness and push state.

func (ExecHost) Launch

func (ExecHost) Launch(_ context.Context, spec LaunchSpec) (Agent, error)

Launch starts the agent and returns at once. On Unix it gets its own session (setsid), so the poller's signals do not reach it; on Windows it stays in the poller's console group.

func (ExecHost) Occupied

func (ExecHost) Occupied(_ context.Context, path string) (string, error)

Occupied looks for a process whose working directory is inside path, via /proc (Linux). Without /proc it cannot tell and reports none.

func (ExecHost) Prompt

func (h ExecHost) Prompt(ctx context.Context, argv []string, dir string, env []string) error

Prompt runs worker.end's prompt command to completion, bounded.

func (ExecHost) RemoveWorktree

func (ExecHost) RemoveWorktree(ctx context.Context, path string) error

RemoveWorktree removes a worktree git considers clean; git refuses a dirty one on its own, as a second line behind Inspect.

type Host

type Host interface {
	// CreateWorktree adds a worktree at path on a new branch from base.
	CreateWorktree(ctx context.Context, repoPath, path, branch, base string) error
	// Inspect reads a worktree's state; Exists is false for a missing path.
	Inspect(ctx context.Context, path string) (WorktreeState, error)
	// RemoveWorktree removes a clean worktree.
	RemoveWorktree(ctx context.Context, path string) error
	// Launch starts an agent and returns at once; the agent keeps running.
	Launch(ctx context.Context, spec LaunchSpec) (Agent, error)
	// Prompt runs a short command (worker.end's prompt) to completion.
	Prompt(ctx context.Context, argv []string, dir string, env []string) error
	// Occupied names a process (other than the poller) working inside path,
	// or "" when none is seen. Where the host cannot tell (no /proc), it
	// reports none, and worktree.remove leans on the recorded agents alone.
	Occupied(ctx context.Context, path string) (string, error)
	// Alive reports whether a process id is still running.
	Alive(pid int) bool
}

Host is everything the poller does to the machine: git worktrees and agent processes. The real one shells out to git and execs the configured argv; tests fake it.

type LaunchSpec

type LaunchSpec struct {
	Argv []string
	Dir  string
	Env  []string
}

LaunchSpec is one agent start: the argv from host configuration, run in the worktree, with an environment that carries the launch grant and no Hadron credential of the host's.

type Operation

type Operation struct {
	ID            string
	Type          string
	Args          map[string]any
	WorkerID      string
	Status        string
	Result        map[string]any
	FailureReason string
	// LaunchGrant is set only on a collected worker.start.
	LaunchGrant string
}

Operation is a collected host operation.

type Poller

type Poller struct {

	// BindPoll is how often a pending start asks whether its session bound.
	BindPoll time.Duration
	// Fatal, when set, decides that a failed check-in ends Run: a refused or
	// revoked host key does not heal by retrying, and looping on it would
	// hide it.
	Fatal func(error) bool

	// DeliveryStep, when set, is told how each delivery step's batch read
	// went (delivery.StepRead, StepReadFailed, StepOffByScope).
	DeliveryStep func(at time.Time, read string)
	// WakeStep, when set, is told how each step's wake read went (cli#906
	// row 1).
	WakeStep func(delivery.WakeRead)
	// contains filtered or unexported fields
}

Poller is one host's loop.

func New

func New(cfg *Config, server Server, host Host, state *State, key string, emit func(Event)) *Poller

New builds a poller. key is the host key, kept only to scrub it from launched agents' environments (defence in depth: it is never put there).

func (*Poller) DeliveryEnabled added in v0.25.0

func (p *Poller) DeliveryEnabled() bool

DeliveryEnabled reports whether this run delivers.

func (*Poller) EnableDelivery added in v0.25.0

func (p *Poller) EnableDelivery(src BatchSource, d Deliverer)

EnableDelivery turns chat delivery on.

func (*Poller) EnableWakes added in v0.25.0

func (p *Poller) EnableWakes(src WakeSource, d WakeDeliverer)

EnableWakes turns tick and task wake delivery on (cli#906 row 1). Wakes are read only while chat delivery is on: they share its key, scope and per-worker chain.

func (*Poller) ReportFailures

func (p *Poller) ReportFailures() int

ReportFailures is how many reports did not reach the server; those operations are re-delivered at the next collect.

func (*Poller) Run

func (p *Poller) Run(ctx context.Context) error

Run polls until ctx ends, then waits for operations in flight.

func (*Poller) Step

func (p *Poller) Step(ctx context.Context) error

Step is one check-in: register (always — a host that stops registering lapses), then collect and dispatch.

A worker.start waiting for its bind is re-delivered by every collect and skipped here as in flight. It must not come back with a fresh grant: that would revoke the one its agent was handed. The server stops doing that (Liam, team chat #11415: a re-collect omits a start whose grant is still live), and this poller depends on it rather than pausing collection.

func (*Poller) Wait

func (p *Poller) Wait()

Wait blocks until every dispatched operation has been carried out.

type RegisteredHost

type RegisteredHost struct {
	HostID  string
	Workers []RegisteredHostWorker
}

RegisteredHost is the server's answer to a registration: which worker id each configured reference resolved to.

type RegisteredHostWorker

type RegisteredHostWorker struct {
	WorkerID string
	URN      string
	Name     string
}

RegisteredHostWorker pairs a resolved worker id with its name.

type RegisteredWorker

type RegisteredWorker struct {
	Worker    string // the configured reference
	Family    string
	Worktrees []string
}

RegisteredWorker is one worker in a registration.

type Registration

type Registration struct {
	Label               string
	PollIntervalSeconds int
	Workers             []RegisteredWorker
}

Registration is one registerHost call: the whole worker list.

type Server

type Server interface {
	Register(ctx context.Context, r Registration) (RegisteredHost, error)
	Collect(ctx context.Context) ([]Operation, error)
	// Report returns the operation as the server recorded it.
	Report(ctx context.Context, id, outcome, worktree, reason string) (Operation, error)
}

Server is the host's four server calls, made with the host key.

type State

type State struct {

	// Created maps a configured worker reference to the worktree tokens this
	// poller created for it.
	Created map[string][]string `json:"created"`
	// Keep maps a worktree token to why it must not be removed.
	Keep map[string]string `json:"keep"`
	// HostID is the id the server derived from the host key at the last
	// registration — how `status` finds this host's own registration, since
	// a label is not unique.
	HostID string `json:"hostId"`
	// Agents are the agents this poller launched, by worker id: the
	// worktree each runs in and its process id. A launched agent outlives a
	// poller restart (it runs in its own session), so the in-memory record
	// alone would forget it and let worktree.remove take a tree from under
	// a live agent. A reused pid only makes a check refuse: the safe way.
	Agents map[string]AgentRecord `json:"agents"`
	// contains filtered or unexported fields
}

State is what the poller remembers across restarts: the worktrees it created (so the next registration lists them — the server does not add a created worktree to the registration itself), and the worktrees it must keep because a worker.end on them failed (cor:agt:020:10: never remove a worktree after a failed end).

func LoadState

func LoadState(path string) (*State, error)

LoadState reads the state file; a missing file is an empty state.

func (*State) RegisteredHostID

func (s *State) RegisteredHostID() string

RegisteredHostID is the host id saved at the last registration.

func (*State) Snapshot

func (s *State) Snapshot() (map[string][]string, map[string]string)

Snapshot copies the state for display.

type WakeDeliverer added in v0.25.0

type WakeDeliverer interface {
	DeliverWake(ctx context.Context, w delivery.Wake, r delivery.Recipient) delivery.Outcome
}

WakeDeliverer carries one wake to its recipient; *delivery.Deliverer is one.

type WakePage added in v0.25.0

type WakePage struct {
	Wakes []delivery.Wake
	// BudgetExhausted: WAKE_BUDGET_EXHAUSTED. Nothing is typed; the
	// withheld wakes stay due.
	BudgetExhausted bool
	// Dormant and Withheld are counted in status only.
	Dormant, Withheld int
}

WakePage is one step's due wakes.

type WakeSource added in v0.25.0

type WakeSource interface {
	Wakes(ctx context.Context) (WakePage, error)
}

WakeSource reads this host's due wakes with the host key (dueWakes, paged to exhaustion over this host's registered workers).

type WorkerConfig

type WorkerConfig struct {
	// Family is the model family it runs as here, e.g. claude or codex.
	Family string `yaml:"family"`
	// Launch is the argv that starts its agent in a worktree. The poller adds
	// HADRON_LAUNCH_GRANT to its environment and nothing else of Hadron's.
	Launch []string `yaml:"launch"`
	// End is an optional argv that prompts the running agent to end its own
	// session with a handoff (worker.end). It is never a kill.
	End []string `yaml:"end"`
	// EndWait bounds how long worker.end waits for the session to end
	// (default 15m).
	EndWait string `yaml:"end_wait"`
	// Worktrees are this worker's existing worktree name tokens, relative to
	// worktrees_root, beyond the ones this poller created.
	Worktrees []string `yaml:"worktrees"`
	// TUI is the agent interface its terminal shows, claude or codex, when
	// the model family does not say (a glm worker runs in Claude Code).
	// Delivery holds a terminal that shows anything else.
	TUI string `yaml:"tui"`
}

WorkerConfig is one worker this host can launch.

type WorktreeState

type WorktreeState struct {
	Exists bool
	// IsWorktree is false for a path that exists but is not a git worktree.
	IsWorktree bool
	Branch     string
	// Dirty is uncommitted or untracked changes.
	Dirty bool
	// Upstream is the branch's upstream, empty when it has none.
	Upstream string
	// Ahead counts commits not on the upstream.
	Ahead int
}

WorktreeState is what worktree.create and worktree.remove decide on.

Jump to

Keyboard shortcuts

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