code wiki / _hdl_build / nx_sov_build_run.nx

nx_sov_build_run.nx source

↩ module page · 1868 lines · 123764 B

1// nx_sov_build_run.nx -- FULLY SOVEREIGN, REUSABLE build runner (operator: "no sh and no c build, 2// from the hardware layer up"). Generalizes nx_retire_gcc_orchestrator from hardcoded targets to ANY 3// module passed as argv[1]. The build path is bits-up sovereign end to end: 4// nx_cc_sovereign.elf <module>.nx -> _build/<module>.s (the team's SELF-HOSTED compiler) 5// nxasm_x86_main.elf <module>.s -> _build/<module>.sov.elf (the team's x86-64 assembler+linker) 6// _build/<module>.sov.elf (run it; exit = its exit) 7// ⚠OUTPUT PATH IS _build/, RELATIVE TO CWD -- NOT /tmp/. These three lines said /tmp/ until 2026-07-30, 8// left stale by the flock change that moved artifacts to a per-target _build/<name>.lock+.s+.sov.elf so 9// concurrent sweeps stop overwriting each other mid-build. A doc that disagrees with the code is a defect, 10// not a nit: it is why callers hunt for the artifact, and it hid a REAL brick hazard -- nx_hostctl 11// cmd_buildrun (nx_hostctl.nx) ONCE read only /tmp/<name>.sov.elf, which made this runner and that 12// supervisor a MATCHED PAIR that had to ship together or every /api/build failed. 13// CLOSED -- VERIFIED 2026-08-17 BY READING THE LIVE SOURCE, not by assuming: cmd_buildrun now probes the 14// ABSOLUTE .../nishihost/buildroot/_build/<name>.sov.elf FIRST (the parent is not in the child's CWD, so 15// it must name it absolutely), falls back to /tmp/ for pre-flock builders, and its failure line NAMES 16// BOTH paths. The pairing constraint no longer holds and this runner ships alone. 17// The old line also cited :3089 while the code now sits near :3571 -- a stale line number is the tell. 18// * A STALE HAZARD NOTE IS ITSELF A HAZARD: it makes the next reader either avoid a safe change or 19// hunt a defect that was already fixed. This estate has the receipt -- a standing RED-gate list that 20// nobody re-measured cost NINE sessions the SAME nine investigations, and 5 of the 9 were already 21// green. Re-measure a warning before repeating it. 22// NO gcc, NO bash, NO .sh anywhere. Orchestration is NishiLang sys_fork/dup3/execve/wait4. 23// Recompile-retry guards the known-good compiler's empty-.s nondeterminism. Usage: 24// nx_sov_build_run.elf <module-basename-in-runtime/_hdl_build> 25 26 27// license_tier: ORIGINAL Reuses the _run spine from nx_retire_gcc_orchestrator. 28import "nx_syscalls.nx" 29import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 30import "nx_sha256.nx" // CONTENT identity for the tree-canon admission (2026-08-06) 31import "nx_import.nx" // the SAME closure resolver nx_cc uses -> content-addressed build cache (B2, 2026-08-17) 32import "nx_builddeploy_lib.nx" // THE ruler for "may this build write a consumer-visible binary" (2026-08-26) 33import "nx_build_key_identity_lib.nx" 34import "nx_build_canon_input_lib.nx" // Refuse absent or malformed admission evidence before compilation. 35 36// TOOLCHAIN NAMES -- the binaries that BUILD everything else, so they may never be installed as a 37// side effect of a build (see the refusal at the install site; seq1464). 38// ⚠BOTH COMPILER NAMES: the module is `nx_compile_x86` but it ships as `nx_cc_sovereign.elf`, so a 39// guard that knew only one name would leave the other door open -- which is exactly the door the 40// regressed compiler came through. `nx_sov_build_run` is listed because a runner that can overwrite 41// ITSELF mid-build is the same hazard pointed inward. 42// st_mtime seconds, -1 when absent. Offset 88 is the channel proven by _freshness_gate T5 and used 43// unchanged by nx_staging_guard / nx_tree_diff / nx_gate_verdict_lib -- same offset, never a fourth copy 44// of the number. Used by the canon admission to tell STALENESS from a FORK. 45const SBR_STATBUF: i64 = 256 46const SBR_STAT_MTIME_OFF: i64 = 88 47func sbr_mtime(path: *u8) -> i64 { 48 let sb: *u8 = sys_mmap(SBR_STATBUF) 49 if sys_fstatat(path, sb) != 0 { sys_munmap(sb, SBR_STATBUF); return 0 - 1 } 50 let p: *i64 = ((sb as i64) + SBR_STAT_MTIME_OFF) as *i64 51 let v: i64 = p[0] 52 sys_munmap(sb, SBR_STATBUF) 53 return v 54} 55 56func sbr_name_is(a: *u8, b: *u8) -> i64 { 57 var i: i64 = 0 58 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 59 if b[i] != (0 as u8) { return 0 } 60 return 1 61} 62func sbr_is_toolchain(name: *u8) -> i64 { 63 if sbr_name_is(name, "nx_compile_x86" as *u8) == 1 { return 1 } 64 if sbr_name_is(name, "nx_cc_sovereign" as *u8) == 1 { return 1 } 65 if sbr_name_is(name, "nxasm_x86_main" as *u8) == 1 { return 1 } 66 if sbr_name_is(name, "nxasm_x86" as *u8) == 1 { return 1 } 67 if sbr_name_is(name, "nx_sov_build_run" as *u8) == 1 { return 1 } 68 return 0 69} 70 71// ⚠⚠A DAEMON'S SERVING-ROOT TWIN IS NOT A CACHE, IT IS THE DEPLOYED ARTIFACT (2026-08-07). 72// refresh-IF-PRESENT is right for an ordinary organ: the next fork picks up the fresh bytes and 73// LM-026 staleness is retired. For a DAEMON it is a DEPLOY -- the sovereign supervisor health-probes, 74// finds the process unresponsive, and respawns FROM DISK, so the refreshed file becomes the running 75// binary with no promote, no canary, no health gate, no .prev and no rollback. 76// This is the SAME argument the toolchain guard above already makes, applied to the class that has the 77// same property: the artifact is re-executed by something other than the caller. 78// ★★★★★★ A FLAG WHOSE NAME PROMISES NO SIDE EFFECT, ON A TREE WHERE A SUPERVISOR RESPAWNS FROM 79// DISK, IS A DEPLOY WITH EXTRA STEPS. 80// MEASURED INCIDENT: `nx_sov_build_run nx_tools_api_serve --build-only` refreshed the serving-root 81// twin; the supervisor respawned onto it; netstat then showed ZERO listeners on :18096 with four live 82// instances. The agent surface was down for every seat. The caller had explicitly chosen NOT to deploy 83// and believed --build-only meant it. 84// Daemons have exactly ONE lawful update route: an organ_kind.conf row plus health-checked 85// /api/deploy, which validates, banks .prev and auto-rolls-back. This makes that route the only one 86// BY CONSTRUCTION rather than by convention -- the same upgrade the toolchain got after seq1464. 87// 88// RETURNS 1 declared daemon · 0 not a daemon · -1 UNPROVEN (conf unreadable). 89// The catastrophic few are ALSO hardcoded, so a missing or edited conf cannot silently re-open the 90// hole on the two surfaces every other seat depends on. ★ DATA-DRIVE THE LIST, HARDCODE THE BLAST. 91func sbr_is_daemon(name: *u8) -> i64 { 92 if sbr_name_is(name, "nx_tools_api_serve" as *u8) == 1 { return 1 } 93 if sbr_name_is(name, "nx_mgmt_api" as *u8) == 1 { return 1 } 94 let szp: *i64 = sys_mmap(16) as *i64 95 // ⚠THIS PATH MUST TOLERATE THE CWD ANCHOR -- FIXED 2026-08-15, AND IT HAD DISABLED THE GUARD. 96 // main() chdir's into buildroot/ whenever the runner is started from the serving root (which is how 97 // nx_job_run, the supervisor and every operator invoke it). This BARE path therefore resolved to 98 // buildroot/knowledge/status/organ_kind.conf, WHICH DOES NOT EXIST -- the conf lives at the nishihost 99 // root (37,596 B, verified both ways). sys_read_file returned 0, this returned -1 = UNPROVEN, and the 100 // caller's UNPROVEN branch WRITES THE BINARY ANYWAY and prints its warning AFTERWARDS. 101 // NET EFFECT, and it is not theoretical: the daemon guard has been silently OFF since the anchor 102 // landed, so every daemon built through this lane went straight to the SERVING ROOT with no canary, 103 // no health gate and no .prev -- the supervisor respawns from disk, so that write IS a deploy. 104 // MEASURED TODAY: a `--build-only` invocation deployed nx_docportal_admin_daemon. 105 // This same file ALREADY learned the lesson for tree-canon ("the manifest is ../knowledge/... because 106 // main() anchors CWD to buildroot") and applied it there. It was missed here. 107 // ★★★★★★A GUARD THAT CANNOT READ ITS OWN RULEBOOK DOES NOT FAIL LOUD, IT FAILS ABSENT -- AND AN 108 // ABSENT GUARD IS INDISTINGUISHABLE FROM A GUARD THAT APPROVED. The build-admission check twenty 109 // lines below gets the same question right (rc=5 CANNOT-MEASURE -> REFUSE); this one waved through. 110 // Anchored path FIRST, bare path second, so the answer is correct from EITHER cwd rather than 111 // trading one broken vantage for the other. 112 var buf: *u8 = sys_read_file("../knowledge/status/organ_kind.conf" as *u8, szp) 113 if (buf as i64) == 0 { buf = sys_read_file("knowledge/status/organ_kind.conf" as *u8, szp) } 114 if (buf as i64) == 0 { return 0 - 1 } 115 let n: i64 = szp[0] 116 if n <= 0 { return 0 - 1 } 117 var nl: i64 = 0 118 while name[nl] != (0 as u8) { nl = nl + 1 } 119 let pat: *u8 = "daemon" as *u8 120 var i: i64 = 0 121 while i < n { 122 // ⚠⚠THE EOL SCAN USED A SENTINEL THAT CLOBBERED ITS OWN CURSOR -- FIXED 2026-08-15, AND IT HAD 123 // MADE THIS ENTIRE LOOKUP DEAD. The old form was: 124 // while e < n { if buf[e] == 10 { e = n + 1 } else { e = e + 1 } } 125 // if e > n { e = e - 1 } 126 // On finding a newline it wrote n+1 INTO e to break, destroying the position it had just found; 127 // e-1 then yields n, never the newline index. So every "line" spanned the whole rest of the 128 // buffer, `i = e + 1` pushed i past n, and the outer loop ran EXACTLY ONCE -- on a first line 129 // that begins with '#' and is skipped as a comment. sbr_is_daemon therefore returned 0 for every 130 // name whenever the file was readable: not a wrong answer, a lookup that never ran. 131 // ★★★★★★THE TELL WAS ALREADY IN THE SOURCE: the two hardcoded `nx_tools_api_serve` / 132 // `nx_mgmt_api` early-returns above are a workaround for a table lookup that never matched. WHEN 133 // A DATA-DRIVEN CHECK HAS HARDCODED EXCEPTIONS, SUSPECT THE LOOKUP, NOT THE DATA. 134 // This is the estate's named recurring defect ("a loop-exit sentinel written into the search 135 // cursor erases the answer -- use a flag"); separate cursor, explicit flag, cursor preserved. 136 var e: i64 = i 137 var eol: i64 = 0 138 while eol == 0 { 139 if e >= n { eol = 1 } else { 140 if buf[e] == (10 as u8) { eol = 1 } else { e = e + 1 } 141 } 142 } 143 if buf[i] != (35 as u8) { 144 if i + nl < e { 145 var k: i64 = 0 146 var ok: i64 = 1 147 while k < nl { if buf[i+k] != name[k] { ok = 0; k = nl } else { k = k + 1 } } 148 if ok == 1 { 149 if buf[i+nl] == (32 as u8) { 150 var j: i64 = i + nl 151 var found: i64 = 0 152 while j + 6 <= e { 153 var m: i64 = 0 154 var eq: i64 = 1 155 while m < 6 { if buf[j+m] != pat[m] { eq = 0; m = 6 } else { m = m + 1 } } 156 if eq == 1 { found = 1; j = e } 157 j = j + 1 158 } 159 if found == 1 { return 1 } 160 } 161 } 162 } 163 } 164 i = e + 1 165 } 166 return 0 167} 168const SBR_MAGIC_4096: i64 = 4096 // read-chunk size (folded from the buildroot branch's rule-11 sweep, 2026-07-29 merge) 169 170// ---- CONTENT IDENTITY FOR THE TREE-CANON ADMISSION (2026-08-06) ---- 171// The admission below used to compare BYTE SIZES, and its own comment said so honestly: 172// "SIZE IS A SCREEN, NOT PROOF OF IDENTITY -- equal-size rewrites pass here; 173// nx_treecanon_gate stays the tree-level instrument." 174// THAT DELEGATION WAS NOT AN IMPLEMENTATION. nx_treecanon_gate compared sizes too, so NOTHING in 175// the build lane ever checked content, and each half looked reasonable only because it assumed the 176// other half was doing the work. A MUTATION-CLASS DEFECT PRESERVES SIZE BY CONSTRUCTION, and one 177// had already escaped into this very buildroot: _hdl_build/nx_media_extract.nx held `if hit != 0` 178// where the authoring tree held `if hit == 0` -- one byte, 4389 bytes on BOTH sides. 179// Canon rows now compare sha256 whenever the authoring tree publishes a digest manifest, and fall 180// back to the byte-size screen when it does not. STRICTLY STRONGER, NEVER WEAKER. 181const SBR_SHA_HEX: i64 = 64 182const SBR_QMARK: i64 = 63 // '?' fills the digest slot when a file is unreadable / row absent 183// sys_read_file reserves filesize+16, but 4 GiB when lseek(END) reports 0 -- freeing an empty file 184// as "0+16" would leak 4 GiB of address space per call and walk into RLIMIT_AS. 185const SBR_EMPTY_RESERVE: i64 = 4294967296 186func sbr_sha_hex(path: *u8, out: *u8) -> i64 { 187 let ln: *i64 = sys_mmap(16) as *i64 188 ln[0] = 0 189 let buf: *u8 = sys_read_file(path, ln) 190 if (buf as i64) == 0 { sys_munmap(ln as *u8, 16); return 0 - 1 } 191 let n: i64 = ln[0] 192 let d: *u8 = sys_mmap(48) 193 sha256_digest(buf, n, d) 194 if n > 0 { sys_munmap(buf, n + 16) } else { sys_munmap(buf, SBR_EMPTY_RESERVE + 16) } 195 sys_munmap(ln as *u8, 16) 196 var i: i64 = 0 197 while i < 32 { 198 let v: i64 = d[i] as i64 199 let hi: i64 = (v >> 4) & 15 200 let lo: i64 = v & 15 201 if hi < 10 { out[i*2] = (48+hi) as u8 } else { out[i*2] = (87+hi) as u8 } 202 if lo < 10 { out[i*2+1] = (48+lo) as u8 } else { out[i*2+1] = (87+lo) as u8 } 203 i = i + 1 204 } 205 sys_munmap(d, 48) 206 out[SBR_SHA_HEX] = 0 as u8 207 return 0 208} 209 210const SBR_OK: i64 = 0 211const SBR_USAGE: i64 = 2 212const SBR_COMPILE_FAIL: i64 = 3 213const SBR_ASM_FAIL: i64 = 4 214const SBR_ADMIT_REFUSED: i64 = 6 // build admission said no; distinct from a compile/assemble failure 215 // (3 and 4 raw would COLLIDE with COMPILE_FAIL/ASM_FAIL -- a retry-later 216 // signal must never decode as a broken build) 217const SBR_CANON_REFUSED: i64 = 7 // canon evidence unavailable, invalid, or divergent 218const SBR_CANON_PATH_BYTES: i64 = 512 219const SBR_RUNTIME_PREFIX_BYTES: i64 = 8 220const SBR_MIN_ASM_BYTES: i64 = 128 // empty/failed .s is ~0-byte header; the smallest real program (_min42) is 540B 221const SBR_MAX_RETRIES: i64 = 12 222// ---- BUILD DEADLINE (2026-08-16) --------------------------------------------------------------- 223// A build that HANGS is not a build that FAILED, and the two demand OPPOSITE responses -- the same 224// law the exit codes above already encode ("a retry-later signal must never decode as a broken 225// build"). MEASURED that day: nx_cc SPUN FOREVER on a specific source (a br_comp_border variant in 226// nx_browser_bisect.nx). With no deadline anywhere in this runner the child burned a core 227// indefinitely; two such spinners took the host from load_centi=542 to 1394, at which point 228// nx_ctxtop REFUSED to run ("host already saturated"). Only a human noticing stopped it. 229// nx_gate_build_sweep already carries "timeout per-build deadline in ms, default 180000" -- this is 230// that same shape, applied to the runner every organ is built through. 231const SBR_TIMEOUT_RC: i64 = 0 - 9 // sbr_run sentinel: NEGATIVE, so it cannot collide with a real 232 // exit code (0..255) nor with the 128+signal convention above 233const SBR_BUILD_TIMEOUT: i64 = 8 // process exit code; next free after CANON_REFUSED=7 234const SBR_SIGKILL: i64 = 9 235const SBR_POLL_MS: i64 = 100 // wait4(WNOHANG) poll interval; fork+WNOHANG liveness is already 236 // host-proven by nx_initlive_probe -- composed, not re-verified 237const SBR_NO_DEADLINE: i64 = 0 // explicit: RUNNING the built program must never be time-boxed 238// DERIVED FROM MEASUREMENT, NOT TASTE: the largest closure this runner compiles in practice is 239// nx_browser's 29-import set (9,243,150 B of .s), which completed in under 5 minutes on this host, 240// timed repeatedly on 2026-08-16. 900000 ms is 3x that worst observed case, so a legitimate build 241// cannot trip it while an infinite loop is still bounded. Raise it here if a bigger closure appears 242// -- and if you do, say what you measured. 243const SBR_BUILD_DEADLINE_MS: i64 = 900000 244 245func sbr_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 246// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 247// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 248// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 249// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 250func sbr_putn(v: i64) -> i64 { nxi_out(v); return 0 } 251 252// append NUL-terminated s into dst at off; return new offset (no NUL written) 253func sbr_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i } 254 255// bounded byte-range equality on (base,offset,len) pairs -- conf/manifest lines are not NUL-terminated 256func sbr_ceq_at(a: *u8, ao: i64, al: i64, b: *u8, bo: i64, bl: i64) -> i64 { 257 if al != bl { return 0 } 258 var i: i64 = 0 259 while i < al { if a[ao+i] != b[bo+i] { return 0 } i = i + 1 } 260 return 1 261} 262 263func sbr_exitcode(status: i64) -> i64 { return (status >> 8) & 0xff } 264 265// Decimal argv value -> integer, or -1 on ANYTHING malformed (empty, sign, units, stray char). 266// REFUSES rather than defaulting: a deadline that silently became 0 would restore the exact 267// unbounded-hang this flag exists to prevent, and it would do it while looking configured. 268func sbr_atoi(s: *u8) -> i64 { 269 var v: i64 = 0 270 var i: i64 = 0 271 while s[i] != (0 as u8) { 272 let c: i64 = s[i] as i64 273 if c < 48 { return 0 - 1 } 274 if c > 57 { return 0 - 1 } 275 v = v * 10 + (c - 48) 276 i = i + 1 277 } 278 if i == 0 { return 0 - 1 } 279 return v 280} 281 282// fork + optional stdout/stderr redirect + execve; parent waits; returns child's WEXITSTATUS, 283// OR 128+signal if the child died to a signal (shell convention). Without the signal check a 284// SEGFAULTED tool decodes as rc=0 = silent fake success (caught live 2026-06-09: nxasm segfaulted 285// on the 2.4MB compiler .s, the runner reported the assemble step OK, and the missing ELF 286// surfaced later as a confusing execve-127). Composes [[feedback-evidence-driven-live-grading]]. 287// redir_err>=0 -> child stderr (e.g. /dev/null to mute nx_cc's symbol-table dump). 288// deadline_ms > 0 time-boxes the child: poll wait4(WNOHANG), and on expiry SIGKILL it, REAP it (never 289// leave a zombie behind a timeout) and return SBR_TIMEOUT_RC. deadline_ms = SBR_NO_DEADLINE waits 290// forever, which is correct for RUNNING the built program -- a daemon or a long job must not be shot. 291func sbr_run(path: *u8, argv: *i64, envp: *i64, redir_out: i64, redir_err: i64, deadline_ms: i64) -> i64 { 292 let pid: i64 = sys_fork() 293 if pid == 0 { 294 if redir_out >= 0 { sys_dup3(redir_out, 1, 0) } 295 if redir_err >= 0 { sys_dup3(redir_err, 2, 0) } 296 sys_execve(path, argv, envp) 297 sys_exit(127) 298 } 299 let st: *i64 = sys_mmap(16) as *i64 300 if deadline_ms <= 0 { 301 sys_wait4(pid, st, 0) 302 } else { 303 // Deadline measured against the CLOCK, not against a count of sleeps: accumulating the poll 304 // interval drifts by however long each wait4 itself took, and a drifting deadline is a 305 // threshold nobody can state. 306 let t0: i64 = sys_now_ms() 307 var settled: i64 = 0 308 while settled == 0 { 309 let r: i64 = sys_wait4(pid, st, WNOHANG) 310 if r == pid { settled = 1 } 311 if settled == 0 { 312 if (sys_now_ms() - t0) >= deadline_ms { 313 nx_kill(pid, SBR_SIGKILL) 314 sys_wait4(pid, st, 0) 315 sbr_puts("[nx_sov_build_run] PROCESS-TIMEOUT after " as *u8) 316 sbr_putn(deadline_ms) 317 sbr_puts("ms -- child pid " as *u8) 318 sbr_putn(pid) 319 sbr_puts(" (" as *u8) 320 sbr_puts(path) 321 sbr_puts(") was KILLED and reaped. The process exceeded its deadline.\n" as *u8) 322 sbr_puts(" This is not a successful test or a source rejection. Inspect the named process and\n" as *u8) 323 sbr_puts(" retained stage evidence before selecting a retry or repair.\n" as *u8) 324 return SBR_TIMEOUT_RC 325 } 326 sys_sleep_ms(SBR_POLL_MS) 327 } 328 } 329 } 330 let sig: i64 = st[0] & 0x7f 331 if sig != 0 { return 128 + sig } 332 return sbr_exitcode(st[0]) 333} 334 335// byte-size of a file (open, read in chunks, count) -- the determinism check for the empty-.s guard. 336func sbr_filesize(path: *u8) -> i64 { 337 let fd: i64 = sys_openat_rd(path) 338 if fd < 0 { return 0 } 339 let buf: *u8 = sys_mmap(SBR_MAGIC_4096) 340 var total: i64 = 0 341 var n: i64 = sys_read(fd, buf, SBR_MAGIC_4096) 342 while n > 0 { total = total + n; n = sys_read(fd, buf, SBR_MAGIC_4096) } 343 sys_close(fd) 344 return total 345} 346 347// ---- CONTENT-ADDRESSED BUILD CACHE (B2, 2026-08-17) -------------------------------------------------- 348// The compiler is FAST (measured 80ms/gate) but re-emits the whole unchanged import closure every build. 349// This caches the FINAL .sov.elf keyed on a hash that covers EVERY compile input -- the fully-expanded 350// source closure (via nx_cc's OWN resolver expand_imports, composition not a second ruler), the compiler 351// AND assembler binaries, and the -g flag. A hit is byte-identical BY CONSTRUCTION: identical key => every 352// input identical => the deterministic toolchain emits identical bytes. FAIL-OPEN everywhere -- a miss, 353// an expand overflow, or any error just compiles normally, so the cache can only speed up, never break. 354// Toolchain targets are never cached (their own binary is in every key; a self-cache is meaningless). 355// COMPLETED 2026-08-23 (rung B2 of /compare/toolchain, watch symbol bld_cache_cas): the first cut stored 356// an entry by plain copy and served any entry that OPENED. That left three gaps the rung's own risk row 357// names: (1) a hit was never byte-verified, so a truncated or mid-write entry would be served as a HIT; 358// (2) the store was not atomic, so a concurrent reader could see a half-written elf; (3) a MISS was 359// SILENT, so "the cache missed" and "the cache is not there" were the same absence of output. 360// Now: every entry carries a SIDECAR (<key>.sov.elf.sha: sha256= bytes= key=) written AFTER the elf, 361// both via tmp+renameat; a hit is served ONLY after the cached bytes re-hash to the sidecar, and the 362// copy into _build/ is re-hashed too; every outcome prints ONE line, BUILD-CACHE HIT | MISS reason=<r> 363// | STORE, so a gate can assert on STATE (the .s the compiler writes is ABSENT on a hit) and a reader 364// can see which path ran. Kill switches: `cache_enabled=0` in ../knowledge/buildcache.conf (one parser 365// with cache_max_bytes) and the --no-cache lane flag. bld_cache_cas is the lookup+verify+serve entry; 366// bld_cache_cas_store the populate. Still fail-open on every error: the worst outcome is a compile. 367const SBR_CACHE_EXPAND_CAP: i64 = 33554432 // 32 MiB: biggest measured closure .s is ~9.2 MB (nx_browser); 368 // expand_imports FAILS LOUD on overflow and we fail-open, so a 369 // larger closure simply skips the cache -- safe, never wrong. 370 371// whole-file copy for cache <-> artifact. returns 0 ok, -1 fail (caller treats -1 as "no cache", fail-open). 372// COPY WHAT A COPY MEANS -- MODE IS PART OF THE FILE (2026-08-18). This copier opened its destination 373// 0644 for every byte it wrote, so a BUILD-CACHE HIT reproduced the artifact byte-perfect and INERT: 374// nx_sov_build_run removes the stale _build/<t>.sov.elf before every build, so the hit path always 375// CREATED the file fresh at 0644, and `nx_sov_build_run <t>` then ran it -> exit 127. Measured live 376// the day after B2 shipped (nx_vcodec_layout_gate: BUILD-CACHE HIT ... run-exit=127). B2's own proof 377// was byte identity -- structurally blind to mode, exactly the nx_filecopy defect banked 2026-08-14. 378// The fix is the same one nx_filecopy took: stat the SOURCE and apply ITS mode (st_mode is a u32 at 379// stat offset 24, the channel nx_offc_install proves), announcing UNKNOWN when it cannot stat rather 380// than silently leaving 0644. Every consumer inherits it: a nxasm-emitted 0755 artifact stored into 381// the cache is 0755 there, and a hit that copies it back is 0755 again -- no per-call-site chmod, one 382// mechanism. 383const SBR_STAT_MODE_OFF: i64 = 24 384const SBR_MODE_PERM_MASK: i64 = 511 // 0777: permission bits only; type bits never travel 385func sbr_src_mode(path: *u8) -> i64 { 386 let sb: *u8 = sys_mmap(SBR_STATBUF) 387 if sys_fstatat(path, sb) != 0 { sys_munmap(sb, SBR_STATBUF); return 0 - 1 } 388 let m: i64 = (sb[SBR_STAT_MODE_OFF] as i64) + ((sb[SBR_STAT_MODE_OFF + 1] as i64) << 8) 389 sys_munmap(sb, SBR_STATBUF) 390 return m & SBR_MODE_PERM_MASK 391} 392// STAT-BASED SIZE. Returns -1 when the path is ABSENT and >=0 when it is present, so "gone" and "empty" 393// are two answers rather than one. sbr_filesize() below reads the whole file and returns 0 for BOTH -- an 394// absent-and-empty conflation that is harmless where it is used for a .s byte count and NOT harmless on the 395// one axis that matters here: the artifact a cache HIT claims to have delivered. st_size is the 64-bit field 396// at offset 48 of x86-64 struct stat; st_mode at offset 24 is the same buffer sbr_src_mode already proves, 397// so this opens no new syscall channel. Assembled top byte down, so no intermediate multiplier overflows. 398const SBR_STAT_SIZE_OFF: i64 = 48 399const SBR_STAT_SIZE_W: i64 = 8 400func sbr_stat_size(path: *u8) -> i64 { 401 let sb: *u8 = sys_mmap(SBR_STATBUF) 402 if sys_fstatat(path, sb) != 0 { sys_munmap(sb, SBR_STATBUF); return 0 - 1 } 403 var v: i64 = 0 404 var i: i64 = SBR_STAT_SIZE_W - 1 405 while i >= 0 { 406 v = v * 256 + (sb[SBR_STAT_SIZE_OFF + i] as i64) 407 i = i - 1 408 } 409 sys_munmap(sb, SBR_STATBUF) 410 return v 411} 412func sbr_copyfile(srcp: *u8, dstp: *u8) -> i64 { 413 let ln: *i64 = sys_mmap(16) as *i64 414 ln[0] = 0 415 let buf: *u8 = sys_read_file(srcp, ln) 416 if (buf as i64) == 0 { sys_munmap(ln as *u8, 16); return 0 - 1 } 417 let n: i64 = ln[0] 418 let smode: i64 = sbr_src_mode(srcp) 419 let fd: i64 = sys_openat_wr(dstp, 0x1a4) 420 if fd < 0 { if n > 0 { sys_munmap(buf, n + 16) } sys_munmap(ln as *u8, 16); return 0 - 1 } 421 var off: i64 = 0 422 while off < n { 423 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off) 424 if w <= 0 { sys_close(fd); if n > 0 { sys_munmap(buf, n + 16) } sys_munmap(ln as *u8, 16); return 0 - 1 } 425 off = off + w 426 } 427 sys_close(fd) 428 if n > 0 { sys_munmap(buf, n + 16) } 429 sys_munmap(ln as *u8, 16) 430 // apply the source's permission bits AFTER the write (openat's mode is masked by umask and only 431 // applies to a CREATED file; an existing destination keeps its old bits -- fchmodat sets both cases). 432 if smode >= 0 { nx_chmod(dstp, smode) } 433 if smode < 0 { sbr_puts("[nx_sov_build_run] copy: source mode UNKNOWN (stat failed) -- destination left at the create default\n" as *u8) } 434 return 0 435} 436 437// 32-byte digest -> 64 lowercase hex chars at out (no NUL written). ONE encoder: sbr_cache_key and the 438// sidecar verify used to carry their own copy of this loop. 439func bld_cache_cas_hex(d: *u8, out: *u8) -> i64 { 440 var i: i64 = 0 441 while i < 32 { 442 let v: i64 = d[i] as i64 443 let hi: i64 = (v >> 4) & 15 444 let lo: i64 = v & 15 445 if hi < 10 { out[i*2] = (48+hi) as u8 } else { out[i*2] = (87+hi) as u8 } 446 if lo < 10 { out[i*2+1] = (48+lo) as u8 } else { out[i*2+1] = (87+lo) as u8 } 447 i = i + 1 448 } 449 return 0 450} 451 452// Compute the cache key hex (64 chars into out, null-terminated). Returns 0 on success, -1 fail-open. 453// Debug identity additionally binds the estate-relative root source path via bki_digest. 454// Release identity retains the content/toolchain/mode key for cross-path reuse. 455// THE INCUMBENT'S FLAVOUR, READ FROM ITS BYTES (2026-09-05, operator: "first byte fix"). A debug build carries the 456// DWARF section-name string .debug_line (nxasm writes it under -g; nx_contentdiff partitions exactly these runs as 457// lost_sectname); a release build carries no such run. Returns 1 = debug, 0 = release, -1 = unreadable / absent. 458// The needle is ASSEMBLED from two halves at runtime so THIS organ's own binary never holds it contiguously -- a 459// self-rebuild would otherwise read its own literal as evidence and default every runner build to debug (the 460// detector-counts-itself class, nx_gatelaw_gate 2026-09-03). sys_read_file sizes from the file: no cap, no short read. 461func sbr_incumbent_flavour(path: *u8) -> i64 { 462 let ln: *i64 = sys_mmap(16) as *i64 463 ln[0] = 0 464 let buf: *u8 = sys_read_file(path, ln) 465 if (buf as i64) == 0 { sys_munmap(ln as *u8, 16); return 0 - 1 } 466 let n: i64 = ln[0] 467 let needle: *u8 = sys_mmap(32); var no: i64 = 0 468 no = sbr_cat(needle, no, ".debug_" as *u8); no = sbr_cat(needle, no, "line" as *u8); needle[no] = 0 as u8 469 var found: i64 = 0 470 var i: i64 = 0 471 while found == 0 { 472 if i + no > n { found = 0 - 1 } else { 473 var j: i64 = 0 474 var ok: i64 = 1 475 while j < no { 476 if buf[i + j] != needle[j] { ok = 0; j = no } else { j = j + 1 } 477 } 478 if ok == 1 { found = 1 } 479 i = i + 1 480 } 481 } 482 sys_munmap(needle, 32) 483 sys_munmap(buf, n + 16) 484 sys_munmap(ln as *u8, 16) 485 if found == 1 { return 1 } 486 return 0 487} 488 489func sbr_cache_key(src: *u8, compiler: *u8, asm_tool: *u8, want_debug: i64, out: *u8) -> i64 { 490 let ebuf: *u8 = sys_mmap(SBR_CACHE_EXPAND_CAP) 491 let ectx: *ExpandCtx = expand_ctx_new(ebuf, SBR_CACHE_EXPAND_CAP) 492 let erc: i64 = expand_imports(ectx, src) 493 if erc < 0 { sys_munmap(ebuf, SBR_CACHE_EXPAND_CAP); return 0 - 1 } 494 expand_imports(ectx, "runtime/nx_crash.nx" as *u8) // match nx_cc's default guard append (dedup-safe) 495 let opp: *i64 = ectx.out_pos 496 let elen: i64 = *opp 497 let d0: *u8 = sys_mmap(48) 498 sha256_digest(ebuf, elen, d0) 499 let km: *u8 = sys_mmap(256) 500 var kp: i64 = 0 501 bld_cache_cas_hex(d0, km) 502 kp = SBR_SHA_HEX 503 if sbr_sha_hex(compiler, ((km as i64) + kp) as *u8) != 0 { sys_munmap(ebuf, SBR_CACHE_EXPAND_CAP); return 0 - 1 } 504 kp = kp + SBR_SHA_HEX 505 if sbr_sha_hex(asm_tool, ((km as i64) + kp) as *u8) != 0 { sys_munmap(ebuf, SBR_CACHE_EXPAND_CAP); return 0 - 1 } 506 kp = kp + SBR_SHA_HEX 507 if want_debug == 1 { km[kp] = 103 as u8 } else { km[kp] = 110 as u8 } // 'g' | 'n' 508 kp = kp + 1 509 let dk: *u8 = sys_mmap(48) 510 var logical_len: i64 = 0 511 while src[logical_len] != (0 as u8) { logical_len = logical_len + 1 } 512 if bki_digest(km, kp, want_debug, src, logical_len, dk) != 0 { sys_munmap(ebuf, SBR_CACHE_EXPAND_CAP); return 0 - 1 } 513 bld_cache_cas_hex(dk, out) 514 out[SBR_SHA_HEX] = 0 as u8 515 sys_munmap(ebuf, SBR_CACHE_EXPAND_CAP) 516 return 0 517} 518 519// ---- bld_cache_cas: the content-addressed LOOKUP + VERIFY + SERVE entry (rung B2 done-rule) ---------- 520// Sidecar grammar (line-anchored, one field per line, order fixed): sha256=<64 hex> NL bytes=<dec> NL key=<64 hex> NL 521// The sidecar is written AFTER the elf (both tmp+renameat), so "sidecar present" implies "elf complete". 522// Path widths DERIVED: cachepath is at most SBR_CACHE_PATHW (the prune census bound); the sidecar adds 523// ".sha" (4) and a tmp adds ".tmp-" (5) plus the target name, which argv bounds only by PATH_MAX -- so a 524// 4x slot is used and ANY overflow is announced and skipped (fail-open), never truncated. 525const SBR_CACHE_SIDE_SUFFIX: *u8 = ".sha" 526const SBR_CACHE_TMP_INFIX: *u8 = ".tmp-" 527const SBR_CACHE_TMPW: i64 = 512 // 4 x SBR_CACHE_PATHW, see derivation above 528const SBR_CACHE_SIDE_MAX: i64 = 256 // sha256= (7+64) + bytes= (6+20 max i64 digits) + key= (4+64) + 3 NL = 168 < 256 529const SBR_CACHE_ELF_SUFFIX_LEN: i64 = 8 // ".sov.elf" -- the key is the SBR_SHA_HEX hex chars before it 530 531// first line of buf[0..n) that starts with `key`: copy the rest of that line (sans NL) into out[0..cap), 532// NUL-terminate, return its length; -1 when absent or longer than cap-1 (an over-long field is 533// malformed, not truncated-and-trusted). 534func bld_cache_cas_field(buf: *u8, n: i64, key: *u8, out: *u8, cap: i64) -> i64 { 535 var klen: i64 = 0 536 while key[klen] != (0 as u8) { klen = klen + 1 } 537 var i: i64 = 0 538 while i < n { 539 var at_start: i64 = 0 540 if i == 0 { at_start = 1 } 541 if i > 0 { if buf[i-1] == (10 as u8) { at_start = 1 } } 542 if at_start == 1 { 543 var k: i64 = 0 544 var hit: i64 = 1 545 while k < klen { 546 if i + k >= n { hit = 0; k = klen } 547 if hit == 1 { if buf[i+k] != key[k] { hit = 0; k = klen } } 548 if k < klen { k = k + 1 } 549 } 550 if hit == 1 { 551 var j: i64 = i + klen 552 var o: i64 = 0 553 var scanning: i64 = 1 554 while scanning == 1 { 555 if j >= n { scanning = 0 } 556 if scanning == 1 { if buf[j] == (10 as u8) { scanning = 0 } } 557 if scanning == 1 { 558 if o >= cap - 1 { return 0 - 1 } 559 out[o] = buf[j]; o = o + 1; j = j + 1 560 } 561 } 562 out[o] = 0 as u8 563 return o 564 } 565 } 566 i = i + 1 567 } 568 return 0 - 1 569} 570 571// Verify the entry at cachepath against its sidecar. 1 = verified (hex of the artifact's sha256 is left in 572// shaout, its size in *bytesout) | 0 = sha or size MISMATCH | -1 no entry | -2 sidecar absent | -3 sidecar 573// malformed. Reads the WHOLE artifact and re-hashes it every time: a hit that is not re-hashed is a hit 574// that trusts the disk, and the only reason to have a sidecar is not to. 575func bld_cache_cas_verify(cachepath: *u8, shaout: *u8, bytesout: *i64) -> i64 { 576 let ce: i64 = sys_openat_rd(cachepath) 577 if ce < 0 { return 0 - 1 } 578 sys_close(ce) 579 let side: *u8 = sys_mmap(SBR_CACHE_TMPW) 580 var so: i64 = sbr_cat(side, 0, cachepath) 581 so = sbr_cat(side, so, SBR_CACHE_SIDE_SUFFIX); side[so] = 0 as u8 582 let sl: *i64 = sys_mmap(16) as *i64 583 sl[0] = 0 584 let sbuf: *u8 = sys_read_file(side, sl) 585 if (sbuf as i64) == 0 { sys_munmap(side, SBR_CACHE_TMPW); sys_munmap(sl as *u8, 16); return 0 - 2 } 586 let sn: i64 = sl[0] 587 let want_sha: *u8 = sys_mmap(SBR_CACHE_SIDE_MAX) 588 let want_bytes_s: *u8 = sys_mmap(SBR_CACHE_SIDE_MAX) 589 let f1: i64 = bld_cache_cas_field(sbuf, sn, "sha256=" as *u8, want_sha, SBR_CACHE_SIDE_MAX) 590 let f2: i64 = bld_cache_cas_field(sbuf, sn, "bytes=" as *u8, want_bytes_s, SBR_CACHE_SIDE_MAX) 591 if sn > 0 { sys_munmap(sbuf, sn + 16) } else { sys_munmap(sbuf, SBR_EMPTY_RESERVE + 16) } 592 sys_munmap(sl as *u8, 16) 593 sys_munmap(side, SBR_CACHE_TMPW) 594 if f1 != SBR_SHA_HEX { return 0 - 3 } 595 if f2 <= 0 { return 0 - 3 } 596 let want_bytes: i64 = sbr_atoi(want_bytes_s) 597 if want_bytes < 0 { return 0 - 3 } 598 if sbr_sha_hex(cachepath, shaout) != 0 { return 0 - 1 } 599 // BY STAT, NOT BY WHOLE-FILE READ: sbr_filesize returns 0 for an ABSENT file, so an entry that vanished 600 // between the open above and here would be reported as a size MISMATCH (which EVICTS) instead of as the 601 // absence it is. -1 is routed to no-entry, the honest answer. 602 let have_bytes: i64 = sbr_stat_size(cachepath) 603 bytesout[0] = have_bytes 604 if have_bytes < 0 { return 0 - 1 } 605 if have_bytes != want_bytes { return 0 } 606 var i: i64 = 0 607 while i < SBR_SHA_HEX { if shaout[i] != want_sha[i] { return 0 } i = i + 1 } 608 return 1 609} 610 611func bld_cache_cas_say(name: *u8, what: *u8, key: *u8) -> i64 { 612 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": BUILD-CACHE " as *u8); sbr_puts(what) 613 if key != (0 as *u8) { sbr_puts(" key=" as *u8); sbr_puts(key) } 614 return 0 615} 616func bld_cache_cas_nl() -> i64 { let b: *u8 = sys_mmap(2); b[0] = 10 as u8; sys_write(1, b, 1); sys_munmap(b, 2); return 0 } 617 618// THE ENTRY. Returns 1 on a verified HIT (elfpath now holds the byte-identical artifact, mode preserved), 619// 0 on any MISS -- and prints exactly one BUILD-CACHE line either way, naming the reason on a miss. 620// sha-mismatch EVICTS the entry and its sidecar (a corrupt entry must not be offered twice); every other 621// miss leaves the store alone so a concurrent populate in flight is never destroyed. 622func bld_cache_cas(cachekey: *u8, cachepath: *u8, elfpath: *u8, name: *u8) -> i64 { 623 let sha: *u8 = sys_mmap(SBR_SHA_HEX + 2) 624 let bp: *i64 = sys_mmap(16) as *i64 625 bp[0] = 0 626 let v: i64 = bld_cache_cas_verify(cachepath, sha, bp) 627 if v == 0 - 1 { bld_cache_cas_say(name, "MISS reason=no-entry" as *u8, cachekey); bld_cache_cas_nl(); return 0 } 628 if v == 0 - 2 { bld_cache_cas_say(name, "MISS reason=sidecar-absent" as *u8, cachekey); bld_cache_cas_nl(); return 0 } 629 if v == 0 - 3 { bld_cache_cas_say(name, "MISS reason=sidecar-malformed" as *u8, cachekey); bld_cache_cas_nl(); return 0 } 630 if v == 0 { 631 let side: *u8 = sys_mmap(SBR_CACHE_TMPW) 632 var so: i64 = sbr_cat(side, 0, cachepath) 633 so = sbr_cat(side, so, SBR_CACHE_SIDE_SUFFIX); side[so] = 0 as u8 634 sys_unlinkat(cachepath) 635 sys_unlinkat(side) 636 bld_cache_cas_say(name, "MISS reason=sha-mismatch (entry EVICTED: cached bytes do not re-hash to their sidecar)" as *u8, cachekey) 637 bld_cache_cas_nl() 638 return 0 639 } 640 if sbr_copyfile(cachepath, elfpath) != 0 { bld_cache_cas_say(name, "MISS reason=copy-failed" as *u8, cachekey); bld_cache_cas_nl(); return 0 } 641 // the COPY is re-hashed too: a short write into _build/ would otherwise be a verified hit of the wrong bytes 642 let sha2: *u8 = sys_mmap(SBR_SHA_HEX + 2) 643 if sbr_sha_hex(elfpath, sha2) != 0 { bld_cache_cas_say(name, "MISS reason=copy-failed" as *u8, cachekey); bld_cache_cas_nl(); return 0 } 644 var i: i64 = 0 645 while i < SBR_SHA_HEX { 646 if sha[i] != sha2[i] { sys_unlinkat(elfpath); bld_cache_cas_say(name, "MISS reason=copy-failed (copy does not re-hash)" as *u8, cachekey); bld_cache_cas_nl(); return 0 } 647 i = i + 1 648 } 649 // ---- THE ARTIFACT ASSERTION (2026-09-04, LANE-D root fix) ------------------------------------ 650 // A CACHE THAT VERIFIES A SIDECAR WITHOUT STAT-ING THE FILE THE SIDECAR DESCRIBES IS ASSERTING A 651 // PROPERTY OF AN ARTIFACT IT NEVER LOOKED AT. Everything above this line judges the CACHE ENTRY and 652 // the COPY OPERATION. Nothing judged the DELIVERED FILE -- so "byte-identical artifact reused" was an 653 // invariant EMERGING from three helpers agreeing, never an asserted fact, and the builder had no 654 // vocabulary in which to report its failure. MEASURED 2026-09-04 on a copy of this builder with the 655 // copy-back chain removed: it printed BUILD-CACHE HIT ... byte-identical artifact reused, exited 0, 656 // and left NO FILE; every downstream consumer then failed against an artifact announced as reused. 657 // This stat is the only check whose SUBJECT is the delivered artifact rather than the operation that 658 // was supposed to produce it, which is exactly why it survives the removal of that operation. 659 // FAIL DIRECTION IS A MISS, NEVER AN ERROR: returning 0 sends the build down the compile path, so the 660 // worst outcome of this check is one compile -- the same fail-open contract the rest of this cache keeps. 661 let delivered: i64 = sbr_stat_size(elfpath) 662 if delivered != bp[0] { 663 sys_unlinkat(elfpath) 664 bld_cache_cas_say(name, "MISS reason=artifact-not-materialised (a HIT was verified but the artifact is not on disk: stat says " as *u8, 0 as *u8) 665 if delivered < 0 { sbr_puts("ABSENT" as *u8) } 666 if delivered >= 0 { sbr_putn(delivered); sbr_puts(" bytes" as *u8) } 667 sbr_puts(", the verified entry is " as *u8); sbr_putn(bp[0]) 668 sbr_puts(" bytes. NOT claiming reuse; compiling instead)" as *u8) 669 bld_cache_cas_nl() 670 return 0 671 } 672 bld_cache_cas_say(name, "HIT" as *u8, cachekey) 673 sbr_puts(" bytes=" as *u8); sbr_putn(bp[0]); sbr_puts(" sha256=" as *u8); sbr_puts(sha) 674 sbr_puts(" -- verified against its sidecar, byte-identical artifact reused, compiler NOT run" as *u8) 675 bld_cache_cas_nl() 676 return 1 677} 678 679// POPULATE after a real build: elf via tmp+renameat, THEN sidecar via tmp+renameat (ordering is the 680// contract bld_cache_cas_verify relies on). Announces STORE / STORE-FAILED reason=. Fail-open. 681func bld_cache_cas_store(elfpath: *u8, cachepath: *u8, name: *u8) -> i64 { 682 var plen: i64 = 0 683 while cachepath[plen] != (0 as u8) { plen = plen + 1 } 684 var nlen: i64 = 0 685 while name[nlen] != (0 as u8) { nlen = nlen + 1 } 686 if plen + 4 + 5 + nlen + 1 >= SBR_CACHE_TMPW { 687 bld_cache_cas_say(name, "STORE-FAILED reason=path-too-long (not truncated; entry skipped)" as *u8, 0 as *u8); bld_cache_cas_nl() 688 return 0 - 1 689 } 690 let tmp: *u8 = sys_mmap(SBR_CACHE_TMPW) 691 var t: i64 = sbr_cat(tmp, 0, cachepath) 692 t = sbr_cat(tmp, t, SBR_CACHE_TMP_INFIX); t = sbr_cat(tmp, t, name); tmp[t] = 0 as u8 693 if sbr_copyfile(elfpath, tmp) != 0 { sys_unlinkat(tmp); bld_cache_cas_say(name, "STORE-FAILED reason=copy" as *u8, 0 as *u8); bld_cache_cas_nl(); return 0 - 1 } 694 let sha: *u8 = sys_mmap(SBR_SHA_HEX + 2) 695 if sbr_sha_hex(tmp, sha) != 0 { sys_unlinkat(tmp); bld_cache_cas_say(name, "STORE-FAILED reason=hash" as *u8, 0 as *u8); bld_cache_cas_nl(); return 0 - 1 } 696 // SAME LAW, STORE SIDE: the sidecar is a claim ABOUT the staged entry, so size it by stat and refuse 697 // to write a sidecar describing a file that is not there (sbr_filesize would have recorded bytes=0 and 698 // produced an entry that self-evicts as sha-mismatch forever after). 699 let nbytes: i64 = sbr_stat_size(tmp) 700 if nbytes < 0 { 701 sys_unlinkat(tmp) 702 bld_cache_cas_say(name, "STORE-FAILED reason=staged-entry-absent (refusing to write a sidecar describing a file that is not there)" as *u8, 0 as *u8) 703 bld_cache_cas_nl() 704 return 0 - 1 705 } 706 if sys_renameat(tmp, cachepath) != 0 { sys_unlinkat(tmp); bld_cache_cas_say(name, "STORE-FAILED reason=rename" as *u8, 0 as *u8); bld_cache_cas_nl(); return 0 - 1 } 707 // sidecar: build the text, write to tmp, rename over <cachepath>.sha 708 let side: *u8 = sys_mmap(SBR_CACHE_TMPW) 709 var so: i64 = sbr_cat(side, 0, cachepath) 710 so = sbr_cat(side, so, SBR_CACHE_SIDE_SUFFIX); side[so] = 0 as u8 711 let stmp: *u8 = sys_mmap(SBR_CACHE_TMPW) 712 var st: i64 = sbr_cat(stmp, 0, side) 713 st = sbr_cat(stmp, st, SBR_CACHE_TMP_INFIX); st = sbr_cat(stmp, st, name); stmp[st] = 0 as u8 714 let txt: *u8 = sys_mmap(SBR_CACHE_SIDE_MAX) 715 var x: i64 = sbr_cat(txt, 0, "sha256=" as *u8); x = sbr_cat(txt, x, sha); txt[x] = 10 as u8; x = x + 1 716 x = sbr_cat(txt, x, "bytes=" as *u8) 717 x = nxi_buf(txt, x, nbytes) // digits only, returns the offset after them (nx_itoa_lib contract) 718 txt[x] = 10 as u8; x = x + 1 719 // key= is the basename of cachepath between "_build/cache/" and ".sov.elf" -- written so a sidecar 720 // found out of place still says which key it belongs to; the reader does not depend on it. 721 x = sbr_cat(txt, x, "key=" as *u8) 722 var kb: i64 = plen - SBR_CACHE_ELF_SUFFIX_LEN - SBR_SHA_HEX 723 if kb < 0 { kb = 0 } 724 var ki: i64 = 0 725 while ki < SBR_SHA_HEX { if kb + ki < plen { txt[x] = cachepath[kb + ki]; x = x + 1 } ki = ki + 1 } 726 txt[x] = 10 as u8; x = x + 1 727 let sfd: i64 = sys_openat_wr(stmp, 0x1a4) 728 if sfd < 0 { bld_cache_cas_say(name, "STORE-FAILED reason=sidecar-open" as *u8, 0 as *u8); bld_cache_cas_nl(); return 0 - 1 } 729 var woff: i64 = 0 730 while woff < x { 731 let w: i64 = sys_write(sfd, ((txt as i64) + woff) as *u8, x - woff) 732 if w <= 0 { sys_close(sfd); sys_unlinkat(stmp); bld_cache_cas_say(name, "STORE-FAILED reason=sidecar-write" as *u8, 0 as *u8); bld_cache_cas_nl(); return 0 - 1 } 733 woff = woff + w 734 } 735 sys_close(sfd) 736 if sys_renameat(stmp, side) != 0 { sys_unlinkat(stmp); bld_cache_cas_say(name, "STORE-FAILED reason=sidecar-rename" as *u8, 0 as *u8); bld_cache_cas_nl(); return 0 - 1 } 737 bld_cache_cas_say(name, "STORE" as *u8, 0 as *u8) 738 sbr_puts(" bytes=" as *u8); sbr_putn(nbytes); sbr_puts(" sha256=" as *u8); sbr_puts(sha) 739 bld_cache_cas_nl() 740 return 0 741} 742 743// ---- BUILD-CACHE BYTE BUDGET (2026-08-18) -- the cap owed since B2 shipped --------------------- 744// One elf per distinct (closure, toolchain, flags) forever is an unbounded append-only store, and a 745// compiler promote orphans an entire key generation at once (measured the day B1 DCE shipped: all 746// 121 entries dead in one promote). The estate's remedy for an unbounded store is a LOUD budget -- 747// and for a CACHE, eviction is additionally safe by construction: every entry is regenerable from 748// source, so a wrong eviction costs one compile, never data. 749// THE BUDGET IS DATA (rules 11+17): ../knowledge/buildcache.conf key cache_max_bytes overrides the 750// default without a rebuild. THE DEFAULT IS DERIVED, NOT TASTE: 13,202 buildable sources x ~45 KB 751// mean post-DCE elf (558 MB over 12,336 built, measured in the B1 full-population sweep) is ~600 MB 752// for one full compiler generation; the budget must hold one generation plus churn or the daily 753// drift census would thrash its own hits. 1 GiB covers that with headroom on the volume. 754// EVICTION IS OLDEST-mtime-FIRST. Documented imprecision: a HIT does not refresh mtime, so mtime is 755// store order, not use order -- a hot entry can be evicted and costs one re-compile on its next 756// miss. Chosen because touching on hit would put a write on the fast path this cache exists to keep 757// read-only. 758const SBR_CACHE_BUDGET_DEFAULT: i64 = 1073741824 759const SBR_CACHE_DIRBUF: i64 = 65536 760// enumeration pool bound. NOT a silent cap: overflow ANNOUNCES and skips the prune entirely (the 761// cache keeps working; only its janitor abstains), because pruning from a partial listing would 762// evict by an incomplete ordering. 32768 slots is 2.5x the whole estate's source count. 763const SBR_CACHE_PRUNE_SLOTS: i64 = 32768 764// Path slot width, DERIVED not hand-counted (the first cut of this constant was 80 and silently 765// truncated every real entry -- "_build/cache/" is 13 bytes, the key is a 64-hex sha256, ".sov.elf" 766// is 8, plus the NUL = 86 -- so the census dropped the very entries it was sizing and the budget 767// never tripped; caught by strace showing an open of "....s"). 128 = the derivation rounded up to 768// a power of two; anything that still does not fit is SKIPPED AND ANNOUNCED, never truncated. 769const SBR_CACHE_PATHW: i64 = 128 770 771// Read one `<key>=<decimal>` row from ../knowledge/buildcache.conf; absent/unreadable/non-numeric -> dflt. 772// ONE parser for every cache conf row (cache_max_bytes, cache_enabled): the second row to need it is the 773// signal it was a shared primitive, not a second copy of the loop. 774func sbr_cache_conf_int(key: *u8, dflt: i64) -> i64 { 775 let lenp: *i64 = sys_mmap(16) as *i64 776 let cf: *u8 = sys_read_file("../knowledge/buildcache.conf" as *u8, lenp) 777 if (cf as i64) == 0 { return dflt } 778 let n: i64 = lenp[0] 779 var klen: i64 = 0 780 while key[klen] != (0 as u8) { klen = klen + 1 } 781 var i: i64 = 0 782 while i < n { 783 var at_start: i64 = 0 784 if i == 0 { at_start = 1 } 785 if i > 0 { if cf[i-1] == (10 as u8) { at_start = 1 } } 786 if at_start == 1 { 787 var k: i64 = 0 788 var hit: i64 = 1 789 while k < klen { 790 if i + k >= n { hit = 0; k = klen } 791 if hit == 1 { if cf[i+k] != key[k] { hit = 0; k = klen } } 792 if k < klen { k = k + 1 } 793 } 794 if hit == 1 { 795 var v: i64 = 0 796 var got: i64 = 0 797 var j: i64 = i + klen 798 var scanning: i64 = 1 799 while scanning == 1 { 800 if j >= n { scanning = 0 } 801 if scanning == 1 { 802 let c: i64 = cf[j] as i64 803 var isdig: i64 = 0 804 if c >= 48 { if c <= 57 { isdig = 1 } } 805 if isdig == 1 { v = v * 10 + (c - 48); got = 1; j = j + 1 } 806 if isdig == 0 { scanning = 0 } 807 } 808 } 809 if got == 1 { return v } 810 } 811 } 812 i = i + 1 813 } 814 return dflt 815} 816// cache_max_bytes: nonpositive is not a budget -> default (the conf cannot switch the janitor off by 0). 817func sbr_cache_budget() -> i64 { 818 let v: i64 = sbr_cache_conf_int("cache_max_bytes=" as *u8, SBR_CACHE_BUDGET_DEFAULT) 819 if v <= 0 { return SBR_CACHE_BUDGET_DEFAULT } 820 return v 821} 822// cache_enabled: 0 disables lookup AND store without a rebuild; any other value (or no row) = enabled. 823func sbr_cache_enabled() -> i64 { 824 let v: i64 = sbr_cache_conf_int("cache_enabled=" as *u8, 1) 825 if v == 0 { return 0 } 826 return 1 827} 828 829// Enforce the byte budget on _build/cache. Called ONLY after a successful store, so its cost rides 830// the slow path (a real compile just happened; one directory walk is noise beside it) and never the 831// hit path. Fail-open at every step: a janitor that cannot run must never break a build. 832func sbr_cache_prune() -> i64 { 833 let budget: i64 = sbr_cache_budget() 834 let dfd: i64 = __syscall(257, 0-100, "_build/cache" as *u8 as i64, 0x10000, 0, 0, 0) 835 if dfd < 0 { return 0 } 836 let mt: *i64 = sys_mmap(SBR_CACHE_PRUNE_SLOTS * 8) as *i64 837 let sz: *i64 = sys_mmap(SBR_CACHE_PRUNE_SLOTS * 8) as *i64 838 let no: *i64 = sys_mmap(SBR_CACHE_PRUNE_SLOTS * 8) as *i64 839 let arena: *u8 = sys_mmap(SBR_CACHE_PRUNE_SLOTS * SBR_CACHE_PATHW) 840 var ap: i64 = 0 841 var ne: i64 = 0 842 var overflow: i64 = 0 843 var total: i64 = 0 844 let dbuf: *u8 = sys_mmap(SBR_CACHE_DIRBUF) 845 var more: i64 = 1 846 while more == 1 { 847 let nb: i64 = sys_getdents64(dfd, dbuf, SBR_CACHE_DIRBUF) 848 if nb <= 0 { more = 0 } else { 849 var p: i64 = 0 850 while p < nb { 851 let reclen: i64 = (dbuf[p+16] as i64) + ((dbuf[p+17] as i64) * 256) 852 if reclen <= 0 { p = nb } else { 853 let nmp: *u8 = ((dbuf as i64) + p + 19) as *u8 854 var isdot: i64 = 0 855 if nmp[0] == (46 as u8) { if nmp[1] == (0 as u8) { isdot = 1 } } 856 if nmp[0] == (46 as u8) { if nmp[1] == (46 as u8) { if nmp[2] == (0 as u8) { isdot = 1 } } } 857 if isdot == 0 { 858 if ne >= SBR_CACHE_PRUNE_SLOTS { overflow = 1 } 859 if overflow == 0 { 860 let path: *u8 = ((arena as i64) + ap) as *u8 861 var q: i64 = 0 862 q = sbr_cat(path, q, "_build/cache/" as *u8) 863 var c: i64 = 0 864 var toolong: i64 = 0 865 while nmp[c] != (0 as u8) { 866 if q < SBR_CACHE_PATHW - 1 { path[q] = nmp[c]; q = q + 1 } 867 if q >= SBR_CACHE_PATHW - 1 { toolong = 1 } 868 c = c + 1 869 } 870 path[q] = 0 as u8 871 if toolong == 1 { 872 sbr_puts("[nx_sov_build_run] BUILD-CACHE-PRUNE SKIP-ENTRY: name exceeds the path slot (SBR_CACHE_PATHW) -- counted out of the census rather than stat'd through a truncated path 873" as *u8) 874 } 875 var fsz: i64 = 0 - 1 876 var fmt: i64 = 0 - 1 877 // Cache accounting needs metadata, not a read of every cached payload. 878 if toolong == 0 { fsz = sbr_stat_size(path) } 879 if toolong == 0 { fmt = sbr_mtime(path) } 880 if fsz >= 0 { if fmt >= 0 { 881 mt[ne] = fmt 882 sz[ne] = fsz 883 no[ne] = ap 884 total = total + fsz 885 ne = ne + 1 886 ap = ap + SBR_CACHE_PATHW 887 } } 888 } 889 } 890 p = p + reclen 891 } 892 } 893 } 894 } 895 sys_close(dfd) 896 if overflow == 1 { 897 sbr_puts("[nx_sov_build_run] BUILD-CACHE-PRUNE SKIPPED: more entries than the enumeration pool (SBR_CACHE_PRUNE_SLOTS) -- pruning from a partial listing would evict by an incomplete ordering. Raise the pool or prune by hand.\n" as *u8) 898 return 0 899 } 900 if total <= budget { return 0 } 901 var evicted: i64 = 0 902 var freed: i64 = 0 903 var draining: i64 = 1 904 while draining == 1 { 905 if total <= budget { draining = 0 } 906 if draining == 1 { 907 var oldest: i64 = 0 - 1 908 var oldmt: i64 = 0 909 var i2: i64 = 0 910 while i2 < ne { 911 if sz[i2] >= 0 { 912 if oldest < 0 { oldest = i2; oldmt = mt[i2] } 913 if mt[i2] < oldmt { oldest = i2; oldmt = mt[i2] } 914 } 915 i2 = i2 + 1 916 } 917 if oldest < 0 { draining = 0 } 918 if oldest >= 0 { 919 let vp: *u8 = ((arena as i64) + no[oldest]) as *u8 920 if sys_unlinkat(vp) == 0 { 921 total = total - sz[oldest] 922 freed = freed + sz[oldest] 923 evicted = evicted + 1 924 } 925 sz[oldest] = 0 - 1 926 } 927 } 928 } 929 if evicted > 0 { 930 sbr_puts("[nx_sov_build_run] BUILD-CACHE-PRUNE evicted=" as *u8); sbr_putn(evicted) 931 sbr_puts(" freed=" as *u8); sbr_putn(freed) 932 sbr_puts("B budget=" as *u8); sbr_putn(budget) 933 sbr_puts("B kept=" as *u8); sbr_putn(ne - evicted) 934 sbr_puts(" (oldest-mtime-first; every entry regenerable by one compile)\n" as *u8) 935 } 936 return evicted 937} 938 939func main(argc: i64, argv: *i64) -> i64 { 940 if argc < 2 { sbr_puts("usage: nx_sov_build_run <module-basename> [--build-only | --qualify [--run-timeout-ms N]] [--install] [--debug] [args-forwarded-to-program...] (--debug passes -g to nx_cc so the ELF carries .debug_line and nx_addr2line can name the faulting source line; --qualify builds and runs with no builder install, conflicts with --build-only/--install, and requires a positive runtime deadline; target side effects are not sandboxed)\n" as *u8); sys_exit(SBR_USAGE); return SBR_USAGE } 941 let name: *u8 = argv[1] as *u8 942 943 // ---- ANCHOR THE CWD 2026-08-07 ------------------------------------------------------------- 944 // EVERY path in this runner is relative to the build tree root: the source probes, the toolchain 945 // (_build/nx_cc_sovereign.elf), the admission binary, ../knowledge/tree_canon.conf, and -- decisively 946 // -- nx_cc's OWN import resolution for `import "nx_syscalls.nx"`. That is correct only when CWD is 947 // already the tree, which holds when a human runs it from buildroot/ and fails for every other 948 // caller: nx_job_run, the supervisor and any operator all start in the SERVING ROOT, where the same 949 // tree sits one level down under buildroot/. 950 // The first attempt at this added buildroot/ prefixes to the source probe. It moved the failure 951 // exactly one step -- SOURCE-NOT-FOUND became "BUILD ADMISSION DID NOT RUN (rc=127)", "TREE-CANON 952 // NOT CHECKED: ../knowledge/tree_canon.conf absent", then COMPILE-FAIL with an EMPTY .s and EMPTY 953 // stderr, because the compiler could not resolve the imports either. Prefixing one path fixed one 954 // path, and there were five. 955 // WHEN EVERY PATH IN A PROGRAM IS RELATIVE TO ONE DIRECTORY, THE FIX IS TO BE IN THAT DIRECTORY, 956 // NOT TO PREFIX EVERY PATH -- and the prefix version is worse than nothing, because it converts a 957 // loud SOURCE-NOT-FOUND into two silent PROCEEDING-UNGUARDED warnings and a misleading compile 958 // error. IT DISARMED TWO GUARDS TO REACH A FAILURE. 959 // Detect, do not assume: only chdir when runtime/ is genuinely absent here AND present under 960 // buildroot/, so a correct invocation from the tree root is completely unaffected. 961 // PROBE A LOAD-BEARING FILE, NOT A DIRECTORY NAME. The first version of this probe asked whether 962 // runtime/_hdl_build EXISTED and was satisfied by a NINE-FILE scratch directory sitting in the 963 // serving root beside the real 6,992-file tree in buildroot/. So the anchor never fired, and worse: 964 // left alone, this runner resolves runtime/_hdl_build/<name>.nx against that scratch dir and would 965 // compile a DIFFERENT tree from the one /api/build builds -- a forked copy reported as success, 966 // which is precisely what the tree-canon admission exists to prevent and which canon cannot see, 967 // because canon compares the laptop against buildroot and has never heard of this third directory. 968 // A DIRECTORY WITH THE RIGHT NAME IS NOT THE TREE; PROBE SOMETHING THE TREE CANNOT LACK. 969 // runtime/nx_syscalls.nx is imported by essentially every organ, so its presence identifies the 970 // real tree and its absence rules out any partial look-alike. 971 // ★ROOT FIX 2026-08-07 -- WHERE `_offc/` POINTS DEPENDS ON WHETHER WE ANCHORED. 972 // Everything below resolves `_offc/<name>.elf` relative to the CWD. When the anchor fires we are 973 // INSIDE buildroot/, so that path is buildroot/_offc/ -- a 106-file SHADOW of the serving root's 974 // 340-file _offc/, which is the one gates and organs actually fork. Two consequences, both 975 // MEASURED 2026-08-07: 976 // (1) --install printed "INSTALLED -> _offc/<name>.elf" and the consumer's copy was untouched. 977 // THE SUCCESS WORD WAS TRUE ABOUT THE COPY AND WRONG ABOUT THE DESTINATION. 978 // (2) Worse, LM-026's refresh-IF-PRESENT decides `do_install` by testing whether that same 979 // shadow path exists -- so for every organ whose twin lives in the SERVING _offc/, the 980 // probe missed, do_install stayed 0, and the rebuild silently did not reach the consumer. 981 // The staleness LM-026 was written to retire has been surviving its own fix. 982 // This prefix makes both the probe and the install name the directory consumers read. It is also 983 // why the toolchain incident this file records could happen at all: buildroot/_offc is REACHABLE, 984 // not inert, so writing there is not a harmless no-op -- it plants a shadow. 985 var offc_pfx: *u8 = "_offc/" as *u8 986 // ★ROOT FIX 2026-08-07 (second deploy location). THE ESTATE HAS TWO CONSUMER DIRECTORIES, and 987 // refresh-IF-PRESENT only ever knew about one. MEASURED: 1,652 .elf are served from the SERVING 988 // ROOT and only 110 from _offc/ -- and 1,551 organs have a ROOT twin with a source but NO _offc 989 // twin, so for 94% of everything deployed the LM-026 probe missed, do_install stayed 0, and a 990 // successful rebuild never reached the consumer. That is the estate's "1,557 deployed / 6 991 // CURRENT" adoption gap, and this line is its mechanism. 992 // A REFRESH THAT KNOWS ONE OF THE TWO PLACES ITS CONSUMERS READ IS BLIND TO MOST OF THEM. 993 var root_pfx: *u8 = "" as *u8 994 let probe_here: i64 = sys_openat_rd("runtime/nx_syscalls.nx" as *u8) 995 if probe_here < 0 { 996 let probe_br: i64 = sys_openat_rd("buildroot/runtime/nx_syscalls.nx" as *u8) 997 if probe_br >= 0 { 998 sys_close(probe_br) 999 sys_chdir("buildroot" as *u8) 1000 offc_pfx = "../_offc/" as *u8 // we are now inside buildroot/; the consumers' _offc is one level up 1001 root_pfx = "../" as *u8 // ...and the serving root, where 1,551 organs actually live 1002 sbr_puts("[nx_sov_build_run] CWD anchored to buildroot/ (started in the serving root)\ 1003" as *u8) 1004 } 1005 } 1006 if probe_here >= 0 { sys_close(probe_here) } 1007 1008 // FLAGS (argv[2]): --build-only (--b*) = skip the run; --install (--i*) = FORCE-install /tmp/<name>.sov.elf 1009 // to _offc/<name>.elf even with NO existing twin (makes a reusable TOOL permanent on first build) + skip run. 1010 // Default = refresh-IF-PRESENT (LM-026) + run. Opt-in so throwaway gates/probes never clutter _offc. 1011 // Everything AFTER the optional flag is FORWARDED to the built program (fwd_start below). 1012 var force_install: i64 = 0 1013 var skip_run: i64 = 0 1014 // --build-only MUST MEAN BUILD ONLY (2026-08-26). Tracked SEPARATELY from skip_run because --install 1015 // also skips the run yet is an explicit, deliberate request TO install -- so skip_run cannot carry 1016 // this meaning. nx_builddeploy_lib owns the resulting decision. 1017 var build_only: i64 = 0 1018 var qualify: i64 = 0 1019 var qualification_deadline_ms:i64=0 1020 var qualification_deadline_given:i64=0 1021 var want_debug: i64 = 0 1022 var want_release: i64 = 0 1023 var fwd_start: i64 = 2 1024 // LANE FLAGS ARE A LOOP, NOT A SLOT (2026-08-15). This read argv[2] ONLY, so a SECOND lane flag was 1025 // silently FORWARDED TO THE BUILT PROGRAM instead of consumed: `--build-only --debug` would have 1026 // compiled WITHOUT debug and passed "--debug" to the target as a program argument. One slot is not a 1027 // parser, and the defect is invisible because both halves succeed at something. 1028 // A lane flag is recognised by its '--' prefix; the first non-flag argument ends the lane's section 1029 // and everything from there is forwarded. With no flags fwd_start stays 2, so the default path is 1030 // byte-identical to the single-slot version it replaces. 1031 // --timeout-ms <n> overrides the build deadline. It keys on 't' because --debug already owns 'd', 1032 // and it is the ONLY lane flag that CONSUMES A VALUE -- hence `extra`, since advancing by one 1033 // would forward the number to the built program as an argument (the exact single-slot defect the 1034 // comment above records). 1035 var deadline_ms: i64 = SBR_BUILD_DEADLINE_MS 1036 var no_cache: i64 = 0 1037 var fi: i64 = 2 1038 var scanning: i64 = 1 1039 while scanning == 1 { 1040 if fi >= argc { scanning = 0 } else { 1041 let fl: *u8 = argv[fi] as *u8 1042 if fl[0] != (45 as u8) { scanning = 0 } else { 1043 if fl[1] != (45 as u8) { scanning = 0 } else { 1044 var extra: i64 = 0 1045 if bd_lane_flag_known(fl)==0 { 1046 sbr_puts("[nx_sov_build_run] REFUSED: unknown build-lane flag.\n" as *u8) 1047 sys_exit(SBR_USAGE); return SBR_USAGE 1048 } 1049 if bd_qualification_flag(fl)==1 { qualify=1 } 1050 if fl[2] == (98 as u8) { skip_run = 1; build_only = 1 } // --build-only 1051 if fl[2] == (105 as u8) { force_install = 1; skip_run = 1 } // --install 1052 if fl[2] == (100 as u8) { want_debug = 1 } // --debug -> -g to nx_cc 1053 if fl[2] == (110 as u8) { no_cache = 1 } // --no-cache: skip the B2 lookup AND store 1054 if fl[2] == (114 as u8) && bd_run_timeout_flag(fl)==0 { want_release = 1 } // --release -> a release build even when the incumbent is debug (announced below) 1055 if fl[2] == (116 as u8) { // --timeout-ms <n> 1056 if (fi + 1) >= argc { 1057 sbr_puts("[nx_sov_build_run] REFUSED: --timeout-ms needs a value in milliseconds.\n" as *u8) 1058 sys_exit(SBR_USAGE); return SBR_USAGE 1059 } 1060 let dv: i64 = sbr_atoi(argv[fi + 1] as *u8) 1061 if dv < 0 { 1062 sbr_puts("[nx_sov_build_run] REFUSED: --timeout-ms value must be a plain decimal count of milliseconds.\n" as *u8) 1063 sys_exit(SBR_USAGE); return SBR_USAGE 1064 } 1065 deadline_ms = dv 1066 extra = 1 1067 } 1068 if bd_run_timeout_flag(fl)==1 { 1069 if fi+1>=argc { sbr_puts("[nx_sov_build_run] REFUSED: --run-timeout-ms needs a value.\n" as *u8);sys_exit(SBR_USAGE);return SBR_USAGE } 1070 qualification_deadline_ms=bd_positive_ms(argv[fi+1] as *u8);qualification_deadline_given=1;extra=1 1071 if qualification_deadline_ms<=0 { sbr_puts("[nx_sov_build_run] REFUSED: run deadline must be positive.\n" as *u8);sys_exit(SBR_USAGE);return SBR_USAGE } 1072 } 1073 fi = fi + 1 + extra 1074 fwd_start = fi 1075 } 1076 } 1077 } 1078 } 1079 if qualification_deadline_given==1 && qualify!=1 { sbr_puts("[nx_sov_build_run] REFUSED: --run-timeout-ms requires --qualify.\n" as *u8);sys_exit(SBR_USAGE);return SBR_USAGE } 1080 let qualification_mode:i64=bd_qualification_mode(qualify,build_only,force_install) 1081 if qualification_mode<0 { 1082 sbr_puts("[nx_sov_build_run] REFUSED: --qualify conflicts with --install or --build-only.\n" as *u8) 1083 sys_exit(SBR_USAGE); return SBR_USAGE 1084 } 1085 if qualification_mode==1 { 1086 if deadline_ms<=0 || argc-fwd_start>18 { 1087 sbr_puts("[nx_sov_build_run] REFUSED: qualification needs a positive deadline and at most 18 forwarded arguments.\n" as *u8) 1088 sys_exit(SBR_USAGE); return SBR_USAGE 1089 } 1090 build_only=1; skip_run=0 1091 sbr_puts("[nx_sov_build_run] INTENT qualification: build and run; builder installs nothing. Target side effects require separate authorization.\n" as *u8) 1092 } 1093 // THE FLAVOUR FOLLOWS THE INCUMBENT (2026-09-05, operator: "first byte fix"). A build's debug/release flavour used to be 1094 // a per-call flag nobody declares per target, so a rebuild that omitted --debug silently produced the OTHER flavour of 1095 // the live binary and /api/promote then read the swap as CAPLOSS 422 permil (nx_wgsl: 823 lost runs, every one a source 1096 // path, a symbol name or a DWARF section name, lost_other=0). A seat then either rebuilt or passed allow_capability_loss 1097 // and disarmed the one guard that catches a real regression. Now, with neither --debug nor --release given, the flavour 1098 // is READ FROM THE INCUMBENT'S BYTES (root_pfx + name + .elf, the serving-root twin this runner holds or refreshes): 1099 // the DWARF line-table section name present = debug. No incumbent = release, the historical default. Every path 1100 // ANNOUNCES flavour= and src= so a reader can tell a derived flavour from a declared one; --release is the explicit 1101 // switch. The announce strings deliberately never spell the section name contiguously (sbr_incumbent_flavour's note). 1102 var flavour_src: *u8 = "argv --debug" as *u8 1103 if want_release == 1 { want_debug = 0; flavour_src = "argv --release" as *u8 } 1104 if want_release == 0 { 1105 if want_debug == 0 { 1106 let incp: *u8 = sys_mmap(256); var io: i64 = 0 1107 io = sbr_cat(incp, io, root_pfx); io = sbr_cat(incp, io, name); io = sbr_cat(incp, io, ".elf" as *u8); incp[io] = 0 as u8 1108 let ifl: i64 = sbr_incumbent_flavour(incp) 1109 if ifl == 1 { want_debug = 1; flavour_src = "incumbent (the serving-root twin carries a DWARF line table)" as *u8 } 1110 if ifl == 0 { flavour_src = "incumbent (the serving-root twin carries no DWARF line table)" as *u8 } 1111 if ifl < 0 { flavour_src = "default (no incumbent to read)" as *u8 } 1112 sys_munmap(incp, 256) 1113 } 1114 } 1115 sbr_puts("[nx_sov_build_run] FLAVOUR " as *u8) 1116 if want_debug == 1 { sbr_puts("debug" as *u8) } else { sbr_puts("release" as *u8) } 1117 sbr_puts(" src=" as *u8); sbr_puts(flavour_src); sbr_puts("\n" as *u8) 1118 // ANNOUNCE THE FEATURE, NOT ONLY ITS FAILURE (the 2026-08-07 law): if this line is absent when 1119 // --debug was passed, the FLAG PARSE missed it -- which is otherwise indistinguishable from the 1120 // compiler ignoring -g, and those have opposite remedies. 1121 if want_debug == 1 { sbr_puts("[nx_sov_build_run] DEBUG BUILD: -g -> nx_cc emits .file/.loc -> nxasm builds .debug_line -> nx_addr2line resolves a fault address to file:line.\n" as *u8) } 1122 // ANNOUNCE A NON-DEFAULT BOUND (same law as --debug above). Deliberately SILENT on the default so 1123 // the ordinary build's output stays byte-identical for anything that parses it -- a safety line 1124 // that regresses every caller's log is not safety. An UNBOUNDED deadline always announces. 1125 if deadline_ms != SBR_BUILD_DEADLINE_MS { 1126 sbr_puts("[nx_sov_build_run] build deadline OVERRIDDEN to " as *u8); sbr_putn(deadline_ms) 1127 if deadline_ms <= 0 { sbr_puts("ms = UNBOUNDED: a hanging tool will burn a core until a human notices.\n" as *u8) } 1128 if deadline_ms > 0 { sbr_puts("ms per compile/assemble step.\n" as *u8) } 1129 } 1130 1131 let src: *u8 = sys_mmap(512); var o: i64 = 0 1132 o = sbr_cat(src, o, "runtime/_hdl_build/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 1133 // fall back to runtime/<name>.nx if the module isn't under _hdl_build (additive; the older slice stack lives in runtime/) 1134 let chk: i64 = sys_openat_rd(src) 1135 if chk < 0 { 1136 o = 0 1137 o = sbr_cat(src, o, "runtime/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 1138 } 1139 if chk >= 0 { sys_close(chk) } 1140 // 2nd fallback: nxasm/<name>.nx -- so the TOOLCHAIN itself (assembler/linker) is built 1141 // through THIS runner's retry guard, never direct 1-shot (2026-06-09 lesson: a direct 1142 // unguarded nxasm rebuild handed back a nondeterministic miscompile that exited pre-pass0). 1143 let chk2: i64 = sys_openat_rd(src) 1144 if chk2 < 0 { 1145 o = 0 1146 o = sbr_cat(src, o, "nxasm/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 1147 } 1148 if chk2 >= 0 { sys_close(chk2) } 1149 // 3rd fallback: runtime/wiki/<name>.nx -- the internal wiki engine (nx_wiki_main) and 1150 // its render organs live here, NOT in runtime/ or _hdl_build/. Mirrors the fallbacks 1151 // above exactly (reopen src; if still missing, rewrite to the next candidate path). 1152 // Added 2026-06-15 (a wiki organ otherwise mis-compiled on a bogus nxasm/<name>.nx path 1153 // and surfaced as a misleading "COMPILE-FAIL (empty .s)"). 1154 let chk3: i64 = sys_openat_rd(src) 1155 if chk3 < 0 { 1156 o = 0 1157 o = sbr_cat(src, o, "runtime/wiki/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 1158 } 1159 if chk3 >= 0 { sys_close(chk3) } 1160 // (The buildroot/ path-prefix fallbacks that briefly lived here were REMOVED: the CWD is anchored 1161 // once at the top of main instead, which fixes the source probe, the toolchain, the admission 1162 // binary, tree_canon.conf and nx_cc's import resolution together rather than one of the five.) 1163 // FINAL existence verdict: if NONE of the candidate paths resolved to a real file, fail 1164 // LOUD with SOURCE-NOT-FOUND rather than running the compiler on a non-existent path 1165 // (which produces an empty .s and the misleading "COMPILE-FAIL" above). Fail-fast at the 1166 // boundary (Cardinal 12 + Cardinal 20: a build that can't find its source must say so). 1167 let chkf: i64 = sys_openat_rd(src) 1168 if chkf < 0 { 1169 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 1170 sbr_puts(": SOURCE-NOT-FOUND (probed runtime/_hdl_build/, runtime/, nxasm/, runtime/wiki/ -- relative to CWD, which is anchored to the build tree at startup)\n" as *u8) 1171 sys_exit(SBR_USAGE); return SBR_USAGE 1172 } 1173 sys_close(chkf) 1174 // BUILD OUTPUT LIVES IN THE REPO, NOT /tmp (operator 2026-07-27). WSL's /tmp is wiped 1175 // between sessions, which silently deleted built ELFs mid-run and made scripts that 1176 // referenced them fail in confusing ways (a documented recurring trap). `_build/` is 1177 // repo-local and durable; mkdir is idempotent (EEXIST is fine, hence the ignored rc). 1178 sys_mkdir("_build\x00" as *u8, 493) 1179 // PER-TARGET BUILD LOCK (seq1260, 2026-07-29): two concurrent builds of the SAME target used to 1180 // interleave on _build/<name>.{s,sov.elf} -- one overwrote the other's artifact MID-SWEEP and a 1181 // BD control curve read garbage (the /api/build shared-capture class at the local tier). Blocking 1182 // flock on _build/<name>.lock serializes per target, held through compile->assemble->install and 1183 // RELEASED before the run phase (a long-running target must never wedge future rebuilds; an 1184 // already-running process keeps its inode across the atomic rename, so rebuild-under-run is safe). 1185 // Cross-target builds never contend; process exit releases (crash-safe); the lock file is NEVER 1186 // unlinked (the documented lock-file law). 1187 let lockpath: *u8 = sys_mmap(256); o = 0 1188 o = sbr_cat(lockpath, o, "_build/" as *u8); o = sbr_cat(lockpath, o, name); o = sbr_cat(lockpath, o, ".lock" as *u8); lockpath[o] = 0 as u8 1189 let lockfd: i64 = sys_openat_wr(lockpath, 0x1a4) 1190 if lockfd >= 0 { sys_flock(lockfd, SYS_LOCK_EX) } 1191 let spath: *u8 = sys_mmap(256); o = 0 1192 o = sbr_cat(spath, o, "_build/" as *u8); o = sbr_cat(spath, o, name); o = sbr_cat(spath, o, ".s" as *u8); spath[o] = 0 as u8 1193 let elfpath: *u8 = sys_mmap(256); o = 0 1194 o = sbr_cat(elfpath, o, "_build/" as *u8); o = sbr_cat(elfpath, o, name); o = sbr_cat(elfpath, o, ".sov.elf" as *u8); elfpath[o] = 0 as u8 1195 // FAIL-LOUD (seq132, 2026-07-19): remove any STALE _build/<name>.sov.elf from a prior build BEFORE 1196 // compiling, so a failed build can never leave the previous working binary behind to be staged as a 1197 // false "BUILT" (the masking trap that cost a 3-reship cascade). 1198 // 1199 // ROOT FIX seq363 (2026-07-30): this used to TRUNCATE the stale artifact to 0 bytes via O_TRUNC and 1200 // leave it there. That achieved the anti-staleness goal but replaced one fail-open with a weaker one: 1201 // after a COMPILE-FAIL a 0-byte file REMAINED on disk, so `does the elf exist?` -- the check every 1202 // naive caller actually writes -- still answered YES for a build that produced nothing. Measured 1203 // 2026-07-30: a deliberately-broken target left _build/<t>.sov.elf at 0 bytes, and separately a PIPE 1204 // swallows this runner's exit code, so neither the artifact nor `$?` was trustworthy. 1205 // 1206 // UNLINK instead. The invariant becomes the one callers already assume: THE ARTIFACT EXISTS IF AND 1207 // ONLY IF THE BUILD SUCCEEDED. Downstream ELF-magic/size guards (nx_hostctl cmd_buildrun, /api/promote, 1208 // md_tc_elf_size) all still hold -- this makes them belt-and-suspenders rather than load-bearing. 1209 // Success path is unchanged: the atomic rename below creates the file fresh. 1210 // MOVED 2026-09-05 (measured on nx_goalmap_gate the day it was built): this unlink used to run HERE, BEFORE 1211 // build admission, tree-canon admission and the build-cache check -- so an admission REFUSAL (which is not 1212 // a build failure) destroyed the prior fossil, and every drift and staleness instrument that compares 1213 // against _build/<t>.sov.elf then measured against an absence (CLAUDE.md 2026-08-28 and 2026-09-04; debt 1214 // 1787078015 recurrence). The unlink now sits immediately before the compile loop, after every refusal has 1215 // had its say and only on a cache MISS, so THE ARTIFACT EXISTS IFF THE LAST ATTEMPT THAT REACHED THE 1216 // COMPILER SUCCEEDED -- the invariant callers already assume, now also true across refusals. 1217 1218 // SOVEREIGN compiler (self-hosted), NOT nx_cc_known_good (= the C 1219 // bootstrap, FORBIDDEN on the build path per operator law -- C is 1220 // for benchmarking only). Fixed 2026-06-09 during C2-deploy. 1221 let compiler: *u8 = "_offc/nx_cc_sovereign.elf" as *u8 1222 // the CURRENT (post-cl-shift-fix, 2026-06-09) sovereign assembler. The older _offc/nxasm_x86.elf 1223 // (2026-05-30) systemically mis-encodes registers->r15 and segfaults even ret-42 -- do NOT use it. 1224 let asm_tool: *u8 = "_offc/nxasm_x86_main.elf" as *u8 1225 1226 let envp: *i64 = sys_mmap(8*4) as *i64 1227 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0 1228 1229 let devnull: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4) // mute nx_cc's symbol-table dump 1230 1231 // ---- BUILD ADMISSION (2026-08-01): consult nx_build_admit BEFORE forking the compiler. 1232 // MEASURED THE NIGHT THIS LANDED: load average 132.58, 334MB available of 36GB, four concurrent 1233 // compiles -- builds were being OOM-killed mid-flight for every seat sharing this host, and two of 1234 // mine died that way. nx_build_admit has existed since 2026-07-30 and NOTHING called it: its own 1235 // header names ma_do_build as the filed rung, and THIS runner -- the path agents actually invoke -- 1236 // was never wired either. A COMPILE IS A HEAVY LAUNCH. 1237 // Refuse LOUDLY rather than pile on and be killed. That is the memfloor law already on the board: 1238 // REFUSE, NEVER SILENTLY SHRINK. A build that never starts is recoverable; a wedged host is not. 1239 // 0 GRANT | 3 DENY-MEM (below the memory floor) | 4 QUEUE (D-state I/O storm, run-queue saturation, or the storm ceiling -- NOT plain 1-minute load: the conf sets max_centiload unreachable) 1240 // 5 CANNOT-MEASURE (unreadable /proc -> fail-CLOSED by construction, never wave through) 1241 // FAIL-OPEN ON ABSENCE ONLY: a missing admitter makes execve return 127 and we proceed, so a tree 1242 // without it still builds -- but any verdict it DOES return is obeyed. Absence is not permission. 1243 let sbr_admit: *u8 = "_build/nx_build_admit.sov.elf" as *u8 1244 // THRESHOLDS ARE EXPLICIT, AND ONLY THE CAUSAL ONE IS ARMED (2026-08-01). 1245 // MEMORY FLOOR 512MB IS ARMED: it is the DIRECTLY MEASURED cause -- builds were OOM-killed 1246 // tonight at 334MB available, which is below this floor, so this check would have refused 1247 // exactly the builds that died instead of letting them pile on and be killed. 1248 // LOAD CEILING IS DELIBERATELY DISABLED (1000000 centiload = load 10000, unreachable) because 1249 // it is UNCALIBRATED for this host: the admitter default is 400 (load 4.00) while this Synology 1250 // IDLES at 17.62/24.80/36.19 and only wedged at 132.58. Arming it at the default would refuse 1251 // every build for every seat -- including this runner own rebuild, a self-bricking guard. 1252 // Picking a number that merely produces the outcome I want would be FITTING, NOT MODELLING. 1253 // TO ARM IT: sample /proc/loadavg over a representative window, set the ceiling above the normal 1254 // distribution and below the wedge point, and record the measurement that justifies the number. 1255 let sbr_aargv: *i64 = sys_mmap(8*6) as *i64 1256 sbr_aargv[0] = sbr_admit as i64 1257 sbr_aargv[1] = "check" as *u8 as i64 1258 // NO THRESHOLDS PASSED (2026-08-18). The comment that stood here said "if this number ever moves, 1259 // move BOTH" -- the very sentence that proves two copies were one too many (and mgmt's max was 1260 // 100000 while this one was 1000000: they had already drifted). nx_build_admit now reads THE ONE 1261 // envelope from knowledge/build_admit.conf (../knowledge/ from this buildroot-anchored CWD) and 1262 // prints envelope_src=conf; every caller that passes nothing gets the same bar as /api/build. 1263 sbr_aargv[2] = 0 1264 var sbr_arc: i64 = sbr_run(sbr_admit, sbr_aargv, envp, 0 - 1, 0 - 1, deadline_ms) 1265 // DURABLE FALLBACK (2026-09-03). The admitter above is the _build FOSSIL, and A REFUSED BUILD DELETES 1266 // THAT FOSSIL -- so one refusal silently DISARMS THIS GUARD for the next build, precisely when the box 1267 // is busiest and the guard matters most. MEASURED THIS DAY, on a genuinely storming host: 1268 // BUILD ADMISSION DID NOT RUN (rc=127 ...). PROCEEDING UNGUARDED 1269 // solely because an earlier refusal had eaten the artifact. The fail-open-on-absence rule documented 1270 // above is deliberate and STAYS -- a tree that has never built the admitter must still bootstrap. 1271 // But ABSENCE-BY-DELETION IS NOT ABSENCE-BY-BOOTSTRAP, and the PROMOTED binary is the durable copy that 1272 // survives a refusal. CWD is buildroot, so the serving root is one level up. 1273 // STRICTLY ADDITIVE BY CONSTRUCTION: it runs only when the first attempt found NOTHING to run, so it can 1274 // only ever ADD a guard where there was none. A present fossil behaves exactly as before, and a tree 1275 // missing both copies now refuses before compilation. 1276 let sbr_rc_notfound: i64 = 127 1277 if sbr_arc == sbr_rc_notfound { 1278 let sbr_admit2: *u8 = "../nx_build_admit.elf" as *u8 1279 sbr_aargv[0] = sbr_admit2 as i64 1280 sbr_arc = sbr_run(sbr_admit2, sbr_aargv, envp, 0 - 1, 0 - 1, deadline_ms) 1281 if sbr_arc != sbr_rc_notfound { 1282 sbr_puts("[nx_sov_build_run] admission fossil absent (a refused build deletes it); GUARDED by the PROMOTED ../nx_build_admit.elf instead.\n" as *u8) 1283 } 1284 } 1285 // EXIT IS SBR_ADMIT_REFUSED(6) FOR ALL THREE DENIALS (restored in the 2026-08-03 fork merge -- 1286 // the NAS branch had regressed to returning the admitter's raw 3/4/5, which COLLIDE with 1287 // COMPILE_FAIL(3)/ASM_FAIL(4): a wait-and-retry signal decoding as a broken build). The REASON 1288 // stays in the message; the code says only "admission refused, not a build failure". 1289 if sbr_arc == 3 { 1290 sbr_puts("[nx_sov_build_run] REFUSED-BUILD-ADMIT rc=3 DENY-MEM -- available memory is BELOW THE FLOOR. Compiling now risks wedging the HOST, not just this build. Wait for headroom and retry.\n" as *u8) 1291 sys_exit(SBR_ADMIT_REFUSED); return SBR_ADMIT_REFUSED 1292 } 1293 if sbr_arc == 4 { 1294 sbr_puts("[nx_sov_build_run] REFUSED-BUILD-ADMIT rc=4 QUEUE -- the BUILD-ADMIT report ABOVE names which conjunct fired (D-state I/O storm, run-queue saturation, or the storm ceiling). THIS RUNNER DELIBERATELY DOES NOT RESTATE THE CAUSE: a hand-copied paraphrase of another organ's verdict drifts from it silently and sends every reader at the wrong cause -- and this line used to name the 1-minute load ceiling, which the shared conf sets UNREACHABLE, so it named the one conjunct that CANNOT fire. Nothing is queued by THIS runner: re-issue once the box clears (admission re-measures per call); pace re-issues, never spin.\n" as *u8) 1295 sys_exit(SBR_ADMIT_REFUSED); return SBR_ADMIT_REFUSED 1296 } 1297 if sbr_arc == 5 { 1298 sbr_puts("[nx_sov_build_run] REFUSED-BUILD-ADMIT rc=5 CANNOT-MEASURE -- /proc unreadable, so admission FAILS CLOSED by construction. A guard that cannot measure must refuse, never wave through.\n" as *u8) 1299 sys_exit(SBR_ADMIT_REFUSED); return SBR_ADMIT_REFUSED 1300 } 1301 if bci_resource_allowed(sbr_arc) != 1 { 1302 sbr_puts("[nx_sov_build_run] REFUSED-BUILD-ADMIT detector_rc=" as *u8); sbr_putn(sbr_arc) 1303 sbr_puts(" -- no explicit GRANT. Restore the admitter or resolve its failure; bootstrap must provision admission before compiling. No compiler launched.\n" as *u8) 1304 sys_exit(SBR_ADMIT_REFUSED); return SBR_ADMIT_REFUSED 1305 } 1306 1307 // ---- TREE-CANON ADMISSION (2026-08-03, debt 1785622769): a build must never compile a copy 1308 // that diverges from canon. ../knowledge/tree_canon.conf (cwd is buildroot; the conf lives at 1309 // the nishihost root) lists tree-root-relative paths that MUST be byte-identical across every 1310 // tree that can build them. Row grammar: '#' comment, blank ignored, '!path' = FREEZE EVERY 1311 // BUILD while divergent (shared infrastructure -- a fork there miscompiles everything 1312 // downstream), 'path' = refuse only the build OF THAT FILE (a lane's own divergence must not 1313 // block unrelated targets). The other tree's sizes come from the last pushed authoring 1314 // manifest (../knowledge/status/treecanon_laptop.mf, produced by nx_treediff on that tree). 1315 // SIZE IS A SCREEN, NOT PROOF OF IDENTITY (nx_treediff's own declared bound) -- equal-size 1316 // rewrites pass here; nx_treecanon_gate stays the tree-level instrument. 1317 // ⚠the manifest is AS FRESH AS ITS LAST PUSH: a stale one can false-pass a scoped row. The 1318 // authoring loop owns pushing it after edits; this check is the floor, not the ceiling. 1319 // Admission requires a valid nonempty rulebook and SHA256 manifest. Bootstrap provisions these 1320 // inputs before using the runner. A size screen remains diagnostic history, not content proof. 1321 let cn_conf_l: *i64 = sys_mmap(16) as *i64 1322 let cn_conf: *u8 = sys_read_file("../knowledge/tree_canon.conf" as *u8, cn_conf_l) 1323 let cn_mf_l: *i64 = sys_mmap(16) as *i64 1324 let cn_mf: *u8 = sys_read_file("../knowledge/status/treecanon_laptop.mf" as *u8, cn_mf_l) 1325 // The DIGEST manifest (nx_treehash on the authoring tree). When present it WINS: it decides 1326 // identity by content, which is the only thing a size screen cannot do. 1327 let cn_hmf_l: *i64 = sys_mmap(16) as *i64 1328 let cn_hmf: *u8 = sys_read_file("../knowledge/status/treecanon_laptop_hash.mf" as *u8, cn_hmf_l) 1329 let cn_rules: i64 = bci_conf_rows(cn_conf, cn_conf_l[0], SBR_CANON_PATH_BYTES - SBR_RUNTIME_PREFIX_BYTES - 1) 1330 if cn_rules <= 0 { 1331 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON INPUT: rulebook missing, empty, malformed or path out of bounds. Provision ../knowledge/tree_canon.conf; no compiler launched.\n" as *u8) 1332 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1333 } 1334 if (cn_hmf as i64) == 0 { 1335 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON INPUT: SHA256 manifest missing. Size-only evidence cannot admit a build; provision ../knowledge/status/treecanon_laptop_hash.mf.\n" as *u8) 1336 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1337 } 1338 if cn_hmf_l[0] <= 0 { 1339 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON INPUT: SHA256 manifest empty; no compiler launched.\n" as *u8) 1340 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1341 } 1342 sbr_puts("[nx_sov_build_run] TREE-CANON RULEBOOK VALID rules=" as *u8); sbr_putn(cn_rules); sbr_puts(" SHA256-evidence=loaded; relevant rows checked below\n" as *u8) 1343 var cn_have: i64 = 0 1344 if (cn_mf as i64) != 0 { cn_have = 1 } 1345 if (cn_hmf as i64) != 0 { cn_have = 1 } 1346 if (cn_conf as i64) != 0 { if cn_have == 1 { 1347 // the canon rows are relative to the runtime root; src is "runtime/<rel>" for every probe 1348 // path except the nxasm/ fallback (toolchain sources are not canon-governed rows today) 1349 var cn_isrt: i64 = 1 1350 let cn_rt: *u8 = "runtime/" as *u8 1351 var cn_i: i64 = 0 1352 while cn_i < 8 { if src[cn_i] != cn_rt[cn_i] { cn_isrt = 0 } cn_i = cn_i + 1 } 1353 var cn_srclen: i64 = 0 1354 while src[cn_srclen] != (0 as u8) { cn_srclen = cn_srclen + 1 } 1355 let cn_path: *u8 = sys_mmap(SBR_CANON_PATH_BYTES) 1356 let cn_cl: i64 = cn_conf_l[0] 1357 let cn_ml: i64 = cn_mf_l[0] 1358 var cn_p: i64 = 0 1359 while cn_p < cn_cl { 1360 var cn_e: i64 = cn_p 1361 var cn_go: i64 = 1 1362 while cn_go == 1 { 1363 if cn_e >= cn_cl { cn_go = 0 } 1364 if cn_go == 1 { if cn_conf[cn_e] == (10 as u8) { cn_go = 0 } } 1365 if cn_go == 1 { cn_e = cn_e + 1 } 1366 } 1367 var cn_len: i64 = cn_e - cn_p 1368 if cn_len > 0 { if cn_conf[cn_p + cn_len - 1] == (13 as u8) { cn_len = cn_len - 1 } } // CRLF-tolerant 1369 var cn_frozen: i64 = 0 1370 var cn_ps: i64 = cn_p 1371 if cn_len > 0 { if cn_conf[cn_ps] == (33 as u8) { cn_frozen = 1; cn_ps = cn_ps + 1; cn_len = cn_len - 1 } } 1372 var cn_live: i64 = 0 1373 if cn_len > 0 { if cn_conf[cn_ps] != (35 as u8) { cn_live = 1 } } 1374 if cn_live == 1 { 1375 // relevant = frozen row, OR the row IS the file this build compiles 1376 var cn_rel: i64 = cn_frozen 1377 if cn_isrt == 1 { if sbr_ceq_at(src, 8, cn_srclen - 8, cn_conf, cn_ps, cn_len) == 1 { cn_rel = 1 } } 1378 if cn_rel == 1 { 1379 var cq: i64 = 0 1380 cq = sbr_cat(cn_path, cq, "runtime/" as *u8) 1381 var ck: i64 = 0 1382 while ck < cn_len { cn_path[cq] = cn_conf[cn_ps + ck]; cq = cq + 1; ck = ck + 1 } 1383 cn_path[cq] = 0 as u8 1384 let cn_nas: i64 = sbr_filesize(cn_path) 1385 var cn_found: i64 = 0 1386 var cn_lap: i64 = 0 - 1 1387 var cn_div: i64 = 0 1388 var cn_hmode: i64 = 0 1389 var cn_hash_ok: i64 = 0 1390 var cn_matches: i64 = 0 1391 if (cn_hmf as i64) != 0 { cn_hmode = 1 } 1392 let cn_lhex: *u8 = sys_mmap(128) 1393 let cn_mhex: *u8 = sys_mmap(128) 1394 // ---- CONTENT MODE: digest rows are "<64hex> <bytes> <relpath>" ---- 1395 if cn_hmode == 1 { 1396 var hq: i64 = 0 1397 while hq < SBR_SHA_HEX { cn_mhex[hq] = SBR_QMARK as u8; hq = hq + 1 } 1398 cn_mhex[SBR_SHA_HEX] = 0 as u8 1399 let cn_hash_rc: i64 = sbr_sha_hex(cn_path, cn_lhex) 1400 if cn_hash_rc == 0 { cn_hash_ok = 1 } 1401 if cn_hash_rc != 0 { 1402 var hz: i64 = 0 1403 while hz < SBR_SHA_HEX { cn_lhex[hz] = SBR_QMARK as u8; hz = hz + 1 } 1404 cn_lhex[SBR_SHA_HEX] = 0 as u8 1405 } 1406 var hp: i64 = 0 1407 let cn_hl: i64 = cn_hmf_l[0] 1408 while hp < cn_hl { 1409 var he: i64 = hp 1410 var hg: i64 = 1 1411 while hg == 1 { 1412 if he >= cn_hl { hg = 0 } 1413 if hg == 1 { if cn_hmf[he] == (10 as u8) { hg = 0 } } 1414 if hg == 1 { he = he + 1 } 1415 } 1416 var hl2: i64 = he - hp 1417 if hl2 > 0 { if cn_hmf[hp + hl2 - 1] == (13 as u8) { hl2 = hl2 - 1 } } 1418 if hl2 > SBR_SHA_HEX + 2 { 1419 // the SECOND space ends the size field; the relpath follows it 1420 var s2: i64 = 0 - 1 1421 var hi2: i64 = SBR_SHA_HEX + 1 1422 while hi2 < hl2 { if s2 < 0 { if cn_hmf[hp + hi2] == (32 as u8) { s2 = hi2 } } hi2 = hi2 + 1 } 1423 if s2 > 0 { 1424 if sbr_ceq_at(cn_hmf, hp + s2 + 1, hl2 - s2 - 1, cn_conf, cn_ps, cn_len) == 1 { 1425 if bci_hash_row(cn_hmf, hp, hl2) != 1 { 1426 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON EVIDENCE: malformed SHA256 row for " as *u8); sbr_puts(cn_path); sbr_puts("\n" as *u8) 1427 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1428 } 1429 cn_found = 1 1430 cn_matches = cn_matches + 1 1431 var hk: i64 = 0 1432 while hk < SBR_SHA_HEX { cn_mhex[hk] = cn_hmf[hp + hk]; hk = hk + 1 } 1433 } 1434 } 1435 } 1436 hp = he + 1 1437 } 1438 if cn_found == 1 { 1439 var hd: i64 = 0 1440 while hd < SBR_SHA_HEX { if cn_lhex[hd] != cn_mhex[hd] { cn_div = 1 } hd = hd + 1 } 1441 } 1442 } 1443 // ---- SIZE MODE (unchanged): only when no digest manifest was published ---- 1444 if cn_hmode == 0 { 1445 var mp: i64 = 0 1446 while mp < cn_ml { 1447 var me: i64 = mp 1448 var mg: i64 = 1 1449 while mg == 1 { 1450 if me >= cn_ml { mg = 0 } 1451 if mg == 1 { if cn_mf[me] == (10 as u8) { mg = 0 } } 1452 if mg == 1 { me = me + 1 } 1453 } 1454 var ml2: i64 = me - mp 1455 if ml2 > 0 { if cn_mf[mp + ml2 - 1] == (13 as u8) { ml2 = ml2 - 1 } } 1456 // split at the first space 1457 var ms: i64 = 0 - 1 1458 var mi: i64 = 0 1459 while mi < ml2 { if ms < 0 { if cn_mf[mp + mi] == (32 as u8) { ms = mi } } mi = mi + 1 } 1460 if ms > 0 { 1461 if sbr_ceq_at(cn_mf, mp + ms + 1, ml2 - ms - 1, cn_conf, cn_ps, cn_len) == 1 { 1462 cn_found = 1 1463 var mv: i64 = 0 1464 var md: i64 = 0 1465 while md < ms { mv = mv * 10 + ((cn_mf[mp + md] as i64) - 48); md = md + 1 } 1466 cn_lap = mv 1467 } 1468 } 1469 mp = me + 1 1470 } 1471 if cn_found == 1 { if cn_lap != cn_nas { cn_div = 1 } } 1472 } 1473 // Timestamps cannot repair missing, duplicate or unreadable evidence. 1474 if cn_matches != 1 { 1475 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON EVIDENCE: expected one SHA256 row for " as *u8); sbr_puts(cn_path) 1476 sbr_puts(" matches=" as *u8); sbr_putn(cn_matches); sbr_puts("\n" as *u8) 1477 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1478 } 1479 if cn_nas <= 0 { 1480 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON EVIDENCE: source absent or empty: " as *u8); sbr_puts(cn_path); sbr_puts("\n" as *u8) 1481 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1482 } 1483 if cn_hash_ok != 1 { 1484 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON EVIDENCE: source digest unreadable: " as *u8); sbr_puts(cn_path); sbr_puts("\n" as *u8) 1485 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1486 } 1487 // a canon row ABSENT from the authoring manifest is divergent in EITHER mode 1488 if cn_found == 0 { cn_div = 1 } 1489 if cn_nas == 0 { cn_div = 1 } 1490 // ---- STALENESS IS NOT A FORK (2026-08-07) ------------------------------------------ 1491 // This guard REFUSED on any content difference whatever, and the manifest it compares 1492 // against is a snapshot of ONE laptop refreshed by a SessionStart hook. So one seat 1493 // saving a widely-imported file made EVERY build of EVERY dependent target fail for 1494 // EVERY seat until somebody re-ran the manifest push. Measured live: another seat was 1495 // editing runtime/nx_syscalls.nx -- which essentially every organ imports -- and it 1496 // changed three times in ~20 minutes; each time converge+push+build succeeded 1497 // IMMEDIATELY, proving the refusal was staleness and not a defect in the code. 1498 // ★A GUARD THAT IS RIGHT ABOUT CONTENT IDENTITY CAN STILL BE WRONG ABOUT AVAILABILITY: 1499 // IT TURNED ONE SEAT'S SAVE INTO AN ESTATE-WIDE BUILD OUTAGE. 1500 // 1501 // THE HARM IT ACTUALLY EXISTS TO PREVENT is compiling a copy that is BEHIND the 1502 // authoring tree -- that is how the unfreed-mmap fix got applied twice. Being AHEAD is 1503 // not that harm: it is just the newest work, and building does not write source. 1504 // The NAS cannot do symbol containment here (the manifest carries only sha+bytes+path, 1505 // never the laptop's content), but it CAN answer the question that decides the case: 1506 // was this file modified AFTER the snapshot was taken? 1507 // nas mtime > manifest mtime -> the snapshot is simply STALE about this file, 1508 // this tree holds the newer bytes -> PROCEED, LOUDLY. 1509 // nas mtime <= manifest mtime -> the snapshot SAW this content and still disagrees 1510 // -> a real fork, the laptop is ahead -> REFUSE. 1511 // No new constant, no new data, and it fails CLOSED whenever either stat is unreadable. 1512 if cn_div == 1 { 1513 // ⚠cn_conf is the CONF FILE'S CONTENT BUFFER, not a path -- the row is a 1514 // (buffer, offset, length) triple. Passing it to a stat() stats garbage, which 1515 // returns -1 and silently skipped this whole check: the first version of this 1516 // fix REFUSED exactly as before and looked like the branch was simply wrong. 1517 // cn_path is the resolved tree-relative path the refusal message already prints. 1518 // ⚠And the manifest is ../knowledge/... because main() anchors CWD to buildroot. 1519 // A PATH THAT IS RIGHT FOR THE MESSAGE IS NOT AUTOMATICALLY RIGHT FOR A SYSCALL. 1520 let cn_fmt: i64 = sbr_mtime(cn_path) 1521 let cn_mmt: i64 = sbr_mtime("../knowledge/status/treecanon_laptop_hash.mf" as *u8) 1522 if cn_fmt > 0 { 1523 if cn_mmt > 0 { 1524 if cn_fmt > cn_mmt { 1525 sbr_puts("[nx_sov_build_run] TREE-CANON STALE-NOT-FORKED: canon path " as *u8) 1526 sbr_puts(cn_path) 1527 sbr_puts(" differs from the authoring manifest, but THIS TREE'S COPY IS NEWER THAN THE SNAPSHOT (file mtime " as *u8) 1528 sbr_putn(cn_fmt) 1529 sbr_puts(" > manifest mtime " as *u8) 1530 sbr_putn(cn_mmt) 1531 sbr_puts("), so the manifest is behind, not the tree. PROCEEDING on the newer bytes -- re-run the treecanon push to re-sync the authoring tree.\ 1532" as *u8) 1533 cn_div = 0 1534 } 1535 } 1536 } 1537 } 1538 if cn_div == 1 { 1539 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON rc=7: canon path " as *u8) 1540 sbr_puts(cn_path) 1541 if cn_hmode == 1 { 1542 sbr_puts(" DIVERGES across trees by CONTENT (sha256) -- this tree=" as *u8) 1543 sbr_puts(cn_lhex) 1544 sbr_puts(" authoring manifest=" as *u8) 1545 sbr_puts(cn_mhex) 1546 sbr_puts(" ('?' repeated = file unreadable, or the row is absent from the manifest). " as *u8) 1547 } 1548 if cn_hmode == 0 { 1549 sbr_puts(" DIVERGES across trees by SIZE (this tree=" as *u8); sbr_putn(cn_nas) 1550 sbr_puts("B, authoring manifest=" as *u8); sbr_putn(cn_lap) 1551 sbr_puts("B; -1 = absent). NOTE: this is the SIZE SCREEN, so equal-size rewrites would have PASSED -- push a nx_treehash digest manifest to close that class. " as *u8) 1552 } 1553 sbr_puts("Building now would compile a FORKED copy -- the exact class that applied the unfreed-mmap fix twice. Remedy: body-diff and converge the file in BOTH trees (merge per file, never wholesale), regenerate + push knowledge/status/treecanon_laptop_hash.mf (and treecanon_laptop.mf), then rebuild.\n" as *u8) 1554 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 1555 } 1556 } 1557 } 1558 cn_p = cn_e + 1 1559 } 1560 } } 1561 1562 // ---- compile+ASSEMBLE with recompile-retry until the .s ASSEMBLES CLEANLY (compiler nondeterminism guard). 1563 // ROOT FIX 2026-06-25: the old guard stopped at the first NON-EMPTY .s -- but nx_cc's nondeterminism also 1564 // TRUNCATES (a non-empty .s missing the trailing `main` -> nxasm rc=102 "UNDEFINED label: main", caught x3 1565 // assembling nx_raci_sov / nx_pattern_library). So success now REQUIRES rc_a==0 from nxasm, not merely 1566 // non-empty bytes -> truncation-nondeterminism becomes reliability. The assemble lands in tmpelf (atomic 1567 // rename after the loop). If a real DETERMINISTIC miscompile, the retries exhaust + we LOUD-fail saying so. ---- 1568 // ---- CONTENT-ADDRESSED BUILD CACHE: try a hit BEFORE compiling (B2, 2026-08-17). FAIL-OPEN: any 1569 // problem here leaves cache_hit=0 and the build proceeds EXACTLY as before this block existed. ---- 1570 var cache_hit: i64 = 0 1571 var cache_have_key: i64 = 0 1572 let cachekey: *u8 = sys_mmap(SBR_SHA_HEX + 2) 1573 let cachepath: *u8 = sys_mmap(SBR_CACHE_TMPW) 1574 // EVERY path prints one BUILD-CACHE line (HIT | MISS reason=...), so a silent build can never be 1575 // mistaken for a cached one, and a gate can assert on the reason rather than on the absence of a word. 1576 var cache_route: i64 = 0 // 0 = consult the store | 1 = toolchain target | 2 = --no-cache | 3 = conf disabled 1577 if sbr_is_toolchain(name) == 1 { cache_route = 1 } 1578 if cache_route == 0 { if no_cache == 1 { cache_route = 2 } } 1579 if cache_route == 0 { if sbr_cache_enabled() == 0 { cache_route = 3 } } 1580 if cache_route == 1 { bld_cache_cas_say(name, "MISS reason=toolchain-target (a toolchain binary is in every key; it is never cached)" as *u8, 0 as *u8); bld_cache_cas_nl() } 1581 if cache_route == 2 { bld_cache_cas_say(name, "MISS reason=disabled-by-flag (--no-cache)" as *u8, 0 as *u8); bld_cache_cas_nl() } 1582 if cache_route == 3 { bld_cache_cas_say(name, "MISS reason=disabled-by-conf (../knowledge/buildcache.conf cache_enabled=0)" as *u8, 0 as *u8); bld_cache_cas_nl() } 1583 if cache_route == 0 { 1584 let cache_key_rc: i64 = sbr_cache_key(src, compiler, asm_tool, want_debug, cachekey) 1585 if cache_key_rc != 0 { 1586 bld_cache_cas_say(name, "MISS reason=key-unavailable (closure could not be expanded for hashing; building normally)" as *u8, 0 as *u8); bld_cache_cas_nl() 1587 } 1588 if cache_key_rc == 0 { 1589 cache_have_key = 1 1590 sys_mkdir("_build/cache" as *u8, 0x1ed) // 0755; idempotent; already-exists is fine 1591 var cq: i64 = 0 1592 cq = sbr_cat(cachepath, cq, "_build/cache/" as *u8) 1593 cq = sbr_cat(cachepath, cq, cachekey) 1594 cq = sbr_cat(cachepath, cq, ".sov.elf" as *u8); cachepath[cq] = 0 as u8 1595 cache_hit = bld_cache_cas(cachekey, cachepath, elfpath, name) 1596 } 1597 } 1598 // FAIL-LOUD unlink of the stale fossil (seq132 / seq363), MOVED HERE 2026-09-05: every refusal above has 1599 // already exited and a cache HIT has just materialised the artifact, so only a MISS that is about to 1600 // compile may remove the prior binary. A refused build now leaves the last good fossil in place. 1601 if cache_hit == 0 { sys_unlinkat(elfpath) } 1602 let tmpelf: *u8 = sys_mmap(256); o = 0 1603 o = sbr_cat(tmpelf, o, "_build/_sbr_asm_out." as *u8); o = sbr_cat(tmpelf, o, name); tmpelf[o] = 0 as u8 1604 var asmbytes: i64 = 0 1605 var rc_a: i64 = 0 - 1 1606 var built: i64 = 0 1607 var tries: i64 = 0 1608 if cache_hit == 1 { built = 1; tries = SBR_MAX_RETRIES } // BUILD-CACHE HIT: artifact already in elfpath, skip compile+assemble 1609 while tries < SBR_MAX_RETRIES { 1610 let sfd: i64 = sys_openat_wr(spath, 0x1a4) 1611 // FLAG POSITION IS LOAD-BEARING, NOT STYLISTIC. nx_cc's argv walk EXITS at the first non-flag 1612 // argument (nx_compile_x86.nx: `path_addr = arg as i64; i = argc`), so ANY flag placed AFTER the 1613 // source path is never seen. -g therefore goes BEFORE src. Had it gone after, the build would 1614 // have succeeded, printed the DEBUG BUILD banner, and produced a binary with no .debug_line -- 1615 // i.e. nx_addr2line would still say "built without -g" and the flag would read as broken rather 1616 // than misplaced. Widened to 6 slots so a future third flag cannot silently overrun the 4 this 1617 // needs (compiler, -g, src, NULL); mmap rounds to a page either way, so the cost is zero and the 1618 // declared size states the intent. 1619 let cc: *i64 = sys_mmap(8*6) as *i64 1620 var ci: i64 = 0 1621 cc[ci] = compiler as i64; ci = ci + 1 1622 if want_debug == 1 { cc[ci] = "-g" as *u8 as i64; ci = ci + 1 } 1623 cc[ci] = src as i64; ci = ci + 1 1624 cc[ci] = 0 1625 let rc_c: i64 = sbr_run(compiler, cc, envp, sfd, devnull, deadline_ms) 1626 sys_close(sfd) 1627 // A HANG IS NOT RETRIED. The retry loop above exists for nx_cc's nondeterministic .s 1628 // truncation -- a real, transient class. A timeout is the opposite: deterministic, and each 1629 // retry would burn another full deadline before failing the same way. 1630 if rc_c == SBR_TIMEOUT_RC { 1631 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 1632 sbr_puts(": COMPILE-TIMEOUT -- NOT retried, and NOT a compile failure. nx_cc never answered, so\n" as *u8) 1633 sbr_puts(" nothing was judged about this source. Do not hunt a syntax error: minimise the input that\n" as *u8) 1634 sbr_puts(" hangs the compiler and file it against the compiler lane.\n" as *u8) 1635 sys_exit(SBR_BUILD_TIMEOUT); return SBR_BUILD_TIMEOUT 1636 } 1637 asmbytes = sbr_filesize(spath) 1638 if rc_c == 0 { if asmbytes > SBR_MIN_ASM_BYTES { 1639 let aa2: *i64 = sys_mmap(8*4) as *i64 1640 aa2[0] = asm_tool as i64; aa2[1] = spath as i64; aa2[2] = tmpelf as i64; aa2[3] = 0 1641 rc_a = sbr_run(asm_tool, aa2, envp, 0 - 1, 0 - 1, deadline_ms) 1642 if rc_a == SBR_TIMEOUT_RC { 1643 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 1644 sbr_puts(": ASSEMBLE-TIMEOUT -- NOT retried. nxasm never answered on this .s.\n" as *u8) 1645 sys_exit(SBR_BUILD_TIMEOUT); return SBR_BUILD_TIMEOUT 1646 } 1647 if rc_a == 0 { built = 1; tries = SBR_MAX_RETRIES } 1648 } } 1649 if tries != SBR_MAX_RETRIES { tries = tries + 1 } 1650 } 1651 if built == 0 { 1652 if asmbytes <= SBR_MIN_ASM_BYTES { 1653 // ROOT-CAUSE SURFACE (2026-07-06): the retry loop mutes nx_cc's stderr, so a plain source error 1654 // (undefined function / parse / arg-count) hid behind this opaque "empty .s" (cost a ~15-build blind 1655 // hunt). Re-run the compiler ONCE with stderr VISIBLE so the REAL diagnostic always prints. Same 1656 // one-look answer as _offc/nx_ccdiag.elf, now automatic on every failure. 1657 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": COMPILE-FAIL (empty .s) -- nx_cc says:\n---- nx_cc stderr ----\n" as *u8) 1658 let dcc: *i64 = sys_mmap(8*4) as *i64 1659 dcc[0] = compiler as i64; dcc[1] = src as i64; dcc[2] = 0 1660 sbr_run(compiler, dcc, envp, devnull, 0 - 1, deadline_ms) // stdout->devnull, stderr VISIBLE = the real cause 1661 sbr_puts("----------------------\n(usually an undefined fn / missing import / parse error -- NOT a codegen crash; fix the source above)\n" as *u8) 1662 sys_exit(SBR_COMPILE_FAIL); return SBR_COMPILE_FAIL 1663 } 1664 if rc_a > 128 { 1665 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": NXASM-CRASHED sig=" as *u8); sbr_putn(rc_a - 128); sbr_puts(" (assembler died on this .s -> capacity/robustness bug)\n" as *u8) 1666 sys_exit(SBR_ASM_FAIL); return SBR_ASM_FAIL 1667 } 1668 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": NXASM-FAIL rc=" as *u8); sbr_putn(rc_a) 1669 sbr_puts(" after recompile-retries -- DETERMINISTIC (not the nondeterministic-truncation class). rc=102 = nxasm 'UNDEFINED label: main' = USUALLY a no-main LIBRARY built standalone -> run its _gate/importer (the one with main), NOT the lib. Else a real deterministic compiler/source miscompile.\n" as *u8) 1670 sys_exit(SBR_ASM_FAIL); return SBR_ASM_FAIL 1671 } 1672 1673 // CORRECTED 2026-06-25: the old comment here claimed the rc=6/102 "ctx-x-output-path" class was 1674 // "non-reproducible / nxasm outpath codegen CLEAN". That was WRONG -- the real rc=102 is nxasm's pass-2 1675 // "UNDEFINED label: main" (axc_label_resolve), i.e. nx_cc emitted a TRUNCATED .s missing `main`. It is the 1676 // COMPILER truncation-nondeterminism, not the output path. The retry loop above now requires a clean assemble, 1677 // so a truncated .s no longer escapes. tmpelf holds the good ELF -> atomic rename install (temp+rename kept 1678 // for atomicity = never overwrite a running binary mid-write). 1679 // On a BUILD-CACHE HIT elfpath already holds the byte-identical artifact, so skip the rename and the 1680 // store; on a MISS, install the fresh ELF and populate the content-addressed cache (fail-open). 1681 if cache_hit == 0 { 1682 sys_renameat(tmpelf, elfpath) 1683 if cache_have_key == 1 { bld_cache_cas_store(elfpath, cachepath, name); sbr_cache_prune() } 1684 } 1685 1686 // ---- LM-026 ROOT FIX: refresh the INSTALLED artifact if one exists ----------------------------- 1687 // Historical trap (cost a full session on the MMU rung): this runner builds to /tmp/<name>.sov.elf, 1688 // but gates/organs fork _offc/<name>.elf -- so a rebuild left _offc STALE and source edits silently 1689 // never took effect. Now, on a SUCCESSFUL build, if _offc/<name>.elf ALREADY EXISTS (i.e. <name> is 1690 // an installed artifact that something forks), atomically refresh it from the fresh build. This is 1691 // refresh-IF-PRESENT only: throwaway gate/probe builds (run straight from /tmp, no _offc twin) never 1692 // get a spurious _offc entry. Atomic (write .sbrtmp + renameat) = no torn binary, retires LM-026 at 1693 // the source. The reactive nx_offc_install guardrail stays as the belt-and-suspenders detector. 1694 let offcpath: *u8 = sys_mmap(256); o = 0 1695 o = sbr_cat(offcpath, o, offc_pfx); o = sbr_cat(offcpath, o, name); o = sbr_cat(offcpath, o, ".elf" as *u8); offcpath[o] = 0 as u8 1696 // The SECOND consumer location: <serving-root>/<name>.elf. Probed independently of _offc, 1697 // because most organs have exactly one of the two and the old code only ever looked at _offc. 1698 let rootpath: *u8 = sys_mmap(256); var ro: i64 = 0 1699 ro = sbr_cat(rootpath, ro, root_pfx); ro = sbr_cat(rootpath, ro, name); ro = sbr_cat(rootpath, ro, ".elf" as *u8); rootpath[ro] = 0 as u8 1700 let oex: i64 = sys_openat_rd(offcpath) 1701 let rex: i64 = sys_openat_rd(rootpath) 1702 var has_offc: i64 = 0 1703 var has_root: i64 = 0 1704 if oex >= 0 { sys_close(oex); has_offc = 1 } 1705 if rex >= 0 { sys_close(rex); has_root = 1 } 1706 // ---- THE DECISION IS DELEGATED, NOT MADE HERE (2026-08-26) ---- 1707 // nx_builddeploy_lib owns "may this build write a consumer-visible binary?", so the QUEUED lane and 1708 // the SEAT lane cannot disagree, and a fixture can hold every combination of its inputs. The probes 1709 // stay here (does a twin exist, is this a toolchain, is this a declared daemon); the JUDGEMENT moved. 1710 // 1711 // WHAT CHANGED: --build-only now installs NOTHING. WHY, measured 2026-08-26 from the queue's OWN 1712 // artifact knowledge/store/planrun-build-nx_gate_roster_run-: 1713 // [nx_sov_build_run] nx_gate_roster_run: DEPLOYED to serving root (twin refresh, live for the 1714 // next fork) -> ../nx_gate_roster_run.elf 1715 // /api/build's load-refusal path forks nx_job_run nx_buildq add; nx_buildq seeds the plan step 1716 // "nx_sov_build_run <target> --build-only"; nx_orchestrate fires it from the SERVING ROOT -- where 1717 // this runner's CWD anchor DOES fire, so root_pfx becomes "../" and the twin refresh lands on the 1718 // LIVE binary. The caller had been told only that its build was deferred and to promote deliberately 1719 // afterwards. Every promote-lane guard -- nx_contentdiff, nx_behaveprobe, the capability-loss check, 1720 // the .prev bank -- was bypassed by construction, with no seat anywhere in the loop. 1721 // A FLAG WHOSE MEANING DEPENDS ON THE CALLER'S CWD CANNOT BE REASONED ABOUT FROM ITS NAME. 1722 let bd_tc: i64 = sbr_is_toolchain(name) 1723 let bd_dk: i64 = sbr_is_daemon(name) 1724 // What the twin probes ALONE would have wanted, kept only so the refusals below can say "there was 1725 // something here and I declined to write it" instead of going silent. 1726 var bd_would: i64 = 0 1727 if has_offc == 1 { bd_would = 1 } 1728 if has_root == 1 { bd_would = 1 } 1729 if force_install == 1 { bd_would = 1 } 1730 let bd_loc: i64 = bd_locations(has_offc, has_root, force_install, build_only, bd_tc, bd_dk) 1731 let bd_reason: i64 = bd_why(has_offc, has_root, force_install, build_only, bd_tc, bd_dk) 1732 let bd_held: i64 = bd_root_held(has_root, force_install, build_only, bd_tc, bd_dk) 1733 has_offc = bd_writes_offc(bd_loc) 1734 has_root = bd_writes_root(bd_loc) 1735 var do_install: i64 = 0 1736 if bd_loc != BD_LOC_NONE { do_install = 1 } 1737 // A RECEIPT MUST NAME THE CONSEQUENCE, AND AN UNWRITTEN LIVE BINARY IS A RESULT, NOT AN ABSENCE. 1738 // Silence here is exactly what let the queued-build class run unnoticed for weeks: the only way to 1739 // learn what a build had done to the serving root was to hash it afterwards and compare. 1740 if bd_held == 1 { 1741 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 1742 sbr_puts(": SERVING-ROOT TWIN HELD -- a live binary exists at " as *u8); sbr_puts(rootpath) 1743 sbr_puts(" and was deliberately NOT written.\n" as *u8) 1744 if bd_reason == BD_WHY_BUILD_ONLY { 1745 if qualify==1 { sbr_puts(" REASON: --qualify. Builder installs NOTHING; private runtime check follows.\n" as *u8) } else { sbr_puts(" REASON: --build-only. This build installed NOTHING, anywhere. AWAITING PROMOTE.\n" as *u8) } 1746 sbr_puts(" Ship it deliberately with nx_organ_ship <target>, which runs the content ruler, the\n" as *u8) 1747 sbr_puts(" behaviour probe and the gate BEFORE it promotes. Do not re-run this builder without\n" as *u8) 1748 sbr_puts(" the flag to apply it -- that is a deploy with none of those guards.\n" as *u8) 1749 } 1750 if bd_dk == 1 { 1751 sbr_puts(" REASON: this target is a DECLARED DAEMON. The supervisor respawns from disk, so that\n" as *u8) 1752 sbr_puts(" write IS a deploy -- no canary, no health gate, no .prev. Deploy it deliberately via\n" as *u8) 1753 sbr_puts(" POST /api/deploy (validated, .prev-banked, async health probe + auto-rollback).\n" as *u8) 1754 } 1755 sbr_puts(" Staged artifact: " as *u8); sbr_puts(elfpath); sbr_puts("\n" as *u8) 1756 } 1757 // ⚠⚠TOOLCHAIN IS NEVER AUTO-INSTALLED (rule 26, added 2026-07-30 after seq1464; RESTORED in the 1758 // 2026-08-03 fork merge -- the NAS branch had dropped this entire guard, so any successful build 1759 // of a toolchain binary silently replaced the deployed one, canary-less, even under --build-only). 1760 // MEASURED INCIDENT: buildroot/_offc/nx_cc_sovereign.elf was found LIVE at 520103 bytes -- the 1761 // output of a rebuild made while buildroot was stale -- instead of the 543126 that had been 1762 // deliberately promoted. The hub was therefore compiling EVERY organ with a compiler whose 1763 // optimiser was 11712 source-bytes short, and its .s came out 16430B larger from identical 1764 // sources. Nobody chose that; a build installed it. 1765 // 1766 // THE INVARIANT: refresh-IF-PRESENT is right for ordinary organs (it retires LM-026 staleness) and 1767 // CATASTROPHIC for the toolchain, because the toolchain is what BUILT the thing being installed -- 1768 // a bad build then replaces the compiler that produced it and every subsequent build inherits the 1769 // damage, with no promote, no canary and no .prev. That is a self-modifying build path. 1770 // The toolchain has exactly ONE lawful update route: /api/promote_toolchain, which ELF-validates, 1771 // banks .prev, chmod +x, CANARY-COMPILES AND RUNS, and auto-rolls-back. This guard makes that route 1772 // the only one BY CONSTRUCTION rather than by convention. 1773 // ★Applies to --install too: an explicit flag must not be able to brick the toolchain either. 1774 if bd_tc == 1 { 1775 if bd_would == 1 { 1776 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 1777 sbr_puts(": REFUSED to auto-install a TOOLCHAIN binary into _offc (rule 26 / seq1464).\n" as *u8) 1778 sbr_puts(" The build artifact is staged and usable; promote it deliberately via\n" as *u8) 1779 sbr_puts(" POST /api/promote_toolchain (ELF-validated, .prev-banked, canary-run, auto-rollback).\n" as *u8) 1780 } 1781 do_install = 0 1782 } 1783 if do_install == 1 { 1784 let il: *i64 = sys_mmap(16) as *i64 1785 let ib: *u8 = sys_read_file(elfpath, il) 1786 if (ib as i64) != 0 { 1787 // Refresh EVERY consumer location that actually holds a twin -- an organ forked from the 1788 // serving root got nothing when only _offc was refreshed. Both writes are atomic 1789 // (.sbrtmp + renameat), so neither can leave a torn binary. 1790 if has_offc == 1 { 1791 let otmp: *u8 = sys_mmap(256); var oo: i64 = 0 1792 oo = sbr_cat(otmp, oo, offc_pfx); oo = sbr_cat(otmp, oo, name); oo = sbr_cat(otmp, oo, ".elf.sbrtmp" as *u8); otmp[oo] = 0 as u8 1793 let ofd: i64 = sys_openat_wr(otmp, 493) // 0755 = executable (refresh AND force-install-new) 1794 if ofd >= 0 { sys_write(ofd, ib, il[0]); sys_close(ofd); sys_renameat(otmp, offcpath) } 1795 } 1796 if has_root == 1 { 1797 // has_root SURVIVED bd_locations, so this target is provably NOT a declared daemon, not 1798 // the toolchain, and the caller's intent permits an install. The daemon refusal moved up 1799 // to the held-twin reasons, where it prints even when nothing is installed at all -- 1800 // down here it could only ever speak on a path that had already decided to write. 1801 let rtmp: *u8 = sys_mmap(256); var rr: i64 = 0 1802 rr = sbr_cat(rtmp, rr, root_pfx); rr = sbr_cat(rtmp, rr, name); rr = sbr_cat(rtmp, rr, ".elf.sbrtmp" as *u8); rtmp[rr] = 0 as u8 1803 let rfd: i64 = sys_openat_wr(rtmp, 493) 1804 if rfd >= 0 { sys_write(rfd, ib, il[0]); sys_close(rfd); sys_renameat(rtmp, rootpath) } 1805 // Say DEPLOYED, not REFRESHED. The old word is why a caller who had decided not to 1806 // deploy read this line and believed nothing had shipped. 1807 // ★ A RECEIPT MUST NAME THE CONSEQUENCE, NOT THE MECHANISM. 1808 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 1809 sbr_puts(": DEPLOYED to serving root (twin refresh, live for the next fork) -> " as *u8) 1810 sbr_puts(rootpath); sbr_puts("\n" as *u8) 1811 if bd_dk < 0 { 1812 sbr_puts(" ⚠ daemon-check UNPROVEN: knowledge/status/organ_kind.conf unreadable, so\n" as *u8) 1813 sbr_puts(" this target's kind was never established. Verify it is not a daemon.\n" as *u8) 1814 } 1815 } 1816 // Print the path that was ACTUALLY written, not a hardcoded one. The old message said 1817 // "_offc/<name>.elf" unconditionally while writing buildroot/_offc/<name>.elf, so the 1818 // receipt could not be used to check the claim. VERIFY BY ARTIFACT, AND LET THE RECEIPT 1819 // NAME THE ARTIFACT IT WROTE. 1820 if force_install == 1 { sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": INSTALLED -> " as *u8); sbr_puts(offcpath); sbr_puts(" (--install)\n" as *u8) } 1821 } 1822 } 1823 1824 // release the per-target build lock HERE -- artifacts are final (atomic renames done); the run 1825 // phase below must not hold it (a daemon target would wedge every future rebuild of itself). 1826 if lockfd >= 0 { sys_flock(lockfd, SYS_LOCK_UN); sys_close(lockfd) } 1827 1828 // ---- build-only mode: argv[2] starting "--b" (--build-only) SKIPS the run, so a sovereign 1829 // bring-up/supervisor can build a dependency without triggering its side effects (e.g. a worker 1830 // would otherwise start a live download). The build path above is byte-for-byte unchanged; 1831 // this only short-circuits before the run. Default (no 2nd arg) behaves exactly as before. ---- 1832 // --build-only OR --install both SKIP the run (skip_run was set from argv[2] at the top). 1833 if skip_run == 1 { 1834 // ARTIFACT PATH IS PART OF THE OUTPUT CONTRACT (2026-07-30). --build-only used to print only 1835 // "asm=NNNNB", so the caller was told the build SUCCEEDED but not WHERE the ELF landed -- and the 1836 // header comments still said /tmp/<name>.sov.elf while the code has written _build/<name>.sov.elf 1837 // since the flock change. Every caller therefore had to go hunting (measured: 3 shell round-trips 1838 // to locate one artifact). A tool that produces a file MUST say where it put it; the run path 1839 // below already prints elf=<path>, so build-only was the odd one out. Same line shape as the run 1840 // branch so existing log scrapers see a superset, never a changed field (rule 19). 1841 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": asm=" as *u8); sbr_putn(asmbytes) 1842 sbr_puts("B SOVEREIGN build (nx_cc->nxasm_x86, no gcc, no run) elf=" as *u8); sbr_puts(elfpath) 1843 sbr_puts("\n" as *u8) 1844 sys_exit(SBR_OK); return SBR_OK 1845 } 1846 1847 // ---- run the sovereign ELF WITH FORWARDED ARGS; report; exit with its code ---- 1848 // ARG-FORWARDING (2026-07-14 debt-eat): argv[fwd_start..argc) pass through to the built program, so a 1849 // daemon/tool builds AND launches with its runtime args in one command (e.g. a cap-secret path). Before 1850 // this, extra args were silently DROPPED -- caught live when the self-gated git server ran AUTH OFF. 1851 let rr: *i64 = sys_mmap(8 * 20) as *i64 1852 rr[0] = elfpath as i64 1853 var ra: i64 = 1 1854 var ai: i64 = fwd_start 1855 while ai < argc { if ra < 19 { rr[ra] = argv[ai]; ra = ra + 1 } ai = ai + 1 } 1856 rr[ra] = 0 1857 // NO DEADLINE HERE, deliberately: this runs the BUILT PROGRAM, which may be a daemon or a long 1858 // job. Time-boxing a build tool is safety; time-boxing the user's program is a wrong answer. 1859 var run_deadline:i64=SBR_NO_DEADLINE 1860 if qualify==1 { run_deadline=deadline_ms; if qualification_deadline_given==1 { run_deadline=qualification_deadline_ms } } 1861 let rc_r: i64 = sbr_run(elfpath, rr, envp, 0 - 1, 0 - 1, run_deadline) 1862 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": asm=" as *u8); sbr_putn(asmbytes) 1863 sbr_puts("B SOVEREIGN(nx_cc->nxasm_x86, no gcc) elf=" as *u8); sbr_puts(elfpath) 1864 sbr_puts(" run-exit=" as *u8); sbr_putn(rc_r); sbr_puts("\n" as *u8) 1865 if qualify==1 && rc_r==SBR_TIMEOUT_RC { sbr_puts("[nx_sov_build_run] QUALIFICATION-TIMEOUT: test process stopped; not a passing gate.\n" as *u8);sys_exit(SBR_BUILD_TIMEOUT);return SBR_BUILD_TIMEOUT } 1866 sys_exit(rc_r) 1867 return rc_r 1868}