code wiki / _hdl_build / nx_mgmt_deploy_data_candidate_t185.nx

nx_mgmt_deploy_data_candidate_t185.nx source

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