code wiki / _hdl_build / nx_mgmt_tools_register_gate.nx
nx_mgmt_tools_register_gate.nx source
↩ module page · 221 lines · 13401 B
1// nx_mgmt_tools_register_gate.nx -- prove POST /api/tools/register is fail-closed + idempotent. In-process
2// referee (no socket): crafts request BYTES, feeds the pure handler ma_do_tools_register. Auth is the SAME
3// ma_level_of gate as every other write route, so this focuses on the NEW logic:
4// T1 missing name -> 400
5// T2 path-escape elf basename ("../x") -> 400 invalid basename (sanitize refuses)
6// T3 no confirm=yes -> 400
7// T4 IDEMPOTENT: a name already in tool_allowlist.conf -> ALREADY-REGISTERED, file UNCHANGED (no dup row)
8// T5 elf not a real ELF under nishihost -> 400 (never register a phantom tool)
9// (The positive append is proven LIVE post-deploy -- its ELF path is the NAS-absolute nishihost dir.)
10// Runs FS under /tmp. license_tier: ORIGINAL expect_exit: 0
11import "nx_mgmt_api.nx"
12import "nx_syscalls.nx"
13import "nx_gate_verdict.nx"
14
15// sized: fixture args-column scratch; matches MA_ARGSDEC_CAP order (pinned args are short strings)
16const GT_ARGCAP: i64 = 4096
17
18func w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
19func pn(v: i64) -> i64 {
20 let b: *u8 = sys_mmap(32) as *u8
21 var x: i64 = v; if x < 0 { sys_write(1,"-" as *u8,1); x = 0 - x }
22 var i: i64 = 31
23 if x == 0 { b[i] = 48 as u8; i = i - 1 }
24 while x > 0 { b[i] = (48 + x % 10) as u8; x = x / 10; i = i - 1 }
25 sys_write(1, (b as i64 + i + 1) as *u8, 31 - i)
26 return 0
27}
28func glen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
29func gcat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){d[o+i]=s[i]; i=i+1} return o+i }
30func gcatn(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64=0; while i<n {d[o+i]=s[i]; i=i+1} return o+n }
31func gcontains(hay: *u8, n: i64, needle: *u8) -> i64 {
32 let nn: i64 = glen(needle)
33 var i: i64 = 0
34 while i + nn <= n {
35 var m: i64 = 1; var j: i64 = 0
36 while j < nn { if (hay[i+j] as i64) != (needle[j] as i64) { m = 0; j = nn } else { j = j + 1 } }
37 if m == 1 { return 1 }
38 i = i + 1
39 }
40 return 0
41}
42func mkreq(dst: *u8, line: *u8, bod: *u8) -> i64 {
43 let bn: i64 = glen(bod)
44 var o: i64 = gcat(dst, 0, line)
45 o = gcat(dst, o, " HTTP/1.1\r\nHost: x\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8)
46 let t: *u8 = sys_mmap(24); var m: i64 = bn; var k: i64 = 0
47 if m == 0 { t[0]=48 as u8; k=1 }
48 while m > 0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
49 var z: i64 = 0
50 while z < k { dst[o]=t[k-1-z]; o=o+1; z=z+1 }
51 o = gcat(dst, o, "\r\n\r\n" as *u8)
52 o = gcatn(dst, o, bod, bn)
53 return o
54}
55func wfile(path: *u8, buf: *u8, n: i64) -> i64 {
56 let fd: i64 = sys_openat_wr(path, 0x1a4); if fd < 0 { return 0 - 1 }
57 sys_write(fd, buf, n); sys_close(fd); return 0
58}
59func fsize(path: *u8) -> i64 {
60 let szp: *i64 = sys_mmap(16) as *i64
61 let b: *u8 = sys_read_file(path, szp)
62 if (b as i64) == 0 { return 0 - 1 }
63 return szp[0]
64}
65
66func main() -> i64 {
67 w("=== nx_mgmt_tools_register_gate -- POST /api/tools/register, fail-closed + idempotent ===\n" as *u8)
68 sys_mkdir("/tmp/nx_treg_gt" as *u8, 0x1ed)
69 sys_chdir("/tmp/nx_treg_gt" as *u8)
70 let out: *u8 = sys_mmap(262144)
71 let req: *u8 = sys_mmap(262144)
72 var fails: i64 = 0
73
74 // seed an allowlist with one existing tool (for the idempotency test).
75 let seed: *u8 = "existingtool\t/x/y.elf\tGREEN\n" as *u8
76 wfile("tool_allowlist.conf" as *u8, seed, glen(seed))
77 let base_sz: i64 = fsize("tool_allowlist.conf" as *u8)
78
79 // T1 missing name
80 let n1: i64 = mkreq(req, "POST /api/tools/register" as *u8, "elf=nx_x.elf&confirm=yes" as *u8)
81 let r1: i64 = ma_do_tools_register(req, n1, out)
82 var t1: i64 = 1
83 if gcontains(out, r1, "400" as *u8) == 0 { t1 = 0 }
84 if gcontains(out, r1, "missing name" as *u8) == 0 { t1 = 0 }
85 if t1 == 1 { w("T1 PASS missing name -> 400\n" as *u8) } else { fails = fails + 1; w("T1 FAIL\n" as *u8) }
86
87 // T2 path-escape elf basename
88 let n2: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=evil&elf=../etc/passwd&confirm=yes" as *u8)
89 let r2: i64 = ma_do_tools_register(req, n2, out)
90 var t2: i64 = 1
91 if gcontains(out, r2, "400" as *u8) == 0 { t2 = 0 }
92 if gcontains(out, r2, "invalid elf path" as *u8) == 0 { t2 = 0 }
93 if t2 == 1 { w("T2 PASS path-escape basename -> 400\n" as *u8) } else { fails = fails + 1; w("T2 FAIL\n" as *u8); sys_write(1, out, r2); w("\n" as *u8) }
94
95 // T3 no confirm
96 let n3: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=newtool&elf=nx_x.elf" as *u8)
97 let r3: i64 = ma_do_tools_register(req, n3, out)
98 var t3: i64 = 1
99 if gcontains(out, r3, "400" as *u8) == 0 { t3 = 0 }
100 if gcontains(out, r3, "confirm=yes" as *u8) == 0 { t3 = 0 }
101 if t3 == 1 { w("T3 PASS no-confirm -> 400\n" as *u8) } else { fails = fails + 1; w("T3 FAIL\n" as *u8); sys_write(1, out, r3); w("\n" as *u8) }
102
103 // T4 IDEMPOTENT: register a name already present -> ALREADY, allowlist file UNCHANGED
104 let n4: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=existingtool&elf=nx_x.elf&confirm=yes" as *u8)
105 let r4: i64 = ma_do_tools_register(req, n4, out)
106 var t4: i64 = 1
107 if gcontains(out, r4, "ALREADY-REGISTERED" as *u8) == 0 { t4 = 0 }
108 if fsize("tool_allowlist.conf" as *u8) != base_sz { t4 = 0 } // no dup row appended
109 if t4 == 1 { w("T4 PASS idempotent (already-registered, no dup row)\n" as *u8) } else { fails = fails + 1; w("T4 FAIL\n" as *u8); sys_write(1, out, r4); w("\n" as *u8) }
110
111 // T5 elf not a real ELF under nishihost (the NAS path is absent locally) -> 400
112 let n5: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=newtool&elf=nx_definitely_absent.elf&confirm=yes" as *u8)
113 let r5: i64 = ma_do_tools_register(req, n5, out)
114 var t5: i64 = 1
115 if gcontains(out, r5, "400" as *u8) == 0 { t5 = 0 }
116 if gcontains(out, r5, "not found or not a valid ELF" as *u8) == 0 { t5 = 0 }
117 if fsize("tool_allowlist.conf" as *u8) != base_sz { t5 = 0 } // nothing appended for a phantom elf
118 if t5 == 1 { w("T5 PASS phantom-elf -> 400, allowlist untouched\n" as *u8) } else { fails = fails + 1; w("T5 FAIL\n" as *u8); sys_write(1, out, r5); w("\n" as *u8) }
119
120 // T6 UPDATE CAN NEVER CREATE: update=yes on an unknown name -> 400, allowlist untouched
121 let n6: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=ghosttool&elf=nx_x.elf&confirm=yes&update=yes" as *u8)
122 let r6: i64 = ma_do_tools_register(req, n6, out)
123 var t6: i64 = 1
124 if gcontains(out, r6, "400" as *u8) == 0 { t6 = 0 }
125 if gcontains(out, r6, "update target not registered" as *u8) == 0 { t6 = 0 }
126 if fsize("tool_allowlist.conf" as *u8) != base_sz { t6 = 0 }
127 if t6 == 1 { w("T6 PASS update-on-unknown -> 400, allowlist untouched\n" as *u8) } else { fails = fails + 1; w("T6 FAIL\n" as *u8); sys_write(1, out, r6); w("\n" as *u8) }
128
129 // T7 UNIT: md_allow_update_row REPLACES the existing row (new elf visible, no dup, .prev banked)
130 var t7: i64 = 1
131 if md_allow_update_row("existingtool" as *u8, "/new/path.elf" as *u8, 0 as *u8, 0) != 1 { t7 = 0 }
132 let szp7: *i64 = sys_mmap(16) as *i64
133 let c7: *u8 = sys_read_file("tool_allowlist.conf" as *u8, szp7)
134 if (c7 as i64) == 0 { t7 = 0 } else {
135 if gcontains(c7, szp7[0], "/new/path.elf" as *u8) == 0 { t7 = 0 }
136 if gcontains(c7, szp7[0], "/x/y.elf" as *u8) == 1 { t7 = 0 }
137 }
138 if fsize("tool_allowlist.conf.prev" as *u8) <= 0 { t7 = 0 }
139 if t7 == 1 { w("T7 PASS update-row replaces (new elf in, old row out, .prev banked)\n" as *u8) } else { fails = fails + 1; w("T7 FAIL\n" as *u8) }
140
141 // T8 UNIT liar-killer: update-row on a missing name REFUSES, file byte-count unchanged
142 let pre8: i64 = fsize("tool_allowlist.conf" as *u8)
143 var t8: i64 = 1
144 if md_allow_update_row("neverexisted" as *u8, "/new/path2.elf" as *u8, 0 as *u8, 0) != 0 { t8 = 0 }
145 if fsize("tool_allowlist.conf" as *u8) != pre8 { t8 = 0 }
146 if t8 == 1 { w("T8 PASS update-row not-found refuses, file untouched\n" as *u8) } else { fails = fails + 1; w("T8 FAIL\n" as *u8) }
147
148 // T9 UNIT tri-state (seq722 root fix): ma_schema_has_name on empty file -> 0; seeded row -> 1;
149 // prefix and absent names -> 0 (the \t terminator is load-bearing).
150 sys_mkdir("knowledge" as *u8, 0x1ed)
151 wfile("knowledge/tool_schemas.conf" as *u8, "" as *u8, 0)
152 var t9: i64 = 1
153 if ma_schema_has_name("existingtool" as *u8) != 0 { t9 = 0 }
154 let sseed: *u8 = "othertool\tsome title\t0\t1\t0\t1\tdesc\n" as *u8
155 wfile("knowledge/tool_schemas.conf" as *u8, sseed, glen(sseed))
156 if ma_schema_has_name("othertool" as *u8) != 1 { t9 = 0 }
157 if ma_schema_has_name("other" as *u8) != 0 { t9 = 0 }
158 if ma_schema_has_name("existingtool" as *u8) != 0 { t9 = 0 }
159 if t9 == 1 { w("T9 PASS schema-has-name tri-state (empty->0, seeded->1, prefix/absent->0)\n" as *u8) } else { fails = fails + 1; w("T9 FAIL\n" as *u8) }
160
161 // T10 UPSERT: append DECODES %20 + SCRUBS %09 (row-injection foreclosed); append-if-absent guard
162 // skips the second write byte-exact (no dup rows, curated rows safe).
163 var t10: i64 = 1
164 let tb: *u8 = "hello%20world%09x" as *u8
165 if ma_schema_append_row("existingtool" as *u8, tb, 0, glen(tb)) != 1 { t10 = 0 }
166 let szp10: *i64 = sys_mmap(16) as *i64
167 let c10: *u8 = sys_read_file("knowledge/tool_schemas.conf" as *u8, szp10)
168 if (c10 as i64) == 0 { t10 = 0 } else {
169 if gcontains(c10, szp10[0], "existingtool\thello world x\t0\t1\t0\t1\t" as *u8) == 0 { t10 = 0 }
170 if gcontains(c10, szp10[0], "%20" as *u8) == 1 { t10 = 0 }
171 }
172 if ma_schema_has_name("existingtool" as *u8) != 1 { t10 = 0 }
173 let pre10: i64 = fsize("knowledge/tool_schemas.conf" as *u8)
174 if ma_schema_has_name("existingtool" as *u8) == 0 { ma_schema_append_row("existingtool" as *u8, tb, 0, glen(tb)) }
175 if fsize("knowledge/tool_schemas.conf" as *u8) != pre10 { t10 = 0 }
176 if t10 == 1 { w("T10 PASS schema upsert (decode+scrub, append-if-absent no-dup)\n" as *u8) } else { fails = fails + 1; w("T10 FAIL\n" as *u8) }
177
178 // T11 UNIT (seq1281): md_allow_get_args returns the pinned-args column exactly; no-args row + absent name -> 0
179 let pseed: *u8 = "pinnedtool\t/x/p.elf\tGREEN\tknowledge/store alpha bravo\nplaintool\t/x/q.elf\tGREEN\n" as *u8
180 wfile("tool_allowlist.conf" as *u8, pseed, glen(pseed))
181 var t11: i64 = 1
182 let agot: *u8 = sys_mmap(GT_ARGCAP)
183 let agn: i64 = md_allow_get_args("pinnedtool" as *u8, agot, GT_ARGCAP - 1)
184 if agn <= 0 { t11 = 0 } else {
185 if gcontains(agot, agn, "knowledge/store alpha bravo" as *u8) == 0 { t11 = 0 }
186 }
187 if md_allow_get_args("plaintool" as *u8, agot, GT_ARGCAP - 1) != 0 { t11 = 0 }
188 if md_allow_get_args("neverwas" as *u8, agot, GT_ARGCAP - 1) != 0 { t11 = 0 }
189 if t11 == 1 { w("T11 PASS get-args (pinned col exact, no-args/absent -> 0)\n" as *u8) } else { fails = fails + 1; w("T11 FAIL\n" as *u8) }
190
191 // T12 PRESERVE COMPOSE (seq1281): update WITHOUT args (via the get-args fetch the handler now does)
192 // -> pinned args survive byte-exact; an explicit clear still drops them.
193 var t12: i64 = 1
194 let pres: *u8 = sys_mmap(GT_ARGCAP)
195 let prn: i64 = md_allow_get_args("pinnedtool" as *u8, pres, GT_ARGCAP - 1)
196 if md_allow_update_row("pinnedtool" as *u8, "/x/p2.elf" as *u8, pres, prn) != 1 { t12 = 0 }
197 if md_allow_get_args("pinnedtool" as *u8, agot, GT_ARGCAP - 1) <= 0 { t12 = 0 } else {
198 if gcontains(agot, glen(agot), "knowledge/store alpha bravo" as *u8) == 0 { t12 = 0 }
199 }
200 if md_allow_update_row("pinnedtool" as *u8, "/x/p3.elf" as *u8, 0 as *u8, 0) != 1 { t12 = 0 }
201 if md_allow_get_args("pinnedtool" as *u8, agot, GT_ARGCAP - 1) != 0 { t12 = 0 }
202 if t12 == 1 { w("T12 PASS preserve-compose (omit -> pinned survive; explicit clear -> dropped)\n" as *u8) } else { fails = fails + 1; w("T12 FAIL\n" as *u8) }
203
204 // ---- D001 ANCHOR + DERIVED-BOOLEAN RUNG 2026-08-06 -------------------------------------------
205 // This gate PASSED while emitting NO verdict= token (probe: run_exit=0 emits_verdict=0), so nothing
206 // in the estate could judge it. Its teeth are INLINE (no helper), so the idiom-H helper transform
207 // does not apply and there is nothing to hang a gv_check on without touching 12 separate sites.
208 // nx_gate_dry_apply refuses to map a `<x> == 0` guard directly -- correctly, because gv_verdict
209 // scores ctr[1]==0 as RED and a direct map would INVERT this gate. Its own skip text names the way
210 // out: "needs a DERIVED boolean -- a separate rung". That is exactly this: pass = total - fails.
211 // NOT a direct operand map, so the inversion hazard does not apply.
212 // GT_TEETH must match the number of teeth above. It is not a new rot risk -- the old GREEN string
213 // ALREADY hardcoded "12/12"; this only makes the same constant machine-readable instead of prose.
214 let gt_teeth: i64 = 12
215 let ctr: *i64 = gv_ctr()
216 ctr[1] = gt_teeth
217 ctr[0] = gt_teeth - fails
218 let rc__g: i64 = gv_verdict("MGMT-TOOLS-REGISTER-GATE" as *u8, ctr, "fail-closed (name/path/confirm/phantom-elf) + idempotent (no dup rows) + update verb (replace-atomic, can-never-create) + schema upsert (decode+scrub, append-if-absent) + pinned-args preserve (omit-safe, explicit-clear)" as *u8)
219 sys_exit(rc__g)
220 return rc__g
221}