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}