nx_http_probe_lib.nx
buildroot/runtime/nx_http_probe_lib.nx
about
nx_http_probe_lib.nx -- THE BOUNDED LOOPBACK HTTP LIVENESS PROBE + DEAD-MAN HEARTBEAT STAMPER.
WHY THIS EXISTS (2026-08-20). The estate's only compensating control over a FAIL-OPEN authentication
path was a curl one-liner in cron.reg:
curl -s --max-time 8 http://127.0.0.1:18096/api/cap/status | grep -q '"secure":true' \
&& date +ts=%s > knowledge/status/capplane.log
The cron row FIRED and the command FAILED, and because the row DISCARDS ALL OUTPUT its own failure was
unobservable: 7 of 8 conjuncts were proven true and the 8th ("does curl run") was structurally
unmeasurable. The cap-plane watch went stale at 10.9x its budget and nobody could say which link broke.
**** A PROBE THAT FAILS SILENTLY IS NOT A CONTROL, IT IS A DECORATION. ****
So the single disqualifying property for the replacement is a silent failure mode. EVERY exit path here
carries a NAMED reason, printed on stdout AND stderr, and the verdict line is LAST so a positional
reader (gv_last_line, nx_gate_green) can anchor on it without matching text anywhere else.
WHY A LIB AND NOT ONE FILE: the CLI (nx_http_probe.nx) and the gate (nx_http_probe_gate.nx) both need
the same logic, and a gate that re-implements its subject proves nothing. One ruler, two callers.
LOOPBACK-ONLY BY CONSTRUCTION -- THIS IS A SAFETY PROPERTY, NOT A LIMITATION. The destination address
is hard-pinned to 127.0.0.1 and cannot be passed in. cron.reg's own cap-plane comment records why:
during the 2026-08-06 outage the PUBLIC url returned insecure_placeholder/secure=false while loopback
:18096 returned provisioned/secure=true, because the public :443 answer came from a DIFFERENT BINARY
(DSM nginx co-squat, D008) -- and that sent the diagnosis down a false path for an hour.
**** A HEALTH ENDPOINT ANSWERS FOR THE PROCESS THAT SERVED IT, NEVER FOR THE SERVICE. ****
A sibling seat had to re-derive that this session after taking the convenient answer. Pinning the
address makes the mistake unrepresentable rather than merely documented.
COMPOSED, NOT RE-IMPLEMENTED. Three proven primitives do the work:
nx_connect_bounded (nx_connect.nx) -- SO_RCVTIMEO does NOT bound connect(); this does
nx_http_client_build_request(nx_http_client.nx) -- the ONE request-format definition in the estate
sys_set_socket_timeout (nx_syscalls.nx) -- SO_RCVTIMEO/SO_SNDTIMEO
The only new code is a DEADLINE-AWARE drain. nx_http_client_get's own _drain has no read bound, so a
daemon that accepts and then wedges -- WHICH IS EXACTLY THE 2026-08-06 INCIDENT -- parks the probe
forever. On a */5 beat that piles up processes, which is a resource bug even when the feature works.
nx_http_client_get is otherwise the right organ and is deliberately left byte-untouched: editing a lib
with ~100 consumers to add one caller's timeout is a fleet-wide blast radius for a local need.
THE BUDGET IS AN ARGUMENT, NOT A CONSTANT. Rule 11: no magic numbers. The caller states the budget and
states its derivation at the call site (cron.reg). Worst-case wall time is ~2x budget: the connect leg
and the receive leg each get the full budget, deliberately, so that "slow to connect" and "slow to
answer" are separately diagnosable instead of sharing one squeezed allowance. Pick budget <= beat/2.
dependencies 4 imports · 2 importers
imports: nx_syscalls.nxnx_connect.nxnx_http_client.nxnx_gate.nx
imported by: nx_http_probe.nxnx_http_probe_gate.nx
structs
| none |
consts
| 58 | const HP_EXIT_OK: i64 = 0 |
| 59 | const HP_EXIT_USAGE: i64 = 1 |
| 60 | const HP_EXIT_CONNECT_FAILED: i64 = 2 |
| 61 | const HP_EXIT_PATTERN_ABSENT: i64 = 3 |
| 62 | const HP_EXIT_STATUS_NOT_2XX: i64 = 4 |
| 63 | const HP_EXIT_STAMP_WRITE_FAILED:i64 = 5 |
| 64 | const HP_EXIT_RECV_TIMEOUT: i64 = 6 |
| 65 | const HP_EXIT_SEND_FAILED: i64 = 7 |
| 66 | const HP_EXIT_BODY_TRUNCATED: i64 = 8 |
| 67 | const HP_EXIT_PATH_TOO_LONG: i64 = 9 |
| 68 | const HP_EXIT_SOCKET_FAILED: i64 = 10 |
| 69 | const HP_EXIT_EMPTY_RESPONSE: i64 = 11 |
| 70 | const HP_EXIT_NOT_HTTP: i64 = 12 |
| 74 | const HP_LEGACY_DEFAULT_PORT: i64 = 8080 |
| 78 | const HP_BODY_CAP: i64 = 4194304 |
| 82 | const HP_REQ_CAP: i64 = 4096 |
| 86 | const HP_PATH_MAX: i64 = 2048 |
| 88 | const HP_ADDR_BYTES: i64 = 16 |
| 89 | const HP_STAMP_CAP: i64 = 64 |
| 90 | const HP_MODE_0644: i64 = 420 |
| 91 | const HP_FD_STDOUT: i64 = 1 |
| 92 | const HP_FD_STDERR: i64 = 2 |
| 93 | const HP_ASCII_ZERO: i64 = 48 |
| 94 | const HP_ASCII_NINE: i64 = 57 |
| 95 | const HP_ASCII_SPACE: i64 = 32 |
| 96 | const HP_ASCII_NL: i64 = 10 |
| 97 | const HP_B10: i64 = 10 |
| 98 | const HP_STATUS_DIGITS: i64 = 3 |
| 99 | const HP_STATUS_2XX_LO: i64 = 200 |
| 100 | const HP_STATUS_2XX_HI: i64 = 300 |
| 101 | const HP_MS_PER_SEC: i64 = 1000 |
| 102 | const HP_FLAG_SLOTS: i64 = 32 |
| 104 | const HP_LOOPBACK_A: i64 = 127 |
| 105 | const HP_LOOPBACK_B: i64 = 0 |
| 106 | const HP_LOOPBACK_C: i64 = 0 |
| 107 | const HP_LOOPBACK_D: i64 = 1 |
| 110 | const HP_FL_CLEAN_EOF: i64 = 0 |
| 111 | const HP_FL_CAP_HIT: i64 = 1 |
| 112 | const HP_FL_DEADLINE: i64 = 2 |
| 113 | const HP_FL_READS: i64 = 3 |
functions
| 115 | func hp_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 117 | func hp_werr(s: *u8) -> i64 { sys_write(HP_FD_STDERR, s, hp_strlen(s)); return 0 } |
| 121 | func hp_streq(a: *u8, b: *u8) -> i64 |
| 131 | func hp_nerr(v: i64) -> i64 |
| 149 | func hp_atoi(buf: *u8, off: i64, len: i64) -> i64 |
| 163 | func hp_contains(buf: *u8, n: i64, needle: *u8) -> i64 |
| 181 | func hp_status_of(buf: *u8, n: i64) -> i64 |
| 213 | func hp_drain(fd: i64, buf: *u8, cap: i64, deadline_sec: i64, fl: *i64) -> i64 |
| 250 | func hp_write_stamp(path: *u8, ts: i64) -> i64 |
| 282 | func hp_reason(code: i64) -> *u8 |
| 304 | func hp_emit(code: i64) -> i64 |
| 324 | func hp_run(port: i64, path: *u8, expect: *u8, budget_sec: i64, stamp_path: *u8, verbose: i64) -> i64 |