code wiki / _hdl_build / nx_swarm_discover.nx

nx_swarm_discover.nx source

↩ module page · 228 lines · 10919 B

1// nx_swarm_discover.nx -- SF-DISCOVER: find GPU inference capacity on the LAN and register it. 2// 3// WHY. The swarm's endpoint SSOT (knowledge/swarm_nodes.conf) is HAND-MAINTAINED, and that is how 4// the estate lost its GPU for weeks: DHCP moved the laptop .193 -> .192 and five hardcoded copies 5// went stale in the same instant while every swarm gate still read GREEN. Binding the five copies to 6// one row fixed the DUPLICATION; it did not fix the fact that a human still has to notice and type. 7// ★★★★★A REGISTRY THAT ONLY A HUMAN CAN UPDATE GOES STALE AT EXACTLY THE MOMENT IT MATTERS -- when 8// hardware moves, which is the same moment nobody is watching. 9// 10// MEASURED 2026-08-04, and it is the reason this exists: a sweep of every live host on 192.168.8.0/24 11// found EXACTLY ONE inference endpoint (the laptop 5080 at .192:7861). The "west dual 3090" row in 12// the SSOT pointed at 10.0.4.13 -- a different, unreachable subnet -- so the estate believed it had 13// 48GB of VRAM it could not touch. ★A CONFIG ROW IS A CLAIM ABOUT THE WORLD, AND AN UNVERIFIED ONE 14// IS JUST A WISH WITH AN IP ADDRESS. 15// 16// WHAT IT DOES: probe a host range for real inference endpoints, VERIFY each by asking it to 17// identify itself, and report capacity. With `apply`, APPEND newly-found endpoints to the SSOT. 18// ⛔IT NEVER DELETES OR REWRITES AN EXISTING ROW. A discovery pass that can silently drop a node is 19// a worse failure than a stale row: the stale row fails loudly at dispatch, a deleted one vanishes. 20// Absent hosts are REPORTED, never auto-removed -- a host being off is not proof it is gone. 21// 22// nx_swarm_discover scan [a.b.c] -- probe .1-.254, report what actually answers 23// nx_swarm_discover apply [a.b.c] -- same, then APPEND unregistered finds to swarm_nodes.conf 24// license_tier: ORIGINAL expect_exit: 0 25// module: nishi-core.swarm.discover 26import "nx_syscalls.nx" 27import "nx_estate_path.nx" 28import "nx_connect.nx" 29const SD_MAGIC_1024: i64 = 1024 30 31const SD_PORT_SD: i64 = 7861 // sd-server / our engine (OpenAI-shaped) 32const SD_PORT_OLLAMA: i64 = 11434 // ollama 33const SD_HOST_LO: i64 = 1 34const SD_HOST_HI: i64 = 254 35const SD_BUF: i64 = 65536 36const SD_TMO_MS: i64 = 400 // bounded: a full /24 must not take an hour 37 38func sw(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 39func sn(v: i64) -> i64 { 40 let t: *u8 = sys_mmap(32); let b: *u8 = sys_mmap(32) 41 var m: i64 = v; var k: i64 = 0 42 if m == 0 { t[0] = 48 as u8; k = 1 } 43 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 44 var i: i64 = 0 45 while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 46 sys_write(1, b, k); return 0 47} 48func sd_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 49func sd_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var a: i64 = o; while s[i] != (0 as u8) { d[a] = s[i]; a = a + 1; i = i + 1 } return a } 50func sd_catn(d: *u8, o: i64, v: i64) -> i64 { 51 if v < 0 { d[o] = 45 as u8; return sd_catn(d, o + 1, 0 - v) } 52 // NEGATIVES (2026-08-07). Without this the `while m > 0` loop below never runs for a 53 // negative value and this function emits ZERO CHARACTERS, silently corrupting whatever 54 // format it is writing into. Handled AT THE SIGNATURE so it is independent of which 55 // cursor variable the body happens to use. Non-negative input is byte-identical (rule 19). 56 if v < 0 { d[o] = 45 as u8; return sd_catn(d, o + 1, 0 - v) } 57 let t: *u8 = sys_mmap(32) 58 var m: i64 = v; var k: i64 = 0 59 if m == 0 { t[0] = 48 as u8; k = 1 } 60 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 61 var a: i64 = o; var j: i64 = k - 1 62 while j >= 0 { d[a] = t[j]; a = a + 1; j = j - 1 } 63 return a 64} 65 66// GET <path> from a.b.c.d:port. Returns bytes read, or -1. Bounded connect AND bounded read, so a 67// black-holed host costs SD_TMO_MS, not a 127-second kernel timeout. 68func sd_probe(a: i64, b: i64, c: i64, d: i64, port: i64, path: *u8, out: *u8, cap: i64) -> i64 { 69 let fd: i64 = sys_socket(2, 1, 0) 70 if fd < 0 { return 0 - 1 } 71 sys_set_socket_timeout(fd, 2) 72 let sa: *u8 = sys_mmap(16) 73 sa[0] = 2 as u8; sa[1] = 0 as u8 74 sa[2] = ((port >> 8) & 0xff) as u8; sa[3] = (port & 0xff) as u8 75 sa[4] = a as u8; sa[5] = b as u8; sa[6] = c as u8; sa[7] = d as u8 76 var z: i64 = 8 77 while z < 16 { sa[z] = 0 as u8; z = z + 1 } 78 if nx_connect_bounded(fd, sa, 16, SD_TMO_MS) != 0 { sys_close(fd); return 0 - 1 } 79 let req: *u8 = sys_mmap(SD_MAGIC_1024) 80 var o: i64 = sd_cat(req, 0, "GET " as *u8) 81 o = sd_cat(req, o, path) 82 o = sd_cat(req, o, " HTTP/1.1\r\nHost: probe\r\nConnection: close\r\n\r\n" as *u8) 83 sys_write(fd, req, o) 84 var t: i64 = 0 85 var r: i64 = 1 86 while r > 0 { 87 if t >= cap { r = 0 } else { r = sys_read(fd, ((out as i64) + t) as *u8, cap - t); if r > 0 { t = t + r } } 88 } 89 sys_close(fd) 90 return t 91} 92 93func sd_has(buf: *u8, n: i64, needle: *u8) -> i64 { 94 let m: i64 = sd_len(needle) 95 if m == 0 { return 0 } 96 if m > n { return 0 } 97 var i: i64 = 0 98 while i <= n - m { 99 var j: i64 = 0 100 var same: i64 = 1 101 while j < m { if buf[i + j] != needle[j] { same = 0; j = m } else { j = j + 1 } } 102 if same == 1 { return 1 } 103 i = i + 1 104 } 105 return 0 106} 107 108// Is this a REAL inference endpoint, or just something listening? ★A PORT BEING OPEN IS NOT A 109// CAPABILITY -- a DSM login page answered our first sweep on :8000 and would have been "a node". 110// Verified = it identifies as a model server. 111func sd_verify(a: i64, b: i64, c: i64, d: i64, port: i64, buf: *u8) -> i64 { 112 var n: i64 = 0 113 if port == SD_PORT_OLLAMA { 114 n = sd_probe(a, b, c, d, port, "/api/tags" as *u8, buf, SD_BUF) 115 if n > 0 { if sd_has(buf, n, "models" as *u8) == 1 { return 1 } } 116 return 0 117 } 118 n = sd_probe(a, b, c, d, port, "/v1/models" as *u8, buf, SD_BUF) 119 if n <= 0 { return 0 } 120 if sd_has(buf, n, "\"object\"" as *u8) == 1 { return 1 } 121 if sd_has(buf, n, "\"data\"" as *u8) == 1 { return 1 } 122 if sd_has(buf, n, "model" as *u8) == 1 { return 1 } 123 return 0 124} 125 126// already in the SSOT? (substring match on "a.b.c.d:port" in an uncommented row) 127func sd_registered(a: i64, b: i64, c: i64, d: i64, port: i64) -> i64 { 128 let buf: *u8 = sys_mmap(SD_BUF) 129 let fd: i64 = ep_open_rd("knowledge/swarm_nodes.conf" as *u8) 130 if fd < 0 { return 0 } 131 var n: i64 = 0 132 var r: i64 = 1 133 while r > 0 { r = sys_read(fd, ((buf as i64) + n) as *u8, SD_BUF - n); if r > 0 { n = n + r } } 134 sys_close(fd) 135 let want: *u8 = sys_mmap(64) 136 var o: i64 = sd_catn(want, 0, a); want[o] = 46 as u8; o = o + 1 137 o = sd_catn(want, o, b); want[o] = 46 as u8; o = o + 1 138 o = sd_catn(want, o, c); want[o] = 46 as u8; o = o + 1 139 o = sd_catn(want, o, d); want[o] = 58 as u8; o = o + 1 140 o = sd_catn(want, o, port) 141 want[o] = 0 as u8 142 return sd_has(buf, n, want) 143} 144 145func sd_append_row(a: i64, b: i64, c: i64, d: i64, port: i64, idx: i64) -> i64 { 146 var fd: i64 = sys_openat_append("knowledge/swarm_nodes.conf" as *u8, 420) 147 if fd < 0 { fd = sys_openat_append("/volume1/homes/elderwesto/nishihost/knowledge/swarm_nodes.conf" as *u8, 420) } 148 if fd < 0 { return 0 - 1 } 149 let ln: *u8 = sys_mmap(512) 150 var o: i64 = sd_cat(ln, 0, "R\tgpu-found-" as *u8) 151 o = sd_catn(ln, o, idx) 152 o = sd_cat(ln, o, "\tdiscovered\t" as *u8) 153 o = sd_catn(ln, o, a); ln[o] = 46 as u8; o = o + 1 154 o = sd_catn(ln, o, b); ln[o] = 46 as u8; o = o + 1 155 o = sd_catn(ln, o, c); ln[o] = 46 as u8; o = o + 1 156 o = sd_catn(ln, o, d); ln[o] = 58 as u8; o = o + 1 157 o = sd_catn(ln, o, port) 158 o = sd_cat(ln, o, "\tauto-discovered inference endpoint\tVERIFIED by /v1/models or /api/tags at discovery time; re-verify before relying on it\n" as *u8) 159 var w: i64 = 0 160 while w < o { let r: i64 = sys_write(fd, ((ln as i64) + w) as *u8, o - w); if r <= 0 { w = o } else { w = w + r } } 161 sys_close(fd) 162 return 0 163} 164 165func main(argc: i64, argv: *i64) -> i64 { 166 ep_anchor() 167 var apply: i64 = 0 168 if argc >= 2 { let v: *u8 = argv[1] as *u8; if v[0] == (97 as u8) { apply = 1 } } 169 // default subnet 192.168.8 -- the estate LAN 170 var oa: i64 = 192 171 var ob: i64 = 168 172 var oc: i64 = 8 173 if argc >= 3 { 174 let s: *u8 = argv[2] as *u8 175 var f: i64 = 0 176 var i: i64 = 0 177 var vv: i64 = 0 178 var seen: i64 = 0 179 while f < 3 { 180 let ch: i64 = s[i] & 0xff 181 if ch >= 48 { if ch <= 57 { vv = vv * 10 + (ch - 48); seen = 1; i = i + 1 } else { i = i + 1 } } 182 else { 183 if seen == 1 { 184 if f == 0 { oa = vv } else { if f == 1 { ob = vv } else { oc = vv } } 185 f = f + 1; vv = 0; seen = 0 186 if ch == 0 { f = 3 } 187 } else { if ch == 0 { f = 3 } else { i = i + 1 } } 188 } 189 } 190 if seen == 1 { if f < 3 { oc = vv } } 191 } 192 193 sw("=== NX-SWARM-DISCOVER scanning \x00" as *u8); sn(oa); sw(".\x00" as *u8); sn(ob); sw(".\x00" as *u8); sn(oc) 194 sw(".1-254 for inference endpoints (bounded probes)\n\x00" as *u8) 195 196 let buf: *u8 = sys_mmap(SD_BUF) 197 var found: i64 = 0 198 var added: i64 = 0 199 var h: i64 = SD_HOST_LO 200 while h <= SD_HOST_HI { 201 var pi: i64 = 0 202 while pi < 2 { 203 var port: i64 = SD_PORT_SD 204 if pi == 1 { port = SD_PORT_OLLAMA } 205 if sd_verify(oa, ob, oc, h, port, buf) == 1 { 206 found = found + 1 207 sw(" FOUND \x00" as *u8); sn(oa); sw(".\x00" as *u8); sn(ob); sw(".\x00" as *u8); sn(oc) 208 sw(".\x00" as *u8); sn(h); sw(":\x00" as *u8); sn(port) 209 if sd_registered(oa, ob, oc, h, port) == 1 { sw(" (already in the SSOT)\n\x00" as *u8) } 210 else { 211 if apply == 1 { 212 if sd_append_row(oa, ob, oc, h, port, found) == 0 { added = added + 1; sw(" -> APPENDED to swarm_nodes.conf\n\x00" as *u8) } 213 else { sw(" -> APPEND FAILED (conf unwritable)\n\x00" as *u8) } 214 } else { sw(" UNREGISTERED (run `apply` to add)\n\x00" as *u8) } 215 } 216 } 217 pi = pi + 1 218 } 219 h = h + 1 220 } 221 sw("NX-SWARM-DISCOVER endpoints_found=\x00" as *u8); sn(found) 222 sw(" appended=\x00" as *u8); sn(added) 223 sw("\nenvelope: probes are BOUNDED and verification requires a model-server response, so an open\n\x00" as *u8) 224 sw("port alone never becomes a node. Absent hosts are NOT removed -- a machine being off is not\n\x00" as *u8) 225 sw("proof it is gone, and a deleted row fails silently where a stale one fails loudly.\n\x00" as *u8) 226 if found == 0 { sw("NX-SWARM-DISCOVER NONE -- no inference capacity answered on this subnet\n\x00" as *u8) } 227 return 0 228}