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}