code wiki / _hdl_build / nx_mgmt_tools_register_gate.nx
nx_mgmt_tools_register_gate.nx source
↩ module page · 206 lines · 12353 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"
13
14// sized: fixture args-column scratch; matches MA_ARGSDEC_CAP order (pinned args are short strings)
15const GT_ARGCAP: i64 = 4096
16
17func w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
18func pn(v: i64) -> i64 {
19 let b: *u8 = sys_mmap(32) as *u8
20 var x: i64 = v; if x < 0 { sys_write(1,"-" as *u8,1); x = 0 - x }
21 var i: i64 = 31
22 if x == 0 { b[i] = 48 as u8; i = i - 1 }
23 while x > 0 { b[i] = (48 + x % 10) as u8; x = x / 10; i = i - 1 }
24 sys_write(1, (b as i64 + i + 1) as *u8, 31 - i)
25 return 0
26}
27func glen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
28func 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 }
29func 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 }
30func gcontains(hay: *u8, n: i64, needle: *u8) -> i64 {
31 let nn: i64 = glen(needle)
32 var i: i64 = 0
33 while i + nn <= n {
34 var m: i64 = 1; var j: i64 = 0
35 while j < nn { if (hay[i+j] as i64) != (needle[j] as i64) { m = 0; j = nn } else { j = j + 1 } }
36 if m == 1 { return 1 }
37 i = i + 1
38 }
39 return 0
40}
41func mkreq(dst: *u8, line: *u8, bod: *u8) -> i64 {
42 let bn: i64 = glen(bod)
43 var o: i64 = gcat(dst, 0, line)
44 o = gcat(dst, o, " HTTP/1.1\r\nHost: x\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8)
45 let t: *u8 = sys_mmap(24); var m: i64 = bn; var k: i64 = 0
46 if m == 0 { t[0]=48 as u8; k=1 }
47 while m > 0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
48 var z: i64 = 0
49 while z < k { dst[o]=t[k-1-z]; o=o+1; z=z+1 }
50 o = gcat(dst, o, "\r\n\r\n" as *u8)
51 o = gcatn(dst, o, bod, bn)
52 return o
53}
54func wfile(path: *u8, buf: *u8, n: i64) -> i64 {
55 let fd: i64 = sys_openat_wr(path, 0x1a4); if fd < 0 { return 0 - 1 }
56 sys_write(fd, buf, n); sys_close(fd); return 0
57}
58func fsize(path: *u8) -> i64 {
59 let szp: *i64 = sys_mmap(16) as *i64
60 let b: *u8 = sys_read_file(path, szp)
61 if (b as i64) == 0 { return 0 - 1 }
62 return szp[0]
63}
64
65func main() -> i64 {
66 w("=== nx_mgmt_tools_register_gate -- POST /api/tools/register, fail-closed + idempotent ===\n" as *u8)
67 sys_mkdir("/tmp/nx_treg_gt" as *u8, 0x1ed)
68 sys_chdir("/tmp/nx_treg_gt" as *u8)
69 let out: *u8 = sys_mmap(262144)
70 let req: *u8 = sys_mmap(262144)
71 var fails: i64 = 0
72
73 // seed an allowlist with one existing tool (for the idempotency test).
74 let seed: *u8 = "existingtool\t/x/y.elf\tGREEN\n" as *u8
75 wfile("tool_allowlist.conf" as *u8, seed, glen(seed))
76 let base_sz: i64 = fsize("tool_allowlist.conf" as *u8)
77
78 // T1 missing name
79 let n1: i64 = mkreq(req, "POST /api/tools/register" as *u8, "elf=nx_x.elf&confirm=yes" as *u8)
80 let r1: i64 = ma_do_tools_register(req, n1, out)
81 var t1: i64 = 1
82 if gcontains(out, r1, "400" as *u8) == 0 { t1 = 0 }
83 if gcontains(out, r1, "missing name" as *u8) == 0 { t1 = 0 }
84 if t1 == 1 { w("T1 PASS missing name -> 400\n" as *u8) } else { fails = fails + 1; w("T1 FAIL\n" as *u8) }
85
86 // T2 path-escape elf basename
87 let n2: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=evil&elf=../etc/passwd&confirm=yes" as *u8)
88 let r2: i64 = ma_do_tools_register(req, n2, out)
89 var t2: i64 = 1
90 if gcontains(out, r2, "400" as *u8) == 0 { t2 = 0 }
91 if gcontains(out, r2, "invalid elf path" as *u8) == 0 { t2 = 0 }
92 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) }
93
94 // T3 no confirm
95 let n3: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=newtool&elf=nx_x.elf" as *u8)
96 let r3: i64 = ma_do_tools_register(req, n3, out)
97 var t3: i64 = 1
98 if gcontains(out, r3, "400" as *u8) == 0 { t3 = 0 }
99 if gcontains(out, r3, "confirm=yes" as *u8) == 0 { t3 = 0 }
100 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) }
101
102 // T4 IDEMPOTENT: register a name already present -> ALREADY, allowlist file UNCHANGED
103 let n4: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=existingtool&elf=nx_x.elf&confirm=yes" as *u8)
104 let r4: i64 = ma_do_tools_register(req, n4, out)
105 var t4: i64 = 1
106 if gcontains(out, r4, "ALREADY-REGISTERED" as *u8) == 0 { t4 = 0 }
107 if fsize("tool_allowlist.conf" as *u8) != base_sz { t4 = 0 } // no dup row appended
108 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) }
109
110 // T5 elf not a real ELF under nishihost (the NAS path is absent locally) -> 400
111 let n5: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=newtool&elf=nx_definitely_absent.elf&confirm=yes" as *u8)
112 let r5: i64 = ma_do_tools_register(req, n5, out)
113 var t5: i64 = 1
114 if gcontains(out, r5, "400" as *u8) == 0 { t5 = 0 }
115 if gcontains(out, r5, "not found or not a valid ELF" as *u8) == 0 { t5 = 0 }
116 if fsize("tool_allowlist.conf" as *u8) != base_sz { t5 = 0 } // nothing appended for a phantom elf
117 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) }
118
119 // T6 UPDATE CAN NEVER CREATE: update=yes on an unknown name -> 400, allowlist untouched
120 let n6: i64 = mkreq(req, "POST /api/tools/register" as *u8, "name=ghosttool&elf=nx_x.elf&confirm=yes&update=yes" as *u8)
121 let r6: i64 = ma_do_tools_register(req, n6, out)
122 var t6: i64 = 1
123 if gcontains(out, r6, "400" as *u8) == 0 { t6 = 0 }
124 if gcontains(out, r6, "update target not registered" as *u8) == 0 { t6 = 0 }
125 if fsize("tool_allowlist.conf" as *u8) != base_sz { t6 = 0 }
126 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) }
127
128 // T7 UNIT: md_allow_update_row REPLACES the existing row (new elf visible, no dup, .prev banked)
129 var t7: i64 = 1
130 if md_allow_update_row("existingtool" as *u8, "/new/path.elf" as *u8, 0 as *u8, 0) != 1 { t7 = 0 }
131 let szp7: *i64 = sys_mmap(16) as *i64
132 let c7: *u8 = sys_read_file("tool_allowlist.conf" as *u8, szp7)
133 if (c7 as i64) == 0 { t7 = 0 } else {
134 if gcontains(c7, szp7[0], "/new/path.elf" as *u8) == 0 { t7 = 0 }
135 if gcontains(c7, szp7[0], "/x/y.elf" as *u8) == 1 { t7 = 0 }
136 }
137 if fsize("tool_allowlist.conf.prev" as *u8) <= 0 { t7 = 0 }
138 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) }
139
140 // T8 UNIT liar-killer: update-row on a missing name REFUSES, file byte-count unchanged
141 let pre8: i64 = fsize("tool_allowlist.conf" as *u8)
142 var t8: i64 = 1
143 if md_allow_update_row("neverexisted" as *u8, "/new/path2.elf" as *u8, 0 as *u8, 0) != 0 { t8 = 0 }
144 if fsize("tool_allowlist.conf" as *u8) != pre8 { t8 = 0 }
145 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) }
146
147 // T9 UNIT tri-state (seq722 root fix): ma_schema_has_name on empty file -> 0; seeded row -> 1;
148 // prefix and absent names -> 0 (the \t terminator is load-bearing).
149 sys_mkdir("knowledge" as *u8, 0x1ed)
150 wfile("knowledge/tool_schemas.conf" as *u8, "" as *u8, 0)
151 var t9: i64 = 1
152 if ma_schema_has_name("existingtool" as *u8) != 0 { t9 = 0 }
153 let sseed: *u8 = "othertool\tsome title\t0\t1\t0\t1\tdesc\n" as *u8
154 wfile("knowledge/tool_schemas.conf" as *u8, sseed, glen(sseed))
155 if ma_schema_has_name("othertool" as *u8) != 1 { t9 = 0 }
156 if ma_schema_has_name("other" as *u8) != 0 { t9 = 0 }
157 if ma_schema_has_name("existingtool" as *u8) != 0 { t9 = 0 }
158 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) }
159
160 // T10 UPSERT: append DECODES %20 + SCRUBS %09 (row-injection foreclosed); append-if-absent guard
161 // skips the second write byte-exact (no dup rows, curated rows safe).
162 var t10: i64 = 1
163 let tb: *u8 = "hello%20world%09x" as *u8
164 if ma_schema_append_row("existingtool" as *u8, tb, 0, glen(tb)) != 1 { t10 = 0 }
165 let szp10: *i64 = sys_mmap(16) as *i64
166 let c10: *u8 = sys_read_file("knowledge/tool_schemas.conf" as *u8, szp10)
167 if (c10 as i64) == 0 { t10 = 0 } else {
168 if gcontains(c10, szp10[0], "existingtool\thello world x\t0\t1\t0\t1\t" as *u8) == 0 { t10 = 0 }
169 if gcontains(c10, szp10[0], "%20" as *u8) == 1 { t10 = 0 }
170 }
171 if ma_schema_has_name("existingtool" as *u8) != 1 { t10 = 0 }
172 let pre10: i64 = fsize("knowledge/tool_schemas.conf" as *u8)
173 if ma_schema_has_name("existingtool" as *u8) == 0 { ma_schema_append_row("existingtool" as *u8, tb, 0, glen(tb)) }
174 if fsize("knowledge/tool_schemas.conf" as *u8) != pre10 { t10 = 0 }
175 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) }
176
177 // T11 UNIT (seq1281): md_allow_get_args returns the pinned-args column exactly; no-args row + absent name -> 0
178 let pseed: *u8 = "pinnedtool\t/x/p.elf\tGREEN\tknowledge/store alpha bravo\nplaintool\t/x/q.elf\tGREEN\n" as *u8
179 wfile("tool_allowlist.conf" as *u8, pseed, glen(pseed))
180 var t11: i64 = 1
181 let agot: *u8 = sys_mmap(GT_ARGCAP)
182 let agn: i64 = md_allow_get_args("pinnedtool" as *u8, agot, GT_ARGCAP - 1)
183 if agn <= 0 { t11 = 0 } else {
184 if gcontains(agot, agn, "knowledge/store alpha bravo" as *u8) == 0 { t11 = 0 }
185 }
186 if md_allow_get_args("plaintool" as *u8, agot, GT_ARGCAP - 1) != 0 { t11 = 0 }
187 if md_allow_get_args("neverwas" as *u8, agot, GT_ARGCAP - 1) != 0 { t11 = 0 }
188 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) }
189
190 // T12 PRESERVE COMPOSE (seq1281): update WITHOUT args (via the get-args fetch the handler now does)
191 // -> pinned args survive byte-exact; an explicit clear still drops them.
192 var t12: i64 = 1
193 let pres: *u8 = sys_mmap(GT_ARGCAP)
194 let prn: i64 = md_allow_get_args("pinnedtool" as *u8, pres, GT_ARGCAP - 1)
195 if md_allow_update_row("pinnedtool" as *u8, "/x/p2.elf" as *u8, pres, prn) != 1 { t12 = 0 }
196 if md_allow_get_args("pinnedtool" as *u8, agot, GT_ARGCAP - 1) <= 0 { t12 = 0 } else {
197 if gcontains(agot, glen(agot), "knowledge/store alpha bravo" as *u8) == 0 { t12 = 0 }
198 }
199 if md_allow_update_row("pinnedtool" as *u8, "/x/p3.elf" as *u8, 0 as *u8, 0) != 1 { t12 = 0 }
200 if md_allow_get_args("pinnedtool" as *u8, agot, GT_ARGCAP - 1) != 0 { t12 = 0 }
201 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) }
202
203 if fails == 0 { w("MGMT-TOOLS-REGISTER-GATE 12/12 GREEN -- 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)\n" as *u8); sys_exit(0); return 0 }
204 w("MGMT-TOOLS-REGISTER-GATE RED fails=" as *u8); pn(fails); w("\n" as *u8)
205 sys_exit(1); return 1
206}