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
- Variables
- func CanonicalFamily(s string) string
- func SameFamily(a, b string) bool
- func ValidNameToken(s string) bool
- type Agent
- type AgentRecord
- type BatchSource
- type Config
- func (c *Config) CheckWorktreeAliases(s *State) error
- func (c *Config) DeliveryStateDir() string
- func (c *Config) ReadHostKey() (string, error)
- func (c *Config) Recipient(s *State, ref, workerID string) delivery.Recipient
- func (c *Config) RefFor(id, urn, name string) string
- func (c *Config) WorkerRefs() []string
- func (c *Config) WorktreePath(token string) string
- type Deliverer
- type DeliveryConfig
- type Event
- type ExecHost
- func (ExecHost) Alive(pid int) bool
- func (ExecHost) CreateWorktree(ctx context.Context, repoPath, path, branch, base string) error
- func (ExecHost) Inspect(ctx context.Context, path string) (WorktreeState, error)
- func (ExecHost) Launch(_ context.Context, spec LaunchSpec) (Agent, error)
- func (ExecHost) Occupied(_ context.Context, path string) (string, error)
- func (h ExecHost) Prompt(ctx context.Context, argv []string, dir string, env []string) error
- func (ExecHost) RemoveWorktree(ctx context.Context, path string) error
- type Host
- type LaunchSpec
- type Operation
- type Poller
- func (p *Poller) DeliveryEnabled() bool
- func (p *Poller) EnableDelivery(src BatchSource, d Deliverer)
- func (p *Poller) EnableWakes(src WakeSource, d WakeDeliverer)
- func (p *Poller) ReportFailures() int
- func (p *Poller) Run(ctx context.Context) error
- func (p *Poller) Step(ctx context.Context) error
- func (p *Poller) Wait()
- type RegisteredHost
- type RegisteredHostWorker
- type RegisteredWorker
- type Registration
- type Server
- type State
- type WakeDeliverer
- type WakePage
- type WakeSource
- type WorkerConfig
- type WorktreeState
Constants ¶
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.
const ( Succeeded = "SUCCEEDED" Failed = "FAILED" )
Outcomes a host reports.
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" )
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 ¶
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.
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.
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
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
SameFamily reports whether two explicit families are one launch identity. An empty or malformed family is never one, even against itself.
func ValidNameToken ¶
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 ¶
AgentRecord is one launched agent.
type BatchSource ¶ added in v0.25.0
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 ¶
LoadConfig reads and validates a host configuration file.
func (*Config) CheckWorktreeAliases ¶ added in v0.25.0
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
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 ¶
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
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
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 ¶
WorkerRefs lists the configured worker references in a stable order.
func (*Config) WorktreePath ¶
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) CreateWorktree ¶
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) Launch ¶
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 ¶
Occupied looks for a process whose working directory is inside path, via /proc (Linux). Without /proc it cannot tell and reports none.
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 ¶
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 ¶
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
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 ¶
ReportFailures is how many reports did not reach the server; those operations are re-delivered at the next collect.
func (*Poller) Step ¶
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.
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 ¶
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 (*State) RegisteredHostID ¶
RegisteredHostID is the host id saved at the last registration.
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
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.