code wiki / _hdl_build / nx_job_run.nx

nx_job_run.nx source

↩ module page · 261 lines · 14266 B

1// nx_job_run.nx v3 -- GUARDED ASYNC JOB RUNNER. Eats the async-fetch >15s edge-timeout debt the 2// good-form way: not a cron workaround, not an arbitrary executor. 3// SAFE BY CONSTRUCTION (rule 26 never-brick / rule 12 boundary-defense) -- FOUR fail-closed gates: 4// (1) CLASS: deny-by-class via nx_job_launch_bound -- WIDENED 2026-07-31, and this is exactly the 5// "filed rung" the previous line named for itself. The old rule launched ONLY organs whose 6// name contained *research_fetch*, which meant the ecosystem had NO route to run a general 7// organ and every generator fell back to shell. Now: generators, rollups, censuses and page 8// emitters are launchable; DAEMONS and SERVERS are refused (they never exit, so an async 9// launch wedges the slot forever) and so is the CONTROL PLANE (deploy/promote/restart/kill/ 10// mgmt/hostctl/supervisor). Fail-closed on an empty name. Deny-by-class, not allow-by-one- 11// substring: a new generator is launchable the day it is allowlisted with no edit here, while 12// a new daemon is refused by default -- a rule nothing must remember beats a list somebody 13// must maintain. Proven by nx_job_launch_bound_gate 15/15 GREEN, including back-compat teeth 14// for every organ the old *research_fetch* rule allowed. 15// (2) VETTED: the name must resolve to a GREEN elf in tool_allowlist.conf -- the SAME trust 16// boundary nx_plan_run uses; NEVER an arbitrary caller-supplied path. 17// (3) NO-PIN-BYPASS: refuses pinned rows -- can never run a pinned elf with non-pinned args. 18// (4) SCRATCH-ONLY: out-file confined to /tmp/ with no '..' -- writes land only on disposable scratch. 19// Mechanism (mirrors nx_hostctl cmd_buildrun): fork -> child redirects stdout+stderr to out-file via 20// sys_dup3 + execve the resolved elf with the caller's [args...]; PARENT returns IMMEDIATELY with a 21// job handle (does NOT wait4) -> child is orphaned + reaped by init, runs to completion async while 22// /mcp already answered -> a slow research-fetch no longer times out the edge. 23// nx_job_run <tool-name(*research_fetch*)> <out-file:/tmp/...> [args...] -> JOB-STARTED pid=.. elf=.. out=.. 24// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 25import "nx_syscalls.nx" 26import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 27import "nx_job_launch_bound.nx" 28import "nx_jobcancel_lib.nx" // ES29 (2026-09-15): the cancel verb -- the launcher is the one surface that can stop what it started 29const JR_MAGIC_1024: i64 = 1024 30const JR_VERB_CANCEL: *u8 = "cancel" 31 32const JR_OMODE: i64 = 0x1a4 33const JR_STDOUT: i64 = 1 34const JR_STDERR: i64 = 2 35const JR_MAXARGV: i64 = 32 36const JR_NL: i64 = 10 37const JR_TAB: i64 = 9 38const JR_HASH: i64 = 35 39 40func jr_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 41func jr_puts(s: *u8) -> i64 { sys_write(JR_STDOUT, s, jr_slen(s)); return 0 } 42func jr_werr(s: *u8) -> i64 { sys_write(JR_STDERR, s, jr_slen(s)); return 0 } 43// append a NUL-terminated string into a buffer; returns the new offset. Pairs with ccz_cat_num from 44// nx_itoa_lib so the journal line is built with ZERO allocation per field. 45func jr_cat(d: *u8, off: i64, s: *u8) -> i64 { 46 var o: i64 = off 47 var i: i64 = 0 48 while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } 49 return o 50} 51// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 52// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 53// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 54// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 55func jr_putn(v: i64) -> i64 { nxi_out(v); return 0 } 56func jr_slice_eq(buf: *u8, start: i64, end: i64, s: *u8) -> i64 { 57 var i: i64 = start 58 var j: i64 = 0 59 while i < end { 60 if s[j] == (0 as u8) { return 0 } 61 if buf[i] != s[j] { return 0 } 62 i = i + 1 63 j = j + 1 64 } 65 if s[j] != (0 as u8) { return 0 } 66 return 1 67} 68func jr_contains(hay: *u8, needle: *u8) -> i64 { 69 if needle[0] == (0 as u8) { return 1 } 70 var i: i64 = 0 71 while hay[i] != (0 as u8) { 72 var j: i64 = 0 73 var m: i64 = 1 74 while m == 1 { 75 if needle[j] == (0 as u8) { return 1 } 76 if hay[i+j] == (0 as u8) { m = 0 } else { if hay[i+j] != needle[j] { m = 0 } else { j = j + 1 } } 77 } 78 i = i + 1 79 } 80 return 0 81} 82func jr_is_tmp(p: *u8) -> i64 { 83 if p[0] != (47 as u8) { return 0 } 84 if p[1] != (116 as u8) { return 0 } 85 if p[2] != (109 as u8) { return 0 } 86 if p[3] != (112 as u8) { return 0 } 87 if p[4] != (47 as u8) { return 0 } 88 return 1 89} 90func jr_has_dotdot(p: *u8) -> i64 { 91 var i: i64 = 0 92 while p[i] != (0 as u8) { 93 if p[i] == (46 as u8) { if p[i+1] == (46 as u8) { return 1 } } 94 i = i + 1 95 } 96 return 0 97} 98 99func main(argc: i64, argv: *i64) -> i64 { 100 if argc < 3 { jr_werr("usage: nx_job_run <tool-name(allowlisted GREEN, non-daemon)> <out-file:/tmp/...> [args...]\n" as *u8); sys_exit(1); return 1 } 101 let name: *u8 = argv[1] as *u8 102 let outp: *u8 = argv[2] as *u8 103 104 // ES29 CANCEL VERB (2026-09-15): `nx_job_run cancel <out-file>` stops the job THIS launcher recorded for that 105 // out-file and nothing else -- the journal row must exist (REFUSED-ABSENT) and the live pid's argv[0] must be 106 // the elf the row names (REFUSED-MISMATCH), so a recycled pid, a stranger or a daemon is never signalled. 107 // TERM, a grace from knowledge/jobrun.conf (cancel_grace_ms), then KILL; the outcome is journaled beside the 108 // launch. MEASURED NEED: two one-shots held the host at load 27 for 30 h with no sovereign way to stop them. 109 if jr_slice_eq(name, 0, jr_slen(name), JR_VERB_CANCEL) == 1 { 110 if jr_is_tmp(outp) == 0 { jr_werr("REFUSED: out-file must be under /tmp/ (write-surface defense)\n" as *u8); sys_exit(2); return 2 } 111 if jr_has_dotdot(outp) == 1 { jr_werr("REFUSED: out-file must not contain '..'\n" as *u8); sys_exit(2); return 2 } 112 let grace: i64 = jc_conf_grace(JC_CONF, JC_GRACE_MS_DEFAULT) 113 let cpid: *i64 = sys_mmap(8) as *i64 114 let code: i64 = jc_cancel(JC_JRNL, outp, grace, JC_POLL_MS, cpid) 115 jr_puts("JOB-CANCEL out=" as *u8); jr_puts(outp) 116 jr_puts(" pid=" as *u8); jr_putn(cpid[0]) 117 jr_puts(" outcome=" as *u8); jr_puts(jc_outcome_name(code)) 118 jr_puts(" grace_ms=" as *u8); jr_putn(grace) 119 jr_puts(" journal=" as *u8); jr_puts(JC_JRNL) 120 jr_puts("\n" as *u8) 121 sys_exit(code); return code 122 } 123 124 if jlb_denied(name) == 1 { jr_werr("REFUSED: daemons, servers, supervisors and control-plane organs (deploy/promote/restart/kill/mgmt/hostctl) are never async-launchable -- they outlive the call or mutate the live plane. Generators are launchable once vetted GREEN in tool_allowlist.conf.\n" as *u8); sys_exit(6); return 6 } 125 if jr_is_tmp(outp) == 0 { jr_werr("REFUSED: out-file must be under /tmp/ (write-surface defense)\n" as *u8); sys_exit(2); return 2 } 126 if jr_has_dotdot(outp) == 1 { jr_werr("REFUSED: out-file must not contain '..'\n" as *u8); sys_exit(2); return 2 } 127 128 let lenp: *i64 = sys_mmap(8) as *i64 129 lenp[0] = 0 130 let buf: *u8 = sys_read_file("tool_allowlist.conf" as *u8, lenp) 131 let n: i64 = lenp[0] 132 if n <= 0 { jr_werr("REFUSED: tool registry unreadable\n" as *u8); sys_exit(3); return 3 } 133 134 let elfbuf: *u8 = sys_mmap(JR_MAGIC_1024) 135 var found: i64 = 0 136 var was_pinned: i64 = 0 137 var i: i64 = 0 138 while i < n { 139 var le: i64 = i 140 var s: i64 = 1 141 while s == 1 { if le >= n { s = 0 } else { if buf[le] == (JR_NL as u8) { s = 0 } else { le = le + 1 } } } 142 if le > i { if buf[i] != (JR_HASH as u8) { 143 var ne: i64 = i 144 var s2: i64 = 1 145 while s2 == 1 { if ne >= le { s2 = 0 } else { if buf[ne] == (JR_TAB as u8) { s2 = 0 } else { ne = ne + 1 } } } 146 if ne > i { if found == 0 { if jr_slice_eq(buf, i, ne, name) == 1 { 147 var ps: i64 = ne + 1 148 var pe: i64 = ps 149 var s3: i64 = 1 150 while s3 == 1 { if pe >= le { s3 = 0 } else { if buf[pe] == (JR_TAB as u8) { s3 = 0 } else { pe = pe + 1 } } } 151 var gs: i64 = pe + 1 152 var ge: i64 = gs 153 var s4: i64 = 1 154 while s4 == 1 { if ge >= le { s4 = 0 } else { if buf[ge] == (JR_TAB as u8) { s4 = 0 } else { ge = ge + 1 } } } 155 if jr_slice_eq(buf, gs, ge, "GREEN" as *u8) == 1 { 156 if ge >= le { 157 if pe > ps { 158 var eo: i64 = 0 159 var k: i64 = ps 160 while k < pe { elfbuf[eo] = buf[k]; eo = eo + 1; k = k + 1 } 161 elfbuf[eo] = 0 as u8 162 found = 1 163 } 164 } else { was_pinned = 1 } 165 } 166 } } } 167 } } 168 i = le + 1 169 } 170 171 if found == 0 { 172 if was_pinned == 1 { jr_werr("REFUSED: '" as *u8); jr_werr(name); jr_werr("' is PINNED; nx_job_run only launches UNPINNED GREEN tools (anti-pin-bypass)\n" as *u8); sys_exit(5); return 5 } 173 jr_werr("REFUSED: '" as *u8); jr_werr(name); jr_werr("' is not an unpinned GREEN tool in the registry\n" as *u8); sys_exit(4); return 4 174 } 175 176 // ★EXISTENCE CHECK BEFORE THE FORK (2026-08-01, debt 1785623939). Every guard above validates 177 // the NAME -- registered, GREEN, unpinned, not a daemon -- and NOTHING validated that the elf 178 // the row points at still EXISTS. After execve fails in the child there is no way to report it: 179 // the parent has already returned JOB-STARTED and never wait4's. Reproduced live: a tool row 180 // registered against a `.sov.elf.new` STAGING artifact, which /api/promote then consumed, 181 // printed a confident `JOB-STARTED pid=8938` and simply never wrote its out-file. ABSENCE OF 182 // OUTPUT WAS THE ONLY SIGNAL -- the same reported-green-wrote-nothing shape that cost a whole 183 // session elsewhere today. 184 // ★★★★★A LAUNCHER THAT VALIDATES THE NAME BUT NOT THE ARTIFACT REPORTS SUCCESS FOR A PROCESS 185 // THAT CANNOT START. Fail LOUD here, where a caller can still see it, instead of silently in a 186 // child nobody reaps. 187 let jr_probe: i64 = sys_openat_rd(elfbuf) 188 if jr_probe < 0 { 189 jr_werr("REFUSED: registry row for '" as *u8); jr_werr(name) 190 jr_werr("' points at a MISSING elf: " as *u8); jr_werr(elfbuf) 191 jr_werr("\n the row is dangling -- most often it was registered against a .sov.elf.new STAGING\n" as *u8) 192 jr_werr(" artifact that a later /api/promote consumed. Rebuild the target to regenerate it, or\n" as *u8) 193 jr_werr(" re-register the name against the PROMOTED .elf. BUILD -> PROMOTE -> THEN REGISTER.\n" as *u8) 194 sys_exit(7); return 7 195 } 196 sys_close(jr_probe) 197 198 let pid: i64 = sys_fork() 199 if pid == 0 { 200 let of: i64 = sys_openat_wr(outp, JR_OMODE) 201 if of >= 0 { sys_dup3(of, JR_STDOUT, 0); sys_dup3(of, JR_STDERR, 0) } 202 let av: *i64 = sys_mmap(JR_MAXARGV*8) as *i64 203 av[0] = elfbuf as i64 204 var a: i64 = 3 205 var kk: i64 = 1 206 while a < argc { if kk < JR_MAXARGV - 1 { av[kk] = argv[a]; kk = kk + 1 } a = a + 1 } 207 av[kk] = 0 208 let envp: *i64 = sys_mmap(16) as *i64 209 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64 210 envp[1] = 0 211 sys_execve(elfbuf, av, envp) 212 sys_exit(127) 213 } 214 // ★★★RECORD THE ORGAN WE RAN, NOT OURSELVES. nx_catalog and nx_wirecensus derive INVOKED from TAB 215 // field 2 of actlog.jrnl, and a direct fork logged NOTHING -- so every organ driven the estate's own 216 // recommended way (`nx_job_run <tool> /tmp/out`) read REGISTERED-DARK, "never run". MEASURED 217 // 2026-08-14: nx_dstate, nx_gateaddr and nx_bodyfit_canon_gate all read never-run while being 218 // executed repeatedly in the same session. That made the estate's HEADLINE ADOPTION SIGNAL A FLOOR 219 // RATHER THAN A VALUE -- daily-used capability counted as unadopted. 220 // nx_wirecensus_lib calls this axis "WEAK BUT POSITIVE ... Seats log", i.e. it depended on a human 221 // REMEMBERING to journal. ★A MEASUREMENT THAT DEPENDS ON BEING REMEMBERED IS A MEASUREMENT THAT 222 // UNDERCOUNTS; the launcher is the one thing that always knows, so the launcher records it. 223 // ★OUTCOME IS `started`, NEVER `ok`: this parent deliberately does NOT wait4 -- that IS the async 224 // contract -- so it CANNOT observe the exit. Writing `ok` would fabricate an outcome nobody measured. 225 // ★FAIL-SOFT: an unopenable journal NEVER affects the launch. The job is the deliverable; the record 226 // is bookkeeping, and bookkeeping must not be able to break the work. 227 let lg: i64 = sys_openat_append("knowledge/status/actlog.jrnl" as *u8, JR_OMODE) 228 if lg >= 0 { 229 let ln: *u8 = sys_mmap(JR_MAGIC_1024) 230 var o: i64 = ccz_cat_num(ln, 0, sys_now_realtime_sec()) 231 ln[o] = JR_TAB as u8; o = o + 1 232 o = jr_cat(ln, o, "jobrun" as *u8) 233 ln[o] = JR_TAB as u8; o = o + 1 234 o = jr_cat(ln, o, name) 235 ln[o] = JR_TAB as u8; o = o + 1 236 o = jr_cat(ln, o, "launch" as *u8) 237 ln[o] = JR_TAB as u8; o = o + 1 238 o = jr_cat(ln, o, "started" as *u8) 239 ln[o] = JR_TAB as u8; o = o + 1 240 o = jr_cat(ln, o, "forked by nx_job_run pid=" as *u8) 241 o = ccz_cat_num(ln, o, pid) 242 o = jr_cat(ln, o, " -- exit NOT observed (async runner does not wait4)" as *u8) 243 ln[o] = JR_NL as u8; o = o + 1 244 sys_write(lg, ln, o) 245 sys_close(lg) 246 sys_munmap(ln, JR_MAGIC_1024) 247 } 248 // ES29: the launch row the cancel verb resolves; fail-soft like the actlog row above (the job is the deliverable) 249 jc_journal(JC_JRNL, JC_KIND_LAUNCH, sys_now_realtime_sec(), pid, elfbuf, outp, name) 250 jr_puts("JOB-STARTED pid=" as *u8) 251 jr_putn(pid) 252 jr_puts(" tool=" as *u8) 253 jr_puts(name) 254 jr_puts(" elf=" as *u8) 255 jr_puts(elfbuf) 256 jr_puts(" out=" as *u8) 257 jr_puts(outp) 258 jr_puts("\n" as *u8) 259 sys_exit(0) 260 return 0 261}