Documentation
¶
Index ¶
- Variables
- func AgentSearchPaths() []string
- func AgentSearchPathsFor(scope Scope) []string
- func ControlAddrDirs() []string
- func Defaults() *viper.Viper
- func EnvKeyFor(path string) string
- func ReadControlAddr() (addr string, from string, err error)
- func RemoveControlAddr() error
- func WriteControlAddr(addr string) (string, error)
- type Config
- type ControlAddr
- type ErrAmbiguousControlAddr
- type FileOutputConfig
- type LoggingConfig
- type LokiOutputConfig
- type MetricsConfig
- type PathType
- type Scope
- type SecurityConfig
- type ShutdownConfig
- type SupervisorConfig
- type TimeoutConfig
- type TransportConfig
- type WatchdogConfig
Constants ¶
This section is empty.
Variables ¶
var ( // QUICStreamTimeout is the max time for a single QUIC stream operation. // Rationale: Covers network latency + agent marshaling/unmarshaling (< 10s typical). QUICStreamTimeout = 10 * time.Second // QUICIdleTimeout is the connection idle timeout before automatic closure. // Rationale: Balances connection reuse vs resource cleanup (1 minute idle acceptable). QUICIdleTimeout = 60 * time.Second )
QUIC Transport Timeouts
var ( // ClientPendingTimeout is the max wait for an agent to enter PENDING state. // Rationale: Fast-fail if supervisor doesn't acknowledge command quickly. ClientPendingTimeout = 2 * time.Second // ClientTerminalTimeout is the max wait for an agent to reach terminal state (RUNNING/STOPPED/FAILED). // Rationale: Covers Python startup, health checks, socket binding (< 20s for well-behaved agents). ClientTerminalTimeout = 20 * time.Second )
Client Lifecycle Timeouts
var ( // SupervisorStartDeadline is the max time for an agent to report ready after start command. // Rationale: Aligns with ClientTerminalTimeout to prevent supervisor-client mismatch. SupervisorStartDeadline = 20 * time.Second // SupervisorShutdownTimeout is the graceful shutdown timeout before force-kill. // Rationale: Allows agents to flush logs, close connections (5s is reasonable grace period). SupervisorShutdownTimeout = 5 * time.Second )
Supervisor Timeouts
var ( // TestAgentStartTimeout is the max time to wait for agent start in tests. // Rationale: CI environments may be slow; 2-minute buffer covers edge cases. TestAgentStartTimeout = 120 * time.Second // TestAgentStopTimeout is the max time to wait for agent stop in tests. // Rationale: Covers graceful shutdown + Python interpreter cleanup. TestAgentStopTimeout = 60 * time.Second )
Test Timeouts (more generous for CI environments)
Functions ¶
func AgentSearchPaths ¶
func AgentSearchPaths() []string
AgentSearchPaths returns the system-scope search path.
It is the compatibility spelling for AgentSearchPathsFor(ScopeSystem); every existing caller is a system manager.
func AgentSearchPathsFor ¶
AgentSearchPathsFor returns the ordered directories to search, highest precedence first. Discovery is first-ID-wins, so an agent found in an earlier directory MASKS one of the same ID found later - which is what makes these tiers an override mechanism rather than a concatenation.
Every directory is namespaced by the PRODUCT rather than by the kernel (GAPI-DIV-061): gapid searches /usr/lib/gapi/agents, goblind searches /usr/lib/goblin/agents. This function is reached inside goblind through agentmgr's discovery, so it is one of the kernel surfaces an operator who has never heard of gapi would otherwise meet.
THE ORDERING RULE, taken from systemd and XDG and applied to both scopes: configuration beats runtime beats data beats vendor. An operator's edit outranks a package's file, and a transient unit outranks the installed one it shadows.
System scope, highest to lowest, with <p> the product name:
- <PREFIX>_DEV_AGENTS explicit development override
- /etc/<p>/agents operator-authored
- /run/<p>/agents transient, generated at runtime
- /usr/local/lib/<p>/agents locally installed
- /usr/lib/<p>/agents package-owned
User scope, highest to lowest:
- <PREFIX>_DEV_AGENTS explicit development override
- $XDG_CONFIG_HOME/<p>/agents the user's own
- /etc/<p>/user/agents operator-provided, for all users
- $XDG_RUNTIME_DIR/<p>/agents transient
- $XDG_DATA_HOME/<p>/agents user-installed
- ~/.<p>/agents LEGACY, see below
- /usr/lib/<p>/user/agents package-owned user agents
SYSTEM SCOPE CONTAINS NO HOME-DIRECTORY PATH, and that is a security boundary rather than tidiness. agentmgr's safeToExecute already refuses world-writable or foreign-owned binaries at EXECUTION time; keeping user-writable directories out of the system list is the same defence at DISCOVERY time, and the two are not substitutes.
There is deliberately no implicit ./agents tier. It made discovery depend on the working directory a daemon happened to be started from - a daemon launched from the wrong directory silently discovered nothing, and 'agent new' run outside a checkout silently wrote a tree into whatever directory the operator was standing in. Development now names its directory explicitly through <PREFIX>_DEV_AGENTS, which is also what every test and script in this repo already did.
Environment overrides:
- <PREFIX>_AGENT_PATH: colon-separated directories PREPENDED to the tiers below. It adds precedence; it does not replace the path.
- <PREFIX>_AGENT_PATH_EXCLUSIVE: search ONLY what AGENT_PATH names.
- <PREFIX>_DEV_AGENTS: highest-priority directory in either scope.
- <PREFIX>_SKIP_SYSTEM_AGENTS: drop the package-owned tiers.
AGENT_PATH used to REPLACE the whole search path, and the replacement was load-bearing in two places rather than one, which is why the exclusive switch exists rather than the additive behaviour simply landing on its own (GAPI-DIV-063). A packaged install set AGENT_PATH to one directory, so the tiers above were dead code in the only configuration that ships; and test/adk's harness set it to fence discovery to a fixture directory, without which the checkout's own agents starve the fixtures' state transitions (GAPI-DIV-021). Additive fixes the first. The switch preserves the second, and a fence is a thing you ask for rather than a side effect of naming a directory.
func ControlAddrDirs ¶
func ControlAddrDirs() []string
ControlAddrDirs returns the candidate directories, highest priority first. A system daemon under systemd has no XDG_RUNTIME_DIR and gets exactly one entry; that is normal, not degraded.
func Defaults ¶
Defaults returns a viper carrying the environment bindings and every registered default, with no config file read.
It exists so that the thing which DEFINES the defaults can also be the thing that DOCUMENTS them. The configuration reference and the <product>.conf.5 man page are generated from this viper joined with a reflection walk of Config, so a key cannot appear in the documentation without being reachable in the code, or change its value without the page changing with it. That is goal 4's claim made mechanical rather than promised.
It is product-aware: transport.address, metrics.addr and logging.file.path all resolve through core/product, so calling this under gapid and under goblind yields the same key set with different values - which is what lets one renderer produce both products' pages from one schema.
Like Load, this panics on an unset product identity rather than guessing one (GAPI-DIV-061).
func EnvKeyFor ¶
EnvKeyFor renders a dotted config path as the environment variable that overrides it: under gapid, "supervisor.pid1Mode" becomes GAPI_SUPERVISOR_PID1MODE; under goblind, GOBLIN_SUPERVISOR_PID1MODE.
The prefix was the literal "RUNTIME" until GAPI-DIV-059 and the literal "GAPI" until GAPI-DIV-061. Neither could be chosen by the process embedding the kernel, so an operator of goblind - which links this package as a library - had to configure it under a name belonging to a component they are not meant to know exists. It now comes from core/product, set once by the binary.
Both renames are HARD - no fallback reads an old spelling, decided by the operator. A deployed RUNTIME_CONFIG or, on goblind, a deployed GAPI_CONFIG therefore yields default config rather than an error, which is why each carries a release note.
func ReadControlAddr ¶
ReadControlAddr returns the single published address and the file it came from, or empty strings when no daemon has published one.
The source is returned rather than discarded because the caller cannot otherwise report it, and "a bare timeout that names neither address" is the failure this entry was filed for.
More than one live daemon yields *ErrAmbiguousControlAddr, listing every candidate. A missing directory is NOT an error: no daemon has run, which is the ordinary state of a clean host.
func RemoveControlAddr ¶
func RemoveControlAddr() error
RemoveControlAddr deletes THIS PROCESS'S entry on shutdown.
Only its own, which is the second bug the shared-file version had: removing every tier's file meant one daemon shutting down unpublished another daemon that was still running.
Absence is success: shutdown runs on paths where the write never happened (a daemon that failed before binding), and erroring there would turn an orderly stop into a noisy one.
func WriteControlAddr ¶
WriteControlAddr publishes addr under this process's pid in the highest tier it can write, and returns the file it used.
Falling through to the next tier is deliberate: an unprivileged daemon cannot create /run/<p> and must not be fatal for it, while a systemd unit with RuntimeDirectory= can. Only exhausting every tier is an error, and it names them all - a daemon that could not publish is reachable only with an explicit flag, and the operator needs to know that before the first control call fails.
Types ¶
type Config ¶
type Config struct {
Transport TransportConfig `mapstructure:"transport"`
Security SecurityConfig `mapstructure:"security"`
Metrics MetricsConfig `mapstructure:"metrics"`
Logging LoggingConfig `mapstructure:"logging"`
Timeouts TimeoutConfig `mapstructure:"timeouts"`
Supervisor SupervisorConfig `mapstructure:"supervisor"`
}
type ControlAddr ¶
ControlAddr is one published address and where it came from.
func LiveControlAddrs ¶
func LiveControlAddrs() ([]ControlAddr, error)
LiveControlAddrs returns every address published by a process that is still running, highest-priority tier first.
Entries whose process is gone are skipped rather than reported: a daemon killed abruptly leaves one behind, and treating that as an error would make every later client fail on someone else's crash.
type ErrAmbiguousControlAddr ¶
type ErrAmbiguousControlAddr struct {
Candidates []ControlAddr
}
ErrAmbiguousControlAddr is returned when more than one live daemon has published an address. It is an ERROR rather than a choice on purpose: with two daemons running, a client that was given no address cannot know which one is meant, and picking one would be a coin flip that looks like a decision.
func (*ErrAmbiguousControlAddr) Error ¶
func (e *ErrAmbiguousControlAddr) Error() string
type FileOutputConfig ¶
type FileOutputConfig struct {
Enabled bool `mapstructure:"enabled"`
Path string `mapstructure:"path"`
MaxSize int `mapstructure:"maxSize"` // MB
MaxBackups int `mapstructure:"maxBackups"` // Number of old files to keep
MaxAge int `mapstructure:"maxAge"` // Days
Compress bool `mapstructure:"compress"`
}
type LoggingConfig ¶
type LoggingConfig struct {
Level string `mapstructure:"level"` // trace, debug, info, warn, error
Format string `mapstructure:"format"` // json, console
File FileOutputConfig `mapstructure:"file"`
Loki LokiOutputConfig `mapstructure:"loki"`
}
type LokiOutputConfig ¶
type MetricsConfig ¶
type PathType ¶
type PathType int
PathType represents the type of agent path
func ClassifyPath ¶
ClassifyPath reports which tier a path belongs to.
It classifies against the SAME lists that are searched, so the label a log line carries cannot drift from the precedence that produced it - they were two literal lists that disagreed about /etc before GAPI-DIV-061, and /etc's position has now changed.
type Scope ¶
type Scope int
Scope selects which tier list is searched.
It is an explicit parameter and MUST NOT be inferred from the effective uid. A system daemon commonly runs as an unprivileged service user - nix/module.nix creates exactly such a user - and deriving scope from privilege would silently flip that daemon into user scope, where it would discover a different agent set than the operator installed. systemd has the same property: 'systemctl --user' run by root manages root's USER instance, because the scope was asked for rather than deduced.
const ( // ScopeSystem is the machine-wide manager: the daemon an operator // installs and an init system starts. ScopeSystem Scope = iota // ScopeUser is the per-user manager, selected by --user. The tier // list is defined so that implementing --user is wiring rather than // a redesign of the search path. ScopeUser )
type SecurityConfig ¶
type SecurityConfig struct {
VerifyKey string `mapstructure:"verifyKey"` // Path to public key
}
type ShutdownConfig ¶
type ShutdownConfig struct {
GracePeriod string `mapstructure:"gracePeriod"`
}
type SupervisorConfig ¶
type SupervisorConfig struct {
ProductionMode bool `mapstructure:"productionMode"`
// Pid1Mode activates the Phase-0 pre-userspace boot sequence
// (subreaper, PID-1 signals, kmsg, early mounts). Off by default:
// gapid runs as an ordinary supervisor unless it IS init.
Pid1Mode bool `mapstructure:"pid1Mode"`
// NoEarlyMounts skips the mount phase (the OCI runtime owns mounts
// in a container).
NoEarlyMounts bool `mapstructure:"noEarlyMounts"`
Watchdog WatchdogConfig `mapstructure:"watchdog"`
Shutdown ShutdownConfig `mapstructure:"shutdown"`
}
type TimeoutConfig ¶
type TimeoutConfig struct {
QUICStream string `mapstructure:"quicStream"`
QUICIdle string `mapstructure:"quicIdle"`
ClientPending string `mapstructure:"clientPending"`
ClientTerminal string `mapstructure:"clientTerminal"`
SupervisorStart string `mapstructure:"supervisorStart"`
SupervisorShutdown string `mapstructure:"supervisorShutdown"`
}