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}