code wiki / _hdl_build / nx_swarm_discover.nx
nx_swarm_discover.nx source
↩ module page · 222 lines · 10420 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 let t: *u8 = sys_mmap(32)
52 var m: i64 = v; var k: i64 = 0
53 if m == 0 { t[0] = 48 as u8; k = 1 }
54 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
55 var a: i64 = o; var j: i64 = k - 1
56 while j >= 0 { d[a] = t[j]; a = a + 1; j = j - 1 }
57 return a
58}
59
60// GET <path> from a.b.c.d:port. Returns bytes read, or -1. Bounded connect AND bounded read, so a
61// black-holed host costs SD_TMO_MS, not a 127-second kernel timeout.
62func sd_probe(a: i64, b: i64, c: i64, d: i64, port: i64, path: *u8, out: *u8, cap: i64) -> i64 {
63 let fd: i64 = sys_socket(2, 1, 0)
64 if fd < 0 { return 0 - 1 }
65 sys_set_socket_timeout(fd, 2)
66 let sa: *u8 = sys_mmap(16)
67 sa[0] = 2 as u8; sa[1] = 0 as u8
68 sa[2] = ((port >> 8) & 0xff) as u8; sa[3] = (port & 0xff) as u8
69 sa[4] = a as u8; sa[5] = b as u8; sa[6] = c as u8; sa[7] = d as u8
70 var z: i64 = 8
71 while z < 16 { sa[z] = 0 as u8; z = z + 1 }
72 if nx_connect_bounded(fd, sa, 16, SD_TMO_MS) != 0 { sys_close(fd); return 0 - 1 }
73 let req: *u8 = sys_mmap(SD_MAGIC_1024)
74 var o: i64 = sd_cat(req, 0, "GET " as *u8)
75 o = sd_cat(req, o, path)
76 o = sd_cat(req, o, " HTTP/1.1\r\nHost: probe\r\nConnection: close\r\n\r\n" as *u8)
77 sys_write(fd, req, o)
78 var t: i64 = 0
79 var r: i64 = 1
80 while r > 0 {
81 if t >= cap { r = 0 } else { r = sys_read(fd, ((out as i64) + t) as *u8, cap - t); if r > 0 { t = t + r } }
82 }
83 sys_close(fd)
84 return t
85}
86
87func sd_has(buf: *u8, n: i64, needle: *u8) -> i64 {
88 let m: i64 = sd_len(needle)
89 if m == 0 { return 0 }
90 if m > n { return 0 }
91 var i: i64 = 0
92 while i <= n - m {
93 var j: i64 = 0
94 var same: i64 = 1
95 while j < m { if buf[i + j] != needle[j] { same = 0; j = m } else { j = j + 1 } }
96 if same == 1 { return 1 }
97 i = i + 1
98 }
99 return 0
100}
101
102// Is this a REAL inference endpoint, or just something listening? ★A PORT BEING OPEN IS NOT A
103// CAPABILITY -- a DSM login page answered our first sweep on :8000 and would have been "a node".
104// Verified = it identifies as a model server.
105func sd_verify(a: i64, b: i64, c: i64, d: i64, port: i64, buf: *u8) -> i64 {
106 var n: i64 = 0
107 if port == SD_PORT_OLLAMA {
108 n = sd_probe(a, b, c, d, port, "/api/tags" as *u8, buf, SD_BUF)
109 if n > 0 { if sd_has(buf, n, "models" as *u8) == 1 { return 1 } }
110 return 0
111 }
112 n = sd_probe(a, b, c, d, port, "/v1/models" as *u8, buf, SD_BUF)
113 if n <= 0 { return 0 }
114 if sd_has(buf, n, "\"object\"" as *u8) == 1 { return 1 }
115 if sd_has(buf, n, "\"data\"" as *u8) == 1 { return 1 }
116 if sd_has(buf, n, "model" as *u8) == 1 { return 1 }
117 return 0
118}
119
120// already in the SSOT? (substring match on "a.b.c.d:port" in an uncommented row)
121func sd_registered(a: i64, b: i64, c: i64, d: i64, port: i64) -> i64 {
122 let buf: *u8 = sys_mmap(SD_BUF)
123 let fd: i64 = ep_open_rd("knowledge/swarm_nodes.conf" as *u8)
124 if fd < 0 { return 0 }
125 var n: i64 = 0
126 var r: i64 = 1
127 while r > 0 { r = sys_read(fd, ((buf as i64) + n) as *u8, SD_BUF - n); if r > 0 { n = n + r } }
128 sys_close(fd)
129 let want: *u8 = sys_mmap(64)
130 var o: i64 = sd_catn(want, 0, a); want[o] = 46 as u8; o = o + 1
131 o = sd_catn(want, o, b); want[o] = 46 as u8; o = o + 1
132 o = sd_catn(want, o, c); want[o] = 46 as u8; o = o + 1
133 o = sd_catn(want, o, d); want[o] = 58 as u8; o = o + 1
134 o = sd_catn(want, o, port)
135 want[o] = 0 as u8
136 return sd_has(buf, n, want)
137}
138
139func sd_append_row(a: i64, b: i64, c: i64, d: i64, port: i64, idx: i64) -> i64 {
140 var fd: i64 = sys_openat_append("knowledge/swarm_nodes.conf" as *u8, 420)
141 if fd < 0 { fd = sys_openat_append("/volume1/homes/elderwesto/nishihost/knowledge/swarm_nodes.conf" as *u8, 420) }
142 if fd < 0 { return 0 - 1 }
143 let ln: *u8 = sys_mmap(512)
144 var o: i64 = sd_cat(ln, 0, "R\tgpu-found-" as *u8)
145 o = sd_catn(ln, o, idx)
146 o = sd_cat(ln, o, "\tdiscovered\t" as *u8)
147 o = sd_catn(ln, o, a); ln[o] = 46 as u8; o = o + 1
148 o = sd_catn(ln, o, b); ln[o] = 46 as u8; o = o + 1
149 o = sd_catn(ln, o, c); ln[o] = 46 as u8; o = o + 1
150 o = sd_catn(ln, o, d); ln[o] = 58 as u8; o = o + 1
151 o = sd_catn(ln, o, port)
152 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)
153 var w: i64 = 0
154 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 } }
155 sys_close(fd)
156 return 0
157}
158
159func main(argc: i64, argv: *i64) -> i64 {
160 ep_anchor()
161 var apply: i64 = 0
162 if argc >= 2 { let v: *u8 = argv[1] as *u8; if v[0] == (97 as u8) { apply = 1 } }
163 // default subnet 192.168.8 -- the estate LAN
164 var oa: i64 = 192
165 var ob: i64 = 168
166 var oc: i64 = 8
167 if argc >= 3 {
168 let s: *u8 = argv[2] as *u8
169 var f: i64 = 0
170 var i: i64 = 0
171 var vv: i64 = 0
172 var seen: i64 = 0
173 while f < 3 {
174 let ch: i64 = s[i] & 0xff
175 if ch >= 48 { if ch <= 57 { vv = vv * 10 + (ch - 48); seen = 1; i = i + 1 } else { i = i + 1 } }
176 else {
177 if seen == 1 {
178 if f == 0 { oa = vv } else { if f == 1 { ob = vv } else { oc = vv } }
179 f = f + 1; vv = 0; seen = 0
180 if ch == 0 { f = 3 }
181 } else { if ch == 0 { f = 3 } else { i = i + 1 } }
182 }
183 }
184 if seen == 1 { if f < 3 { oc = vv } }
185 }
186
187 sw("=== NX-SWARM-DISCOVER scanning \x00" as *u8); sn(oa); sw(".\x00" as *u8); sn(ob); sw(".\x00" as *u8); sn(oc)
188 sw(".1-254 for inference endpoints (bounded probes)\n\x00" as *u8)
189
190 let buf: *u8 = sys_mmap(SD_BUF)
191 var found: i64 = 0
192 var added: i64 = 0
193 var h: i64 = SD_HOST_LO
194 while h <= SD_HOST_HI {
195 var pi: i64 = 0
196 while pi < 2 {
197 var port: i64 = SD_PORT_SD
198 if pi == 1 { port = SD_PORT_OLLAMA }
199 if sd_verify(oa, ob, oc, h, port, buf) == 1 {
200 found = found + 1
201 sw(" FOUND \x00" as *u8); sn(oa); sw(".\x00" as *u8); sn(ob); sw(".\x00" as *u8); sn(oc)
202 sw(".\x00" as *u8); sn(h); sw(":\x00" as *u8); sn(port)
203 if sd_registered(oa, ob, oc, h, port) == 1 { sw(" (already in the SSOT)\n\x00" as *u8) }
204 else {
205 if apply == 1 {
206 if sd_append_row(oa, ob, oc, h, port, found) == 0 { added = added + 1; sw(" -> APPENDED to swarm_nodes.conf\n\x00" as *u8) }
207 else { sw(" -> APPEND FAILED (conf unwritable)\n\x00" as *u8) }
208 } else { sw(" UNREGISTERED (run `apply` to add)\n\x00" as *u8) }
209 }
210 }
211 pi = pi + 1
212 }
213 h = h + 1
214 }
215 sw("NX-SWARM-DISCOVER endpoints_found=\x00" as *u8); sn(found)
216 sw(" appended=\x00" as *u8); sn(added)
217 sw("\nenvelope: probes are BOUNDED and verification requires a model-server response, so an open\n\x00" as *u8)
218 sw("port alone never becomes a node. Absent hosts are NOT removed -- a machine being off is not\n\x00" as *u8)
219 sw("proof it is gone, and a deleted row fails silently where a stale one fails loudly.\n\x00" as *u8)
220 if found == 0 { sw("NX-SWARM-DISCOVER NONE -- no inference capacity answered on this subnet\n\x00" as *u8) }
221 return 0
222}