code wiki / _hdl_build / nx_edge_watchdog.nx

nx_edge_watchdog.nx source

↩ module page · 220 lines · 9887 B

1// nx_edge_watchdog.nx -- OFF-BOX LIVENESS WATCHDOG for the family's public surfaces (F620). 2// BORN FROM THE 2026-07-20 NAS OUTAGE: every monitor we had (vqoe gate, cron-watch, deploy-ready, 3// reconciler, supervisor, self-heal) runs ON THE NAS -- so when the NAS died, the monitoring died WITH it 4// and NOTHING alerted; the outage was noticed only because a human's tool calls started timing out. 5// ★LAW: a monitor that shares a failure domain with its subject is not a monitor. 6// This organ therefore runs on the LAPTOP (or any host that is not the NAS) and probes the PUBLIC edge 7// exactly as a family member's browser would -- WAN-side, no LAN shortcuts, no NAS-side dependency. 8// 9// CONTRACT (data-driven, rule 11): targets live in a conf, one per line "<label> <https-url> <needle>". 10// A target is UP iff the fetch returns 200 AND the body contains <needle> (content proof, not port-open -- 11// the "wedged but alive" class the recovery doctrine calls out). State is a tiny append-only log plus a 12// LATCHED alert file that a human/automation can watch; consecutive failures must reach `fail_min` before 13// alerting, so one flaky fetch never cries wolf, and recovery CLEARS the latch with an explicit RECOVERED 14// line (an alert that cannot clear itself is noise). 15// usage: nx_edge_watchdog [conf] [statelog] [alertfile] 16// defaults: knowledge/registry/edge_watch.conf · knowledge/status/edge_watch.log · knowledge/status/EDGE_ALERT 17// exit: 0 all UP · 1 one or more DOWN (so a scheduler/pulse can branch on it) 18// license_tier: ORIGINAL No hw writes (Rule 26). 19import "nx_syscalls.nx" 20import "nx_x509_trust_store.nx" 21import "nx_trust_store_load_from_certdata.nx" 22import "nx_https_url_connect.nx" 23import "nx_https_fetch_follow.nx" 24const EW_MAGIC_4194304: i64 = 4194304 25const EW_MAGIC_2048: i64 = 2048 26const EW_MAGIC_65536: i64 = 65536 27const EW_MAGIC_4096: i64 = 4096 28 29const EW_CAP: i64 = 262144 // per-probe body cap (a liveness needle never needs more) 30const EW_CONF_CAP: i64 = 16384 31const EW_MAXT: i64 = 16 // DECLARED envelope: at most 16 targets per run 32const EW_FAILMIN: i64 = 2 // consecutive failures before the latch trips 33 34func ew_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 35func ew_cat(dst: *u8, off: i64, s: *u8) -> i64 { 36 var i: i64 = 0 37 while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } 38 return off + i 39} 40func ew_catn(dst: *u8, off: i64, v: i64) -> i64 { 41 var m: i64 = v 42 var o: i64 = off 43 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m } 44 let t: *u8 = sys_mmap(32) 45 var k: i64 = 0 46 if m == 0 { t[0] = 48 as u8; k = 1 } 47 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 48 var j: i64 = 0 49 while j < k { dst[o+j] = t[k-1-j]; j = j + 1 } 50 return o + k 51} 52func ew_contains(b: *u8, n: i64, needle: *u8) -> i64 { 53 var nl: i64 = 0 54 while needle[nl] != (0 as u8) { nl = nl + 1 } 55 if nl == 0 { return 1 } 56 var i: i64 = 0 57 while i + nl <= n { 58 var k: i64 = 0 59 var ok: i64 = 1 60 while k < nl { if b[i+k] != needle[k] { ok = 0; k = nl } else { k = k + 1 } } 61 if ok == 1 { return 1 } 62 i = i + 1 63 } 64 return 0 65} 66// copy a whitespace-delimited token from cb[p..e) into out; returns the index just past it 67// ⚠NishiLang has NO `break` -- use an explicit flag, never a sentinel-index trick (the first cut of this 68// fn did `p = e + 1` to leave the loop then tried to undo it arithmetically, which collapses to `e` and 69// parsed ZERO targets: the organ built clean and silently watched nothing. Flag-based is the only idiom.) 70func ew_tok(cb: *u8, p0: i64, e: i64, out: *u8) -> i64 { 71 var p: i64 = p0 72 var sc: i64 = 1 73 while sc == 1 { 74 if p >= e { sc = 0 } else { 75 let c: i64 = cb[p] as i64 76 if c == 32 { p = p + 1 } else { if c == 9 { p = p + 1 } else { sc = 0 } } 77 } 78 } 79 var o: i64 = 0 80 var sc2: i64 = 1 81 while sc2 == 1 { 82 if p >= e { sc2 = 0 } else { 83 let c: i64 = cb[p] as i64 84 if c == 32 { sc2 = 0 } else { if c == 9 { sc2 = 0 } else { if c == 13 { sc2 = 0 } else { 85 out[o] = cb[p] as u8 86 o = o + 1 87 p = p + 1 88 } } } 89 } 90 } 91 out[o] = 0 as u8 92 return p 93} 94 95func main(argc: i64, argv: *i64) -> i64 { 96 var confp: *u8 = "knowledge/registry/edge_watch.conf" as *u8 97 var logp: *u8 = "knowledge/status/edge_watch.log" as *u8 98 var alertp: *u8 = "knowledge/status/EDGE_ALERT" as *u8 99 if argc >= 2 { confp = argv[1] as *u8 } 100 if argc >= 3 { logp = argv[2] as *u8 } 101 if argc >= 4 { alertp = argv[3] as *u8 } 102 103 let tsb: *i64 = sys_mmap(16) as *i64 104 tsb[0] = 0 105 sys_clock_gettime_real(tsb) 106 let now: i64 = tsb[0] 107 108 let r: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt" as *u8, 512, EW_MAGIC_4194304) 109 if r <= 0 { ew_w("EDGE-WATCH RED: cannot load trust store (run from the repo root)\n" as *u8); sys_exit(2); return 2 } 110 let store: *TrustStore = r as *TrustStore 111 112 let lenb: *i64 = sys_mmap(8) as *i64 113 let cb: *u8 = sys_read_file(confp, lenb) 114 if (cb as i64) == 0 { ew_w("EDGE-WATCH RED: no target conf at " as *u8); ew_w(confp); ew_w("\n" as *u8); sys_exit(2); return 2 } 115 let cn: i64 = lenb[0] 116 117 let body: *u8 = sys_mmap(EW_CAP) 118 let st: *i64 = sys_mmap(8) as *i64 119 let lab: *u8 = sys_mmap(512) 120 let url: *u8 = sys_mmap(EW_MAGIC_2048) 121 let ndl: *u8 = sys_mmap(512) 122 let ob: *u8 = sys_mmap(EW_MAGIC_65536) 123 var o: i64 = 0 124 var ntar: i64 = 0 125 var ndown: i64 = 0 126 127 var i: i64 = 0 128 while i < cn { 129 var e: i64 = i 130 var sc: i64 = 1 131 while sc == 1 { if e >= cn { sc = 0 } else { if cb[e] == (10 as u8) { sc = 0 } else { e = e + 1 } } } 132 if e > i { if cb[i] != (35 as u8) { if ntar < EW_MAXT { 133 var p: i64 = ew_tok(cb, i, e, lab) 134 p = ew_tok(cb, p, e, url) 135 p = ew_tok(cb, p, e, ndl) 136 if url[0] != (0 as u8) { 137 ntar = ntar + 1 138 st[0] = 0 139 let n: i64 = nx_https_fetch_follow(url, store, body, EW_CAP, 6, st) 140 var up: i64 = 0 141 if st[0] == 200 { if n > 0 { if ew_contains(body, n, ndl) == 1 { up = 1 } } } 142 o = ew_catn(ob, o, now) 143 o = ew_cat(ob, o, "\t" as *u8) 144 o = ew_cat(ob, o, lab) 145 o = ew_cat(ob, o, "\t" as *u8) 146 if up == 1 { o = ew_cat(ob, o, "UP" as *u8) } else { o = ew_cat(ob, o, "DOWN" as *u8); ndown = ndown + 1 } 147 o = ew_cat(ob, o, "\tstatus=" as *u8) 148 o = ew_catn(ob, o, st[0]) 149 o = ew_cat(ob, o, " bytes=" as *u8) 150 o = ew_catn(ob, o, n) 151 o = ew_cat(ob, o, "\t" as *u8) 152 o = ew_cat(ob, o, url) 153 o = ew_cat(ob, o, "\n" as *u8) 154 } 155 } } } 156 i = e + 1 157 } 158 159 // append this round to the state log (append-only: history is how you see a flap vs a real outage) 160 let lf: i64 = sys_openat_append(logp, 0x1a4) 161 if lf >= 0 { sys_write(lf, ob, o); sys_close(lf) } 162 sys_write(1, ob, o) 163 164 // LATCH: consecutive-failure counter lives in the alert file itself, so the organ is stateless. 165 // fail_min consecutive DOWN rounds trip it; a clean round CLEARS it with an explicit RECOVERED line. 166 if ndown > 0 { 167 let ab: *u8 = sys_read_file(alertp, lenb) 168 // parse ONLY the LEADING integer -- stop at the first non-digit. (First cut scanned the whole 169 // line and swallowed the epoch + ratio digits too, yielding a nonsense streak of 1178458015234.) 170 var prev: i64 = 0 171 if (ab as i64) != 0 { if lenb[0] > 0 { 172 var q: i64 = 0 173 var pc: i64 = 1 174 while pc == 1 { 175 if q >= lenb[0] { pc = 0 } else { 176 let c: i64 = ab[q] as i64 177 var isd: i64 = 0 178 if c >= 48 { if c <= 57 { isd = 1 } } 179 if isd == 1 { prev = prev * 10 + (c - 48); q = q + 1 } else { pc = 0 } 180 } 181 } 182 } } 183 let streak: i64 = prev + 1 184 let alb: *u8 = sys_mmap(EW_MAGIC_4096) 185 var ao: i64 = ew_catn(alb, 0, streak) 186 ao = ew_cat(alb, ao, " consecutive failed rounds; last epoch " as *u8) 187 ao = ew_catn(alb, ao, now) 188 ao = ew_cat(alb, ao, "; targets_down=" as *u8) 189 ao = ew_catn(alb, ao, ndown) 190 ao = ew_cat(alb, ao, "/" as *u8) 191 ao = ew_catn(alb, ao, ntar) 192 ao = ew_cat(alb, ao, "\n" as *u8) 193 let af: i64 = sys_openat_wr(alertp, 0x1a4) 194 if af >= 0 { sys_write(af, alb, ao); sys_close(af) } 195 if streak >= EW_FAILMIN { 196 let nb: *u8 = sys_mmap(256) 197 var no: i64 = ew_cat(nb, 0, "\n*** EDGE-WATCH ALERT: the family's public surface is DOWN -- " as *u8) 198 no = ew_catn(nb, no, streak) 199 no = ew_cat(nb, no, " consecutive rounds, " as *u8) 200 no = ew_catn(nb, no, ndown) 201 no = ew_cat(nb, no, "/" as *u8) 202 no = ew_catn(nb, no, ntar) 203 no = ew_cat(nb, no, " targets ***\n" as *u8) 204 sys_write(1, nb, no) 205 } 206 sys_exit(1) 207 return 1 208 } 209 // all UP -> clear the latch (an alert that cannot clear itself becomes noise nobody reads) 210 let ab2: *u8 = sys_read_file(alertp, lenb) 211 if (ab2 as i64) != 0 { if lenb[0] > 0 { 212 let cf: i64 = sys_openat_wr(alertp, 0x1a4) 213 if cf >= 0 { sys_write(cf, "0 RECOVERED\n" as *u8, 12); sys_close(cf) } 214 let lf2: i64 = sys_openat_append(logp, 0x1a4) 215 if lf2 >= 0 { sys_write(lf2, "RECOVERED all targets UP\n" as *u8, 25); sys_close(lf2) } 216 ew_w("EDGE-WATCH RECOVERED (latch cleared)\n" as *u8) 217 } } 218 sys_exit(0) 219 return 0 220}