code wiki / _hdl_build / nx_builddeploy_lib_qualify_20260910.nx

nx_builddeploy_lib_qualify_20260910.nx source

↩ module page · 150 lines · 8771 B

1// nx_builddeploy_lib.nx -- THE ONE RULER FOR "MAY THIS BUILD WRITE A CONSUMER-VISIBLE BINARY?" 2// 3// WHY THIS EXISTS. nx_sov_build_run decides, inline and in three scattered places, whether a successful 4// build ALSO replaces the binary consumers fork. Those guards were written on three different days for 5// three different incidents -- toolchain (rule 26), the daemon supervisor-respawn class, and now the 6// queued-build class -- and each only ever covered the case that motivated it. A LAW APPLIED IN ONE VERB 7// AND NOT ITS SIBLING IS HALF A LAW: the flag literally named --build-only was never part of the decision 8// at all, so it built AND deployed, and nothing in the estate could test that it did. 9// 10// MEASURED 2026-08-26 from the queue's OWN artifact (knowledge/store/planrun-build-nx_gate_roster_run-): 11// [nx_sov_build_run] nx_gate_roster_run: DEPLOYED to serving root (twin refresh, live for the next 12// fork) -> ../nx_gate_roster_run.elf 13// That row was seeded by /api/build's own load-refusal path: nx_mgmt_api ma_build_refusal_body forks 14// nx_job_run nx_buildq add, and nx_buildq seeds the plan step "nx_sov_build_run <target> --build-only". 15// The refusal receipt tells the caller the result "lands in knowledge/store/planrun-build-<target>-; 16// promote deliberately afterwards". By the time the caller reads that row the binary is ALREADY LIVE: 17// no nx_contentdiff, no nx_behaveprobe, no capability-loss check, no .prev bank. 18// A CALLER TOLD ITS WORK WAS MERELY DEFERRED HAD ITS SERVING BINARY REPLACED, WITH NO SEAT IN THE LOOP. 19// 20// THE HIDDEN VARIABLE IS THE CALLER'S WORKING DIRECTORY, AND IT IS WHY THIS SURVIVED SO LONG. 21// The same binary, the same flag and the same target deploy or do not deploy depending only on where the 22// caller stood. /api/build (nx_hostctl cmd_buildrun) chdir's INTO buildroot before exec, so the builder's 23// CWD anchor never fires, root_pfx stays empty, the twin probe asks for buildroot/<name>.elf -- which does 24// not exist -- and nothing is installed, which is why the build/promote split has held there and why 25// hundreds of BEHIND rows exist on the drift board. The queue's runner starts in the SERVING ROOT, the 26// anchor fires, root_pfx becomes "../", and the identical command deploys. The "../" in the receipt above 27// is the PROOF the anchor fired: no other branch can emit that prefix. 28// A FLAG WHOSE MEANING DEPENDS ON THE CALLER'S CWD CANNOT BE REASONED ABOUT FROM ITS NAME. 29// 30// WHAT THIS LIB IS. A pure integer decision -- no I/O, no paths, no forks -- so a gate holds every 31// combination of its inputs with a fixture instead of reproducing a host state. The CALLER keeps its 32// probes (does a twin exist, is this a toolchain, is this a declared daemon); this owns the JUDGEMENT. 33// Because there is only ONE of it, the queued lane and the seat lane cannot disagree. 34// 35// SCOPE STATED HONESTLY -- WHAT THIS DELIBERATELY DOES NOT CHANGE: 36// (1) dkind < 0 (organ_kind.conf unreadable, so daemon-ness UNPROVEN) still permits a serving-root 37// write on the DEFAULT path, exactly as the incumbent does, and the caller still warns. That is an 38// abstention-that-acquits and it is a real residual, but it is a DIFFERENT lane from this one and 39// narrowing it would change the default build path for every caller. It is encoded faithfully here 40// and pinned by its own tooth so the next reader adjudicates it instead of inheriting it silently. 41// (2) A daemon's _offc twin is still refreshed (only the SERVING-ROOT write is held). Preserved from 42// the incumbent deliberately: this lane must not change any path that previously succeeded. 43// license_tier: ORIGINAL No hw writes (Rule 26). 44 45// ---- consumer locations, as a bitmask ---- 46const BD_LOC_NONE: i64 = 0 47const BD_LOC_OFFC: i64 = 1 48const BD_LOC_ROOT: i64 = 2 49 50// ---- which rule decided; the receipt must NAME it, never just report the outcome ---- 51const BD_WHY_TOOLCHAIN: i64 = 1 // rule 26: only /api/promote_toolchain may move the toolchain 52const BD_WHY_BUILD_ONLY: i64 = 2 // the caller said build only, so this build installs nothing 53const BD_WHY_FORCE: i64 = 3 // --install: explicit, deliberate intent to make it permanent 54const BD_WHY_REFRESH: i64 = 4 // refresh-IF-PRESENT (LM-026): a twin exists and is being kept fresh 55const BD_WHY_NO_TWIN: i64 = 5 // nothing consumes this name yet; nothing to refresh 56 57// Does the caller's intent permit ANY consumer-visible write at all? 58// Split out because it is the whole of the new rule and a gate can hold it alone. 59// --install is DELIBERATE INTENT and outranks --build-only when both are passed, so a caller that asks 60// for an install in the same breath still gets one; --build-only alone can never install anything. 61func bd_intent_permits_install(build_only: i64, force_install: i64) -> i64 { 62 if force_install == 1 { return 1 } 63 if build_only == 1 { return 0 } 64 return 1 65} 66 67// THE DECISION. Returns a BD_LOC_* bitmask of the consumer locations this build may write. 68func bd_locations(has_offc: i64, has_root: i64, force_install: i64, build_only: i64, toolchain: i64, dkind: i64) -> i64 { 69 // Rule 26 first and unconditionally: a build must never be able to replace the compiler that 70 // produced it, not even under an explicit --install. A self-modifying build path has no rollback. 71 if toolchain == 1 { return BD_LOC_NONE } 72 if bd_intent_permits_install(build_only, force_install) == 0 { return BD_LOC_NONE } 73 var loc: i64 = BD_LOC_NONE 74 if has_offc == 1 { loc = loc + BD_LOC_OFFC } 75 if force_install == 1 { 76 if has_offc == 0 { loc = loc + BD_LOC_OFFC } 77 } 78 if has_root == 1 { 79 // A DECLARED DAEMON'S SERVING-ROOT FILE IS NOT AN ARTIFACT, IT IS A DEPLOY: the supervisor 80 // respawns from disk, so writing it ships with no canary, no health gate and no .prev. 81 if dkind != 1 { loc = loc + BD_LOC_ROOT } 82 } 83 return loc 84} 85 86func bd_writes_offc(loc: i64) -> i64 { if (loc & BD_LOC_OFFC) != 0 { return 1 } return 0 } 87func bd_writes_root(loc: i64) -> i64 { if (loc & BD_LOC_ROOT) != 0 { return 1 } return 0 } 88 89// WHY the decision came out as it did, so the receipt can name the rule rather than the mechanism. 90func bd_why(has_offc: i64, has_root: i64, force_install: i64, build_only: i64, toolchain: i64, dkind: i64) -> i64 { 91 if toolchain == 1 { return BD_WHY_TOOLCHAIN } 92 if bd_intent_permits_install(build_only, force_install) == 0 { return BD_WHY_BUILD_ONLY } 93 if force_install == 1 { return BD_WHY_FORCE } 94 let loc: i64 = bd_locations(has_offc, has_root, force_install, build_only, toolchain, dkind) 95 if loc == BD_LOC_NONE { return BD_WHY_NO_TWIN } 96 return BD_WHY_REFRESH 97} 98 99// A SERVING-ROOT TWIN EXISTED AND SOMETHING DELIBERATELY DID NOT WRITE IT. 100// This is the line the caller owes its reader: silence here is what made the queued-build class 101// invisible for weeks. "I built it and left your live binary alone" is a RESULT, not an absence. 102func bd_root_held(has_root: i64, force_install: i64, build_only: i64, toolchain: i64, dkind: i64) -> i64 { 103 if has_root != 1 { return 0 } 104 if toolchain == 1 { return 1 } 105 if bd_intent_permits_install(build_only, force_install) == 0 { return 1 } 106 if dkind == 1 { return 1 } 107 return 0 108} 109 110// Qualification runs the built target but grants no consumer-install intent. 111// Conflicting explicit intents refuse rather than depend on flag order. 112func bd_qualification_mode(qualify:i64, build_only:i64, force_install:i64)->i64 { 113 if qualify==0 { return 0 } 114 if qualify!=1 { return -1 } 115 if build_only==1 || force_install==1 { return -1 } 116 return 1 117} 118func bd_qualification_flag(flag:*u8)->i64 { 119 let wanted:*u8="--qualify" as *u8 120 var i:i64=0 121 while wanted[i]!=0 as u8 { if flag[i]!=wanted[i] { return 0 }; i=i+1 } 122 if flag[i]!=0 as u8 { return 0 } 123 return 1 124} 125 126// Preserve documented incumbent flag families, but never silently run on an unknown flag. 127func bd_lane_flag_known(flag:*u8)->i64 { 128 if flag[0]!=45 as u8 || flag[1]!=45 as u8 { return 0 } 129 let c:i64=flag[2] as i64 130 if c==98 || c==105 || c==100 || c==110 || c==114 || c==116 { return 1 } 131 return bd_qualification_flag(flag) 132} 133 134func bd_run_timeout_flag(flag:*u8)->i64 { 135 let wanted:*u8="--run-timeout-ms" as *u8 136 var i:i64=0;while wanted[i]!=0 as u8 { if flag[i]!=wanted[i] { return 0 };i=i+1 } 137 if flag[i]!=0 as u8 { return 0 };return 1 138} 139 140// Decimal input must fit the signed runtime counter before any arithmetic. 141func bd_positive_ms(s:*u8)->i64 { 142 var v:i64=0;var i:i64=0 143 while s[i]!=0 as u8 { 144 let digit:i64=(s[i] as i64)-48 145 if digit<0 || digit>9 { return -1 } 146 if v>922337203685477580 || (v==922337203685477580 && digit>7) { return -1 } 147 v=v*10+digit;i=i+1 148 } 149 if i==0 || v<=0 { return -1 };return v 150}