code wiki / _hdl_build / nx_job_run.nx
nx_job_run.nx source
↩ module page · 195 lines · 9918 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"
28const JR_MAGIC_1024: i64 = 1024
29
30const JR_OMODE: i64 = 0x1a4
31const JR_STDOUT: i64 = 1
32const JR_STDERR: i64 = 2
33const JR_MAXARGV: i64 = 32
34const JR_NL: i64 = 10
35const JR_TAB: i64 = 9
36const JR_HASH: i64 = 35
37
38func jr_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
39func jr_puts(s: *u8) -> i64 { sys_write(JR_STDOUT, s, jr_slen(s)); return 0 }
40func jr_werr(s: *u8) -> i64 { sys_write(JR_STDERR, s, jr_slen(s)); return 0 }
41// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
42// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
43// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
44// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
45func jr_putn(v: i64) -> i64 { nxi_out(v); return 0 }
46func jr_slice_eq(buf: *u8, start: i64, end: i64, s: *u8) -> i64 {
47 var i: i64 = start
48 var j: i64 = 0
49 while i < end {
50 if s[j] == (0 as u8) { return 0 }
51 if buf[i] != s[j] { return 0 }
52 i = i + 1
53 j = j + 1
54 }
55 if s[j] != (0 as u8) { return 0 }
56 return 1
57}
58func jr_contains(hay: *u8, needle: *u8) -> i64 {
59 if needle[0] == (0 as u8) { return 1 }
60 var i: i64 = 0
61 while hay[i] != (0 as u8) {
62 var j: i64 = 0
63 var m: i64 = 1
64 while m == 1 {
65 if needle[j] == (0 as u8) { return 1 }
66 if hay[i+j] == (0 as u8) { m = 0 } else { if hay[i+j] != needle[j] { m = 0 } else { j = j + 1 } }
67 }
68 i = i + 1
69 }
70 return 0
71}
72func jr_is_tmp(p: *u8) -> i64 {
73 if p[0] != (47 as u8) { return 0 }
74 if p[1] != (116 as u8) { return 0 }
75 if p[2] != (109 as u8) { return 0 }
76 if p[3] != (112 as u8) { return 0 }
77 if p[4] != (47 as u8) { return 0 }
78 return 1
79}
80func jr_has_dotdot(p: *u8) -> i64 {
81 var i: i64 = 0
82 while p[i] != (0 as u8) {
83 if p[i] == (46 as u8) { if p[i+1] == (46 as u8) { return 1 } }
84 i = i + 1
85 }
86 return 0
87}
88
89func main(argc: i64, argv: *i64) -> i64 {
90 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 }
91 let name: *u8 = argv[1] as *u8
92 let outp: *u8 = argv[2] as *u8
93
94 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 }
95 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 }
96 if jr_has_dotdot(outp) == 1 { jr_werr("REFUSED: out-file must not contain '..'\n" as *u8); sys_exit(2); return 2 }
97
98 let lenp: *i64 = sys_mmap(8) as *i64
99 lenp[0] = 0
100 let buf: *u8 = sys_read_file("tool_allowlist.conf" as *u8, lenp)
101 let n: i64 = lenp[0]
102 if n <= 0 { jr_werr("REFUSED: tool registry unreadable\n" as *u8); sys_exit(3); return 3 }
103
104 let elfbuf: *u8 = sys_mmap(JR_MAGIC_1024)
105 var found: i64 = 0
106 var was_pinned: i64 = 0
107 var i: i64 = 0
108 while i < n {
109 var le: i64 = i
110 var s: i64 = 1
111 while s == 1 { if le >= n { s = 0 } else { if buf[le] == (JR_NL as u8) { s = 0 } else { le = le + 1 } } }
112 if le > i { if buf[i] != (JR_HASH as u8) {
113 var ne: i64 = i
114 var s2: i64 = 1
115 while s2 == 1 { if ne >= le { s2 = 0 } else { if buf[ne] == (JR_TAB as u8) { s2 = 0 } else { ne = ne + 1 } } }
116 if ne > i { if found == 0 { if jr_slice_eq(buf, i, ne, name) == 1 {
117 var ps: i64 = ne + 1
118 var pe: i64 = ps
119 var s3: i64 = 1
120 while s3 == 1 { if pe >= le { s3 = 0 } else { if buf[pe] == (JR_TAB as u8) { s3 = 0 } else { pe = pe + 1 } } }
121 var gs: i64 = pe + 1
122 var ge: i64 = gs
123 var s4: i64 = 1
124 while s4 == 1 { if ge >= le { s4 = 0 } else { if buf[ge] == (JR_TAB as u8) { s4 = 0 } else { ge = ge + 1 } } }
125 if jr_slice_eq(buf, gs, ge, "GREEN" as *u8) == 1 {
126 if ge >= le {
127 if pe > ps {
128 var eo: i64 = 0
129 var k: i64 = ps
130 while k < pe { elfbuf[eo] = buf[k]; eo = eo + 1; k = k + 1 }
131 elfbuf[eo] = 0 as u8
132 found = 1
133 }
134 } else { was_pinned = 1 }
135 }
136 } } }
137 } }
138 i = le + 1
139 }
140
141 if found == 0 {
142 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 }
143 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
144 }
145
146 // ★EXISTENCE CHECK BEFORE THE FORK (2026-08-01, debt 1785623939). Every guard above validates
147 // the NAME -- registered, GREEN, unpinned, not a daemon -- and NOTHING validated that the elf
148 // the row points at still EXISTS. After execve fails in the child there is no way to report it:
149 // the parent has already returned JOB-STARTED and never wait4's. Reproduced live: a tool row
150 // registered against a `.sov.elf.new` STAGING artifact, which /api/promote then consumed,
151 // printed a confident `JOB-STARTED pid=8938` and simply never wrote its out-file. ABSENCE OF
152 // OUTPUT WAS THE ONLY SIGNAL -- the same reported-green-wrote-nothing shape that cost a whole
153 // session elsewhere today.
154 // ★★★★★A LAUNCHER THAT VALIDATES THE NAME BUT NOT THE ARTIFACT REPORTS SUCCESS FOR A PROCESS
155 // THAT CANNOT START. Fail LOUD here, where a caller can still see it, instead of silently in a
156 // child nobody reaps.
157 let jr_probe: i64 = sys_openat_rd(elfbuf)
158 if jr_probe < 0 {
159 jr_werr("REFUSED: registry row for '" as *u8); jr_werr(name)
160 jr_werr("' points at a MISSING elf: " as *u8); jr_werr(elfbuf)
161 jr_werr("\n the row is dangling -- most often it was registered against a .sov.elf.new STAGING\n" as *u8)
162 jr_werr(" artifact that a later /api/promote consumed. Rebuild the target to regenerate it, or\n" as *u8)
163 jr_werr(" re-register the name against the PROMOTED .elf. BUILD -> PROMOTE -> THEN REGISTER.\n" as *u8)
164 sys_exit(7); return 7
165 }
166 sys_close(jr_probe)
167
168 let pid: i64 = sys_fork()
169 if pid == 0 {
170 let of: i64 = sys_openat_wr(outp, JR_OMODE)
171 if of >= 0 { sys_dup3(of, JR_STDOUT, 0); sys_dup3(of, JR_STDERR, 0) }
172 let av: *i64 = sys_mmap(JR_MAXARGV*8) as *i64
173 av[0] = elfbuf as i64
174 var a: i64 = 3
175 var kk: i64 = 1
176 while a < argc { if kk < JR_MAXARGV - 1 { av[kk] = argv[a]; kk = kk + 1 } a = a + 1 }
177 av[kk] = 0
178 let envp: *i64 = sys_mmap(16) as *i64
179 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64
180 envp[1] = 0
181 sys_execve(elfbuf, av, envp)
182 sys_exit(127)
183 }
184 jr_puts("JOB-STARTED pid=" as *u8)
185 jr_putn(pid)
186 jr_puts(" tool=" as *u8)
187 jr_puts(name)
188 jr_puts(" elf=" as *u8)
189 jr_puts(elfbuf)
190 jr_puts(" out=" as *u8)
191 jr_puts(outp)
192 jr_puts("\n" as *u8)
193 sys_exit(0)
194 return 0
195}