code wiki / _hdl_build / nx_mgmt_data.nx

nx_mgmt_data.nx source

↩ module page · 2860 lines · 156437 B

1// nx_mgmt_data.nx -- the DATA / ADAPTER layer of the management plane (the OUTER ring; secondary adapters). 2// The ONLY layer that touches the outside world for STATE: it parses snapshot bytes, reads the data-driven 3// config allowlists, and drives the secondary adapters (the hostctl exec + the real-HTTP health probe). It has 4// NO knowledge of transport (no HTTP/socket/auth); the IO ring depends on IT, never the reverse (ports & 5// adapters / dependency inversion). Grounded in knowledge/library/arch_* (three-tier DATA tier, hexagonal 6// secondary adapters, loose coupling). Reuses the SOTA-gated nx_deploy_lib (validate/exec) + nx_http_health_lib 7// (probe) -- DRY. license_tier: ORIGINAL 8import "nx_syscalls.nx" 9import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 10import "_hdl_build/nx_adnet_invoice.nx" 11import "_hdl_build/nx_adnet_creative.nx" 12import "nx_deploy_lib.nx" 13import "nx_http_health_lib.nx" 14import "nx_tool_run.nx" // seq1443: tr_run_capture_to -- the GATE-PROVEN bounded exec (see md_exec_gate_capture) 15import "_hdl_build/nx_staging_guard.nx" // sg_classify -- REFUSE a promote that walks the target backwards (staging hygiene) 16 17// Owned deployment target record for guarded installation. Eight columns are the 18// registry schema, not a maximum record/path size. Legacy resolver callers remain 19// unchanged until their output contracts are migrated. 20import "nx_file_install.nx" 21import "nx_cap_mint.nx" 22const MD_TARGET_COLUMNS: i64=8 23struct NxDeployTargetRecord { 24 bytes: *u8, 25 allocation: i64, 26 name: *u8, 27 kind_text: *u8, 28 staged: *u8, 29 deploy: *u8, 30 health: *u8, 31 rollback: *u8, 32 live: *u8, 33 process: *u8, 34 stage: *u8, 35 code: i64, 36 line: i64, 37 conflicting_line: i64, 38} 39func md_target_init(out: *NxDeployTargetRecord) -> i64 { 40 let raw: *u8=out as *u8;var i: i64=0 41 while i<__size_of(NxDeployTargetRecord) { raw[i]=0 as u8;i=i+1 } 42 out.stage="target-input";out.code=FIO_EINVAL;return 0 43} 44func md_target_close(out: *NxDeployTargetRecord) -> i64 { 45 var rc: i64=0 46 if (out.bytes as i64)!=0 { rc=sys_munmap(out.bytes,out.allocation) } 47 md_target_init(out);return rc 48} 49func md_target_space(c: u8) -> i64 { 50 return (c==(32 as u8) || c==(9 as u8) || c==(13 as u8)) as i64 51} 52func md_target_field(out: *NxDeployTargetRecord,index: i64,value: *u8) -> i64 { 53 if index==0 { out.name=value } 54 if index==1 { out.kind_text=value } 55 if index==2 { out.staged=value } 56 if index==3 { out.deploy=value } 57 if index==4 { out.health=value } 58 if index==5 { out.rollback=value } 59 if index==6 { out.live=value } 60 if index==7 { out.process=value } 61 return 0 62} 63// Reads the complete caller-owned snapshot before selecting a unique name. 64// Missing/malformed/ambiguous records never fall back to compiled-in defaults. 65// Output owns a length-derived row copy; it never aliases caller snapshot bytes. 66// Initialize/close the result between uses; do not reinitialize a live result. 67func md_target_decode(snapshot: *u8,n: i64,name: *u8,name_length: i64,out: *NxDeployTargetRecord) -> i64 { 68 md_target_init(out) 69 if (snapshot as i64)==0 || n<0 || (name as i64)==0 || name_length<=0 { return out.code } 70 var i: i64=0 71 while i<name_length { 72 if (name[i] as i64)<=32 || name[i]==(127 as u8) { return out.code };i=i+1 73 } 74 var selected: i64=0-1;var selected_end: i64=0 75 var cursor: i64=0;var line: i64=1 76 while cursor<n { 77 let end: i64=md_eol(snapshot,n,cursor) 78 i=cursor 79 while i<end { 80 let ch: i64=snapshot[i] as i64 81 if ch==0 || ch==127 || (ch<32 && md_target_space(snapshot[i])==0) { 82 out.stage="registry-control-byte";out.code=FIO_EBADMSG;out.line=line;return out.code 83 } 84 i=i+1 85 } 86 var start: i64=cursor 87 while start<end && md_target_space(snapshot[start])==1 { start=start+1 } 88 if start<end && snapshot[start]!=(35 as u8) { 89 var token_end: i64=start 90 while token_end<end && md_target_space(snapshot[token_end])==0 { token_end=token_end+1 } 91 if md_slice_eq(snapshot,start,token_end-start,name,0,name_length)==1 { 92 if selected>=0 { 93 out.stage="duplicate-target";out.code=FIO_EEXIST;out.conflicting_line=line;return out.code 94 } 95 selected=start;selected_end=end;out.line=line 96 } 97 } 98 cursor=end+1;line=line+1 99 } 100 if selected<0 { out.stage="target-absent";out.code=FI_ENOENT;return out.code } 101 let length: i64=selected_end-selected 102 if length==0x7fffffffffffffff { out.stage="row-size-overflow";out.code=FIO_EINVAL;return out.code } 103 let copy: *u8=sys_mmap(length+1) 104 if (copy as i64)<0 { out.stage="row-allocation";out.code=copy as i64;return out.code } 105 fi_copy(copy,snapshot+selected,length);copy[length]=0 as u8 106 var fields: i64=0;i=0 107 while i<length { 108 while i<length && md_target_space(copy[i])==1 { copy[i]=0 as u8;i=i+1 } 109 if i<length { 110 if fields==MD_TARGET_COLUMNS { break } 111 md_target_field(out,fields,copy+i);fields=fields+1 112 while i<length && md_target_space(copy[i])==0 { i=i+1 } 113 } 114 } 115 var rc: i64=0;var stage: *u8="target-resolved" 116 if fields!=MD_TARGET_COLUMNS || i<length { rc=FIO_EBADMSG;stage="target-columns" } 117 if rc==0 && fi_path_valid(out.live)==0 { rc=FIO_EBADMSG;stage="target-live-path" } 118 if rc==0 { if md_streq(out.kind_text,"toolchain-v2")==1 { if md_streq(out.deploy,"-")!=1 || md_streq(out.health,"-")!=1 || md_streq(out.rollback,"-")!=1 { rc=FIO_EBADMSG;stage="versioned-target-actions" } } } 119 if rc!=0 { 120 let original_line: i64=out.line 121 sys_munmap(copy,length+1);md_target_init(out);out.line=original_line 122 } else { out.bytes=copy;out.allocation=length+1 } 123 out.stage=stage;out.code=rc;return rc 124} 125 126func md_target_read(path: *u8,snapshot_allowance: i64,name: *u8,name_length: i64,out: *NxDeployTargetRecord) -> i64 { 127 md_target_init(out) 128 if snapshot_allowance<=0 || (path as i64)==0 { return out.code } 129 let region: *NxFileReadRegion=sys_mmap(__size_of(NxFileReadRegion)) as *NxFileReadRegion 130 if (region as i64)<0 { out.stage="reader-allocation";out.code=region as i64;return out.code } 131 fio_region_init(region);var rc: i64=fio_region_open(path,region) 132 var stage: *u8=region.stage 133 let n: i64=region.total;var bytes: *u8=0 as *u8 134 if rc==0 && n>snapshot_allowance { rc=FIO_EINVAL;stage="snapshot-admission" } 135 if rc==0 && n==0 { rc=FI_ENOENT;stage="target-absent" } 136 if rc==0 { 137 bytes=sys_mmap(n) 138 if (bytes as i64)<0 { rc=bytes as i64;bytes=0 as *u8;stage="snapshot-allocation" } 139 } 140 if rc==0 { 141 let got: i64=fio_region_next(region,bytes,n) 142 if got!=n { rc=got;if rc>=0 { rc=FIO_EIO };stage=region.stage } 143 } 144 fio_region_close(region) 145 if rc==0 && region.code!=0 { rc=region.code;stage=region.stage } 146 if rc==0 { rc=md_target_decode(bytes,n,name,name_length,out) } 147 else { out.code=rc;out.stage=stage } 148 if (bytes as i64)!=0 { sys_munmap(bytes,n) } 149 sys_munmap(region as *u8,__size_of(NxFileReadRegion)) 150 return rc 151} 152 153// One data-plane operation: resolve the requested registered target and apply the 154// caller-approved immutable intent. Authentication and approval belong to the 155// management boundary; merely supplying a digest does not confer authority. 156struct NxRegisteredInstallResult { 157 target: NxDeployTargetRecord, 158 install: NxFileInstallResult, 159 stage: *u8, 160 code: i64, 161} 162func md_registered_install_init(out: *NxRegisteredInstallResult) -> i64 { 163 md_target_init(&out.target) 164 out.install.stage="not-started";out.install.code=0;out.install.already_published=0 165 out.install.io.stage="not-started";out.install.io.code=0 166 out.install.io.written=0;out.install.io.close_code=0 167 fio_replace_init(&out.install.replacement) 168 out.stage="approval-input";out.code=FIO_EINVAL;return 0 169} 170func md_registered_install_close(out: *NxRegisteredInstallResult) -> i64 { 171 let rc: i64=md_target_close(&out.target) 172 md_registered_install_init(out);return rc 173} 174// Owns the resolved row until close, so reporting never relies on a later 175// registry reread. A failed resolver cannot dispatch installation or restart. 176// The caller accounts separately for snapshot, intent, scratch and result memory. 177func md_install_registered(registry: *u8,snapshot_allowance: i64,target: *u8,target_length: i64,intent: *u8,intent_allowance: i64,approved_intent_digest: *u8,scratch: *u8,scratch_bytes: i64,out: *NxRegisteredInstallResult) -> i64 { 178 md_registered_install_init(out) 179 if (approved_intent_digest as i64)==0 || fi_path_valid(intent)==0 || intent_allowance<=0 || (scratch as i64)==0 || scratch_bytes<=0 { return out.code } 180 out.stage="target-resolution" 181 out.code=md_target_read(registry,snapshot_allowance,target,target_length,&out.target) 182 if out.code!=0 { return out.code } 183 out.stage="artifact-installation" 184 out.code=fi_install_bound(intent,intent_allowance,scratch,scratch_bytes,out.target.live,approved_intent_digest,&out.install) 185 if out.code==0 { out.stage="artifact-installed-serving-unverified" } 186 return out.code 187} 188 189// ---- parse primitives over a buffer (slices, no null terminators) ----------------------------------- 190func md_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 191 192// ASCII '2' -- the generation suffix on <live>.prev2. NAMED so the rotation carries no bare literal. 193const MD_CH_DIGIT_TWO: i64 = 50 194// THE ONE ROLLBACK-CHAIN ROTATION (2026-08-21). Rotate <prev> one generation deeper to <prev>2 so that a 195// SECOND promote of the same target cannot destroy the pre-change binary. Returns 1 if a generation was 196// rotated, 0 if there was nothing to rotate. Fail-safe: any failure leaves both slots exactly as found. 197// 198// MEASURED THIS SESSION: two promotes of ONE target left live and .prev BOTH holding new builds and the 199// original GONE -- and an older-generation binary CANNOT be rebuilt. The slot holds "the PREVIOUS 200// PROMOTE", never "the last known good", so the SECOND promote is the one that leaves you with no way 201// back. The only defence was the operator remembering to copy the artifact aside first, and 202// *A SAFETY PROPERTY THAT DEPENDS ON SOMEONE REMEMBERING IS AN ADOPTION GAP WITH EXTRA STEPS. 203// 204// *WHY A FUNCTION AND NOT N INLINE EDITS: this estate implements the live -> .prev bank FIVE TIMES 205// (nx_mgmt_data x3, nx_mgmt_api x1, nx_hostctl x1), found by TRACING the call chain rather than by 206// assuming -- the first two candidates I inspected were a staged-slot bank and a static-content promote, 207// neither of which is this. EVERY COPY OF A PATTERN IS A PLACE A FIX CAN FAIL TO ARRIVE, which 208// nx_hostctl.nx says in those words about its own 27 deploy twins. All FOUR copies inside this daemon 209// now call THIS. nx_hostctl is a SEPARATE BINARY and carries its own twin -- NAMED here so the next 210// reader inherits the fact instead of re-deriving it. 211// Bounded deliberately: exactly one extra file per target, no directory, no new import. 212func md_rotate_prev(prevp: *u8) -> i64 { 213 let pf: i64 = sys_openat_rd(prevp) 214 if pf < 0 { return 0 } 215 sys_close(pf) 216 let p2: *u8 = sys_mmap(256) 217 var i: i64 = 0 218 while prevp[i] != (0 as u8) { p2[i] = prevp[i]; i = i + 1 } 219 p2[i] = MD_CH_DIGIT_TWO as u8 220 p2[i + 1] = 0 as u8 221 if sys_renameat(prevp, p2) != 0 { return 0 } 222 return 1 223} 224 225func md_cat_slice(d: *u8, o: i64, src: *u8, off: i64, len: i64) -> i64 { 226 var i: i64 = 0 227 while i < len { d[o] = src[off + i]; o = o + 1; i = i + 1 } 228 return o 229} 230 231func md_tok_eq(src: *u8, off: i64, len: i64, s: *u8) -> i64 { 232 let sl: i64 = md_len(s) 233 if sl != len { return 0 } 234 var i: i64 = 0 235 while i < len { if (src[off + i] as i64) != (s[i] as i64) { return 0 } i = i + 1 } 236 return 1 237} 238 239func md_slice_atoi(src: *u8, off: i64, len: i64) -> i64 { 240 var v: i64 = 0 241 var i: i64 = 0 242 while i < len { 243 let c: i64 = src[off + i] as i64 244 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 245 i = i + 1 246 } 247 return v 248} 249 250// index of '\n' at-or-after start, or n. 251func md_eol(snap: *u8, n: i64, start: i64) -> i64 { 252 var i: i64 = start 253 var f: i64 = 0 254 while f == 0 { 255 if i >= n { f = 1 } else { if (snap[i] as i64) == 10 { f = 1 } else { i = i + 1 } } 256 } 257 return i 258} 259 260// split snap[ls..le) on spaces into up-to-maxf (offs,lens) absolute slices. returns field count. 261func md_split(snap: *u8, ls: i64, le: i64, offs: *i64, lens: *i64, maxf: i64) -> i64 { 262 var nf: i64 = 0 263 var i: i64 = ls 264 while i < le { 265 var sk: i64 = 1 266 while sk == 1 { if i >= le { sk = 0 } else { if (snap[i] as i64) == 32 { i = i + 1 } else { sk = 0 } } } 267 if i < le { 268 let st: i64 = i 269 var sc: i64 = 1 270 while sc == 1 { if i >= le { sc = 0 } else { if (snap[i] as i64) == 32 { sc = 0 } else { i = i + 1 } } } 271 if nf < maxf { offs[nf] = st; lens[nf] = i - st; nf = nf + 1 } 272 } 273 } 274 return nf 275} 276 277func md_slice_eq(a: *u8, ao: i64, al: i64, b: *u8, bo: i64, bl: i64) -> i64 { 278 if al != bl { return 0 } 279 var i: i64 = 0 280 while i < al { if (a[ao + i] as i64) != (b[bo + i] as i64) { return 0 } i = i + 1 } 281 return 1 282} 283 284func md_copy_slice_z(dst: *u8, src: *u8, off: i64, len: i64, cap: i64) -> i64 { 285 var n: i64 = len 286 if n > cap - 1 { n = cap - 1 } 287 var i: i64 = 0 288 while i < n { dst[i] = src[off + i]; i = i + 1 } 289 dst[n] = 0 as u8 290 return n 291} 292 293// ---- file / config adapters ------------------------------------------------------------------------- 294func md_read_file(path: *u8, szbox: *i64) -> *u8 { 295 szbox[0] = 0 296 return sys_read_file(path, szbox) 297} 298 299// ---- ADNET BILLING (debt 1785513943): the outside-world half of the invoice route ---------------- 300// Lives HERE, in the DATA ring, not in nx_mgmt_api: that ring owns transport only (see the api header). 301// The money math stays in nx_adnet_bill and the join in nx_adnet_invoice -- this function is purely the 302// file access those two are deliberately free of. 303// FAIL-CLOSED: an unreadable INVENTORY or RATE CARD returns 0 (the route answers 503) rather than an 304// empty invoice -- "no rows" and "could not read the rows" must never look alike to a biller. An absent 305// JOURNAL is different and legitimate: it means zero events, so it degrades to an empty count. 306const MD_ADNET_INV: *u8 = "/volume1/homes/elderwesto/nishihost/sites/nishifamily/synth/adnet_inventory.txt" as *u8 307const MD_ADNET_RATES: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/status/adnet_rates.conf" as *u8 308const MD_ADNET_SERVED: *u8 = "/volume1/homes/elderwesto/nishihost/adnet_impressions.log" as *u8 309const MD_ADNET_CLICKS: *u8 = "/volume1/homes/elderwesto/nishihost/adnet_clicks.log" as *u8 310const MD_ADNET_VIEW: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/status/adnet_viewable.log" as *u8 311 312// ---- ADNET CREATIVE INTAKE (debt 1785512202): the outside-world half of the upload route -------- 313// Validation and naming live in nx_adnet_creative (pure, gated 13/13); this is only the file write. 314// CONTENT-ADDRESSED, so the write is IDEMPOTENT by construction (rule 10): re-uploading identical bytes 315// lands on the identical path. No overwrite hazard, no version skew, and the URL doubles as a cache key. 316// Returns the ACR_* verdict; urlout receives the first-party url ONLY on ACR_OK. 317const MD_ADNET_SYNTH: *u8 = "/volume1/homes/elderwesto/nishihost/sites/nishifamily/synth/" as *u8 318 319func md_adnet_creative_store(b: *u8, n: i64, urlout: *u8, urlcap: i64) -> i64 { 320 urlout[0] = 0 as u8 321 let v: i64 = acr_validate(b, n) 322 if v != ACR_OK { return v } 323 let nm: *u8 = sys_mmap(64) 324 if acr_name(b, n, nm, 64) == 0 { return ACR_NOT_PNG } 325 let path: *u8 = sys_mmap(512) 326 var o: i64 = 0 327 var i: i64 = 0 328 while MD_ADNET_SYNTH[i] != (0 as u8) { path[o] = MD_ADNET_SYNTH[i]; o = o + 1; i = i + 1 } 329 i = 0 330 while nm[i] != (0 as u8) { path[o] = nm[i]; o = o + 1; i = i + 1 } 331 path[o] = 0 as u8 332 let fd: i64 = sys_openat_wr(path, 420) 333 if fd < 0 { return 0 - 1 } 334 let w: i64 = sys_write(fd, b, n) 335 sys_close(fd) 336 if w != n { return 0 - 1 } 337 if acr_url(b, n, urlout, urlcap) == 0 { return 0 - 1 } 338 return ACR_OK 339} 340 341func md_adnet_invoice_report(out: *u8, cap: i64) -> i64 { 342 let bx: *i64 = sys_mmap(16) as *i64 343 let inv: *u8 = md_read_file(MD_ADNET_INV, bx) 344 if (inv as i64) == 0 { return 0 } 345 let iln: i64 = bx[0] 346 let bx2: *i64 = sys_mmap(16) as *i64 347 let rates: *u8 = md_read_file(MD_ADNET_RATES, bx2) 348 if (rates as i64) == 0 { return 0 } 349 let rln: i64 = bx2[0] 350 let bx3: *i64 = sys_mmap(16) as *i64 351 var served: *u8 = md_read_file(MD_ADNET_SERVED, bx3) 352 var sln: i64 = bx3[0] 353 if (served as i64) == 0 { served = "" as *u8; sln = 0 } 354 let bx4: *i64 = sys_mmap(16) as *i64 355 var view: *u8 = md_read_file(MD_ADNET_VIEW, bx4) 356 var vln: i64 = bx4[0] 357 if (view as i64) == 0 { view = "" as *u8; vln = 0 } 358 let bx5: *i64 = sys_mmap(16) as *i64 359 var clk: *u8 = md_read_file(MD_ADNET_CLICKS, bx5) 360 var cln: i64 = bx5[0] 361 if (clk as i64) == 0 { clk = "" as *u8; cln = 0 } 362 return ainv_report(inv, iln, rates, rln, served, sln, view, vln, clk, cln, out, cap) 363} 364 365// resolve a deploy target NAME (slice nm[off..off+len)) against the allowlist file -> kind + src/sub/url 366// copied null-terminated into caller buffers. 1 = resolved, 0 = unknown (fail-closed). '#' = comment line. 367// COMPILED-IN fallback for the deploy plane's OWN bootstrap targets, so a freshly-deployed mgmt API can deploy 368// mgmtapi/hostctl/torrentstack off-LAN WITHOUT first getting an updated deploy_targets.conf onto the NAS (the 369// file isn't upload-able off-LAN). The file (md_resolve_target) still WINS when present -> it stays the 370// extensible SSOT; this only covers the plane's self-knowledge. Each: kind, src(staged .new), sub(promote), url(health), rb(rollback). 371func md_builtin_target(nm: *u8, off: i64, len: i64, kindb: *i64, srcbuf: *u8, subbuf: *u8, urlbuf: *u8, rbbuf: *u8) -> i64 { 372 if md_slice_eq(nm, off, len, "mgmtapi" as *u8, 0, 7) == 1 { 373 // health = LOCAL TCP-connect to the mgmt API's own port :18098 (robust). The old "https://.../api/" HTTP 374 // probe ran nx_research_fetch from the NAS -> nishifamily.com, which hits DSM's loopback nginx (coin-flip) 375 // -> flaky false-rollback. The new mgmt respawns on :18098 within the 30s retry window -> port-connect greens. 376 kindb[0]=0; md_copy_slice_z(srcbuf, "nx_mgmt_api.elf.new" as *u8, 0, 19, 512); md_copy_slice_z(subbuf, "mgmtdeploy" as *u8, 0, 10, 64); md_copy_slice_z(urlbuf, "port:18098" as *u8, 0, 10, 256); md_copy_slice_z(rbbuf, "mgmtrollback" as *u8, 0, 12, 64); return 1 } 377 if md_slice_eq(nm, off, len, "hostctl" as *u8, 0, 7) == 1 { 378 // health = LOCAL TCP-connect to sites.elf :8443 (robust, like torrentstack). The old "https://.../api/" 379 // HTTP-fetch probe needed nx_research_fetch+CA from the mgmt cwd + hit the :443 DSM-nginx coin-flip + raced 380 // the self-swap -> it ALWAYS false-rolled-back (why no hostctl deploy landed since 07-09). sites.elf stays 381 // up across a self-swap (only the supervisor re-execs), so the port-connect greens reliably. 382 kindb[0]=0; md_copy_slice_z(srcbuf, "nx_hostctl.new" as *u8, 0, 14, 512); md_copy_slice_z(subbuf, "selfswap" as *u8, 0, 8, 64); md_copy_slice_z(urlbuf, "port:8443" as *u8, 0, 9, 256); md_copy_slice_z(rbbuf, "superrollback" as *u8, 0, 13, 64); return 1 } 383 if md_slice_eq(nm, off, len, "torrentstack" as *u8, 0, 12) == 1 { 384 kindb[0]=0; md_copy_slice_z(srcbuf, "nx_torrent_daemon.sov.elf.new" as *u8, 0, 29, 512); md_copy_slice_z(subbuf, "torrentdeploy" as *u8, 0, 13, 64); md_copy_slice_z(urlbuf, "port:8097" as *u8, 0, 9, 256); md_copy_slice_z(rbbuf, "torrentrollback" as *u8, 0, 15, 64); return 1 } 385 // ethical CLEAN-SERVE daemon (:8102, /clean) -- first-class builtin so it deploys purely over the API (no NAS 386 // deploy_targets.conf write). hostctl cleanservedeploy promotes the .new + guard respawns; health = TCP :8102. 387 if md_slice_eq(nm, off, len, "cleanserve" as *u8, 0, 10) == 1 { 388 kindb[0]=0; md_copy_slice_z(srcbuf, "nx_clean_serve_daemon.elf.new" as *u8, 0, 29, 512); md_copy_slice_z(subbuf, "cleanservedeploy" as *u8, 0, 16, 64); md_copy_slice_z(urlbuf, "port:8102" as *u8, 0, 9, 256); md_copy_slice_z(rbbuf, "cleanserverollback" as *u8, 0, 18, 64); return 1 } 389 // DOCPORTAL admin daemon (:18456, /search + /doc + /api) -- first-class builtin so the SEARCH daemon deploys 390 // purely over the API (no more manual .sov.elf.new swap). hostctl docportaldeploy promotes the .sov.elf.new the 391 // build stages + guard respawns; health = local TCP :18456; rollback = docportalrollback (.prev -> live). 392 if md_slice_eq(nm, off, len, "docportal" as *u8, 0, 9) == 1 { 393 kindb[0]=0; md_copy_slice_z(srcbuf, "nx_docportal_admin_daemon.sov.elf.new" as *u8, 0, 37, 512); md_copy_slice_z(subbuf, "docportaldeploy" as *u8, 0, 15, 64); md_copy_slice_z(urlbuf, "port:18456" as *u8, 0, 10, 256); md_copy_slice_z(rbbuf, "docportalrollback" as *u8, 0, 17, 64); return 1 } 394 return 0 395} 396func md_resolve_target(cfgpath: *u8, nm: *u8, off: i64, len: i64, kindb: *i64, srcbuf: *u8, subbuf: *u8, urlbuf: *u8, rbbuf: *u8) -> i64 { 397 let szp: *i64 = sys_mmap(16) as *i64 398 var buf: *u8 = md_read_file(cfgpath, szp) 399 // primary path (knowledge/hosting/, the root-owned data plane) ABSENT -> fall back to the operator-writable 400 // bootstrap conf in the daemon cwd (nishihost/deploy_targets.conf). knowledge/ is root-owned (the root mgmt 401 // daemon created it), so the elderwesto bootstrap that REGISTERS deploy targets can only write the cwd -- this 402 // fallback is what lets a new target (e.g. relate) be registered WITHOUT root. Primary still WINS when present. 403 if (buf as i64) == 0 { buf = md_read_file("deploy_targets.conf" as *u8, szp) } 404 // both configs ABSENT -> still honor the compiled-in bootstrap targets, else the whole deploy plane is dead 405 // off-LAN when the NAS lacks the files (the live-400 that caught this). 406 if (buf as i64) == 0 { return md_builtin_target(nm, off, len, kindb, srcbuf, subbuf, urlbuf, rbbuf) } 407 let n: i64 = szp[0] 408 let offs: *i64 = sys_mmap(64) as *i64 409 let lens: *i64 = sys_mmap(64) as *i64 410 var cur: i64 = 0 411 while cur < n { 412 let le: i64 = md_eol(buf, n, cur) 413 var isc: i64 = 0 414 if le > cur { if (buf[cur] as i64) == 35 { isc = 1 } } 415 if isc == 0 { 416 let nf: i64 = md_split(buf, cur, le, offs, lens, 8) 417 if nf >= 5 { 418 if md_slice_eq(buf, offs[0], lens[0], nm, off, len) == 1 { 419 // Versioned-only rows cannot fall through to legacy hostctl action execution. 420 if md_slice_eq(buf,offs[1],lens[1],"toolchain-v2",0,12)==1 { return 2 } 421 kindb[0] = md_slice_atoi(buf, offs[1], lens[1]) 422 md_copy_slice_z(srcbuf, buf, offs[2], lens[2], 512) 423 md_copy_slice_z(subbuf, buf, offs[3], lens[3], 64) 424 md_copy_slice_z(urlbuf, buf, offs[4], lens[4], 256) 425 // OPTIONAL 6th field = per-target rollback sub (generalized deploy: a torrent target must 426 // roll back the TORRENT binary, not sites.elf). Absent (5-field legacy rows) -> "rollback". 427 if nf >= 6 { md_copy_slice_z(rbbuf, buf, offs[5], lens[5], 64) } else { md_copy_slice_z(rbbuf, "rollback" as *u8, 0, 8, 64) } 428 return 1 429 } 430 } 431 } 432 cur = le + 1 433 } 434 // not in the file -> try the compiled-in bootstrap targets (off-LAN self-enable). Fail-closed if neither. 435 return md_builtin_target(nm, off, len, kindb, srcbuf, subbuf, urlbuf, rbbuf) 436} 437 438// FAIL-CLOSED allowlist of artifact names that /api/upload may STAGE (write <name>.upload -> <name>.new). These 439// are the deployable binaries the on-NAS supervisor promotes from *.new (HC_*_NEW in nx_hostctl). 1 = allowed, 440// 0 = refused (unknown target -> 400, NOTHING written). Names checked as a slice (nm[off..off+len)) so the caller 441// can hand a query-string slice without copying. Data lives HERE (the DATA ring), not buried in the transport layer. 442// NOTE the deliberate absence of directory separators in every entry -- an upload target is a BARE basename, so a 443// caller can never traverse ('/' or '..' would fail every md_slice_eq below), which keeps the staging write pinned 444// to the mgmt daemon's cwd by construction (defense-in-depth over the allowlist itself). 445func md_upload_target_ok(nm: *u8, off: i64, len: i64) -> i64 { 446 if md_slice_eq(nm, off, len, "nx_mgmt_api.elf" as *u8, 0, 15) == 1 { return 1 } 447 if md_slice_eq(nm, off, len, "sites.elf" as *u8, 0, 9) == 1 { return 1 } 448 if md_slice_eq(nm, off, len, "nx_gallery_serve.elf" as *u8, 0, 20) == 1 { return 1 } 449 if md_slice_eq(nm, off, len, "nx_gallery_gateway.elf" as *u8, 0, 22) == 1 { return 1 } 450 if md_slice_eq(nm, off, len, "nx_docportal_admin_daemon.elf" as *u8, 0, 29) == 1 { return 1 } 451 if md_slice_eq(nm, off, len, "nx_hostctl" as *u8, 0, 10) == 1 { return 1 } 452 if md_slice_eq(nm, off, len, "nx_wiki_gw.elf" as *u8, 0, 14) == 1 { return 1 } 453 if md_slice_eq(nm, off, len, "nx_hub_gw.elf" as *u8, 0, 13) == 1 { return 1 } 454 if md_slice_eq(nm, off, len, "nx_torrent_gw.elf" as *u8, 0, 17) == 1 { return 1 } 455 // P1 off-LAN parity: the torrent STACK binaries (deployed cross-dir into /volume1/ai/torrent/ by the 456 // torrentdeploy hostctl sub). Staged as <name>.new in nishihost cwd like every other target. 457 if md_slice_eq(nm, off, len, "nx_torrent_daemon.sov.elf" as *u8, 0, 25) == 1 { return 1 } 458 if md_slice_eq(nm, off, len, "nx_torrent_seedeval.elf" as *u8, 0, 23) == 1 { return 1 } 459 // build-over-API: the tree-pack primitive elf + the source-tree blob (unpacked by /api/unpack via nx_treepack). 460 if md_slice_eq(nm, off, len, "nx_treepack.elf" as *u8, 0, 15) == 1 { return 1 } 461 if md_slice_eq(nm, off, len, "buildsrc.pack" as *u8, 0, 13) == 1 { return 1 } 462 if md_slice_eq(nm, off, len, "buildknow.pack" as *u8, 0, 14) == 1 { return 1 } 463 // /api/compare server-side regen: the hub generator elf (exec'd by md_cmp_regen; updatable over the API). 464 if md_slice_eq(nm, off, len, "nx_swcompare_hub.elf" as *u8, 0, 20) == 1 { return 1 } 465 // /api/compare/publish staging slot: page bytes arrive chunked here, then publish pins them by sha256. 466 if md_slice_eq(nm, off, len, "compare.page" as *u8, 0, 12) == 1 { return 1 } 467 // the Relationship OS daemon (binds loopback :8027; conf row `relate` promotes it once hostctl ships relatedeploy). 468 if md_slice_eq(nm, off, len, "nx_relate_daemon.elf" as *u8, 0, 20) == 1 { return 1 } 469 // the site-visuals editor (loopback :18466; cut over API-pure via /api/restart service=siteedit which 470 // promotes the staged .new -> the FULL editor deploy loop is upload+restart, zero ssh). 471 if md_slice_eq(nm, off, len, "nx_siteedit_daemon.elf" as *u8, 0, 22) == 1 { return 1 } 472 // seq1433 HALF-WIRED DEPLOY LOOP FIXED: md_direct_restart_ok mapped service=toolsapi -> nx_tools_api_serve.elf 473 // and hc_restart_ok/hc_guard_tapi allowed+respawned it, but there was NO upload row -- so the staging slot 474 // nx_tools_api_serve.elf.new could never be written over the API and /api/restart toolsapi could only ever 475 // re-promote a STALE artifact. A restart verb without a staging slot is not a deploy loop. This is the daemon 476 // that gates EVERY agent capability = the one binary the ecosystem could not update API-first (cf. galxgw seq1049). 477 if md_slice_eq(nm, off, len, "nx_tools_api_serve.elf" as *u8, 0, 22) == 1 { return 1 } 478 // the Nishi Pulse survey/insights daemon (:8031, cron-reconciled; /api/restart service=survey promotes 479 // the staged .new -> the survey deploy loop is upload+restart, zero ssh/scp). 480 if md_slice_eq(nm, off, len, "nx_survey_daemon.elf" as *u8, 0, 20) == 1 { return 1 } 481 // the ETHICAL CLEAN-SERVE daemon (:8102, /clean -- neutralize attacks + PRESERVE safe ads + safety receipt; 482 // SSRF-guarded public fetch proxy). Deploy loop = /api/upload + /api/deploy target=cleanserve (hostctl supervise). 483 if md_slice_eq(nm, off, len, "nx_clean_serve_daemon.elf" as *u8, 0, 25) == 1 { return 1 } 484 // the Nishi Office daemon (:8030, cron-reconciled) + its client JS. Deploy loop = /api/upload + /api/restart 485 // service=office (daemon: promote .new + kill -> nx_office_reconcile respawns) / officejs (JS: promote only, 486 // the daemon reads office_app.js per-request). Zero ssh -- matches the survey pattern. 487 if md_slice_eq(nm, off, len, "nx_office_daemon.elf" as *u8, 0, 20) == 1 { return 1 } 488 if md_slice_eq(nm, off, len, "office_app.js" as *u8, 0, 13) == 1 { return 1 } 489 // THE BUILD TOOLCHAIN ITSELF (seq891/903). Staged as <name>.new in nishihost cwd like every other 490 // target, then promoted into buildroot/_offc by /api/promote_toolchain -- which validates the ELF, 491 // banks .prev, chmod +x, CANARY-COMPILES and auto-rolls-back. Uploading merely STAGES; it can never 492 // touch the live compiler, so these rows are safe on their own. Closes the gap where the ecosystem 493 // could deploy every service over its own API but not the compiler that builds them. 494 if md_slice_eq(nm, off, len, "nx_cc_sovereign.elf" as *u8, 0, 19) == 1 { return 1 } 495 if md_slice_eq(nm, off, len, "nxasm_x86_main.elf" as *u8, 0, 18) == 1 { return 1 } 496 if md_slice_eq(nm, off, len, "nx_sov_build_run.elf" as *u8, 0, 20) == 1 { return 1 } 497 return 0 498} 499 500// ---- /api/unpack: resolve a fail-closed unpack destination (dest-key -> staged .pack + abs NAS dir) ---------- 501// NEVER-BRICK (#26): only allowlisted dest keys resolve; an unknown key -> 400, nothing written. Each key maps to 502// the STAGED pack (<key>.pack.new, from /api/upload) + a FIXED abs dir under nishihost (nx_treepack writes only 503// under it). Extend by adding a row. `buildsrc` = the runtime source tree for build-over-API. 504func md_unpack_resolve(nm: *u8, off: i64, len: i64, packbuf: *u8, destbuf: *u8) -> i64 { 505 if md_slice_eq(nm, off, len, "buildsrc" as *u8, 0, 8) == 1 { 506 md_copy_slice_z(packbuf, "buildsrc.pack.new" as *u8, 0, 17, 128) 507 md_copy_slice_z(destbuf, "/volume1/homes/elderwesto/nishihost/buildroot/runtime" as *u8, 0, 53, 256) 508 return 1 509 } 510 // `buildknow` = buildroot/knowledge DATA ring (2026-08-05, debt 1785937893): compare .q/.axes corpus banks 511 // and other knowledge data the buildroot-CWD generators (gapmap frontier) read; same staged-pack discipline. 512 if md_slice_eq(nm, off, len, "buildknow" as *u8, 0, 9) == 1 { 513 md_copy_slice_z(packbuf, "buildknow.pack.new" as *u8, 0, 18, 128) 514 md_copy_slice_z(destbuf, "/volume1/homes/elderwesto/nishihost/buildroot/knowledge" as *u8, 0, 55, 256) 515 return 1 516 } 517 return 0 518} 519// fork+exec the on-NAS nx_treepack (unpack mode) with (packpath, destpath); capture stdout -> outpath; exit code. 520func md_exec_treepack(packpath: *u8, destpath: *u8, outpath: *u8) -> i64 { 521 let helf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_treepack.elf" as *u8 522 let args: *i64 = sys_mmap(32) as *i64 523 args[0] = "unpack" as *u8 as i64 524 args[1] = packpath as i64 525 args[2] = destpath as i64 526 return dep_run_capture(helf, args, 3, outpath) 527} 528 529// ---- CONTENT-PUBLISH namespace (publish-from-anywhere for STATIC site files, 2026-07-06) ------------------ 530// A content target is `sites/nishifamily/synth/<basename>`: FIXED directory prefix (extend = add a prefix row 531// here, data-ring) + strict basename charset [a-z0-9_.-] (first char alphanumeric, no ".." run, bounded) + 532// extension in {.html, .png, .stl}. '/' is impossible inside the basename by charset, and the prefix is fixed, 533// so path traversal is impossible BY CONSTRUCTION. Services (.elf) stay on md_upload_target_ok + /api/deploy 534// (health-checked promotion); this namespace is static files promoted by /api/promote_content (atomic 535// .prev-backed swap -- no health-check needed, and it can never touch a binary or leave the site down). 536func md_content_ext_ok(nm: *u8, off: i64, len: i64) -> i64 { 537 if len > 5 { 538 var m: i64 = 1 539 let e1: *u8 = ".html" as *u8 540 var i: i64 = 0 541 while i < 5 { if (nm[off + len - 5 + i] as i64) != (e1[i] as i64) { m = 0; i = 5 } else { i = i + 1 } } 542 if m == 1 { return 1 } 543 } 544 if len > 4 { 545 var m2: i64 = 1 546 let e2: *u8 = ".png" as *u8 547 var j: i64 = 0 548 while j < 4 { if (nm[off + len - 4 + j] as i64) != (e2[j] as i64) { m2 = 0; j = 4 } else { j = j + 1 } } 549 if m2 == 1 { return 1 } 550 var m3: i64 = 1 551 let e3: *u8 = ".stl" as *u8 552 var k: i64 = 0 553 while k < 4 { if (nm[off + len - 4 + k] as i64) != (e3[k] as i64) { m3 = 0; k = 4 } else { k = k + 1 } } 554 if m3 == 1 { return 1 } 555 // whole-site static set (site-factory publish): the self-emitted sitemap.xml + robots.txt 556 var m4: i64 = 1 557 let e4: *u8 = ".xml" as *u8 558 var k4: i64 = 0 559 while k4 < 4 { if (nm[off + len - 4 + k4] as i64) != (e4[k4] as i64) { m4 = 0; k4 = 4 } else { k4 = k4 + 1 } } 560 if m4 == 1 { return 1 } 561 var m5: i64 = 1 562 let e5: *u8 = ".txt" as *u8 563 var k5: i64 = 0 564 while k5 < 4 { if (nm[off + len - 4 + k5] as i64) != (e5[k5] as i64) { m5 = 0; k5 = 4 } else { k5 = k5 + 1 } } 565 if m5 == 1 { return 1 } 566 } 567 // the sovereign video-client set (2026-07-11): app.v2.js + nx_video_client.wasm ride /api/upload -> 568 // /api/promote_content like every other static file (retires the nx_aw_send ssh push). Same trust rank 569 // as .html (which can carry <script> anyway); binaries (.elf) stay OUT of this namespace by construction. 570 if len > 3 { 571 var m6: i64 = 1 572 let e6: *u8 = ".js" as *u8 573 var k6: i64 = 0 574 while k6 < 3 { if (nm[off + len - 3 + k6] as i64) != (e6[k6] as i64) { m6 = 0; k6 = 3 } else { k6 = k6 + 1 } } 575 if m6 == 1 { return 1 } 576 } 577 if len > 5 { 578 var m7: i64 = 1 579 let e7: *u8 = ".wasm" as *u8 580 var k7: i64 = 0 581 while k7 < 5 { if (nm[off + len - 5 + k7] as i64) != (e7[k7] as i64) { m7 = 0; k7 = 5 } else { k7 = k7 + 1 } } 582 if m7 == 1 { return 1 } 583 } 584 // the EVIDENCE workstream (2026-07-16): every published evidence run carries api.json machine detail 585 // beside its index.html (dashboards speak plain english; machines get JSON). Same trust rank as .txt. 586 if len > 5 { 587 var m8: i64 = 1 588 let e8: *u8 = ".json" as *u8 589 var k8: i64 = 0 590 while k8 < 5 { if (nm[off + len - 5 + k8] as i64) != (e8[k8] as i64) { m8 = 0; k8 = 5 } else { k8 = k8 + 1 } } 591 if m8 == 1 { return 1 } 592 } 593 return 0 594} 595// prefix TABLE (the data ring this namespace was designed to grow by): returns the matched prefix length, 596// or -1. Each row is a FIXED site subdirectory; extend = add a row. 597func md_content_pfx(nm: *u8, off: i64, len: i64) -> i64 { 598 let p1: *u8 = "sites/nishifamily/synth/" as *u8 599 let l1: i64 = 24 600 if len > l1 { 601 var i: i64 = 0 602 var m: i64 = 1 603 while i < l1 { if (nm[off + i] as i64) != (p1[i] as i64) { m = 0; i = l1 } else { i = i + 1 } } 604 if m == 1 { return l1 } 605 } 606 let p2: *u8 = "sites/nishifamily/swgpu/" as *u8 607 let l2: i64 = 24 608 if len > l2 { 609 var i2: i64 = 0 610 var m2: i64 = 1 611 while i2 < l2 { if (nm[off + i2] as i64) != (p2[i2] as i64) { m2 = 0; i2 = l2 } else { i2 = i2 + 1 } } 612 if m2 == 1 { return l2 } 613 } 614 // the SITE-FACTORY showcase (generated archetype gallery) -- publishes via upload+promote_content 615 let p3: *u8 = "sites/nishifamily/factory/" as *u8 616 let l3: i64 = 26 617 if len > l3 { 618 var i3: i64 = 0 619 var m3: i64 = 1 620 while i3 < l3 { if (nm[off + i3] as i64) != (p3[i3] as i64) { m3 = 0; i3 = l3 } else { i3 = i3 + 1 } } 621 if m3 == 1 { return l3 } 622 } 623 // the SOVEREIGN-INFINIGEN showcases (/world, /gsplat) -- retires the flaky ssh-cat push (2026-07-09): 624 // publish = /api/upload (chunked+staged) -> /api/promote_content (atomic .prev-backed swap) 625 let p4: *u8 = "sites/nishifamily/world/" as *u8 626 let l4: i64 = 24 627 if len > l4 { 628 var i4: i64 = 0 629 var m4: i64 = 1 630 while i4 < l4 { if (nm[off + i4] as i64) != (p4[i4] as i64) { m4 = 0; i4 = l4 } else { i4 = i4 + 1 } } 631 if m4 == 1 { return l4 } 632 } 633 let p5: *u8 = "sites/nishifamily/gsplat/" as *u8 634 let l5: i64 = 25 635 if len > l5 { 636 var i5: i64 = 0 637 var m5: i64 = 1 638 while i5 < l5 { if (nm[off + i5] as i64) != (p5[i5] as i64) { m5 = 0; i5 = l5 } else { i5 = i5 + 1 } } 639 if m5 == 1 { return l5 } 640 } 641 // WHOLESALE-emitted multi-page sites (site-factory R-SITESHAPE): subdir paths allowed under this 642 // prefix via the guarded '/' rule in md_content_target_ok (never doubled, ".." runs still refused). 643 let p6: *u8 = "sites/nishifamily/wholesale/" as *u8 644 let l6: i64 = 28 645 if len > l6 { 646 var i6: i64 = 0 647 var m6: i64 = 1 648 while i6 < l6 { if (nm[off + i6] as i64) != (p6[i6] as i64) { m6 = 0; i6 = l6 } else { i6 = i6 + 1 } } 649 if m6 == 1 { return l6 } 650 } 651 // the public generate-UI over nx_gen (R10 of the Infinigen ladder) 652 let p7: *u8 = "sites/nishifamily/generate/" as *u8 653 let l7: i64 = 27 654 if len > l7 { 655 var i7: i64 = 0 656 var m7: i64 = 1 657 while i7 < l7 { if (nm[off + i7] as i64) != (p7[i7] as i64) { m7 = 0; i7 = l7 } else { i7 = i7 + 1 } } 658 if m7 == 1 { return l7 } 659 } 660 // the sovereign VIDEO CODEC client set (2026-07-11): index.html + app.v2.js + nx_video_client.wasm + 661 // ver.txt. Retires the last ssh (nx_aw_send) in the codec ship loop -- deploy8XX becomes /api/upload -> 662 // /api/promote_content, and the ship gate verifies the :8443 sovereign edge. 663 let p8: *u8 = "sites/nishifamily/video/" as *u8 664 let l8: i64 = 24 665 if len > l8 { 666 var i8: i64 = 0 667 var m8: i64 = 1 668 while i8 < l8 { if (nm[off + i8] as i64) != (p8[i8] as i64) { m8 = 0; i8 = l8 } else { i8 = i8 + 1 } } 669 if m8 == 1 { return l8 } 670 } 671 // the EVIDENCE workstream namespace (2026-07-16, operator: "publish evidence consistent workstream"): 672 // /evidence/<run>/ = nx_evidence_pack output (index.html + api.json + screenshots/recordings), 673 // published via the proven upload->promote_content lane (nx_content_ship ship.manifest). Subdir runs 674 // ride the same guarded '/' rule as wholesale/. 675 let p9: *u8 = "sites/nishifamily/evidence/" as *u8 676 let l9: i64 = 27 677 if len > l9 { 678 var i9: i64 = 0 679 var m9: i64 = 1 680 while i9 < l9 { if (nm[off + i9] as i64) != (p9[i9] as i64) { m9 = 0; i9 = l9 } else { i9 = i9 + 1 } } 681 if m9 == 1 { return l9 } 682 } 683 // the EXPERIENTIAL census page (2026-07-16): EMITTED by nx_s21_census (sync-by-construction) and 684 // republished through this lane on every census re-run -- the page can never drift from disk truth. 685 let p10: *u8 = "sites/nishifamily/experiential/" as *u8 686 let l10: i64 = 31 687 if len > l10 { 688 var i10: i64 = 0 689 var m10: i64 = 1 690 while i10 < l10 { if (nm[off + i10] as i64) != (p10[i10] as i64) { m10 = 0; i10 = l10 } else { i10 = i10 + 1 } } 691 if m10 == 1 { return l10 } 692 } 693 // the COMPARE hub artifacts (2026-08-05, debt 1785937233): /compare api.json + index.html + openapi.json 694 // are laptop-generated (registry is laptop-owned by design) and ship through the proven 695 // upload -> promote_content lane; per-domain spoke pages stay NAS-regen-owned (nx_compare_regen). 696 // This row closes the hub-vs-spoke drift class: the hub gets a DOOR instead of a frozen snapshot. 697 let p11: *u8 = "sites/nishifamily/compare/" as *u8 698 let l11: i64 = 26 699 if len > l11 { 700 var i11: i64 = 0 701 var m11: i64 = 1 702 while i11 < l11 { if (nm[off + i11] as i64) != (p11[i11] as i64) { m11 = 0; i11 = l11 } else { i11 = i11 + 1 } } 703 if m11 == 1 { return l11 } 704 } 705 return 0 - 1 706} 707func md_content_target_ok(nm: *u8, off: i64, len: i64) -> i64 { 708 let pl: i64 = md_content_pfx(nm, off, len) 709 if pl < 0 { return 0 } 710 if len <= pl + 4 { return 0 } // needs prefix + at least an "a.png"-sized basename 711 if len > pl + 64 { return 0 } // bounded basename 712 let c0: i64 = nm[off + pl] as i64 // first basename char: alphanumeric only (blocks ".x" "-x" "..") 713 var ok0: i64 = 0 714 if c0 >= 97 { if c0 <= 122 { ok0 = 1 } } 715 if c0 >= 48 { if c0 <= 57 { ok0 = 1 } } 716 if ok0 == 0 { return 0 } 717 var j: i64 = pl 718 var prevdot: i64 = 0 719 var prevslash: i64 = 0 720 while j < len { 721 let c: i64 = nm[off + j] as i64 722 var okc: i64 = 0 723 if c >= 97 { if c <= 122 { okc = 1 } } 724 if c >= 48 { if c <= 57 { okc = 1 } } 725 if c == 95 { okc = 1 } 726 if c == 45 { okc = 1 } 727 // subdir separator for multi-page sites: never doubled, never after a dot (with the ".."-run 728 // refusal below and the pinned prefix, traversal stays impossible by construction). 729 if c == 47 { 730 if prevslash == 1 { return 0 } 731 if prevdot == 1 { return 0 } 732 okc = 1 733 prevslash = 1 734 } else { prevslash = 0 } 735 if c == 46 { 736 if prevdot == 1 { return 0 } // ".." run -> refuse 737 okc = 1 738 prevdot = 1 739 } else { prevdot = 0 } 740 if okc == 0 { return 0 } 741 j = j + 1 742 } 743 return md_content_ext_ok(nm, off, len) 744} 745 746// ---- COMPARE namespace (Nishi Compare registry SSOT + server-side hub regen, 2026-07-09) -------------------- 747// The CONCURRENT-WORK coordination plane for /compare: many sessions publish comparisons, so the shared registry 748// + hub are mutated through THIS one serialized daemon instead of racing raw file writes. The unit of mutation is 749// the COMPARISON RECORD keyed by its /compare/<domain> href segment: different-domain upserts are commutative 750// (merge, no clobber possible); same-domain upserts replace, with the previous line preserved in registry.hist 751// (additive-only). After a mutation the hub index.html + api.json are regenerated SERVER-SIDE from the SSOT by the 752// on-NAS nx_swcompare_hub.elf, so the published surface can never reflect a session's stale partial registry. 753// Installs are sanity-gated + .prev-backed atomic renames (never-brick: a failed regen leaves live files untouched). 754 755// extract the /compare/<domain> merge key from a registry line (field 3 of title|kind|href|radar|stat). 756// Returns domain length copied into domb (NUL-terminated), or 0 if the line/href is malformed. Charset [a-z0-9_-]. 757func md_cmp_domain_of(src: *u8, off: i64, len: i64, domb: *u8, cap: i64) -> i64 { 758 var p: i64 = 0 759 var f: i64 = 0 760 while p < len { 761 if (src[off + p] as i64) == 124 { f = f + 1; if f == 2 { p = p + 1; break } } 762 p = p + 1 763 } 764 if f != 2 { return 0 } 765 let pfx: *u8 = "/compare/" as *u8 766 var k: i64 = 0 767 while k < 9 { 768 if p + k >= len { return 0 } 769 if (src[off + p + k] as i64) != (pfx[k] as i64) { return 0 } 770 k = k + 1 771 } 772 var q: i64 = p + 9 773 var o: i64 = 0 774 while q < len { 775 let c: i64 = src[off + q] as i64 776 if c == 124 { break } 777 if c == 47 { break } 778 var okc: i64 = 0 779 if c >= 97 { if c <= 122 { okc = 1 } } 780 if c >= 48 { if c <= 57 { okc = 1 } } 781 if c == 95 { okc = 1 } 782 if c == 45 { okc = 1 } 783 if okc == 0 { return 0 } 784 if o < cap - 1 { domb[o] = src[off + q]; o = o + 1 } 785 q = q + 1 786 } 787 domb[o] = 0 as u8 788 if o < 1 { return 0 } 789 return o 790} 791 792func md_cmp_ws(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 } 793func md_cmp_wn(fd: i64, v: i64) -> i64 { 794 var m: i64 = v 795 if m < 0 { md_cmp_ws(fd, "-" as *u8); m = 0 - m } 796 let t: *u8 = sys_mmap(24) 797 var k: i64 = 0 798 if m == 0 { t[0] = 48 as u8; k = 1 } 799 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 800 let o2: *u8 = sys_mmap(24) 801 var i: i64 = 0 802 while i < k { o2[i] = t[k - 1 - i]; i = i + 1 } 803 sys_write(fd, o2, k) 804 return 0 805} 806 807// merge ONE registry line into the SSOT by domain key: replace the matching entry (old line -> .hist) or append. 808// Atomic (write registry.tmp -> rename); additive history appended AFTER the rename lands. Returns the new entry 809// count, or -1 on write failure (registry untouched -- the tmp+rename never half-writes the live file). 810func md_cmp_upsert(line: *u8, ln: i64, domb: *u8, domn: i64, replacedb: *i64) -> i64 { 811 let szp: *i64 = sys_mmap(16) as *i64 812 let old: *u8 = md_read_file("knowledge/compare/registry" as *u8, szp) 813 let on: i64 = szp[0] 814 let nb: *u8 = sys_mmap(262144) 815 let d2: *u8 = sys_mmap(128) 816 let oldline: *u8 = sys_mmap(4096) 817 var oldn: i64 = 0 818 var o: i64 = 0 819 var entries: i64 = 0 820 var replaced: i64 = 0 821 if (old as i64) != 0 { 822 var i: i64 = 0 823 while i < on { 824 let le: i64 = md_eol(old, on, i) 825 var wrote: i64 = 0 826 if le > i { 827 if (old[i] as i64) != 35 { 828 let dl2: i64 = md_cmp_domain_of(old, i, le - i, d2, 120) 829 if dl2 > 0 { 830 entries = entries + 1 831 if md_slice_eq(d2, 0, dl2, domb, 0, domn) == 1 { 832 replaced = 1 833 wrote = 1 834 oldn = 0 835 var c: i64 = 0 836 while c < (le - i) { if c < 4090 { oldline[c] = old[i + c]; oldn = c + 1 } c = c + 1 } 837 var w2: i64 = 0 838 while w2 < ln { nb[o] = line[w2]; o = o + 1; w2 = w2 + 1 } 839 nb[o] = 10 as u8 840 o = o + 1 841 } 842 } 843 } 844 } 845 if wrote == 0 { 846 var c2: i64 = i 847 while c2 < le { nb[o] = old[c2]; o = o + 1; c2 = c2 + 1 } 848 nb[o] = 10 as u8 849 o = o + 1 850 } 851 i = le + 1 852 } 853 } 854 if replaced == 0 { 855 var w3: i64 = 0 856 while w3 < ln { nb[o] = line[w3]; o = o + 1; w3 = w3 + 1 } 857 nb[o] = 10 as u8 858 o = o + 1 859 entries = entries + 1 860 } 861 let fd: i64 = sys_openat_wr("knowledge/compare/registry.tmp" as *u8, 0x1a4) 862 if fd < 0 { replacedb[0] = replaced; return 0 - 1 } 863 sys_write(fd, nb, o) 864 sys_close(fd) 865 if sys_renameat("knowledge/compare/registry.tmp" as *u8, "knowledge/compare/registry" as *u8) != 0 { 866 replacedb[0] = replaced 867 return 0 - 1 868 } 869 let hf: i64 = sys_openat_append("knowledge/compare/registry.hist" as *u8, 0x1a4) 870 if hf >= 0 { 871 md_cmp_ws(hf, "ts=" as *u8) 872 md_cmp_wn(hf, sys_now_realtime_sec()) 873 md_cmp_ws(hf, " op=upsert domain=" as *u8) 874 var hd: i64 = 0 875 while hd < domn { sys_write(hf, ((domb as i64) + hd) as *u8, 1); hd = hd + 1 } 876 md_cmp_ws(hf, " replaced=" as *u8) 877 md_cmp_wn(hf, replaced) 878 md_cmp_ws(hf, "\n" as *u8) 879 if replaced == 1 { if oldn > 0 { 880 md_cmp_ws(hf, " prev: " as *u8) 881 sys_write(hf, oldline, oldn) 882 md_cmp_ws(hf, "\n" as *u8) 883 } } 884 sys_close(hf) 885 } 886 replacedb[0] = replaced 887 return entries 888} 889 890// install a generator-captured output file as a live docroot file: sanity (size + first byte) -> write tmp -> 891// back up live -> rename tmp over live; on failure the previous live file is restored (mirror of promote_content). 892func md_cmp_install(srcp: *u8, tmpp: *u8, prevp: *u8, livep: *u8, firstc: i64) -> i64 { 893 let szp: *i64 = sys_mmap(16) as *i64 894 let b: *u8 = md_read_file(srcp, szp) 895 let n: i64 = szp[0] 896 if (b as i64) == 0 { return 0 } 897 if n < 200 { return 0 } 898 if (b[0] as i64) != firstc { return 0 } 899 let fd: i64 = sys_openat_wr(tmpp, 0x1a4) 900 if fd < 0 { return 0 } 901 sys_write(fd, b, n) 902 sys_close(fd) 903 var had: i64 = 0 904 let pf: i64 = sys_openat_rd(livep) 905 if pf >= 0 { sys_close(pf); had = 1 } 906 if had == 1 { md_rotate_prev(prevp); if sys_renameat(livep, prevp) != 0 { return 0 } } 907 if sys_renameat(tmpp, livep) != 0 { 908 if had == 1 { sys_renameat(prevp, livep) } 909 return 0 910 } 911 return 1 912} 913 914// regenerate the /compare hub (index.html + api.json) from the registry SSOT via the on-NAS hub generator elf. 915// Fail-safe: generator output must pass sanity before install; a missing elf / bad output leaves live files alone. 916func md_cmp_regen() -> i64 { 917 let helf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_swcompare_hub.elf" as *u8 918 // Capture the generator output to an ELDERWESTO-OWNED scratch dir (knowledge/compare/, CWD=nishihost), NOT 919 // world-writable /tmp. WHY (2026-07-14 root cause): a root-era mgmt run left /tmp/nx_ma_cmp_*.out root-owned; 920 // after the root->elderwesto guard migration this daemon could no longer OVERWRITE them -> dep_run_capture's 921 // sys_openat_wr failed (EACCES), it ran the generator WITHOUT redirect, and md_cmp_install re-installed the 922 // STALE file every regen (silent "OK", frozen hub). A path this daemon owns always truncates fresh -> correct 923 // install, or an empty capture that fails md_cmp_install's sanity gate -> honest REGEN-FAILED (never stale). 924 let a1: *i64 = sys_mmap(16) as *i64 925 a1[0] = "html" as *u8 as i64 926 dep_run_capture(helf, a1, 1, "knowledge/compare/.regen_html.out" as *u8) 927 let a2: *i64 = sys_mmap(16) as *i64 928 a2[0] = "json" as *u8 as i64 929 dep_run_capture(helf, a2, 1, "knowledge/compare/.regen_json.out" as *u8) 930 let ok1: i64 = md_cmp_install("knowledge/compare/.regen_html.out" as *u8, "sites/nishifamily/compare/index.html.tmp2" as *u8, "sites/nishifamily/compare/index.html.prev" as *u8, "sites/nishifamily/compare/index.html" as *u8, 60) 931 let ok2: i64 = md_cmp_install("knowledge/compare/.regen_json.out" as *u8, "sites/nishifamily/compare/api.json.tmp2" as *u8, "sites/nishifamily/compare/api.json.prev" as *u8, "sites/nishifamily/compare/api.json" as *u8, 123) 932 if ok1 == 1 { if ok2 == 1 { return 1 } } 933 return 0 934} 935 936// validate a bare compare DOMAIN atom: charset [a-z0-9_-], first char alphanumeric, len 1..60 -> copy NUL-terminated. 937// Path segments are built ONLY from this validated atom + fixed literals, so traversal is impossible by construction. 938func md_cmp_dom_ok(src: *u8, off: i64, len: i64, domb: *u8) -> i64 { 939 if len < 1 { return 0 } 940 if len > 60 { return 0 } 941 let c0: i64 = src[off] as i64 942 var ok0: i64 = 0 943 if c0 >= 97 { if c0 <= 122 { ok0 = 1 } } 944 if c0 >= 48 { if c0 <= 57 { ok0 = 1 } } 945 if ok0 == 0 { return 0 } 946 var i: i64 = 0 947 while i < len { 948 let c: i64 = src[off + i] as i64 949 var okc: i64 = 0 950 if c >= 97 { if c <= 122 { okc = 1 } } 951 if c >= 48 { if c <= 57 { okc = 1 } } 952 if c == 95 { okc = 1 } 953 if c == 45 { okc = 1 } 954 if okc == 0 { return 0 } 955 domb[i] = src[off + i] 956 i = i + 1 957 } 958 domb[len] = 0 as u8 959 return 1 960} 961func md_cmp_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } d[o + i] = 0 as u8; return o + i } 962 963// publish the STAGED compare.page.new as the live artifact for (domain, kind). kind: 1=page 2=frontier 3=bench 4=api. 964// Server derives the FIXED docroot path from the validated domain atom + a kind enum (no caller-supplied paths at 965// all). Dirs are created as needed; install is sanity-gated + .prev-backed (md_cmp_install). kind=api ALSO refreshes 966// the hub data-link marker knowledge/compare/<domain>-api.json (tmp+rename). Returns 1 ok / 0 fail (live untouched). 967func md_cmp_publish(domb: *u8, kind: i64) -> i64 { 968 let base: *u8 = sys_mmap(512) 969 var o: i64 = md_cmp_cat(base, 0, "sites/nishifamily/compare/" as *u8) 970 o = md_cmp_cat(base, o, domb) 971 sys_mkdir(base, 0x1ed) 972 if kind == 2 { o = md_cmp_cat(base, o, "/frontier" as *u8); sys_mkdir(base, 0x1ed) } 973 if kind == 3 { o = md_cmp_cat(base, o, "/bench" as *u8); sys_mkdir(base, 0x1ed) } 974 let live: *u8 = sys_mmap(512) 975 var lo: i64 = md_cmp_cat(live, 0, base) 976 var fc: i64 = 60 977 if kind == 4 { lo = md_cmp_cat(live, lo, "/api.json" as *u8); fc = 123 } else { lo = md_cmp_cat(live, lo, "/index.html" as *u8) } 978 let tmpp: *u8 = sys_mmap(512) 979 var to: i64 = md_cmp_cat(tmpp, 0, live) 980 to = md_cmp_cat(tmpp, to, ".tmp2" as *u8) 981 let prevp: *u8 = sys_mmap(512) 982 var po: i64 = md_cmp_cat(prevp, 0, live) 983 po = md_cmp_cat(prevp, po, ".prev" as *u8) 984 let oki: i64 = md_cmp_install("compare.page.new" as *u8, tmpp, prevp, live, fc) 985 if oki != 1 { return 0 } 986 if kind == 4 { 987 let mk: *u8 = sys_mmap(512) 988 var mo: i64 = md_cmp_cat(mk, 0, "knowledge/compare/" as *u8) 989 mo = md_cmp_cat(mk, mo, domb) 990 mo = md_cmp_cat(mk, mo, "-api.json" as *u8) 991 let mt: *u8 = sys_mmap(512) 992 var mto: i64 = md_cmp_cat(mt, 0, mk) 993 mto = md_cmp_cat(mt, mto, ".tmp" as *u8) 994 let szp: *i64 = sys_mmap(16) as *i64 995 let b: *u8 = md_read_file("compare.page.new" as *u8, szp) 996 if (b as i64) != 0 { if szp[0] > 0 { 997 let fd: i64 = sys_openat_wr(mt, 0x1a4) 998 if fd >= 0 { sys_write(fd, b, szp[0]); sys_close(fd); sys_renameat(mt, mk) } 999 } } 1000 } 1001 return 1 1002} 1003 1004// map an allowlisted service name -> the proven nx_aw_hostctl surgical-restart sub (fail-closed: unknown -> 0). 1005// ---- RESTART ALLOWLISTS: ONE OWNER PER LANE, TABLE-DRIVEN (2026-08-28) ------------------------------ 1006// WHY THIS SHAPE. These two allowlists were if-chains and the /api/restart error message was a THIRD 1007// hand-written copy of both. That is the duplicate-ruler defect, and THIS FILE ALREADY RECORDS IT 1008// HAPPENING -- seq1433: "this list DRIFTED from the two allowlists it describes -- office/officejs/ 1009// toolsapi were wired into md_direct_restart_ok but never named here, so a caller asking for a service 1010// that IS supported was told it was unknown." That drift was repaired by hand, which fixes the instance 1011// and leaves the shape. ★WHEN TWO THINGS MUST AGREE, MAKE DISAGREEMENT IMPOSSIBLE BY CONSTRUCTION -- 1012// DO NOT COORDINATE BY DISCIPLINE. The rows below are now the ONLY declaration of a restart target: the 1013// resolvers read them and the published message is GENERATED from them (md_tbl_names), so the message 1014// can never again name a route that does not resolve, nor omit one that does. Adding a route is ONE 1015// data row, and nx_restart_routes_gate proves both directions. 1016// 1017// GRAMMAR: <svc>|<target>;<svc>|<target>;... Separators are plain printable ASCII with NO escape 1018// sequence -- a bare tab or newline inside an nx literal is a banked lexer hazard, and '|' is already 1019// the separator the published message uses, so md_tbl_names emits it verbatim. 1020// MATCHING IS EXACT-LENGTH, INHERITED NOT RE-IMPLEMENTED: md_slice_eq refuses on a length mismatch, so 1021// "site" still cannot match "siteedit" and "office" cannot match "officejs" -- the property the if-chain 1022// got from its hand-counted literal lengths, now DERIVED from the row's own field width. That deletes 1023// the hand-counted-length class outright (a literal and a number beside it that drift in silence). 1024const MD_TBL_FIELD: i64 = 124 // '|' -- field separator, and the message's own display separator 1025const MD_TBL_ROW: i64 = 59 // ';' -- row terminator 1026// The resolvers write into a caller-allocated buffer. ONE const owns that size and BOTH the callers in 1027// nx_mgmt_api and the copies here read it, so the allocation and the bound cannot disagree. 1028const MD_RESTART_BUFCAP: i64 = 64 1029// 1030// LANE 1 -- hostctl-sub restarts: the mgmt daemon runs `nx_hostctl <sub>`; hostctl owns the kill+respawn. 1031const MD_RESTART_SUB_TBL: *u8 = "reader|kickreader;torrent|kicktorrent;torrentgw|kicktorrentgw;docportal|kickdocportal;" 1032// 1033// LANE 2 -- DIRECT restarts (no hostctl sub): svc -> the exact process cmdline needle. The mgmt daemon 1034// (root, itself guard-supervised) kills by name; a supervisor respawns the on-disk binary. Promote a 1035// staged .new first and this IS the full API-pure deploy loop. WHY EACH ROW EXISTS, kept with the row it 1036// explains so a reader can still grep the svc name and find its history: 1037// siteedit -- the andelinwest site editor. 1038// sites -- sites.elf = THE EDGE. Restart (kill -> guard respawns) RE-READS proxy_routes.conf, so 1039// this doubles as the API-pure route reload AND an edge redeploy if sites.elf.new is 1040// staged. The needle "sites.elf" is unique to the edge (nx_sites_daemon_v2 and 1041// nx_sites_reconciled do not contain it). 1042// survey -- Nishi Pulse: kill -> the nx_survey_reconcile cron respawns <=60s (not hostctl-guarded; 1043// the reconcile row IS its supervisor -- crash+reboot proven 2026-07-10). 1044// office -- kill -> nx_office_reconcile cron respawns <=60s (SO_REUSEADDR = fast rebind). 1045// officejs -- the client JS: promote office_app.js.new -> live. kill-by-name matches NO process (it 1046// is a file, not a daemon) -> harmless; the daemon reads it fresh on the next request. 1047// toolsapi -- the R0 agent-facing tools daemon :18096 (hostctl guard-supervised): respawn <=15s. 1048// Makes the tools/MCP plane API-pure-deployable. 1049// seed -- BitTorrent SEEDER :6881. ★NOTE THE SPLIT, IT IS DELIBERATE: md_promote_staged would 1050// target nishihost/nx_torrent_seed.elf.new, but the LIVE seeder lives at 1051// /volume1/ai/torrent/ and is promoted there by cmd_torrentdeploy. No such .new exists at 1052// the nishihost root, so the promote half cleanly NO-OPS and only the kill does the work. 1053// Promoting to the nishihost path instead would place a binary NOTHING EVER RUNS while 1054// reporting a successful restart. 1055// clock -- nx_clock_tickless.elf (guard-supervised). Its main loop only exits at 120 windows x 1056// 1800s = 60 HOURS per life, so without this route a promoted clock binary sat on disk 1057// while the running process kept executing the previous code for up to 60 hours -- every 1058// clock fix adopted by TIMEOUT rather than by deploy. ★PROMOTED IS A CLAIM ABOUT THE 1059// DISK; ADOPTED IS A CLAIM ABOUT A PROCESS. Safe: the same respawn the life budget 1060// already performs, just on demand; schedule state persists to knowledge/store/ 1061// clocksched- at every window end and clk_edf_migrate re-arms stale deadlines. 1062// wiki -- nx_wiki_gw.elf :18791 (guard-supervised). Added 2026-08-17 when it was the 1063// worst-committed process on the box (~0.7 MB/beat past 3.0 GB, swap 757->787 permil): 1064// the one daemon driving the host toward the swap cliff was ALSO the one /api/restart 1065// could not bounce, so the only remediation was a hard host action. 1066// email -- nx_email_portal_daemon.elf :18465. Added 2026-08-28 for EXACTLY the reason the wiki row 1067// was, one daemon later: measured worst_committed_kb=2,783,996 (2.78 GB) with swap at 1068// 943-951 permil against a 850 RED bar, nx_resmon verdict=RED sev=2 -- and no sanctioned 1069// way to recycle it. SUPERVISOR VERIFIED BEFORE WIRING, not assumed: daemons.reg row 1070// `email_portal_daemon ... | 18465 | revive | http` is armed to REVIVE, and this daemon is 1071// absent from nx_hostctl's supervise roster, so there is exactly ONE supervisor and this 1072// route cannot create the dueling-supervisor amplifier this estate has hit three times. 1073// Kill -> revive is the same respawn that supervisor already performs on a crash. 1074// ⚠HONEST RESIDUAL: unlike the wiki, a mail portal holds a maildir; the kill is a normal 1075// process death, so anything the daemon had not yet fsynced is lost exactly as it would 1076// be on any crash. Recycle it when the box is quiet, not mid-delivery. 1077const MD_RESTART_DIRECT_TBL: *u8 = "siteedit|nx_siteedit_daemon.elf;sites|sites.elf;survey|nx_survey_daemon.elf;office|nx_office_daemon.elf;officejs|office_app.js;toolsapi|nx_tools_api_serve.elf;seed|nx_torrent_seed.elf;clock|nx_clock_tickless.elf;wiki|nx_wiki_gw.elf;email|nx_email_portal_daemon.elf;" 1078 1079// Scan ONE table row starting at `i`: barbox gets the '|' offset (-1 if the row has none), endbox gets 1080// the row terminator (the ';' or the NUL). ONE scanner, TWO consumers (lookup + names), so a grammar 1081// change cannot land in one and be missed by the other. The cursor is never clobbered to exit -- `end` 1082// is a separate flag-cum-result, the banked remedy for the loop-exit-sentinel class. 1083func md_tbl_row(tbl: *u8, i: i64, barbox: *i64, endbox: *i64) -> i64 { 1084 var p: i64 = i 1085 var bar: i64 = 0 - 1 1086 var end: i64 = 0 - 1 1087 while end < 0 { 1088 let c: i64 = tbl[p] as i64 1089 if c == 0 { end = p } else { 1090 if c == MD_TBL_ROW { end = p } else { 1091 if c == MD_TBL_FIELD { if bar < 0 { bar = p } } 1092 p = p + 1 1093 } 1094 } 1095 } 1096 barbox[0] = bar 1097 endbox[0] = end 1098 return end 1099} 1100 1101// A row is USABLE only with a non-empty name AND a non-empty target. A malformed row is SKIPPED, never 1102// half-read: without this, a row like "x|;" would resolve to an EMPTY needle and md_kill_by_name("") 1103// is a kill request with no subject. The guard is wrong in the direction of doing nothing. 1104func md_tbl_row_ok(i: i64, bar: i64, end: i64) -> i64 { 1105 if bar <= i { return 0 } 1106 if end <= bar + 1 { return 0 } 1107 return 1 1108} 1109 1110// Walk a table; on an EXACT-LENGTH svc match copy that row's target into outbuf and return 1, else 0. 1111// Composes md_slice_eq and md_copy_slice_z -- the same two primitives the if-chains used, so matching 1112// and copying behaviour is inherited rather than rewritten. 1113func md_tbl_lookup(tbl: *u8, svc: *u8, off: i64, len: i64, outbuf: *u8, cap: i64) -> i64 { 1114 let barbox: *i64 = sys_mmap(16) as *i64 1115 let endbox: *i64 = sys_mmap(16) as *i64 1116 var i: i64 = 0 1117 var done: i64 = 0 1118 var hit: i64 = 0 1119 while done == 0 { 1120 if tbl[i] == (0 as u8) { done = 1 } else { 1121 md_tbl_row(tbl, i, barbox, endbox) 1122 let bar: i64 = barbox[0] 1123 let end: i64 = endbox[0] 1124 if md_tbl_row_ok(i, bar, end) == 1 { 1125 if md_slice_eq(svc, off, len, tbl, i, bar - i) == 1 { 1126 md_copy_slice_z(outbuf, tbl, bar + 1, end - bar - 1, cap) 1127 hit = 1 1128 done = 1 1129 } 1130 } 1131 if done == 0 { if tbl[end] == (0 as u8) { done = 1 } else { i = end + 1 } } 1132 } 1133 } 1134 return hit 1135} 1136 1137// Append every declared svc name from a table as "a|b|c" (no trailing separator), returning the new 1138// offset. THE PUBLISHED ERROR MESSAGE IS BUILT FROM THIS, so it is a projection of the resolver's own 1139// data and cannot drift from it. 1140func md_tbl_names(tbl: *u8, dst: *u8, o: i64) -> i64 { 1141 let barbox: *i64 = sys_mmap(16) as *i64 1142 let endbox: *i64 = sys_mmap(16) as *i64 1143 var out: i64 = o 1144 var i: i64 = 0 1145 var first: i64 = 1 1146 var done: i64 = 0 1147 while done == 0 { 1148 if tbl[i] == (0 as u8) { done = 1 } else { 1149 md_tbl_row(tbl, i, barbox, endbox) 1150 let bar: i64 = barbox[0] 1151 let end: i64 = endbox[0] 1152 if md_tbl_row_ok(i, bar, end) == 1 { 1153 if first == 0 { dst[out] = MD_TBL_FIELD as u8; out = out + 1 } 1154 var k: i64 = i 1155 while k < bar { dst[out] = tbl[k]; out = out + 1; k = k + 1 } 1156 first = 0 1157 } 1158 if tbl[end] == (0 as u8) { done = 1 } else { i = end + 1 } 1159 } 1160 } 1161 return out 1162} 1163 1164func md_restart_sub(svc: *u8, off: i64, len: i64, subbuf: *u8) -> i64 { 1165 return md_tbl_lookup(MD_RESTART_SUB_TBL, svc, off, len, subbuf, MD_RESTART_BUFCAP) 1166} 1167 1168// DIRECT-restart allowlist (no hostctl sub needed): svc name -> the exact process cmdline needle. The mgmt 1169// daemon (root, itself guard-supervised) kills by name; the hostctl supervise guard respawns the on-disk 1170// binary <=15s -- so promote-a-staged-.new + /api/restart = the full API-pure editor deploy loop. 1171// THE RESOLVER IS NOW A PROJECTION OF THE TABLE ABOVE. It was an if-chain of hand-counted literal 1172// lengths; every row's reasoning is preserved verbatim in the MD_RESTART_DIRECT_TBL comment block, where 1173// it stays greppable by svc name. Behaviour is EQUIVALENT, not merely similar, and that equivalence is 1174// proven exhaustively (all 9 legacy names -> their exact legacy needles, plus prefix/suffix/case/ 1175// cross-table negatives) by nx_restart_routes_gate rather than asserted here. 1176func md_direct_restart_ok(svc: *u8, off: i64, len: i64, namebuf: *u8) -> i64 { 1177 return md_tbl_lookup(MD_RESTART_DIRECT_TBL, svc, off, len, namebuf, MD_RESTART_BUFCAP) 1178} 1179// -- /api/route: validate + append a proxy_routes.conf row (data ring). Format: "<host> <prefix> <port> <mode>". 1180func md_catn(d: *u8, o: i64, v: i64) -> i64 { 1181 if v == 0 { d[o] = 48 as u8; return o + 1 } 1182 var m: i64 = v 1183 var oo: i64 = o 1184 if m < 0 { d[oo] = 45 as u8; oo = oo + 1; m = 0 - m } 1185 var nd: i64 = 1 1186 var t: i64 = m 1187 while t >= 10 { nd = nd + 1; t = t / 10 } 1188 var i: i64 = nd - 1 1189 while i >= 0 { d[oo + i] = (48 + (m % 10)) as u8; m = m / 10; i = i - 1 } 1190 return oo + nd 1191} 1192// fail-closed validation of a route (null-terminated host/prefix/mode + numeric port). 1193func md_route_valid(host: *u8, prefix: *u8, port: i64, mode: *u8) -> i64 { 1194 let hl: i64 = md_len(host) 1195 if hl < 3 { return 0 } 1196 if hl > 64 { return 0 } 1197 if (host[0] as i64) == 46 { return 0 } 1198 if (host[0] as i64) == 45 { return 0 } 1199 var hasdot: i64 = 0 1200 var i: i64 = 0 1201 while i < hl { 1202 let c: i64 = host[i] as i64 1203 var ok: i64 = 0 1204 if c >= 97 { if c <= 122 { ok = 1 } } 1205 if c >= 48 { if c <= 57 { ok = 1 } } 1206 if c == 45 { ok = 1 } 1207 if c == 46 { ok = 1; hasdot = 1; if i > 0 { if (host[i - 1] as i64) == 46 { return 0 } } } 1208 if ok == 0 { return 0 } 1209 i = i + 1 1210 } 1211 if hasdot == 0 { return 0 } 1212 let pl: i64 = md_len(prefix) 1213 if pl < 2 { return 0 } 1214 if pl > 48 { return 0 } 1215 if (prefix[0] as i64) != 47 { return 0 } 1216 i = 1 1217 while i < pl { 1218 let c: i64 = prefix[i] as i64 1219 var ok: i64 = 0 1220 if c >= 97 { if c <= 122 { ok = 1 } } 1221 if c >= 48 { if c <= 57 { ok = 1 } } 1222 if c == 95 { ok = 1 } 1223 if c == 45 { ok = 1 } 1224 if c == 47 { ok = 1; if (prefix[i - 1] as i64) == 47 { return 0 } } 1225 if c == 46 { ok = 1; if (prefix[i - 1] as i64) == 46 { return 0 } } 1226 if ok == 0 { return 0 } 1227 i = i + 1 1228 } 1229 if port < 1024 { return 0 } 1230 if port > 65535 { return 0 } 1231 var mok: i64 = 0 1232 if md_cstr_eq(mode, "buffered" as *u8) == 1 { mok = 1 } 1233 if md_cstr_eq(mode, "stream" as *u8) == 1 { mok = 1 } 1234 if md_cstr_eq(mode, "gated" as *u8) == 1 { mok = 1 } 1235 if mok == 0 { return 0 } 1236 return 1 1237} 1238// atomically upsert the route row into knowledge/hosting/proxy_routes.conf: preserve every OTHER line, replace 1239// any existing "<host> <prefix> ..." row, append the new one; tmp+rename. Returns 1 ok / 0 write-fail. 1240func md_route_append(confp: *u8, host: *u8, prefix: *u8, port: i64, mode: *u8) -> i64 { 1241 let szp: *i64 = sys_mmap(16) as *i64 1242 szp[0] = 0 1243 let old: *u8 = md_read_file(confp, szp) 1244 let oldn: i64 = szp[0] 1245 // NEVER-BRICK: refuse to write when the existing table is unreadable/empty. A transient read failure 1246 // (fd exhaustion etc.) with old==0 would otherwise REPLACE the populated edge table with one row -> 1247 // every proxied surface incl. /api itself lost = self-lockout. The live table always has rows; a 1248 // genuinely fresh bootstrap is an ssh-once operation, not this API's job. Fail-closed. 1249 if (old as i64) == 0 { return 0 } 1250 if oldn == 0 { return 0 } 1251 // build the dedup match key: "<host> <prefix> " 1252 let mk: *u8 = sys_mmap(160) 1253 var ko: i64 = 0 1254 var a: i64 = 0 1255 while host[a] != (0 as u8) { mk[ko] = host[a]; ko = ko + 1; a = a + 1 } 1256 mk[ko] = 32 as u8; ko = ko + 1 1257 a = 0 1258 while prefix[a] != (0 as u8) { mk[ko] = prefix[a]; ko = ko + 1; a = a + 1 } 1259 mk[ko] = 32 as u8; ko = ko + 1 1260 mk[ko] = 0 as u8 1261 let mkl: i64 = ko 1262 let out: *u8 = sys_mmap(262144) 1263 var o: i64 = 0 1264 if (old as i64) != 0 { 1265 var ls: i64 = 0 1266 while ls < oldn { 1267 var le: i64 = ls 1268 var sc: i64 = 1 1269 while sc == 1 { if le >= oldn { sc = 0 } else { if (old[le] as i64) == 10 { sc = 0 } else { le = le + 1 } } } 1270 // does this line start with the match key? 1271 var m: i64 = 1 1272 if ls + mkl > le + 1 { m = 0 } 1273 if m == 1 { 1274 var j: i64 = 0 1275 while j < mkl { if (old[ls + j] as i64) != (mk[j] as i64) { m = 0; j = mkl } else { j = j + 1 } } 1276 } 1277 if m == 0 { 1278 var k: i64 = ls 1279 while k <= le { if k < oldn { out[o] = old[k]; o = o + 1 } k = k + 1 } 1280 } 1281 ls = le + 1 1282 } 1283 } 1284 // append the new row (ensure a trailing newline precedes if the file didn't end in one) 1285 if o > 0 { if (out[o - 1] as i64) != 10 { out[o] = 10 as u8; o = o + 1 } } 1286 a = 0 1287 while host[a] != (0 as u8) { out[o] = host[a]; o = o + 1; a = a + 1 } 1288 out[o] = 32 as u8; o = o + 1 1289 a = 0 1290 while prefix[a] != (0 as u8) { out[o] = prefix[a]; o = o + 1; a = a + 1 } 1291 out[o] = 32 as u8; o = o + 1 1292 o = md_catn(out, o, port) 1293 out[o] = 32 as u8; o = o + 1 1294 a = 0 1295 while mode[a] != (0 as u8) { out[o] = mode[a]; o = o + 1; a = a + 1 } 1296 out[o] = 10 as u8; o = o + 1 1297 // NEVER-BRICK: bank the pre-edit table as confp+".prev" FIRST (recovery: cp .prev back), then 1298 // atomic write: tmp = confp + ".tmp", rename over confp. Same idiom as binary deploys. 1299 let prevp: *u8 = sys_mmap(512) 1300 var pj: i64 = 0 1301 while confp[pj] != (0 as u8) { prevp[pj] = confp[pj]; pj = pj + 1 } 1302 prevp[pj] = 46 as u8; prevp[pj + 1] = 112 as u8; prevp[pj + 2] = 114 as u8; prevp[pj + 3] = 101 as u8; prevp[pj + 4] = 118 as u8; prevp[pj + 5] = 0 as u8 1303 let pfd: i64 = sys_openat_wr(prevp, 0x1a4) 1304 if pfd >= 0 { sys_write(pfd, old, oldn); sys_close(pfd) } 1305 let tmpp: *u8 = sys_mmap(512) 1306 var tj: i64 = 0 1307 while confp[tj] != (0 as u8) { tmpp[tj] = confp[tj]; tj = tj + 1 } 1308 tmpp[tj] = 46 as u8; tmpp[tj + 1] = 116 as u8; tmpp[tj + 2] = 109 as u8; tmpp[tj + 3] = 112 as u8; tmpp[tj + 4] = 0 as u8 1309 let fd: i64 = sys_openat_wr(tmpp, 0x1a4) 1310 if fd < 0 { return 0 } 1311 sys_write(fd, out, o) 1312 sys_close(fd) 1313 sys_renameat(tmpp, confp) 1314 return 1 1315} 1316// kill every process whose /proc/<pid>/cmdline CONTAINS needle (full-cmdline match -- the 15-char comm 1317// truncation trap). Returns processes signalled. Mirrors the proven hostctl proc_kill_by_name. 1318func md_pk_contains(hay: *u8, hn: i64, needle: *u8, nl: i64) -> i64 { 1319 if nl == 0 { return 0 } 1320 var i: i64 = 0 1321 while i + nl <= hn { 1322 var j: i64 = 0 1323 var ok: i64 = 1 1324 while j < nl { if (hay[i + j] as i64) != (needle[j] as i64) { ok = 0; j = nl } else { j = j + 1 } } 1325 if ok == 1 { return 1 } 1326 i = i + 1 1327 } 1328 return 0 1329} 1330func md_pk_atoi(s: *u8) -> i64 { 1331 var v: i64 = 0 1332 var i: i64 = 0 1333 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } 1334 return v 1335} 1336// promote a staged <cwd>/<name>.new -> live <name> (.prev kept), chmod +x. Returns 1 if a .new existed 1337// and was promoted, else 0 (restart still valid -- just reloads the same on-disk binary). cwd = nishihost. 1338// IDEMPOTENCY for /api/tools/register: is <nm> already the first TAB-field of a line in tool_allowlist.conf? 1339// Reads the (small) allowlist raw; matches a line that starts with "<nm>\t". Fail-open to 0 (absent) so a 1340// missing/unreadable allowlist doesn't block a first registration. 1341func md_allow_has_name(nm: *u8) -> i64 { 1342 let fd: i64 = sys_openat_rd("tool_allowlist.conf" as *u8) 1343 if fd < 0 { return 0 } 1344 let cap: i64 = 1 << 18 1345 let buf: *u8 = sys_mmap(cap) 1346 let n: i64 = sys_read(fd, buf, cap - 1) 1347 sys_close(fd) 1348 if n <= 0 { return 0 } 1349 buf[n] = 0 as u8 1350 let nl: i64 = md_len(nm) 1351 var i: i64 = 0 1352 while i + nl < n { 1353 var ls: i64 = 0 1354 if i == 0 { ls = 1 } else { if buf[i-1] == (10 as u8) { ls = 1 } } 1355 if ls == 1 { 1356 var m: i64 = 1 1357 var j: i64 = 0 1358 while j < nl { if buf[i+j] != nm[j] { m = 0; j = nl } else { j = j + 1 } } 1359 if m == 1 { if buf[i+nl] == (9 as u8) { return 1 } } 1360 } 1361 i = i + 1 1362 } 1363 return 0 1364} 1365 1366// seq1281 (RESTORED AGAIN 2026-07-30 -- 3rd backdate, see seq1439/nx_srcguard): read the existing pinned- 1367// args column (4th TAB field .. EOL) of tool <nm>'s allowlist row into dst. Returns copied length; 0 = no 1368// row / no args / unreadable. Lets register-update PRESERVE pinned args when args= is omitted -- an omitted 1369// field must never silently widen a pinned oracle into caller-controlled argv. 1370func md_allow_get_args(nm: *u8, dst: *u8, cap: i64) -> i64 { 1371 let fd: i64 = sys_openat_rd("tool_allowlist.conf" as *u8) 1372 if fd < 0 { return 0 } 1373 let bcap: i64 = 1 << 18 1374 let buf: *u8 = sys_mmap(bcap) 1375 let n: i64 = sys_read(fd, buf, bcap - 1) 1376 sys_close(fd) 1377 if n <= 0 { return 0 } 1378 buf[n] = 0 as u8 1379 let nl: i64 = md_len(nm) 1380 var i: i64 = 0 1381 while i + nl < n { 1382 var ls: i64 = 0 1383 if i == 0 { ls = 1 } else { if buf[i-1] == (10 as u8) { ls = 1 } } 1384 if ls == 1 { 1385 var m: i64 = 1 1386 var j: i64 = 0 1387 while j < nl { if buf[i+j] != nm[j] { m = 0; j = nl } else { j = j + 1 } } 1388 if m == 1 { if buf[i+nl] == (9 as u8) { 1389 var p: i64 = i + nl + 1 1390 var tabs: i64 = 0 1391 var argst: i64 = 0 1392 while p < n { 1393 let c: i64 = buf[p] as i64 1394 if c == 10 { p = n } else { 1395 if c == 9 { tabs = tabs + 1; if tabs == 2 { argst = p + 1; p = n } } 1396 if p < n { p = p + 1 } 1397 } 1398 } 1399 if argst == 0 { return 0 } 1400 var o: i64 = 0 1401 var q: i64 = argst 1402 while q < n { 1403 if buf[q] == (10 as u8) { q = n } else { 1404 if o < cap - 1 { dst[o] = buf[q]; o = o + 1 } 1405 q = q + 1 1406 } 1407 } 1408 dst[o] = 0 as u8 1409 return o 1410 } } 1411 } 1412 i = i + 1 1413 } 1414 return 0 1415} 1416 1417// atomically REPLACE the tool_allowlist.conf row for tool <nm> -- the register-update verb's mutation 1418// (eats the ssh-once row-repoint class: evidence_checkin/mvault/clock repoints). Preserves every OTHER 1419// line byte-exact, drops the existing "<nm>\t..." row(s), appends the replacement 1420// "<nm>\t<elfp>\tGREEN[\t<args>]" row; banks .prev FIRST then tmp+rename (md_route_append idiom). 1421// UPDATE CAN NEVER CREATE: refuses (0) when no row matches. NEVER-BRICK: refuses when the allowlist is 1422// unreadable/empty so a transient read failure cannot truncate the live tool table. 1 ok / 0 refused. 1423func md_allow_update_row(nm: *u8, elfp: *u8, argp: *u8, argn: i64) -> i64 { 1424 let szp: *i64 = sys_mmap(16) as *i64 1425 szp[0] = 0 1426 let old: *u8 = md_read_file("tool_allowlist.conf" as *u8, szp) 1427 let oldn: i64 = szp[0] 1428 if (old as i64) == 0 { return 0 } 1429 if oldn == 0 { return 0 } 1430 let nl: i64 = md_len(nm) 1431 let out: *u8 = sys_mmap(262144) 1432 var o: i64 = 0 1433 var found: i64 = 0 1434 var ls: i64 = 0 1435 while ls < oldn { 1436 var le: i64 = ls 1437 var sc: i64 = 1 1438 while sc == 1 { if le >= oldn { sc = 0 } else { if (old[le] as i64) == 10 { sc = 0 } else { le = le + 1 } } } 1439 var m: i64 = 0 1440 if ls + nl < le { 1441 if (old[ls + nl] as i64) == 9 { 1442 m = 1 1443 var j: i64 = 0 1444 while j < nl { if old[ls + j] != nm[j] { m = 0; j = nl } else { j = j + 1 } } 1445 } 1446 } 1447 if m == 1 { found = 1 } else { 1448 var k: i64 = ls 1449 while k <= le { if k < oldn { out[o] = old[k]; o = o + 1 } k = k + 1 } 1450 } 1451 ls = le + 1 1452 } 1453 if found == 0 { return 0 } 1454 if o > 0 { if (out[o - 1] as i64) != 10 { out[o] = 10 as u8; o = o + 1 } } 1455 var a: i64 = 0 1456 while nm[a] != (0 as u8) { out[o] = nm[a]; o = o + 1; a = a + 1 } 1457 out[o] = 9 as u8; o = o + 1 1458 a = 0 1459 while elfp[a] != (0 as u8) { out[o] = elfp[a]; o = o + 1; a = a + 1 } 1460 out[o] = 9 as u8; o = o + 1 1461 out[o] = 71 as u8; o = o + 1 1462 out[o] = 82 as u8; o = o + 1 1463 out[o] = 69 as u8; o = o + 1 1464 out[o] = 69 as u8; o = o + 1 1465 out[o] = 78 as u8; o = o + 1 1466 if argn > 0 { 1467 out[o] = 9 as u8; o = o + 1 1468 a = 0 1469 while a < argn { out[o] = argp[a]; o = o + 1; a = a + 1 } 1470 } 1471 out[o] = 10 as u8; o = o + 1 1472 let pfd: i64 = sys_openat_wr("tool_allowlist.conf.prev" as *u8, 0x1a4) 1473 if pfd >= 0 { sys_write(pfd, old, oldn); sys_close(pfd) } 1474 let fd: i64 = sys_openat_wr("tool_allowlist.conf.nxtmp" as *u8, 0x1a4) 1475 if fd < 0 { return 0 } 1476 sys_write(fd, out, o) 1477 sys_close(fd) 1478 sys_renameat("tool_allowlist.conf.nxtmp" as *u8, "tool_allowlist.conf" as *u8) 1479 return 1 1480} 1481 1482func md_streq(a: *u8, b: *u8) -> i64 { 1483 var i: i64 = 0 1484 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 1485 if b[i] != (0 as u8) { return 0 } 1486 return 1 1487} 1488 1489// FAIL-CLOSED promote policy for POST /api/promote -- STRUCTURAL, still by-construction (F-210b eaten 07-18): 1490// (1) compiled-in DENY first: daemons + credential oracles (family substrings, so new members inherit the 1491// refusal). Daemons carry live connections -> the health-checked auto-rollback /api/deploy, NEVER a rename. 1492// The deny can never be overridden by any data plane or later rule. 1493// (2) the enumerated one-shot allows (back-compat fast path). 1494// (3) else STAGED-ARTIFACT rule: a name whose <name>.sov.elf.new exists as a real ELF is promotable. Staging is 1495// only reachable via the owner-gated /api/build//api/upload, so this widens promote to OWNER-only actions, 1496// never to a write cap -- the same stance as nx_fs_write denying the tool allowlist, without the 1497// add-a-name-recompile-mgmt treadmill for every new one-shot organ. 1498func md_contains(a: *u8, sub: *u8) -> i64 { 1499 var i: i64 = 0 1500 while a[i] != (0 as u8) { 1501 var j: i64 = 0 1502 var mism: i64 = 0 1503 var run: i64 = 1 1504 while run == 1 { 1505 if sub[j] == (0 as u8) { run = 0 } else { 1506 if a[i+j] == (0 as u8) { mism = 1; run = 0 } else { 1507 if a[i+j] != sub[j] { mism = 1; run = 0 } else { j = j + 1 } 1508 } 1509 } 1510 } 1511 if mism == 0 { return 1 } 1512 i = i + 1 1513 } 1514 return 0 1515} 1516// ---------- organ ROLE, declared (seq1492) ---------- 1517// 1518// u2605THE DEFECT THIS REPLACES: promote classified by NAME SUBSTRING, so a 1519// one-shot worker called nx_torrent_get was refused as a "daemon" while 1520// /api/deploy refused it as an unknown target. **Two verbs disagreeing about an 1521// artefact's KIND leave it unshippable** -- and the only remaining way to update 1522// it was the raw scp/ssh path that seq1439 identified as the WORK-DESTROYER. A 1523// substring is not a role, exactly as a substring is not a hazard. 1524// 1525// Policy lives in DATA (rule 11): knowledge/status/organ_kind.conf, rows 1526// <name><TAB-or-SPACE>one-shot|oneshot|daemon|oracle|lib 1527// Returns 1 = one-shot (promotable), 2 = daemon/oracle (deploy lane), 0 = undeclared. 1528// Undeclared falls through to the historical name heuristics, so nothing regresses 1529// and the heuristic becomes the DEFAULT rather than the law. 1530// 1531// ★PERMANENT ROOT FIX 2026-07-31 (debt 1785453784, which /api/promote's own 400 text described but 1532// nobody had closed). TWO defects, and fixing only the first would have LOOKED right while still failing: 1533// 1. WRONG PATH. This opened the SINGULAR-less plural "knowledge/organ_kinds.conf", which was renamed to 1534// .RETIRED-seq1754-use-status-organ_kind. open() returned <0 -> return 0 -> EVERY organ fell through 1535// to the name heuristic, so NO declaration anywhere was readable and gate promotion was a coin flip. 1536// Now reads the surviving SSOT knowledge/status/organ_kind.conf. ONE source of truth, not two. 1537// 2. WRONG SEPARATOR. The surviving file is SPACE-separated (`nx_build_admit oneshot`) but this parser 1538// accepted ONLY a TAB (9), so a path-only fix would have found the file, parsed nothing, and still 1539// returned 0 -- a silent no-op that reads as success. Now takes the FIRST tab OR space. 1540// The value test below already tolerates both spellings: it checks only the leading "on", matching 1541// `oneshot` and `one-shot` alike. Verified against the real file before editing, not assumed. 1542func md_organ_kind(nm: *u8) -> i64 { 1543 let fd: i64 = sys_openat_rd("knowledge/status/organ_kind.conf" as *u8) 1544 if fd < 0 { return 0 } 1545 let buf: *u8 = sys_mmap(65536) 1546 let n: i64 = sys_read(fd, buf, 65536) 1547 sys_close(fd) 1548 if n <= 0 { return 0 } 1549 var nl: i64 = 0 1550 while nm[nl] != (0 as u8) { nl = nl + 1 } 1551 var ls: i64 = 0 1552 while ls < n { 1553 var le: i64 = ls 1554 var g: i64 = 1 1555 while g == 1 { if le >= n { g = 0 } else { if buf[le] == (10 as u8) { g = 0 } else { le = le + 1 } } } 1556 if le > ls { if buf[ls] != (35 as u8) { 1557 var sep: i64 = 0 - 1 1558 var q: i64 = ls 1559 while q < le { 1560 if sep < 0 { 1561 if buf[q] == (9 as u8) { sep = q } 1562 else { if buf[q] == (32 as u8) { sep = q } } 1563 } 1564 q = q + 1 1565 } 1566 if sep > ls { 1567 if sep - ls == nl { 1568 var same: i64 = 1 1569 var c: i64 = 0 1570 while c < nl { if buf[ls + c] != nm[c] { same = 0; c = nl } else { c = c + 1 } } 1571 if same == 1 { 1572 let vs: i64 = sep + 1 1573 if vs < le { if buf[vs] == (111 as u8) { if vs + 1 < le { if buf[vs + 1] == (110 as u8) { return 1 } } } } 1574 return 2 1575 } 1576 } 1577 } 1578 } } 1579 ls = le + 1 1580 } 1581 return 0 1582} 1583 1584// Does `w` occur in `nm` starting at a TOKEN BOUNDARY -- the start of the name, 1585// or immediately after '_'? Organ names are underscore-tokenised, so this asks 1586// "is one of the words in this name `w`" instead of "do these letters appear 1587// anywhere". 1588func mdh_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8) { n=n+1 } return n } 1589 1590func mdh_tail_eq(nm: *u8, n: i64, suf: *u8) -> i64 { 1591 let sl: i64 = mdh_len(suf) 1592 if sl > n { return 0 } 1593 var i: i64 = 0 1594 while i < sl { if nm[n - sl + i] != suf[i] { return 0 } i = i + 1 } 1595 return 1 1596} 1597 1598func md_tok_at(nm: *u8, w: *u8) -> i64 { 1599 let wl: i64 = mdh_len(w) 1600 let nl: i64 = mdh_len(nm) 1601 var i: i64 = 0 1602 while i + wl <= nl { 1603 var boundary: i64 = 0 1604 if i == 0 { boundary = 1 } else { if nm[i - 1] == (95 as u8) { boundary = 1 } } 1605 if boundary == 1 { 1606 var j: i64 = 0 1607 var m: i64 = 1 1608 while j < wl { if nm[i + j] != w[j] { m = 0; j = wl } else { j = j + 1 } } 1609 if m == 1 { return 1 } 1610 } 1611 i = i + 1 1612 } 1613 return 0 1614} 1615 1616// Does the name end in one of the verifier suffixes the system already treats as 1617// a declaration (_gate/_test/_kat -- the /api/gate_run bound)? 1618func md_name_is_oracle(nm: *u8) -> i64 { 1619 let n: i64 = mdh_len(nm) 1620 if mdh_tail_eq(nm, n, "_gate" as *u8) == 1 { return 1 } 1621 if mdh_tail_eq(nm, n, "_test" as *u8) == 1 { return 1 } 1622 if mdh_tail_eq(nm, n, "_kat" as *u8) == 1 { return 1 } 1623 return 0 1624} 1625 1626// NON-OVERRIDABLE deny: credential oracles and key material. A conf row must 1627// never be able to make these promotable -- otherwise the role registry becomes 1628// a privilege-escalation surface (config that grants authority). 1629// 1630// ★★★★★ ROOT-FIXED 2026-07-31 (debt 1785511766). This used to ask 1631// md_contains -- a RAW SUBSTRING -- which is the exact defect the sibling 1632// md_promote_deny was already fixed for at seq1789 ("THE SUFFIX IS A 1633// DECLARATION; THE SUBSTRING WAS A GUESS", the nx_survey_serve_gate collision), 1634// left unfixed one layer down here in the NON-OVERRIDABLE deny. 1635// 1636// "vault" means CREDENTIAL CUSTODY here, but as a substring it also matches the 1637// entire MEDIA vault family -- nx_mvault, nx_mvault_coll, nx_mvault_walk. One 1638// substring, two unrelated meanings. The result was that gate-proven media-vault 1639// binaries were unshippable by any sanctioned route, and the 2026-07-23 session 1640// resorted to an ssh cp/mv rename to ship them. 1641// ★★★★★**A GUARD THAT CANNOT BE SATISFIED PRODUCES A BYPASS, NOT SAFETY.** 1642// 1643// TWO CHANGES, BOTH STRICTLY SAFE -- this narrows FALSE positives only, and every 1644// real credential organ below still denies (proven by nx_promote_deny_gate): 1645// 1. TOKEN-BOUNDARY, not substring. "vault" still matches nx_vault_gateway (the 1646// word is a token) but no longer matches nx_mvault (the letters are merely 1647// inside one). A glued credential name like nx_secretstore STILL denies, 1648// because the boundary is checked at the START of the token only. 1649// 2. An ORACLE SUFFIX is exempt. Promoting nx_cap_mint_gate installs 1650// nx_cap_mint_gate.elf -- it CANNOT swap nx_cap_mint.elf -- so a verifier can 1651// never be the credential organ it verifies. Safe by construction, and it 1652// reuses the system's own _gate/_test/_kat rule rather than inventing one. 1653func md_promote_deny_hard(nm: *u8) -> i64 { 1654 if md_name_is_oracle(nm) == 1 { return 0 } 1655 if md_tok_at(nm, "mint" as *u8) == 1 { return 1 } 1656 if md_tok_at(nm, "vault" as *u8) == 1 { return 1 } 1657 if md_tok_at(nm, "secret" as *u8) == 1 { return 1 } 1658 if md_tok_at(nm, "keygen" as *u8) == 1 { return 1 } 1659 if md_tok_at(nm, "login" as *u8) == 1 { return 1 } 1660 return 0 1661} 1662 1663func md_promote_deny(nm: *u8) -> i64 { 1664 // u2605CONVERGED 2026-07-30 (seq1754). This used to consult its OWN role conf 1665 // (knowledge/organ_kinds.conf) -- a SECOND classifier for a concept a 1666 // sibling had already implemented properly as nx_organkind 1667 // (ok_kind_of_path over knowledge/status/organ_kind.conf), wired into the 1668 // promote handler ABOVE this function. Two confs and two readers for one 1669 // concept is the sprawl we keep warning about, and I built half of it by 1670 // not checking /code/tools before starting. 1671 // 1672 // The canonical reader now decides FIRST at the API layer; this function is 1673 // only reached for an UNDECLARED name, where it is the legacy name 1674 // heuristic -- so the duplicate lookup is removed and its rows were merged 1675 // into the canonical conf. deny_hard STAYS: credential oracles must be 1676 // refused non-overridably regardless of any declared kind. 1677 if md_promote_deny_hard(nm) == 1 { return 1 } 1678 if md_contains(nm, "serve" as *u8) == 1 { return 1 } 1679 if md_contains(nm, "daemon" as *u8) == 1 { return 1 } 1680 if md_contains(nm, "mint" as *u8) == 1 { return 1 } 1681 if md_contains(nm, "vault" as *u8) == 1 { return 1 } 1682 if md_contains(nm, "secret" as *u8) == 1 { return 1 } 1683 if md_contains(nm, "keygen" as *u8) == 1 { return 1 } 1684 if md_contains(nm, "login" as *u8) == 1 { return 1 } 1685 if md_contains(nm, "router" as *u8) == 1 { return 1 } 1686 if md_contains(nm, "hostctl" as *u8) == 1 { return 1 } 1687 if md_contains(nm, "signaling" as *u8) == 1 { return 1 } 1688 if md_contains(nm, "gateway" as *u8) == 1 { return 1 } 1689 if md_contains(nm, "torrent" as *u8) == 1 { return 1 } 1690 if md_contains(nm, "mgmt" as *u8) == 1 { return 1 } 1691 if md_contains(nm, "_gw" as *u8) == 1 { return 1 } 1692 if md_streq(nm, "sites" as *u8) == 1 { return 1 } 1693 return 0 1694} 1695func md_staged_elf_ok(nm: *u8) -> i64 { 1696 let p: *u8 = sys_mmap(192) 1697 var o: i64 = 0 1698 var i: i64 = 0 1699 while nm[i] != (0 as u8) { p[o] = nm[i]; o = o + 1; i = i + 1 } 1700 let sfx: *u8 = ".sov.elf.new" as *u8 1701 var j: i64 = 0 1702 while sfx[j] != (0 as u8) { p[o] = sfx[j]; o = o + 1; j = j + 1 } 1703 p[o] = 0 as u8 1704 let fd: i64 = sys_openat_rd(p) 1705 if fd < 0 { return 0 } 1706 let hb: *u8 = sys_mmap(8) 1707 let r: i64 = sys_read(fd, hb, 4) 1708 sys_close(fd) 1709 if r != 4 { return 0 } 1710 if hb[0] != (127 as u8) { return 0 } 1711 if hb[1] != (69 as u8) { return 0 } 1712 if hb[2] != (76 as u8) { return 0 } 1713 if hb[3] != (70 as u8) { return 0 } 1714 return 1 1715} 1716func md_promote_organ_ok(nm: *u8) -> i64 { 1717 if md_promote_deny(nm) == 1 { return 0 } 1718 if md_streq(nm, "nx_ecosystem_maturity_rollup" as *u8) == 1 { return 1 } 1719 if md_streq(nm, "nx_ecomat_seed" as *u8) == 1 { return 1 } 1720 if md_streq(nm, "nx_ecomat_beat" as *u8) == 1 { return 1 } 1721 if md_streq(nm, "nx_ecomat_page" as *u8) == 1 { return 1 } 1722 if md_streq(nm, "nx_tool_argecho" as *u8) == 1 { return 1 } 1723 // 07-17 (eat the ssh-once deploy debt): the fork-exec MCP TOOL organ family -- one-shot elfs the 1724 // tools daemon spawns per call. NOT daemons (those stay refused -> /api/deploy) and NOT the 1725 // credential oracles (nx_session_mint / nx_cap_mint stay OFF this list deliberately). 1726 if md_streq(nm, "nx_shelltool" as *u8) == 1 { return 1 } 1727 if md_streq(nm, "nx_common_tasks" as *u8) == 1 { return 1 } 1728 if md_streq(nm, "nx_frontier_board" as *u8) == 1 { return 1 } 1729 if md_streq(nm, "nx_page_verify" as *u8) == 1 { return 1 } 1730 if md_streq(nm, "nx_store_seed" as *u8) == 1 { return 1 } 1731 if md_streq(nm, "nx_workflow" as *u8) == 1 { return 1 } 1732 if md_streq(nm, "nx_memory" as *u8) == 1 { return 1 } 1733 if md_streq(nm, "nx_heal" as *u8) == 1 { return 1 } 1734 if md_streq(nm, "nx_fs" as *u8) == 1 { return 1 } 1735 if md_streq(nm, "nx_fs_write" as *u8) == 1 { return 1 } 1736 if md_streq(nm, "nx_site_publish" as *u8) == 1 { return 1 } 1737 if md_streq(nm, "nx_https_get" as *u8) == 1 { return 1 } 1738 if md_streq(nm, "nx_verify" as *u8) == 1 { return 1 } 1739 // 07-17 (stem-first-byte-fab lane): the compare-publish pipeline organs -> API-promotable, so a 1740 // brand-new compare domain publishes end-to-end over MCP (build -> promote -> regen), zero shell. 1741 if md_streq(nm, "nx_compare_regen" as *u8) == 1 { return 1 } 1742 if md_streq(nm, "nx_swcompare_matrix" as *u8) == 1 { return 1 } 1743 if md_streq(nm, "nx_swcompare_sota" as *u8) == 1 { return 1 } 1744 if md_streq(nm, "nx_swcompare_hub" as *u8) == 1 { return 1 } 1745 if md_streq(nm, "nx_maturity_board" as *u8) == 1 { return 1 } 1746 // (3) staged-artifact rule: owner-staged one-shot builds are promotable (deny above already refused 1747 // every daemon/oracle shape, so this can only ever admit tool-organ names). 1748 if md_staged_elf_ok(nm) == 1 { return 1 } 1749 return 0 1750} 1751 1752// ---- seq1484: PROMOTION PROVENANCE -- a promote may not walk a binary BACKWARDS ------------------ 1753// THE BLEED THIS STOPS (measured 2026-07-30): the mgmt API was reverted THREE times and the compiler 1754// TWICE in a single session, each time by promoting a binary built elsewhere from a stale tree. Every 1755// existing control passed it: the ELF is valid, the size is plausible, and promote_toolchain's canary 1756// COMPILES AND RUNS it GREEN -- because a stale-but-working binary does all of that perfectly. 1757// ★LIVENESS IS NOT CURRENCY. "It works" cannot distinguish the newest build from last week's. 1758// 1759// The invariant that CAN is the same one already protecting sources (nx_symdrop) and tree pushes 1760// (nx_treepack REFUSED-WOULD-DROP-SYMBOLS): a normal promotion installs content this target has NEVER 1761// held; a revert installs content it ALREADY HELD. So keep an append-only per-target content-hash 1762// history. Staged == newest -> a harmless re-promote, allowed. Staged never seen -> a real advance, 1763// allowed and recorded. Staged matches an EARLIER generation -> THE BINARY WOULD GO BACKWARDS, refused 1764// and named. No build-time provenance, no clock, no size heuristic, nothing to spoof by touching a file. 1765// 1766// ESCAPE HATCH BY DESIGN, NOT BY FLAG: going backwards deliberately is what /api/rollback is FOR, and it 1767// does not route through here. A `force` parameter would just be the hole re-opened under a nicer name. 1768const MD_PROV_HIST: *u8 = "knowledge/promote_history.tsv" as *u8 1769const MD_PROV_CAP: i64 = 1048576 1770const MD_PROV_RDCH: i64 = 262144 1771const MD_PROV_FNV_OFF: i64 = 1469598103934665603 1772const MD_PROV_FNV_PRM: i64 = 1099511628211 1773const MD_PROV_TAB: i64 = 9 1774const MD_PROV_NL: i64 = 10 1775 1776// FNV-1a over a whole file, streamed so a large ELF needs no full-size buffer. 0 = unreadable. 1777func md_prov_hash(path: *u8) -> i64 { 1778 let fd: i64 = sys_openat_rd(path) 1779 if fd < 0 { return 0 } 1780 let b: *u8 = sys_mmap(MD_PROV_RDCH) 1781 var h: i64 = MD_PROV_FNV_OFF 1782 var go: i64 = 1 1783 while go == 1 { 1784 let n: i64 = sys_read(fd, b, MD_PROV_RDCH) 1785 if n <= 0 { go = 0 } else { 1786 var i: i64 = 0 1787 while i < n { h = h ^ (b[i] as i64); h = h * MD_PROV_FNV_PRM; i = i + 1 } 1788 } 1789 } 1790 sys_close(fd) 1791 return h 1792} 1793// Walk the history for `name`. out3[0]=generations seen, out3[1]=1 if h is the NEWEST, out3[2]=index of 1794// an EARLIER generation equal to h (-1 if none). 1795func md_prov_probe(name: *u8, h: i64, out3: *i64) -> i64 { 1796 out3[0] = 0; out3[1] = 0; out3[2] = 0 - 1 1797 let buf: *u8 = sys_mmap(MD_PROV_CAP) 1798 let n: i64 = dp_read(MD_PROV_HIST, buf, MD_PROV_CAP - 1) 1799 if n <= 0 { return 0 } 1800 var nl: i64 = 0 1801 while name[nl] != (0 as u8) { nl = nl + 1 } 1802 var gen: i64 = 0 1803 var ls: i64 = 0 1804 var i: i64 = 0 1805 while i <= n { 1806 var eol: i64 = 0 1807 if i == n { eol = 1 } else { if buf[i] == (MD_PROV_NL as u8) { eol = 1 } } 1808 if eol == 1 { 1809 if i > ls { 1810 var tab: i64 = 0 - 1 1811 var t: i64 = ls 1812 while t < i { if buf[t] == (MD_PROV_TAB as u8) { tab = t; t = i } else { t = t + 1 } } 1813 if tab > 0 { if tab - ls == nl { 1814 var m: i64 = 1 1815 var c: i64 = 0 1816 while c < nl { if buf[ls+c] != name[c] { m = 0; c = nl } else { c = c + 1 } } 1817 if m == 1 { 1818 var v: i64 = 0 1819 var neg: i64 = 0 1820 var k: i64 = tab + 1 1821 if k < i { if buf[k] == (45 as u8) { neg = 1; k = k + 1 } } 1822 while k < i { v = v * 10 + ((buf[k] as i64) - 48); k = k + 1 } 1823 if neg == 1 { v = 0 - v } 1824 if v == h { out3[2] = gen; out3[1] = 1 } else { out3[1] = 0 } 1825 gen = gen + 1 1826 } 1827 } } 1828 } 1829 ls = i + 1 1830 } 1831 i = i + 1 1832 } 1833 out3[0] = gen 1834 return gen 1835} 1836func md_prov_record(name: *u8, h: i64) -> i64 { 1837 let line: *u8 = sys_mmap(512) 1838 var o: i64 = 0 1839 var i: i64 = 0 1840 while name[i] != (0 as u8) { line[o] = name[i]; o = o + 1; i = i + 1 } 1841 line[o] = MD_PROV_TAB as u8; o = o + 1 1842 var m: i64 = h 1843 if m < 0 { line[o] = 45 as u8; o = o + 1; m = 0 - m } 1844 let t: *u8 = sys_mmap(32) 1845 var k: i64 = 0 1846 if m == 0 { t[0] = 48 as u8; k = 1 } 1847 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 1848 var z: i64 = k - 1 1849 while z >= 0 { line[o] = t[z]; o = o + 1; z = z - 1 } 1850 line[o] = MD_PROV_NL as u8; o = o + 1 1851 let fd: i64 = sys_openat_append(MD_PROV_HIST, 0x1a4) 1852 if fd < 0 { return 0 - 1 } 1853 sys_write(fd, line, o) 1854 sys_fsync(fd) 1855 sys_close(fd) 1856 return 0 1857} 1858// 1 = this staged content may be promoted; 0 = it would walk `name` BACKWARDS. 1859func md_prov_ok(name: *u8, stagedpath: *u8) -> i64 { 1860 let h: i64 = md_prov_hash(stagedpath) 1861 if h == 0 { return 1 } // unreadable: leave the decision to the existing checks 1862 let p: *i64 = sys_mmap(64) as *i64 1863 md_prov_probe(name, h, p) 1864 if p[0] == 0 { md_prov_record(name, h); return 1 } // first sighting = the baseline 1865 if p[1] == 1 { return 1 } // identical to the newest = harmless re-promote 1866 if p[2] >= 0 { return 0 } // seen EARLIER but not newest = A REVERT 1867 md_prov_record(name, h) 1868 return 1 1869} 1870 1871// STAGING HYGIENE (2026-08-06, debt 1785531571). md_prov_ok above refuses only content this target 1872// ALREADY HELD, so a staged artifact that was never itself promoted is a FIRST SIGHTING: md_prov_ok 1873// records it as the baseline and returns ALLOW -- even when it PREDATES the live binary it replaces. 1874// MEASURED on this deploy root the same day (nx_staghyg scan): 584 staged artifacts, 88 with a live 1875// counterpart, 31 of those OLDER than the binary they would replace, and 7 that would DROP live 1876// capability tokens DESPITE BEING LARGER -- so neither mtime nor size alone catches the class. 1877// allow_loss=1 is the deliberate operator override (/api/promote allow_capability_loss=yes); 0 is 1878// fail-closed. Placed BEFORE any rename, so a refusal leaves live, .prev AND the staged file untouched. 1879// POST-PROMOTE MIRROR REFRESH (2026-08-14, debt 1786758132). Forks nx_offc_install <bare> refresh. 1880// ★SAFE TO CALL AUTOMATICALLY BECAUSE THE `refresh` VERB CREATES NOTHING: it installs the promoted 1881// binary only where an _offc mirror ALREADY EXISTS (proven both directions -- nx_resmon REFRESHED, 1882// nx_memvel_gate SKIP). An unconditional install here would quietly WIDEN what runners can fork. 1883func md_exec_offc_refresh(bare: *u8) -> i64 { 1884 let oelf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_offc_install.elf" as *u8 1885 let args: *i64 = sys_mmap(16) as *i64 1886 args[0] = bare as i64 1887 args[1] = "refresh" as *u8 as i64 1888 return dep_run(oelf, args, 2) 1889} 1890 1891func md_promote_staged_ex(name: *u8, allow_loss: i64) -> i64 { 1892 let live: *u8 = sys_mmap(160) 1893 let newp: *u8 = sys_mmap(160) 1894 let prevp: *u8 = sys_mmap(160) 1895 var lo: i64 = 0 1896 while name[lo] != (0 as u8) { live[lo] = name[lo]; newp[lo] = name[lo]; prevp[lo] = name[lo]; lo = lo + 1 } 1897 live[lo] = 0 as u8 1898 md_copy_slice_z(newp, name, 0, lo, 160) 1899 md_copy_slice_z(prevp, name, 0, lo, 160) 1900 var no: i64 = lo 1901 let ns: *u8 = ".new" as *u8 1902 var a: i64 = 0 1903 while ns[a] != (0 as u8) { newp[no] = ns[a]; no = no + 1; a = a + 1 } 1904 newp[no] = 0 as u8 1905 // does <name>.new exist? 1906 let nfd: i64 = sys_openat_rd(newp) 1907 if nfd < 0 { return 0 } 1908 sys_close(nfd) 1909 // seq1484 PROVENANCE: refuse a promotion that would walk this target BACKWARDS to content 1910 // it already held. Placed BEFORE any rename, so a refusal leaves live and .prev untouched. 1911 if md_prov_ok(live, newp) == 0 { return 0 - 2 } 1912 // Approval can cover a known measured class, never missing qualification. 1913 let sgcfg: *i64 = sys_mmap_try(__size_of(i64) * SG_C_SLOTS) as *i64 1914 if (sgcfg as i64)<=0{return -3} 1915 let sgout: *i64 = sys_mmap_try(__size_of(i64) * SG_OUT_SLOTS) as *i64 1916 if (sgout as i64)<=0{sys_munmap_direct(sgcfg as *u8,__size_of(i64)*SG_C_SLOTS);return -3} 1917 sg_conf_load(sgcfg) 1918 let sgcls: i64 = sg_classify(live,newp,sys_now_realtime_sec(),sgcfg,sgout) 1919 let sgallow:i64=sg_allows_override(sgcls,allow_loss) 1920 sys_munmap_direct(sgout as *u8,__size_of(i64)*SG_OUT_SLOTS) 1921 sys_munmap_direct(sgcfg as *u8,__size_of(i64)*SG_C_SLOTS) 1922 if sgallow==0{return -3} 1923 var po: i64 = lo 1924 let ps: *u8 = ".prev" as *u8 1925 a = 0 1926 while ps[a] != (0 as u8) { prevp[po] = ps[a]; po = po + 1; a = a + 1 } 1927 prevp[po] = 0 as u8 1928 md_rotate_prev(prevp) // rotate .prev -> .prev2 FIRST: a 2nd promote must not destroy the original 1929 sys_renameat(live, prevp) // keep the old live as .prev (rollback) 1930 sys_renameat(newp, live) // promote .new -> live 1931 nx_chmod(live, 0x1ed) 1932 // ---- MAINTAIN THE _offc MIRROR (2026-08-14, debt 1786758132) -------------------------------- 1933 // THE INVARIANT: _offc/<n>.elf must equal the promoted root binary. nx_job_run and organ-to-organ 1934 // forks resolve _offc/ while promote writes the ROOT, so WITHOUT THIS a promote reports success 1935 // while every caller keeps executing the OLD artifact, with no signal anywhere. 1936 // MEASURED THE DAY THIS LANDED: 20 stale _offc artifacts including the assembler, the build runner 1937 // and nx_mgmt_api itself -- and a stale copy of a CORRECT security scanner read as a VACUOUS 1938 // detector convincingly enough that a sev-8 was filed against the wrong thing. 1939 // ★HOOKED HERE, NOT IN ma_do_promote, BECAUSE THIS IS THE ONE RENAME: /api/restart and /api/deploy 1940 // reach this function WITHOUT passing through the promote handler, so hooking the handler would have 1941 // fixed one path of four. 1942 // ★THE RETURN VALUE IS DELIBERATELY IGNORED AND THE CALL IS LAST: the rename above has ALREADY 1943 // succeeded, so a mirror failure must never un-promote or fail a promotion that worked. Worst case 1944 // the mirror stays stale and the offccensus beat reports it -- which is exactly the pre-existing 1945 // state, so this can only improve on it. 1946 if lo > 4 { 1947 let bare: *u8 = sys_mmap(160) 1948 md_copy_slice_z(bare, name, 0, lo - 4, 160) 1949 md_exec_offc_refresh(bare) 1950 } 1951 return 1 1952} 1953 1954// BACK-COMPAT WRAPPER: every existing call site keeps its exact signature and gets the guard by default 1955// (fail-closed). Only a caller that DELIBERATELY passes the override reaches _ex with allow_loss=1 -- 1956// so wiring the guard cannot be forgotten at a call site, which is the built-but-not-wired class this 1957// ecosystem keeps rediscovering. Returns: 1 promoted, 0 nothing staged, -2 provenance revert, -3 staging hygiene. 1958func md_promote_staged(name: *u8) -> i64 { return md_promote_staged_ex(name, 0) } 1959// ---- TOOLCHAIN PROMOTE (never-brick) -- eats seq891/seq903 ------------------------------------- 1960// THE GAP THIS CLOSES: the ecosystem could build and deploy every SERVICE over its own API but could 1961// NOT update the COMPILER that builds them, so a PROVEN compiler fix could not be landed API-first 1962// (rule 27). Measured cost on 2026-07-30: nx_fnptr_slot_probe was GREEN on the laptop compiler and RED 1963// on the hub compiler, i.e. obj.fn_field(args) silently emitted no indirect call for every organ in the 1964// tree, and the fix existed but had nowhere to go. A toolchain you cannot update is a toolchain whose 1965// bugs are permanent. 1966// 1967// ⚠THE PATH NOT TAKEN (seq903, and it must stay not-taken): shipping the toolchain through 1968// /api/upload + /api/unpack looks tempting because it touches only this module. nx_treepack writes every 1969// output file 0644 NON-EXECUTABLE and UNLINKS-then-recreates on any open failure, so unpacking over 1970// buildroot/_offc/nx_cc_sovereign.elf would either install a non-executable compiler or destroy the live 1971// one -- EVERY BUILD FOR EVERY SEAT, from a call that looks like a routine source sync. 1972// 1973// ⚠MATCHED-PAIR RULE (seq1315): nx_sov_build_run writes _build/<name>.sov.elf while nx_hostctl 1974// cmd_buildrun reads /tmp/<name>.sov.elf, so those two may only ever be promoted TOGETHER. They are 1975// admitted here because that pair-ship is a legitimate wave -- and admitting them is SAFE precisely 1976// because the caller canary-compiles and auto-rolls-back, so a mismatched pair cannot survive a promote. 1977// 1978// DIALECT NOTE: plain-if (no `else`), no empty string literals, <=6 params. This module is IMPORTED by 1979// nx_mgmt_api, and it must be compiled by TODAY'S hub compiler -- the one that still carries seq533 1980// (imported `else` desyncs the parser), seq907 (an empty literal aliases the next literal) and seq239 1981// (>6 params mishandled). The fix ships in a binary that the defect itself has to be able to build. 1982const MD_TC_MIN_ELF: i64 = 4096 // size floor: refuse a truncated upload or an HTML error page 1983const MD_TC_MODE_EXEC: i64 = 0x1ed // 0755 -- a compiler that is not executable is a dead ecosystem 1984 1985func md_toolchain_target_ok(nm: *u8) -> i64 { 1986 if md_streq(nm, "nx_cc_sovereign.elf" as *u8) == 1 { return 1 } 1987 if md_streq(nm, "nxasm_x86_main.elf" as *u8) == 1 { return 1 } 1988 if md_streq(nm, "nx_sov_build_run.elf" as *u8) == 1 { return 1 } 1989 return 0 1990} 1991 1992func md_tc_live(nm: *u8, buf: *u8) -> i64 { 1993 var o: i64 = md_cmp_cat(buf, 0, "buildroot/_offc/" as *u8) 1994 o = md_cmp_cat(buf, o, nm) 1995 return o 1996} 1997func md_tc_prev(nm: *u8, buf: *u8) -> i64 { 1998 var o: i64 = md_tc_live(nm, buf) 1999 o = md_cmp_cat(buf, o, ".prev" as *u8) 2000 return o 2001} 2002func md_tc_staged(nm: *u8, buf: *u8) -> i64 { 2003 var o: i64 = md_cmp_cat(buf, 0, nm) 2004 o = md_cmp_cat(buf, o, ".new" as *u8) 2005 return o 2006} 2007 2008// ELF magic + size floor. Returns the byte size on success, 0 on refusal. Validating the ARTIFACT (not 2009// an exit code) is the seq363/hostctl lesson: a 0-byte or non-ELF file must never reach the live slot. 2010func md_tc_elf_size(p: *u8) -> i64 { 2011 let fd: i64 = sys_openat_rd(p) 2012 if fd < 0 { return 0 } 2013 let sz: i64 = sys_lseek(fd, 0, 2) 2014 if sz < MD_TC_MIN_ELF { sys_close(fd); return 0 } 2015 sys_lseek(fd, 0, 0) 2016 let hb: *u8 = sys_mmap(8) 2017 var ok: i64 = 0 2018 if sys_read(fd, hb, 4) == 4 { 2019 if hb[0] == (0x7f as u8) { 2020 if hb[1] == (69 as u8) { 2021 if hb[2] == (76 as u8) { 2022 if hb[3] == (70 as u8) { ok = 1 } 2023 } 2024 } 2025 } 2026 } 2027 sys_close(fd) 2028 if ok == 0 { return 0 } 2029 return sz 2030} 2031 2032// Install staged <nm>.new -> buildroot/_offc/<nm>, banking the outgoing binary as .prev FIRST. 2033// Returns the installed size, or 0 if nothing was touched. Ordering is deliberate: validate BEFORE 2034// renaming anything, so a refused upload leaves the live compiler completely untouched. 2035const MD_TC_ERR_BACKUP: i64 = 0 - 3 2036func md_tc_install(nm: *u8) -> i64 { 2037 let stagedp: *u8 = sys_mmap(256) 2038 let livep: *u8 = sys_mmap(256) 2039 let prevp: *u8 = sys_mmap(256) 2040 md_tc_staged(nm, stagedp) 2041 md_tc_live(nm, livep) 2042 md_tc_prev(nm, prevp) 2043 let sz: i64 = md_tc_elf_size(stagedp) 2044 if sz == 0 { return 0 } 2045 // seq1484: the canary proves the incoming toolchain WORKS, which a stale-but-working one 2046 // also does. Provenance is what proves it is not last week s build. Checked before any rename. 2047 if md_prov_ok(livep, stagedp) == 0 { return 0 - 2 } 2048 // A failed bank must never consume the candidate or overwrite the live compiler. 2049 let previous: i64=sys_openat_rd(prevp) 2050 if previous >= 0 { 2051 sys_close(previous) 2052 if md_rotate_prev(prevp) != 1 { return MD_TC_ERR_BACKUP } 2053 } 2054 if previous < 0 { if previous != (0-2) { return MD_TC_ERR_BACKUP } } 2055 if sys_renameat(livep, prevp) != 0 { return MD_TC_ERR_BACKUP } 2056 if sys_renameat(stagedp, livep) != 0 { 2057 sys_renameat(prevp, livep) // stage-rename failed: put the old one straight back 2058 nx_chmod(livep, MD_TC_MODE_EXEC) 2059 return 0 2060 } 2061 nx_chmod(livep, MD_TC_MODE_EXEC) 2062 return sz 2063} 2064 2065 2066// Restore buildroot/_offc/<nm>.prev -> live. This is the rollback half of never-brick and it is called 2067// on CANARY FAILURE, so the ecosystem can never be left with a compiler that cannot compile. 2068func md_tc_rollback(nm: *u8) -> i64 { 2069 let livep: *u8 = sys_mmap(256) 2070 let prevp: *u8 = sys_mmap(256) 2071 md_tc_live(nm, livep) 2072 md_tc_prev(nm, prevp) 2073 if md_tc_elf_size(prevp) == 0 { return 0 } 2074 if sys_renameat(prevp, livep) != 0 { return 0 } 2075 nx_chmod(livep, MD_TC_MODE_EXEC) 2076 return 1 2077} 2078 2079// Our own pid. getpid = syscall 39 on x86-64. 2080func md_self_pid() -> i64 { return __syscall(172, 0, 0, 0, 0, 0, 0) } // rv64 getpid=172. Was raw x86 39, which IS an RV64 KEY (umount2) the backend translated to ioctl(16) -> -ENOTTY, so the mgmt API's own pid was -25 (debt idx 2277) 2081// Fork a detached child that waits, then SIGTERMs the given pid. Used so a self-restart can FINISH WRITING 2082// ITS RESPONSE before the process goes away: the reply reaches the caller, then the guard respawns the 2083// already-promoted binary. SIGTERM (not KILL) so a daemon that later grows a drain handler gets to use it. 2084func md_delayed_kill(pid: i64, delay_ms: i64) -> i64 { 2085 let p: i64 = sys_fork() 2086 if p == 0 { 2087 nx_setsid() 2088 sys_sleep_ms(delay_ms) 2089 nx_kill(pid, 15) 2090 sys_exit(0) 2091 } 2092 return p 2093} 2094func md_kill_by_name(needle: *u8) -> i64 { 2095 var self_hit: i64 = 0 2096 let nn: i64 = md_len(needle) 2097 let fd: i64 = sys_openat_rd("/proc" as *u8) 2098 if fd < 0 { return 0 } 2099 let buf: *u8 = sys_mmap(65536) 2100 let path: *u8 = sys_mmap(256) 2101 let clbuf: *u8 = sys_mmap(8192) 2102 var killed: i64 = 0 2103 var run: i64 = 1 2104 while run == 1 { 2105 let n: i64 = sys_getdents64(fd, buf, 65536) 2106 if n <= 0 { run = 0 } else { 2107 var off: i64 = 0 2108 while off < n { 2109 let rec: *u8 = ((buf as i64 + off) as *u8) 2110 let reclen: i64 = dirent_reclen(rec) 2111 if reclen <= 0 { off = n } else { 2112 let name: *u8 = dirent_name(rec) 2113 if name[0] >= (48 as u8) { if name[0] <= (57 as u8) { 2114 var p: i64 = 0 2115 let pre: *u8 = "/proc/" as *u8 2116 var a: i64 = 0 2117 while pre[a] != (0 as u8) { path[p] = pre[a]; p = p + 1; a = a + 1 } 2118 a = 0 2119 while name[a] != (0 as u8) { path[p] = name[a]; p = p + 1; a = a + 1 } 2120 let suf: *u8 = "/cmdline" as *u8 2121 a = 0 2122 while suf[a] != (0 as u8) { path[p] = suf[a]; p = p + 1; a = a + 1 } 2123 path[p] = 0 as u8 2124 let cfd: i64 = sys_openat_rd(path) 2125 if cfd >= 0 { 2126 let cln: i64 = sys_read(cfd, clbuf, 8192) 2127 sys_close(cfd) 2128 if cln > 0 { if md_pk_contains(clbuf, cln, needle, nn) == 1 { 2129 // ---- R5 SEQUENCING: NEVER SIGKILL OURSELVES MID-RESPONSE ---------------- 2130 // TWO defects, one root. (1) mgmt restarting/deploying ITSELF matches its own 2131 // cmdline here and SIGKILLs the process that is writing the reply -- that IS 2132 // the FETCH-FAIL every /api/deploy returns (~12x in one session), and it is why 2133 // the seq1563 deploy lease strands (we die before reaching our own release). 2134 // (2) NEW with SO_REUSEPORT: a hot restart runs old and new under the SAME 2135 // cmdline, so a name-matched kill would murder the freshly-spawned instance too 2136 // -- adopting REUSEPORT without this turns a handoff into an outage. 2137 // So: skip our own pid here, remember it, and schedule a DELAYED self-exit 2138 // after the loop. The reply is written first, THEN we go; the supervise guard 2139 // respawns the already-promoted binary. FETCH-FAIL becomes a real JSON body. 2140 let vpid: i64 = md_pk_atoi(name) 2141 if vpid == md_self_pid() { self_hit = 1 } else { 2142 nx_kill(vpid, 9) 2143 killed = killed + 1 2144 } 2145 } } 2146 } 2147 } } 2148 off = off + reclen 2149 } 2150 } 2151 } 2152 } 2153 sys_close(fd) 2154 // We matched OURSELVES: schedule the exit for AFTER the response is on the wire. 1500ms is the 2155 // reply-write window, not a guess at compile time -- the caller returns immediately after this. 2156 // Counted in `killed` so the JSON stays honest about what is going away. 2157 if self_hit == 1 { md_delayed_kill(md_self_pid(), 1500); killed = killed + 1 } 2158 return killed 2159} 2160 2161// ---- secondary adapters: validate / exec / probe ---------------------------------------------------- 2162func md_validate_artifact(path: *u8, kind: i64) -> i64 { return dep_validate(path, kind) } 2163 2164// drive the proven allowlisted nx_aw_hostctl with one sub -> its exit code (the supervisor/deploy exec port). 2165func md_exec_hostctl(sub: *u8) -> i64 { 2166 // Run the ON-NAS supervisor CLI directly. The mgmt daemon runs under the (root) supervisor, so it can drive the 2167 // surgical restart subs. The prior "_offc/nx_aw_hostctl.elf" is the LAPTOP->NAS bridge and is ABSENT on the NAS, 2168 // so /api/restart + /api/deploy were gate-proven but never live-executable. Absolute path (mgmt cwd=nishihost). 2169 let helf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_hostctl" as *u8 2170 let args: *i64 = sys_mmap(16) as *i64 2171 args[0] = sub as i64 2172 return dep_run(helf, args, 1) 2173} 2174 2175// P2 off-LAN parity: run one allowlisted hostctl sub and CAPTURE its stdout to outpath (for /api/hostctl -> the 2176// phone gets torstat/routerctl/status output). Same on-NAS nx_hostctl the deploy path uses; single argv element 2177// (execve, no shell) so no injection; the allowlist below fail-closes to a curated safe read/action set. 2178func md_exec_hostctl_capture(sub: *u8, outpath: *u8) -> i64 { 2179 let helf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_hostctl" as *u8 2180 let args: *i64 = sys_mmap(16) as *i64 2181 args[0] = sub as i64 2182 return dep_run_capture(helf, args, 1, outpath) 2183} 2184// 2-arg variant (e.g. `buildrun <target>`): run the on-NAS nx_hostctl <sub> <arg>, capture stdout -> outpath. 2185func md_exec_hostctl_capture2(sub: *u8, arg: *u8, outpath: *u8) -> i64 { 2186 let helf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_hostctl" as *u8 2187 let args: *i64 = sys_mmap(16) as *i64 2188 args[0] = sub as i64 2189 args[1] = arg as i64 2190 return dep_run_capture(helf, args, 2, outpath) 2191} 2192// ---- BUILD ADMISSION (seq708/768/1390) -------------------------------------------------------- 2193// THE INCIDENT THIS PREVENTS: /api/build is the one heavyweight mgmt op -- it forks the sovereign nx_cc 2194// toolchain to compile a source tree -- and it did so with NO memory admission. Under a build-heavy 2195// session the host runs out of memory and the supervisor OOM-reaps nx_mgmt_api, taking the WHOLE deploy 2196// path down for every seat. Confirmed twice (seq698/708) and REPRODUCED LIVE 2026-07-30 (seq1390): 2197// both transports died mid-session (503 / status=0, TLS fine so the EDGE was healthy and the BACKEND 2198// was gone) and self-recovered only when the guard respawned it. 2199// 2200// ⚠seq768 recorded this fix as "WRITTEN + BUILT + VERIFIED, staged awaiting one rename" and was marked 2201// EATEN -- but on 2026-07-30 md_exec_build_admit / ma_emit_503 / the ma_do_build call site were found in 2202// NEITHER the laptop SSOT NOR the NAS buildroot (grep: 0 matches across 6972 files). The work never 2203// reached a source tree, so every build-heavy session kept re-rolling the outage. Rebuilt here, in the 2204// SSOT, where a rebuild cannot lose it. ★LAW: a debt is not eaten until its fix is IN A SOURCE TREE -- 2205// "built and staged" is not landed, and a binary nobody can rebuild is a rumour. 2206// 2207// FAIL-OPEN BY DESIGN (rule 26 / F881 ratchet stance): nx_build_admit exits 0 GRANT / 3 DENY (below the 2208// memory floor) / 4 QUEUE (load ceiling) / 2 usage / 5 unreadable-proc. We block ONLY on exit 3, the 2209// definitive memory wedge that actually causes the incident. Load-queueing and unreadable /proc both 2210// fall through to GRANT so admission control can never soft-brick the ecosystem's build path -- a 2211// refused-when-it-should-have-built is a worse failure here than an occasional reap. 2212// Floor 1024 MB; load ceiling deliberately huge so MEMORY is the sole gate (the measured cause). 2213// THE ADMISSION ENVELOPE LIVES IN ONE FILE (2026-08-18): knowledge/build_admit.conf, read by nx_build_admit 2214// itself on every check (argv > conf > host-derived). This path passes NO thresholds any more -- for one 2215// day it carried them as named consts here, which was still a second copy beside nx_sov_build_run's and 2216// nx_orchestrate's bare `headroom:`; three callers on one conf cannot disagree. A caller that MEANS a 2217// different envelope still says so on argv, and the report line prints envelope_src so it shows. 2218func md_exec_build_admit() -> i64 { 2219 let belf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_build_admit.elf" as *u8 2220 let bf: i64 = sys_openat_rd(belf) 2221 if bf < 0 { return 0 } // detector absent -> GRANT (never block on a missing guard) 2222 sys_close(bf) 2223 let args: *i64 = sys_mmap(32) as *i64 2224 args[0] = "check" as *u8 as i64 2225 return dep_run_capture(belf, args, 1, "/tmp/nx_build_admit.out" as *u8) 2226} 2227 2228// ---- GATE-DRY RATCHET RUNNER (2026-07-31, debts 1785529506 / 1785530277) ----------------------- 2229// L009 -- gate organs that hand-roll their verdict instead of inheriting nx_gate_verdict -- is not 2230// merely large, it is GROWING: two warden scans hours apart on 2026-07-31 read 2035/2167 then 2231// 2041/2182. A migration campaign that only removes old breaches LOSES to a tree that adds new ones, 2232// so D001 cannot be closed by migrating alone. The 2026 practice for exactly this shape is a RATCHET 2233// (Notion bans an INCREASE in violation count and requires a deliberate re-bank). 2234// 2235// u26a0AND THE PREDECESSOR THIS WAS SUPPOSED TO COPY DOES NOT EXIST. nx_magicratchet is asserted "wired 2236// into /api/build" in FOUR comments in nx_law_warden.nx, but grep finds ZERO call sites in this source 2237// AND ZERO in the deployed mgmt binary, and a two-build experiment (clean -> BUILT, +3 literals >=1024 2238// -> BUILT, not refused) proves it never fires. So this is written fresh, not modelled on prose. 2239// 2240// FAIL-OPEN, the same stance as build admission and the pre-deploy gate: a missing or unreadable 2241// detector returns -1 and the caller proceeds. A guard that cannot be read must never wedge the build 2242// path for every seat. 2243func md_exec_gatedry(srcpath: *u8) -> i64 { 2244 let gelf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_gatedry.elf" as *u8 2245 let gf: i64 = sys_openat_rd(gelf) 2246 if gf < 0 { return 0 - 1 } 2247 sys_close(gf) 2248 let args: *i64 = sys_mmap(16) as *i64 2249 args[0] = srcpath as i64 2250 return dep_run_capture(gelf, args, 1, "/tmp/nx_gatedry.out" as *u8) 2251} 2252 2253// ---- F881 RULE-11 RATCHET RUNNER (2026-08-15, debt 1785530277) --------------------------------- 2254// The wiring four comments in nx_law_warden.nx asserted as fact, and which the comment above correctly 2255// records as never having existed. Confirmed absent TWICE today before writing this: nx_wiredclaim 2256// reports CLAIMED-UNWIRED call_sites=0, and behaviourally four real /api/build runs of one organ left 2257// its baseline at -1 while a single direct call created one. The chokepoint was exercised; nothing 2258// refused. 2259// 2260// WHY THIS IS SAFE WHERE L009 WAS NOT, and the difference is the whole reason it can land: L009 keyed 2261// "is this gate NEW?" on "has no deployed .elf", which was false for ~94pc of the corpus and turned the 2262// ratchet into a WALL at a shared chokepoint. nx_magicratchet uses the model that comment says a correct 2263// one needs -- a BANKED PER-ORGAN BASELINE, refusing only an INCREASE against the record. First sight of 2264// any organ self-baselines and GRANTS, so the existing corpus passes by construction. 2265// 2266// It was NOT safe to wire before today. Measured and fixed 2026-08-15, both in nx_magicratchet: 2267// - its parser skipped a leading '-', so countfile's -1 (UNMEASURED) read as a COUNT OF 1 and the 2268// fail-open branch could never fire; 2269// - it resolved _hdl_build/<n>.nx unconditionally, blind to every organ under runtime/. 2270// Together those banked baselines from files never opened, arming exactly the false refusal that killed 2271// L009. All four paths are now proven: resolves in either tree, unresolvable -> ALLOW-UNCOUNTED, and 2272// REFUSE still fires naming the offending line. 2273// 2274// FAIL-OPEN, the same stance as build admission and the gate-dry runner: a missing or unreadable 2275// detector returns -1 and the caller proceeds. A guard that cannot be read must never wedge the build 2276// path for every seat. 2277func md_exec_magicratchet(nm: *u8) -> i64 { 2278 let melf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_magicratchet.elf" as *u8 2279 let mf: i64 = sys_openat_rd(melf) 2280 if mf < 0 { return 0 - 1 } 2281 sys_close(mf) 2282 let args: *i64 = sys_mmap(16) as *i64 2283 args[0] = "check" as *u8 as i64 2284 args[1] = nm as i64 2285 return dep_run_capture(melf, args, 2, "/tmp/nx_magicratchet.out" as *u8) 2286} 2287 2288// Does this organ name end in the terminal token `_gate`? TERMINAL, not substring -- the seq1789 2289// lesson banked in nx_organkind_gate T12: `nx_survey_serve_gate` ENDS with _gate, `nx_gate_bite` 2290// merely CONTAINS it, and treating containment as the test misclassifies the second. 2291func md_name_is_gate(nm: *u8) -> i64 { 2292 var n: i64 = 0 2293 while nm[n] != (0 as u8) { n = n + 1 } 2294 if n < 5 { return 0 } 2295 if nm[n-5] != (95 as u8) { return 0 } 2296 if nm[n-4] != (103 as u8) { return 0 } 2297 if nm[n-3] != (97 as u8) { return 0 } 2298 if nm[n-2] != (116 as u8) { return 0 } 2299 if nm[n-1] != (101 as u8) { return 0 } 2300 return 1 2301} 2302 2303// Is this organ ALREADY DEPLOYED? That is the GRANDFATHER TEST and it is what makes this a ratchet 2304// rather than a wall: 2041 existing gates hand-roll their verdicts, and refusing all of them would 2305// stop the ecosystem dead. Only a gate with NO deployed artefact -- i.e. a NEW one -- is held to the 2306// base class. Existing breaches are migrated by their owner lanes, never blocked here. 2307// ---- THE BANKED BASELINE: HOW THE RATCHET TELLS NEW FROM OLD (2026-07-31, debt 1785558585) ---- 2308// MY FIRST ATTEMPT USED "has no deployed .elf" AS THE NEWNESS TEST AND THAT WAS WRONG. The ledger 2309// measures 2877 gate sources against 175 binaries -- ~94pc of gates were NEVER COMPILED -- so 2310// long-existing gates read as NEW and their rebuilds were REFUSED. A ratchet that cannot tell new 2311// from old is a WALL, and a wall at a shared chokepoint stops every seat. Withdrawn within minutes. 2312// 2313// THE CORRECT TEST IS A RECORD, which is what a ratchet actually is. Notion's ESLint ratcheting keeps 2314// a CHECKED-IN file of known violations and requires approval only when the count INCREASES against 2315// it. knowledge/status/gatedry_baseline.out is that record: the enumerated gate corpus at the moment 2316// the ratchet landed (2189 entries). A gate NOT in the record is NEW and is held to the base class; 2317// everything in the record is grandfathered and migrated by its owner lane. 2318// 2319// FAIL-OPEN BY CONSTRUCTION: an absent or unreadable baseline returns 1 (== "known", allow). If the 2320// record cannot be read we CANNOT distinguish new from old, and the only safe answer is to permit -- 2321// otherwise a missing file silently rebuilds the exact wall this replaced. 2322func md_gate_in_baseline(nm: *u8) -> i64 { 2323 let bp: *u8 = "knowledge/status/gatedry_baseline.out" as *u8 2324 let ln: *i64 = sys_mmap(16) as *i64 2325 ln[0] = 0 2326 let buf: *u8 = sys_read_file(bp, ln) 2327 if buf as i64 == 0 { return 1 } 2328 let n: i64 = ln[0] 2329 if n <= 0 { return 1 } 2330 let pat: *u8 = sys_mmap(256) 2331 var o: i64 = 0 2332 pat[o] = (47 as u8) 2333 o = o + 1 2334 var bi: i64 = 0 2335 while nm[bi] != (0 as u8) { pat[o] = nm[bi]; o = o + 1; bi = bi + 1 } 2336 pat[o] = (46 as u8) 2337 o = o + 1 2338 pat[o] = (110 as u8) 2339 o = o + 1 2340 pat[o] = (120 as u8) 2341 o = o + 1 2342 let pn: i64 = o 2343 var k: i64 = 0 2344 while k + pn <= n { 2345 var j: i64 = 0 2346 var hit: i64 = 1 2347 while j < pn { if buf[k+j] != pat[j] { hit = 0; j = pn } else { j = j + 1 } } 2348 if hit == 1 { return 1 } 2349 k = k + 1 2350 } 2351 return 0 2352} 2353 2354func md_organ_deployed(nm: *u8) -> i64 { 2355 let p: *u8 = sys_mmap(256) 2356 let pre: *u8 = "/volume1/homes/elderwesto/nishihost/" 2357 var o: i64 = 0 2358 var i: i64 = 0 2359 while pre[i] != (0 as u8) { p[o] = pre[i]; o = o + 1; i = i + 1 } 2360 i = 0 2361 while nm[i] != (0 as u8) { p[o] = nm[i]; o = o + 1; i = i + 1 } 2362 let suf: *u8 = ".elf" 2363 i = 0 2364 while suf[i] != (0 as u8) { p[o] = suf[i]; o = o + 1; i = i + 1 } 2365 p[o] = 0 as u8 2366 let fd: i64 = sys_openat_rd(p) 2367 if fd < 0 { return 0 } 2368 sys_close(fd) 2369 return 1 2370} 2371 2372// ---- PRE-DEPLOY SAFETY GATE RUNNER (2026-07-30) ------------------------------------------------- 2373// nx_deploy_ready computes deploy_safe/blockers/DEPLOY-BLOCKED and publishes it -- and NOTHING AT THE 2374// DEPLOY CHOKEPOINT EVER CONSULTED IT. It is referenced by ecomat seeding, tooldiff and the cron beat, 2375// but nx_mgmt_api never called it, so the one act the gate exists to guard ran unguarded. MEASURED: 2376// the gate returned verdict DEPLOY-BLOCKED (blockers=1) while two of my own deploys succeeded minutes 2377// apart. A gate that is computed, published and unreachable from the act it guards IS the baseline. 2378// 2379// WHY WE PARSE JSON AND NOT THE EXIT CODE: nx_deploy_ready calls sys_exit(0) UNCONDITIONALLY -- even 2380// when the verdict is DEPLOY-BLOCKED -- so its exit status carries no verdict at all and no caller 2381// checking $? could ever act on it. Fixing that is a published-contract change (other callers may 2382// treat nonzero as failure), so it is filed separately rather than changed underneath them here. 2383// 2384// FAIL-OPEN, same stance as build admission: a missing or unreadable gate returns -1 and the caller 2385// proceeds. A guard that cannot be read must never wedge the deploy path for every seat. 2386func md_exec_deploy_ready(target: *u8) -> i64 { 2387 let delf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_deploy_ready.elf" as *u8 2388 let df: i64 = sys_openat_rd(delf) 2389 if df < 0 { return 0 - 1 } 2390 sys_close(df) 2391 let args: *i64 = sys_mmap(32) as *i64 2392 args[0] = "check" as *u8 as i64 2393 // LOOSE COUPLING (2026-09-02): pass the deploy TARGET so nx_deploy_ready scopes its evidence-honesty block 2394 // to the target's own domain + shared-fate domains (knowledge/registry/deploy_domain.conf), never to an 2395 // unrelated domain's RED. argv[2] is the default manifest because the target rides argv[3]; an EMPTY 2396 // target keeps the old one-arg call, i.e. GLOBAL scope, so no caller changes behaviour by accident. 2397 var nargs: i64 = 1 2398 if target != (0 as *u8) { if target[0] != (0 as u8) { 2399 args[1] = "knowledge/registry/deploy_checks.tsv" as *u8 as i64 2400 args[2] = target as i64 2401 nargs = 3 2402 } } 2403 // Return the gate's EXIT CODE (0 safe / 3 DEPLOY-BLOCKED) so the caller can surface it. Until today this 2404 // code was constant 0 and therefore meaningless; reporting it live is what proves the new contract landed. 2405 let drc0: i64 = dep_run_capture(delf, args, nargs, "/tmp/nx_ma_deploy_ready.out" as *u8); if drc0 >= 0 { return drc0 } 2406 return 0 2407} 2408 2409// mint a ROOT tools-capability token via the on-NAS nx_cap_mint oracle (CLI: <keyfile> <allow-csv> <exp> <nonce> 2410// -> token on stdout, nonzero exit on refusal). The HMAC keyfile is read BY THE ORACLE on-NAS and never crosses 2411// the API. Absolute paths (same stance as the hostctl/treepack exec ports; mgmt cwd=nishihost but explicit wins). 2412// stdout captured -> outpath; caller treats nonzero exit OR empty capture as mint-failed (fail-closed). 2413func md_exec_capmint(allow: *u8, expstr: *u8, noncestr: *u8, outpath: *u8) -> i64 { 2414 let helf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_cap_mint.elf" as *u8 2415 let args: *i64 = sys_mmap(40) as *i64 2416 args[0] = "/volume1/homes/elderwesto/nishihost/tools_cap_secret.key" as *u8 as i64 2417 args[1] = allow as i64 2418 args[2] = expstr as i64 2419 args[3] = noncestr as i64 2420 return dep_run_capture(helf, args, 4, outpath) 2421} 2422// ---- /api/gate_run + /api/proc_kill support (seq1349/1383). RE-APPLIED after a 4th backdate (seq1445). 2423// gate_run bound: name must end gate/test/kat, resolves ONLY a promoted top-level nishihost/<n>.elf, so a pure 2424// verifier is all this route can ever reach -- never a daemon, promoter or deployer. 2425// ⚠ seq1443: dep_run_capture_bounded DUPLICATES tr_run_capture_to (nx_tool_run.nx, seq1412) which is gate-proven 2426// (nx_tool_run_timeout_gate T5 = no leak after a kill) and uses a WATCHDOG FORK because a poll design is not 2427// buildable without sys_fcntl. ADOPT IT next; kept here only so the live verbs stop vanishing from source. 2428// ---- R1 (seq1506): LEASE-GATE THE BUILD PATH ------------------------------------------------------------- 2429// OPERATOR 2026-07-30: "why cant we clearly state when we are switching out or updating and coordinate like 2430// road construction". This is the flagger. Concurrent builds of the SAME target are how a session ships a 2431// regression from a mid-churn snapshot -- it happened twice today (21->19 routes lost, then again). 2432// ADOPTION, NOT INVENTION (seq1410's law, 4th instance today): nx_lease ALREADY EXISTS, is gate-proven, and 2433// had ZERO callers in the build path. We reuse it as a SUBPROCESS via its exit-code contract rather than 2434// importing it -- verified live: acquire=0 prints LS-ACQUIRED, BUSY=3 prints "LS-BUSY <name> holder=<who>", 2435// release=0. Exit codes are the contract, so no import coupling and no second implementation. 2436// TTL is the reason this can never deadlock the ecosystem: a session that dies mid-build cannot hold the 2437// lane closed -- the lease expires on its own. A lock without a TTL would be a worse defect than the race. 2438// ⚠ lease NAME grammar is [a-zA-Z0-9_-] ONLY: "build:X" is REFUSED, so the name is built as "build-<target>". 2439func md_lease_run(verb: *u8, name: *u8, owner: *u8, ttl: *u8, nargs: i64, outpath: *u8) -> i64 { 2440 let elf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_lease.elf" as *u8 2441 let args: *i64 = sys_mmap(48) as *i64 2442 args[0] = verb as i64 2443 args[1] = name as i64 2444 args[2] = owner as i64 2445 args[3] = ttl as i64 2446 return dep_run_capture(elf, args, nargs, outpath) 2447} 2448// Build the lease name "build-<target>" into buf. Target is already [A-Za-z0-9_]-sanitized by the caller. 2449func md_lease_name(target: *u8, buf: *u8) -> i64 { 2450 var o: i64 = 0 2451 let p: *u8 = "build-" as *u8 2452 while p[o] != (0 as u8) { buf[o] = p[o]; o = o + 1 } 2453 var i: i64 = 0 2454 while target[i] != (0 as u8) { buf[o] = target[i]; o = o + 1; i = i + 1 } 2455 buf[o] = 0 as u8 2456 return o 2457} 2458// Build "<prefix><src>" into buf, COPYING ONLY lease-legal chars [a-zA-Z0-9_-] from src. 2459// ⚠ nx_lease REFUSES any other byte, and the things we most want to lock are named with dots 2460// ("nx_tools_api_serve.elf") -- so a naive concat produces LS-REFUSED bad name and the guard silently 2461// never engages. A guard that cannot be named is a guard that does not exist; filter, do not assume. 2462func md_lease_name_pfx(prefix: *u8, src: *u8, buf: *u8) -> i64 { 2463 var o: i64 = 0 2464 while prefix[o] != (0 as u8) { buf[o] = prefix[o]; o = o + 1 } 2465 var i: i64 = 0 2466 while src[i] != (0 as u8) { 2467 let c: i64 = src[i] as i64 2468 var ok: i64 = 0 2469 if c >= 48 { if c <= 57 { ok = 1 } } 2470 if c >= 65 { if c <= 90 { ok = 1 } } 2471 if c >= 97 { if c <= 122 { ok = 1 } } 2472 if c == 95 { ok = 1 } 2473 if c == 45 { ok = 1 } 2474 if ok == 1 { buf[o] = src[i]; o = o + 1 } 2475 i = i + 1 2476 } 2477 buf[o] = 0 as u8 2478 return o 2479} 2480func md_gate_name_ok(nm: *u8) -> i64 { 2481 var n: i64 = 0 2482 while nm[n] != (0 as u8) { n = n + 1 } 2483 if n >= 4 { if nm[n-4] == (103 as u8) { if nm[n-3] == (97 as u8) { if nm[n-2] == (116 as u8) { if nm[n-1] == (101 as u8) { return 1 } } } } } 2484 if n >= 4 { if nm[n-4] == (116 as u8) { if nm[n-3] == (101 as u8) { if nm[n-2] == (115 as u8) { if nm[n-1] == (116 as u8) { return 1 } } } } } 2485 if n >= 3 { if nm[n-3] == (107 as u8) { if nm[n-2] == (97 as u8) { if nm[n-1] == (116 as u8) { return 1 } } } } 2486 return 0 2487} 2488// seq1443 ADOPTION: this now delegates to tr_run_capture_to (runtime/nx_tool_run.nx, seq1412) instead of my 2489// own dep_run_capture_bounded, which was a DUPLICATE of it -- a live instance of the ecosystem's own law that 2490// THE BOTTLENECK IS NOT BUILDING PRIMITIVES, IT IS ADOPTING THEM. 2491// The adopted primitive is STRICTLY better and its header says why mine could not work: bounding the drain 2492// needs O_NONBLOCK on the read end and THERE IS NO sys_fcntl in nx_syscalls, so a poll design is NOT 2493// BUILDABLE. It uses a WATCHDOG FORK -- the watchdog SIGKILLs the worker, the dying worker drops the last 2494// write end, and the parent's blocking read gets its EOF naturally, needing no new syscall. It also closes 2495// wfd BEFORE forking the watchdog (order is load-bearing: fork first and the watchdog inherits the write end, 2496// so the pipe never EOFs -- the exact hang the bound exists to remove, reintroduced by the fix). 2497// It ships nx_tool_run_timeout_gate whose T5 is "second timeout identical (no leak after a kill)" = the 2498// no-leak tooth seq1425 was missing. Capturing to a BUFFER also deletes the /tmp/nx_ma_gaterun.out temp file. 2499// Returns the child's exit code, or TR_ERR_TIMEOUT when the deadline fired. 2500func md_exec_gate_capture(elfpath: *u8, out: *u8, cap: i64, outlen: *i64, deadline_ms: i64) -> i64 { 2501 let argv: *i64 = sys_mmap(32) as *i64 2502 argv[0] = elfpath as i64 2503 argv[1] = 0 2504 return tr_run_capture_to(elfpath, argv, out, cap, outlen, deadline_ms) 2505} 2506// (md_exec_gate_capture_OLD REMOVED 2026-08-07 by a different seat than the one that landed the seq1443 2507// adoption above. It had ZERO callers -- grep finds only its own definition -- but it still called 2508// dep_run_capture_bounded, whose definition went away when md_exec_gate_capture adopted 2509// tr_run_capture_to. DEAD CODE STILL HAS TO COMPILE: that one unreachable line made /api/build fail for 2510// target=nx_mgmt_api for EVERY seat, i.e. it took down the deploy path itself, and the second reported 2511// error -- a bogus "sd_catn arg 3 is a POINTER" at nx_mgmt_api.nx:1907, where dl is plainly `var dl: 2512// i64` -- was a CASCADE of this same undefined name, not a second defect. 2513// A REFACTOR THAT DELETES A DEFINITION BUT LEAVES ITS DEAD CALLER HAS NOT LANDED, IT HAS HALF-LANDED.) 2514// proc_kill bound: >=6 chars AND must contain .elf (our own organs only, never a system process) AND must not 2515// reach the supervisor (killing the guard stops every respawn). Killing a guard-supervised daemon = a restart. 2516func md_str_contains(hay: *u8, pat: *u8) -> i64 { 2517 let hn: i64 = md_len(hay) 2518 let pn: i64 = md_len(pat) 2519 if pn == 0 { return 0 } 2520 if pn > hn { return 0 } 2521 var i: i64 = 0 2522 while i + pn <= hn { 2523 var k: i64 = 0 2524 var hit: i64 = 1 2525 while k < pn { if hay[i+k] != pat[k] { hit = 0; k = pn } else { k = k + 1 } } 2526 if hit == 1 { return 1 } 2527 i = i + 1 2528 } 2529 return 0 2530} 2531func md_proc_kill_needle_ok(nm: *u8) -> i64 { 2532 if md_len(nm) < 6 { return 0 } 2533 if md_str_contains(nm, ".elf" as *u8) == 0 { return 0 } 2534 if md_str_contains(nm, "supervise" as *u8) == 1 { return 0 } 2535 if md_str_contains(nm, "nx_hostctl" as *u8) == 1 { return 0 } 2536 return 1 2537} 2538func md_cstr_eq(a: *u8, b: *u8) -> i64 { var i: i64=0; while a[i]!=(0 as u8) { if a[i]!=b[i] { return 0 } i=i+1 } if b[i]!=(0 as u8) { return 0 } return 1 } 2539// FAIL-CLOSED allowlist for /api/hostctl. Curated to fast (<~15s) read + safe-maintenance subs so a synchronous 2540// API call returns promptly. DEPLOY/selfswap/rollback are DELIBERATELY excluded (they promote/re-exec -> use the 2541// guarded /api/deploy). Long-running (nettap 60s, portmap timeouts) excluded until an async job path exists. 2542func md_hostctl_action_ok(nm: *u8) -> i64 { 2543 if md_cstr_eq(nm, "status" as *u8) == 1 { return 1 } // supervisor snapshot 2544 if md_cstr_eq(nm, "torstat" as *u8) == 1 { return 1 } // per-torrent seedeval + metadata diag 2545 if md_cstr_eq(nm, "routerctl" as *u8) == 1 { return 1 } // GL.iNet dashboard (model/wan/forwards/clients) 2546 if md_cstr_eq(nm, "receipts" as *u8) == 1 { return 1 } // op-receipts ledger (read-only) 2547 if md_cstr_eq(nm, "kicktorrent" as *u8) == 1 { return 1 } // restart the torrent daemon 2548 if md_cstr_eq(nm, "kickseed" as *u8) == 1 { return 1 } // restart the :6881 seeder 2549 if md_cstr_eq(nm, "kickworkers" as *u8) == 1 { return 1 } // restart stale download workers 2550 if md_cstr_eq(nm, "kickseedann" as *u8) == 1 { return 1 } // restart the DHT/LSD announcer 2551 if md_cstr_eq(nm, "trackerrefresh" as *u8) == 1 { return 1 } // refresh the tracker list (detached) 2552 if md_cstr_eq(nm, "galxpipeline" as *u8) == 1 { return 1 } // analysis-on-ingest: thumbnails + NXVI (detached, idempotent) 2553 if md_cstr_eq(nm, "durindexrun" as *u8) == 1 { return 1 } // duration-index batch (detached, idempotent) 2554 if md_cstr_eq(nm, "searchpagerank" as *u8) == 1 { return 1 } // search: PageRank build on the live web shard (detached, idempotent, additive pr:) 2555 if md_cstr_eq(nm, "searchcompact" as *u8) == 1 { return 1 } // search: web-shard compaction (detached, idempotent, verifies-before-swap) 2556 if md_cstr_eq(nm, "durindexstat" as *u8) == 1 { return 1 } // duration-index coverage (read-only) 2557 return 0 2558} 2559 2560// REAL-HTTP health: GET url -> require 200 + non-empty body. 1 healthy / 0 not (the false-green killer). 2561// LOCAL TCP liveness: connect to 127.0.0.1:port -> 1 if something is listening (service up), 0 if refused. 2562// Dependency-free (no external fetcher, no CA store, no edge round-trip) = the robust health signal for a restart. 2563func md_tcp_alive(port: i64) -> i64 { 2564 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0); if fd < 0 { return 0 } 2565 let sa: *u8 = sys_mmap(16) 2566 sa[0]=2 as u8; sa[1]=0 as u8; sa[2]=((port>>8)&0xff) as u8; sa[3]=(port&0xff) as u8 2567 sa[4]=127 as u8; sa[5]=0 as u8; sa[6]=0 as u8; sa[7]=1 as u8 2568 var z: i64=8; while z<16 { sa[z]=0 as u8; z=z+1 } 2569 let r: i64 = nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS); sys_close(fd) 2570 if r == 0 { return 1 } 2571 return 0 2572} 2573// ---- DEPLOY LISTEN VERIFICATION (debt 1786068492) ----------------------------------------------------- 2574// THE DEFECT THIS CLOSES IS NOT "THE CHECK WAS MISSING" -- THE CHECK WAS HERE AND IT GREENED ON THE WRONG 2575// PROCESS. hc_deploy_one promotes, then proc_kill_by_name(SIGKILL), then returns IMMEDIATELY; the deploy 2576// watchdog then called md_tcp_alive() with NO initial delay. SIGKILL is asynchronous -- the kernel releases 2577// the dying process's listening socket some time AFTER the syscall returns -- so a connect issued inside that 2578// window SUCCEEDS AGAINST THE OUTGOING LISTENER and the deploy is recorded DEPLOYED-GREEN having never once 2579// observed the incoming binary. That is precisely why debt 1786068492 records "the auto-rollback health check 2580// did not catch this either": it ran, and it PASSED, on the process being replaced. 2581// AAAAAA A LEVEL-TRIGGERED PROBE CANNOT WITNESS A HANDOFF -- "SOMETHING IS LISTENING" IS TRUE BOTH BEFORE AND 2582// AFTER, SO THE ONE INSTANT THAT DISTINGUISHES A REAL REPLACEMENT FROM A NO-OP IS THE ONLY THING IT NEVER 2583// SAMPLES. The same blindness is why a surviving second instance, or a kill that matched nothing, also greens. 2584// 2585// THIS IS A DETECTOR, NOT A CURE, AND THE DIFFERENCE IS STATED SO NO READER OVER-CLAIMS IT. It does NOT close 2586// the race: with kill-then-respawn there is still an instant at which ZERO processes hold the socket, and only 2587// descriptor handoff removes that (knowledge/compare/trafficsafety.matrix -- nginx, HAProxy and Envoy pass the 2588// fd and never re-bind; HAProxy measured 155 connection failures per million over 180 reloads before moving 2589// it). SO_REUSEPORT is NOT the fix either: it is accept-distribution, and its own documentation states it 2590// CREATES a drop window when the listening set changes. What this buys is that the failure is now NAMED and 2591// LOUD instead of silent and green. 2592// AAAAA AND IT PROVES ONLY THAT THE PORT ANSWERS, NEVER THAT THE RIGHT BINARY ANSWERS -- a promote of an 2593// unadjudicated artifact that listens perfectly is GREEN here and always will be. That is a different axis 2594// (artifact provenance) and it is guarded separately, above, by the routeguard and staging-hygiene classes. 2595// 2596// THREE NAMED OUTCOMES, BECAUSE UNOBSERVED MUST BE ITS OWN BUCKET: folding "I could not witness the handoff" 2597// into either GREEN or ROLLED-BACK is the false-proof-with-an-authoritative-name defect. An axis that cannot 2598// see must ABSTAIN, not acquit. 2599const MD_HP_NEVER_UP: i64 = 0 // the port never accepted inside the up budget -> the incoming process never listened -> ROLL BACK 2600const MD_HP_VERIFIED: i64 = 1 // REFUSED observed, then ACCEPTED -> the socket demonstrably changed hands 2601const MD_HP_UNVERIFIED: i64 = 2 // never observed REFUSED, but the port accepts -> healthy-looking, provenance UNPROVEN 2602 2603// probe budget slots (rule 11: the loops below index these, they never carry a bare number) 2604const MD_PC_UP_TRIES: i64 = 0 2605const MD_PC_UP_TICK_MS: i64 = 1 2606const MD_PC_DOWN_TICK_MS: i64 = 2 2607const MD_PC_SLOTS: i64 = 3 2608 2609// DERIVATION OF EVERY NUMBER HERE, stated because a timeout without a derivation is a magic number wearing a 2610// units suffix: 2611// up_tries x up_tick_ms must exceed the guard-respawn bound hc_deploy_one ITSELF announces to the operator 2612// ("guard respawns the NEW binary ... <=15s"). 10 x 3000 = 30s is 2x that bound and is the value this path 2613// was ALREADY calibrated at -- REUSED, not re-invented, so no deploy that passes today can begin failing 2614// because a budget moved under it. 2615// down_tick_ms is NOT a second budget. The REFUSED edge is a process-teardown transient (SIGKILL -> fd table 2616// release), sub-second on an idle box, and AN EVENT CAN ONLY BE OBSERVED BY SAMPLING FASTER THAN IT LASTS -- 2617// so the down phase samples in milliseconds where the up phase samples in seconds. The down WINDOW is 2618// DERIVED as exactly ONE up-tick (md_probe_down_tries), so this file holds ONE calibrated quantity, not two 2619// that can silently drift apart. 2620func md_probe_conf(out: *i64) -> i64 { 2621 out[MD_PC_UP_TRIES] = 10 2622 out[MD_PC_UP_TICK_MS] = 3000 2623 // DEFAULT 0 = DOWN-WATCH OFF, and that default is a SAFETY DECISION, not a tuning choice. 2624 // MEASURED 2026-08-21 on the live control plane: sampling for the REFUSED edge before deciding 2625 // anything extended the watchdog's life by one up-tick (~3s) BEFORE it could write a verdict, and the 2626 // watchdog is KILLED inside that window -- deploy_status stuck at RUNNING, no terminal state, one 2627 // nx_mgmt_api process left, and NO segfault in dmesg (so killed, not crashed). The previous 2628 // level-triggered probe survived only because it wrote within milliseconds. 2629 // AAAAAA A DETECTOR THAT LENGTHENS THE LIFE OF THE PROCESS THAT MUST SURVIVE TO REPORT ITS VERDICT CAN 2630 // DESTROY THE VERDICT IT EXISTS TO PRODUCE -- and here that verdict IS the never-brick auto-rollback, 2631 // so the "improvement" silently disarmed a safety control it was written to strengthen. 2632 // THE EDGE IS STILL CAUGHT FOR FREE with the watch off: if the FIRST up-sample is REFUSED we have 2633 // witnessed the port down, and a later ACCEPT is a genuine handoff. If the first sample already 2634 // ACCEPTS we never saw it drop -- which is precisely UNVERIFIED. Identical timing to the incumbent, 2635 // zero added lifetime, and the silent false-green still becomes a NAMED state. 2636 out[MD_PC_DOWN_TICK_MS] = 0 2637 let szp: *i64 = sys_mmap(16) as *i64 2638 let buf: *u8 = md_read_file("deploy_probe.conf" as *u8, szp) 2639 if (buf as i64) == 0 { return 0 } // ABSENT -> compiled-in defaults. FAIL-OPEN: a probe that cannot read a conf must still probe. 2640 let n: i64 = szp[0] 2641 let offs: *i64 = sys_mmap(64) as *i64 2642 let lens: *i64 = sys_mmap(64) as *i64 2643 var cur: i64 = 0 2644 while cur < n { 2645 let le: i64 = md_eol(buf, n, cur) 2646 var isc: i64 = 0 2647 if le > cur { if (buf[cur] as i64) == 35 { isc = 1 } } 2648 if isc == 0 { 2649 let nf: i64 = md_split(buf, cur, le, offs, lens, 8) 2650 if nf >= 2 { 2651 let v: i64 = md_slice_atoi(buf, offs[1], lens[1]) 2652 // v > 0 REFUSES a zero or unparsable value rather than adopting it: a 0 tick spins the box and 2653 // a 0 try-count turns the whole probe into an unconditional pass. Defensive at the boundary. 2654 if v > 0 { 2655 if md_tok_eq(buf, offs[0], lens[0], "up_tries" as *u8) == 1 { out[MD_PC_UP_TRIES] = v } 2656 if md_tok_eq(buf, offs[0], lens[0], "up_tick_ms" as *u8) == 1 { out[MD_PC_UP_TICK_MS] = v } 2657 if md_tok_eq(buf, offs[0], lens[0], "down_tick_ms" as *u8) == 1 { out[MD_PC_DOWN_TICK_MS] = v } 2658 } 2659 } 2660 } 2661 cur = le + 1 2662 } 2663 return 1 2664} 2665 2666// The down window is exactly ONE up-tick, DERIVED -- never a second typed constant that can drift. 2667func md_probe_down_tries(cfg: *i64) -> i64 { 2668 let t: i64 = cfg[MD_PC_DOWN_TICK_MS] 2669 if t <= 0 { return 1 } 2670 var k: i64 = cfg[MD_PC_UP_TICK_MS] / t 2671 if k < 1 { k = 1 } 2672 return k 2673} 2674 2675// "port:<N>" grammar, factored out so the deploy watchdog and the legacy 1/0 probe read the SAME grammar. 2676// TWO READERS OF ONE GRAMMAR IS HOW A WIRE MISMATCH IS BORN. 2677func md_url_is_port(url: *u8) -> i64 { 2678 if url[0] != (112 as u8) { return 0 } 2679 if url[1] != (111 as u8) { return 0 } 2680 if url[2] != (114 as u8) { return 0 } 2681 if url[3] != (116 as u8) { return 0 } 2682 if url[4] != (58 as u8) { return 0 } 2683 return 1 2684} 2685func md_url_port(url: *u8) -> i64 { 2686 var pt: i64 = 0 2687 var pi: i64 = 5 2688 while url[pi] != (0 as u8) { if url[pi] >= (48 as u8) { if url[pi] <= (57 as u8) { pt = pt*10 + ((url[pi] as i64) - 48) } } pi = pi + 1 } 2689 return pt 2690} 2691 2692// EDGE-TRIGGERED listener verification on 127.0.0.1:<port>. COMPOSES md_tcp_alive -- the one TCP ruler in this 2693// plane -- and deliberately does NOT fork an HTTP prober: the forked-fetcher leg of md_health_probe below is 2694// recorded IN ITS OWN COMMENT as having ALWAYS false-rolled-back because the fetcher is not reliably present at 2695// the mgmt cwd, and re-introducing a fork here would re-introduce exactly that fragility. 2696// SPLIT FOR TESTABILITY: the budget arrives as a PARAMETER, so a gate can drive THIS EXACT CODE PATH at 2697// millisecond timescales instead of the production 30s one. AAAAA A LOGIC THAT CAN ONLY BE EXERCISED AT 2698// PRODUCTION TIMESCALES IS A LOGIC NOBODY WILL TEST -- and an untested probe is how the level-triggered one 2699// survived. The production entry point (md_probe_listen_edge) is immediately below and loads the conf. 2700func md_probe_listen_edge_cfg(port: i64, cfg: *i64) -> i64 { 2701 // PHASE 1 -- watch for the REFUSED edge. Bounded, and NEVER a rollback trigger on its own: some targets 2702 // restart WITHOUT ever dropping the socket (the hostctl self-swap leaves sites.elf :8443 up throughout), 2703 // and refusing those would be a detector that fails in the DESTRUCTIVE direction. Missing the edge only 2704 // DOWNGRADES the verdict; it can never cause a rollback. 2705 var saw_down: i64 = 0 2706 // OPT-IN ONLY (see md_probe_conf): this watch costs watchdog LIFETIME before any verdict is written, 2707 // and that cost once destroyed the verdict itself. OFF by default; a caller that can afford the extra 2708 // latency before deciding enables it by setting down_tick_ms. With it off the first up-sample below 2709 // still supplies the edge for free. 2710 if cfg[MD_PC_DOWN_TICK_MS] > 0 { 2711 let dtries: i64 = md_probe_down_tries(cfg) 2712 var d: i64 = 0 2713 var scanning: i64 = 1 2714 while scanning == 1 { 2715 if md_tcp_alive(port) == 0 { saw_down = 1; scanning = 0 } 2716 else { 2717 d = d + 1 2718 if d >= dtries { scanning = 0 } else { sys_sleep_ms(cfg[MD_PC_DOWN_TICK_MS]) } 2719 } 2720 } 2721 } 2722 // PHASE 2 -- the incoming process must LISTEN inside the up budget. This is the tooth that fires on the 2723 // exact failure debt 1786068492 describes: a new process that starts, cannot bind, and exits. 2724 var at: i64 = 0 2725 while at < cfg[MD_PC_UP_TRIES] { 2726 if md_tcp_alive(port) == 1 { 2727 if saw_down == 1 { return MD_HP_VERIFIED } 2728 return MD_HP_UNVERIFIED 2729 } 2730 saw_down = 1 // a REFUSED seen during the up wait IS the same edge, observed later 2731 at = at + 1 2732 if at < cfg[MD_PC_UP_TRIES] { sys_sleep_ms(cfg[MD_PC_UP_TICK_MS]) } 2733 } 2734 return MD_HP_NEVER_UP 2735} 2736 2737// PRODUCTION ENTRY POINT: load the conf, then run the SAME logic the gate exercises. There is exactly ONE 2738// implementation of the edge check; this wrapper only supplies the budget. 2739func md_probe_listen_edge(port: i64) -> i64 { 2740 let cfg: *i64 = sys_mmap(8 * MD_PC_SLOTS) as *i64 2741 md_probe_conf(cfg) 2742 return md_probe_listen_edge_cfg(port, cfg) 2743} 2744 2745// Named-outcome health for the DEPLOY watchdog. port:<N> -> edge-verified listener check. Anything else -> the 2746// HTTP leg unchanged and still level-triggered ON PURPOSE: an HTTP 200 with a non-empty body already proves an 2747// APPLICATION answered, which is strictly stronger than "something holds the socket", so an edge phase would 2748// add nothing there. 2749func md_deploy_health(url: *u8) -> i64 { 2750 if md_url_is_port(url) == 1 { return md_probe_listen_edge(md_url_port(url)) } 2751 if md_health_probe(url) == 1 { return MD_HP_VERIFIED } 2752 return MD_HP_NEVER_UP 2753} 2754 2755// The status SUFFIX for each outcome. The FIRST token of the status line is deliberately left to the caller and 2756// unchanged (DEPLOYED-GREEN / ROLLED-BACK), so every existing reader of /api/deploy_status keeps working and 2757// this is purely ADDITIVE -- rule 19. 2758func md_hp_suffix(code: i64) -> *u8 { 2759 if code == MD_HP_VERIFIED { return " listen=VERIFIED" as *u8 } 2760 if code == MD_HP_UNVERIFIED { return " listen=UNVERIFIED-may-be-outgoing-process" as *u8 } 2761 return " listen=NEVER-LISTENED" as *u8 2762} 2763 2764func md_health_probe(url: *u8) -> i64 { 2765 // "port:<N>" -> LOCAL TCP-connect health (no nx_research_fetch/CA/edge dependency -- the robust default for 2766 // restart-targets: the earlier /torrent HTTP probe needed an on-NAS fetcher+CA that isn't at the mgmt cwd, 2767 // so it always failed -> conservative rollback). Otherwise the HTTP-fetch probe below. Both retry 10x3s. 2768 if url[0]==(112 as u8) { if url[1]==(111 as u8) { if url[2]==(114 as u8) { if url[3]==(116 as u8) { if url[4]==(58 as u8) { 2769 var pt: i64=0; var pi: i64=5; while url[pi]!=(0 as u8) { if url[pi]>=(48 as u8) { if url[pi]<=(57 as u8) { pt=pt*10+((url[pi] as i64)-48) } } pi=pi+1 } 2770 // DELEGATES to the edge-verified probe so there is exactly ONE listener-verification implementation in 2771 // this plane. The 1/0 contract of THIS function is preserved EXACTLY (up=1, down=0) -- the richer 2772 // three-state answer is reached through md_deploy_health, so no existing 1/0 consumer changes meaning. 2773 if md_probe_listen_edge(pt) == MD_HP_NEVER_UP { return 0 } 2774 return 1 2775 } } } } } 2776 let pargs: *i64 = sys_mmap(16) as *i64 2777 pargs[0] = url as i64 2778 let pbuf: *u8 = sys_mmap(16384) 2779 // 10 tries x 3s = up to 30s: GENEROUS, because /api/deploy now runs this in a DETACHED watchdog (not on the 2780 // request path) -> it no longer races the edge-proxy read window, so it can wait out a slow guard-respawn 2781 // (~10-15s) and confirm 200+body -> GREEN, instead of a premature conservative rollback. 2782 var attempt: i64 = 0 2783 while attempt < 10 { 2784 dep_run_capture("_offc/nx_research_fetch.elf" as *u8, pargs, 1, "/tmp/nx_ma_deploy_health.out" as *u8) 2785 let pn: i64 = dp_read("/tmp/nx_ma_deploy_health.out" as *u8, pbuf, 16380) 2786 let st: i64 = hh_after(pbuf, pn, "status=" as *u8) 2787 let bbn: i64 = hh_after(pbuf, pn, "body_bytes=" as *u8) 2788 if st == 200 { if bbn > 0 { return 1 } } 2789 attempt = attempt + 1 2790 if attempt < 10 { sys_sleep_ms(3000) } 2791 } 2792 return 0 2793} 2794 2795// Private integration of the existing registered-target and file-session owners. 2796// Canary executable/argv/budgets are supplied by the authenticated host policy, 2797// never copied from request form fields. Caller retains the session through its 2798// durable outcome receipt, then closes it on every path. 2799struct NxToolchainSessionResult { 2800 target: NxDeployTargetRecord, 2801 session: NxFileInstallSession, 2802 reverse_admission: NxFileInstallResult, 2803 publication: NxFileInstallResult, 2804 rollback: NxFileInstallResult, 2805 stage: *u8, 2806 code: i64, 2807 canary_attempted: i64, 2808 canary_exit: i64, 2809 canary_bytes: i64, 2810 canary_truncated: i64, 2811 rollback_attempted: i64, 2812} 2813func md_tc_session_init(out:*NxToolchainSessionResult)->i64 { 2814 md_target_init(&out.target);fi_session_init(&out.session) 2815 fi_session_result_init(&out.reverse_admission);fi_session_result_init(&out.publication);fi_session_result_init(&out.rollback) 2816 out.stage="toolchain-input";out.code=FIO_EINVAL 2817 out.canary_attempted=0;out.canary_exit=0;out.canary_bytes=0;out.canary_truncated=0;out.rollback_attempted=0;return 0 2818} 2819func md_tc_session_close(out:*NxToolchainSessionResult)->i64 { 2820 let released:i64=fi_session_close(&out.session) 2821 let closed:i64=md_target_close(&out.target) 2822 if released!=0 { return released };return closed 2823} 2824func md_tc_session_run(registry:*u8,snapshot_budget:i64,target:*u8,forward:*u8,forward_hash:*u8,reverse:*u8,reverse_hash:*u8,intent_budget:i64,canary:*u8,args:*i64,deadline_ms:i64,scratch:*u8,scratch_bytes:i64,capture:*u8,capture_bytes:i64,out:*NxToolchainSessionResult)->i64 { 2825 if (out as i64)==0 { return FIO_EINVAL } 2826 if out.session.held==1 { return FIO_EEXIST } 2827 md_tc_session_close(out);md_tc_session_init(out) 2828 if (target as i64)==0 || md_toolchain_target_ok(target)!=1 || deadline_ms<=0 || (args as i64)==0 || fi_path_valid(canary)==0 || (capture as i64)==0 || capture_bytes<=0 || (scratch as i64)==0 || scratch_bytes<=0 { return out.code } 2829 out.stage="toolchain-target-resolution" 2830 out.code=md_target_read(registry,snapshot_budget,target,fi_len(target),&out.target) 2831 if out.code!=0 { return out.code } 2832 out.stage="toolchain-target-kind" 2833 if md_streq(out.target.kind_text,"toolchain")!=1 && md_streq(out.target.kind_text,"toolchain-v2")!=1 { out.code=FI_EACCES;return out.code } 2834 out.stage="toolchain-session-begin" 2835 out.code=fi_session_begin(forward,intent_budget,out.target.live,forward_hash,&out.session) 2836 if out.code!=0 { return out.code } 2837 out.stage="toolchain-staged-binding" 2838 if md_streq(out.session.plan.source,out.target.staged)!=1 { out.code=FI_EACCES;return out.code } 2839 out.stage="toolchain-reverse-admission" 2840 out.code=fi_session_prepare_reverse(&out.session,reverse,intent_budget,reverse_hash,scratch,scratch_bytes,&out.reverse_admission) 2841 if out.code!=0 { return out.code } 2842 out.stage="toolchain-publish" 2843 out.code=fi_session_publish(&out.session,scratch,scratch_bytes,&out.publication) 2844 if out.code==0 { 2845 out.stage="toolchain-canary";out.canary_attempted=1 2846 out.canary_exit=tr_run_capture_tr(canary,args,capture,capture_bytes,&out.canary_bytes,deadline_ms,&out.canary_truncated) 2847 if out.canary_exit!=0 || out.canary_truncated!=0 { out.code=FIO_EIO } 2848 } 2849 if out.code!=0 && out.publication.replacement.publication.visible==1 { 2850 out.stage="toolchain-rollback";out.rollback_attempted=1 2851 let restored:i64=fi_session_reverse(&out.session,reverse,intent_budget,reverse_hash,scratch,scratch_bytes,&out.rollback) 2852 if restored!=0 { out.code=restored;out.stage="toolchain-rollback-needs-intervention" } 2853 else { out.stage="toolchain-rejected-restored" } 2854 } else { if out.code==0 { out.stage="toolchain-canary-accepted-receipt-pending" } } 2855 return out.code 2856} 2857// Native minter owns fallible key reads and signing. No shared token file. 2858func md_issue_cap_at(keyfile:*u8,allow:*u8,allen:i64,exp:i64,nonce:i64,out:*u8,capacity:i64)->i64{ 2859 return cm_mint_file(keyfile,allow,allen,exp,nonce,out,capacity) 2860}