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}