Decision 0017 — One probe vocabulary for checks and gates
One shared probe vocabulary for checks and gates, with the SSRF guard applied per face and Location opt-in per kind.
Status: accepted · 2026-09-15
Context
Reactive checks (tcp, ping, http) and post-run gate probes (http, dns,
tls, metric) were two disjoint implementations of the same idea: run a
network assertion, decide up/down from dial/exit/status truth. The overlap was
real and drifting — both implemented HTTP with different default accept ranges
(checks 2xx/3xx, gates 2xx) and only checks carried a dial-time SSRF guard. The
v1.1 roadmap's cert-expiry signal (R2) was already written in the gate package
but unreachable from a check, so an operator needed a second monitor for it.
Decision
Introduce internal/probe: one union Spec (Kind plus the per-kind fields),
one Validate, one Run returning up/down + duration + detail, and one
Location() (netip.Addr, bool) accessor. Both checks and gates build a Spec
and call it; verify keeps its public Probe/Encode/Decode and job routing
unchanged. Checks gain dns, tls (not-after floor), and metric
(op/value/threshold) as a pure Go addition once the kind vocabulary exists.
Two consequences are explicit, not incidental:
check_row.kindwidens by rebuilding bothcheck_rowandcheck_state— SQLite cannot alter a CHECK, andcheck_state's incoming FK means the child is dropped before the parent. Targets stay a flat, additive JSON shape so existing rows and golden tests are not restructured.- The SSRF guard is an option. Checks refuse loopback/link-local/unspecified/
multicast (
AllowLoopback=false); gates allow them (true), because a gate legitimately probes a loopback health endpoint that a check must not. - Location is opt-in per kind. Only
tcp/pingaddress literals (and a bare-IPtlsaddress) classify into a netgroup;dns/tls/metriccarry none, and every netgroup read path goes throughLocationrather than assumingAddressis an IP.
Consequences
dns/tls/metricreach a check for free; the roadmap's cert ladder and metric thresholds need no new engine.- One HTTP implementation, one validation path, one test surface; the two accept-range defaults are preserved as a field and pinned by tests rather than silently unified.
internal/probedoes not importstore; the store builds aSpecfrom a check kind + target, and validation delegates to the sameSpec.Validate.- A future probe kind is a Go addition plus, only if it changes the stored
kindset, one widening migration — the same shape this record already paid for.