nx_mcp_body_gate.nx source
↩ module page · 400 lines · 22844 B
1// nx_mcp_body_gate.nx -- LANE K 2026-08-23. Referee for the MCP-wire body-read root fix in
2// nx_tools_api_serve.nx (tsv_serve_one). THE DEFECT: the daemon read the whole request with ONE
3// sys_read into a 64 KiB buffer, so a body over ~64 KiB (or one arriving in >1 TCP segment) was parsed
4// as a FRAGMENT -- trailing argv/fields silently vanished (measured: a 131072 B write landed as 42045 B)
5// and every source push had to be split into anchored hunks. THE FIX: tsv_serve_one composes the estate's
6// own nx_http_server_read_request (the Content-Length read loop the daemon never used) + a conf-driven
7// named reserve (tools_req_reserve.conf, one owner via EDGE_REQ_MAX) + a 413 that ANNOUNCES limit+received.
8//
9// IT RUNS, IT DOES NOT MERELY COMPILE: it starts the SUBJECT binary on a throwaway port and POSTs REAL
10// bodies to /mcp over a real socket. The proof that the WHOLE body was read is that the JSON-RPC `id` --
11// placed at the very END of a 150 KB body -- is ECHOED in the response; a single-read daemon never sees it.
12// T_BITE spawns the BANKED PRE-FIX binary and shows the SAME probe does NOT echo the end marker -- the
13// non-vacuity law made executable: the tooth fires on the bad binary and is silent on the good one.
14// license_tier: ORIGINAL
15import "nx_http_server.nx" // nx_http_server_addr_loopback + NXS_* + sys_* (transitive)
16import "nx_connect.nx" // nx_connect_bounded + NX_CONN_DEFAULT_MS
17import "nx_gate_verdict.nx" // gv_ctr / gv_check / gv_verdict (exit carries the verdict)
18import "nx_tools_api.nx" // TA_CAP_SECRET (the keyless-CWD dev secret the daemon falls back to) + capt_issue (transitive nx_cap_token)
19
20const MBG_STAGED: *u8 = "/volume1/homes/elderwesto/nishihost/nx_tools_api_serve.sov.elf.new" as *u8
21const MBG_PREFIX: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/bank/nx_tools_api_serve.elf.pre-bodycap-d3b23e08" as *u8
22const MBG_PORT_OK: i64 = 18993
23const MBG_PORT_BAD: i64 = 18994
24const MBG_DEADPORT: i64 = 18995
25const MBG_SRVLOG_OK: *u8 = "/tmp/nx_mcp_body_srv_ok.out" as *u8
26const MBG_SRVLOG_BAD: *u8 = "/tmp/nx_mcp_body_srv_bad.out" as *u8
27const MBG_RESP_CAP: i64 = 1048576
28const MBG_FULL_BYTES: i64 = 150000 // > 64 KiB single-read window, < the 1 MiB ceiling: MUST fully read
29const MBG_SMALL_BYTES: i64 = 1024 // trivially fits: the fix must not regress small bodies
30const MBG_OVER_BYTES: i64 = 1300000 // > EDGE_REQ_MAX (1 MiB): must 413 naming limit + received
31const MBG_CEIL: i64 = 1048576 // EDGE_REQ_MAX / default reserve (edge plaincap-derived)
32const MBG_BIND_WAIT_MS: i64 = 4000
33// ---- D2 teeth (debt 1787505278): reply-before-bookkeeping, proven with a FIFO WALL -------------------
34// The daemon is started from a FIXTURE CWD (no tools_cap_secret.key -> it falls back to TA_CAP_SECRET, so the
35// gate can mint a real cap; its own knowledge/edge_window.conf -> a 3000 ms window / 2000 ms promote decision;
36// its own tool_allowlist.conf naming the estate's sleeper fixture nx_slowtick). knowledge/status/actlog.jrnl
37// in that CWD is a FIFO WITH NO READER: ta_actlog's O_WRONLY open BLOCKS there. The pre-fix ordering runs
38// actlog BEFORE building the reply, so the old binary stalls with the reply unsent; the fixed ordering sends
39// the reply FIRST and stalls only in the deferred bookkeeping. Same wall, both binaries -- the ORDER is the
40// only thing that differs, which is exactly what the tooth must measure. The gate then opens the FIFO for
41// reading to release the blocked child (never leaves a process wedged) and reads the row it wrote.
42const MBG_FIX1: *u8 = "/tmp/nx_mcp_body_gate_fx1" as *u8
43const MBG_FIX2: *u8 = "/tmp/nx_mcp_body_gate_fx2" as *u8
44const MBG_PORT_P1: i64 = 18996
45const MBG_PORT_P2: i64 = 18997
46const MBG_SRVLOG_P1: *u8 = "/tmp/nx_mcp_body_srv_p1.out" as *u8
47const MBG_SRVLOG_P2: *u8 = "/tmp/nx_mcp_body_srv_p2.out" as *u8
48const MBG_FX_WINDOW_MS: i64 = 3000 // fixture edge window (conf row); the promote decision = window - reserve
49const MBG_FX_RESERVE_MS: i64 = 1000
50const MBG_FX_SLEEP_MS: i64 = 4500 // > window: the worker is genuinely mid-run at the decision
51const MBG_FX_RELEASE_WAIT_S: i64 = 10 // after releasing the wall, how long a late reply may take
52const MBG_SYS_OPENAT: i64 = 257 // x86-64 openat (not an rv64 key: passes the translator through)
53const MBG_SYS_MKNODAT: i64 = 259 // x86-64 mknodat (the incumbent nx_autorun_daemon uses exactly this)
54const MBG_AT_FDCWD: i64 = 0 - 100
55const MBG_S_IFIFO_0644: i64 = 4516 // S_IFIFO (0o010000 = 4096) | 0644 (420)
56const MBG_O_RDONLY_NONBLOCK: i64 = 2048 // O_RDONLY(0) | O_NONBLOCK(0x800): release the wall without ever blocking the gate
57const MBG_DIRMODE: i64 = 493 // 0755
58const MBG_CAP_TTL_S: i64 = 3600
59const MBG_EEXIST: i64 = 0 - 17
60// buffer reserves, each named for the ONE thing it holds (rule 11: a purpose, never a value)
61const MBG_CAP_TOKEN_BYTES: i64 = 1024 // a minted cap: allow~exp~nonce.sig, base64url -- far under 1 KiB
62const MBG_CALL_BODY_BYTES: i64 = 4096 // the tools/call JSON envelope + one short argv + the cap
63const MBG_REQ_HEADROOM_BYTES: i64 = 8192 // HTTP request line + headers on top of any body (a handful of lines)
64const MBG_ROW_BYTES: i64 = 4096 // one actlog row drained from the FIFO wall
65
66// rv64 getpid=172 -> the translator maps it for this target (the incumbent ccg_pid uses the same number)
67func mbg_pid() -> i64 { return __syscall(172, 0, 0, 0, 0, 0, 0) }
68func mbg_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
69func mbg_cat(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 }
70func mbg_catn(d: *u8, o: i64, v: i64) -> i64 {
71 if v == 0 { d[o] = 48 as u8; return o + 1 }
72 var x: i64 = v; var ndig: i64 = 0; let tmp: *u8 = sys_mmap(32)
73 while x > 0 { tmp[ndig] = (48 + (x - (x / 10) * 10)) as u8; x = x / 10; ndig = ndig + 1 }
74 var i: i64 = 0
75 while i < ndig { d[o + i] = tmp[ndig - 1 - i]; i = i + 1 }
76 return o + ndig
77}
78func mbg_has(buf: *u8, n: i64, pat: *u8) -> i64 {
79 let pl: i64 = mbg_slen(pat)
80 if pl == 0 { return 1 }
81 var i: i64 = 0
82 while i + pl <= n {
83 var m: i64 = 1; var j: i64 = 0
84 while j < pl { if buf[i + j] != pat[j] { m = 0; j = pl } else { j = j + 1 } }
85 if m == 1 { return 1 }
86 i = i + 1
87 }
88 return 0
89}
90
91// fork+exec `<elf> serve <port>`, stdout/stderr -> logf. Returns the child pid.
92func mbg_spawn(elf: *u8, port: i64, logf: *u8) -> i64 {
93 let ps: *u8 = sys_mmap(32); let po: i64 = mbg_catn(ps, 0, port); ps[po] = 0 as u8
94 let pid: i64 = sys_fork()
95 if pid == 0 {
96 let fd: i64 = sys_openat_wr(logf, 420)
97 if fd >= 0 { sys_dup3(fd, 1, 0); sys_dup3(fd, 2, 0) }
98 let argv: *i64 = sys_mmap(64) as *i64
99 let envp: *i64 = sys_mmap(16) as *i64
100 envp[0] = 0
101 argv[0] = elf as i64
102 argv[1] = "serve" as *u8 as i64
103 argv[2] = ps as i64
104 argv[3] = 0
105 sys_execve(elf, argv, envp)
106 sys_exit(127)
107 }
108 return pid
109}
110// BOUNDED reap (per nx_tools_canary_gate tc_kill): SIGKILL + WNOHANG poll, never blocks the caller.
111func mbg_kill(pid: i64) -> i64 {
112 let st: *i64 = sys_mmap(16) as *i64
113 var tries: i64 = 0
114 while tries < 40 {
115 __syscall(129, pid, 9, 0, 0, 0, 0) // rv64 kill=129
116 let r: i64 = sys_wait4(pid, st, 1)
117 if r == pid { return 1 }
118 if r < 0 { return 1 }
119 sys_sleep_ms(50)
120 tries = tries + 1
121 }
122 return 0
123}
124
125// fork+exec `<elf> serve <port>` with the child chdir'd into `cwd` (the fixture tree), stdout/stderr -> logf.
126func mbg_spawn_cwd(elf: *u8, port: i64, logf: *u8, cwd: *u8) -> i64 {
127 let ps: *u8 = sys_mmap(32); let po: i64 = mbg_catn(ps, 0, port); ps[po] = 0 as u8
128 let pid: i64 = sys_fork()
129 if pid == 0 {
130 let fd: i64 = sys_openat_wr(logf, 420)
131 if fd >= 0 { sys_dup3(fd, 1, 0); sys_dup3(fd, 2, 0) }
132 sys_chdir(cwd)
133 let argv: *i64 = sys_mmap(64) as *i64
134 let envp: *i64 = sys_mmap(16) as *i64
135 envp[0] = 0
136 argv[0] = elf as i64
137 argv[1] = "serve" as *u8 as i64
138 argv[2] = ps as i64
139 argv[3] = 0
140 sys_execve(elf, argv, envp)
141 sys_exit(127)
142 }
143 return pid
144}
145func mbg_writefile(path: *u8, content: *u8) -> i64 {
146 let fd: i64 = sys_openat_wr(path, 420)
147 if fd < 0 { return 0 - 1 }
148 sys_write(fd, content, mbg_slen(content)); sys_close(fd)
149 return 0
150}
151func mbg_path(d: *u8, a: *u8, b: *u8) -> i64 { var o: i64 = mbg_cat(d, 0, a); o = mbg_cat(d, o, b); d[o] = 0 as u8; return o }
152// build one fixture tree: <root>/knowledge/status/actlog.jrnl = FIFO, edge_window.conf, tool_allowlist.conf
153func mbg_fixture(root: *u8) -> i64 {
154 let p: *u8 = sys_mmap(512)
155 sys_mkdir(root, MBG_DIRMODE)
156 mbg_path(p, root, "/knowledge" as *u8); sys_mkdir(p, MBG_DIRMODE)
157 mbg_path(p, root, "/knowledge/status" as *u8); sys_mkdir(p, MBG_DIRMODE)
158 mbg_path(p, root, "/_jobs" as *u8); sys_mkdir(p, MBG_DIRMODE)
159 mbg_path(p, root, "/knowledge/edge_window.conf" as *u8)
160 let cw: *u8 = sys_mmap(128); var co: i64 = mbg_cat(cw, 0, "edge_window_ms " as *u8); co = mbg_catn(cw, co, MBG_FX_WINDOW_MS); co = mbg_cat(cw, co, "\nreply_reserve_ms " as *u8); co = mbg_catn(cw, co, MBG_FX_RESERVE_MS); cw[co] = 10 as u8; cw[co + 1] = 0 as u8
161 mbg_writefile(p, cw)
162 mbg_path(p, root, "/tool_allowlist.conf" as *u8)
163 mbg_writefile(p, "nx_slowtick\t/volume1/homes/elderwesto/nishihost/_offc/nx_slowtick.elf\tGREEN\n" as *u8)
164 mbg_path(p, root, "/knowledge/status/actlog.jrnl" as *u8)
165 let mk: i64 = __syscall(MBG_SYS_MKNODAT, MBG_AT_FDCWD, p as i64, MBG_S_IFIFO_0644, 0, 0, 0)
166 return mk
167}
168// release the FIFO wall: open it for READ without blocking (frees a writer blocked in open), drain what it
169// wrote (the actlog row) into out, close. Returns bytes read (0 = nothing arrived within the retries).
170func mbg_release_wall(root: *u8, out: *u8, cap: i64) -> i64 {
171 let p: *u8 = sys_mmap(512)
172 mbg_path(p, root, "/knowledge/status/actlog.jrnl" as *u8)
173 let fd: i64 = __syscall(MBG_SYS_OPENAT, MBG_AT_FDCWD, p as i64, MBG_O_RDONLY_NONBLOCK, 0, 0, 0)
174 if fd < 0 { return 0 }
175 var total: i64 = 0
176 var tries: i64 = 0
177 while tries < 60 {
178 let r: i64 = sys_read(fd, ((out as i64) + total) as *u8, cap - 1 - total)
179 if r > 0 { total = total + r; tries = 60 } else { sys_sleep_ms(100); tries = tries + 1 }
180 }
181 sys_close(fd)
182 out[total] = 0 as u8
183 return total
184}
185// POST a minted tools/call (nx_slowtick <sleep_ms>) to /mcp on port; the client socket carries a receive
186// timeout of `wait_s`, so a reply that is not on the wire inside the window reads as 0 bytes. out_ms[0] =
187// elapsed from the first write to the end of the read (reply, or the timeout). Returns bytes read; the socket
188// fd is left OPEN in out_fd so a LATE reply can still be read after the wall is released.
189func mbg_post_call(port: i64, sleep_ms: i64, wait_s: i64, out: *u8, outcap: i64, out_ms: *i64, out_fd: *i64) -> i64 {
190 out_fd[0] = 0 - 1
191 let addr: *u8 = sys_mmap(16)
192 nx_http_server_addr_loopback(addr, port)
193 let fd: i64 = sys_socket(2, 1, 0)
194 if fd < 0 { return 0 }
195 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 }
196 sys_set_socket_timeout(fd, wait_s)
197 // mint a real cap for nx_slowtick against the dev secret the keyless fixture CWD makes the daemon use
198 let cap: *u8 = sys_mmap(MBG_CAP_TOKEN_BYTES)
199 let allow: *u8 = "nx_slowtick" as *u8
200 let cl: i64 = capt_issue(TA_CAP_SECRET, mbg_slen(TA_CAP_SECRET), allow, mbg_slen(allow), sys_now_realtime_sec() + MBG_CAP_TTL_S, mbg_pid(), cap, MBG_CAP_TOKEN_BYTES)
201 cap[cl] = 0 as u8
202 let body: *u8 = sys_mmap(MBG_CALL_BODY_BYTES)
203 var bo: i64 = mbg_cat(body, 0, "{\"jsonrpc\":\"2.0\",\"id\":\"PROMO\",\"method\":\"tools/call\",\"params\":{\"name\":\"nx_slowtick\",\"arguments\":{\"argv\":[\"" as *u8)
204 bo = mbg_catn(body, bo, sleep_ms)
205 bo = mbg_cat(body, bo, "\"],\"_cap\":\"" as *u8)
206 bo = mbg_cat(body, bo, cap)
207 bo = mbg_cat(body, bo, "\"}}}" as *u8)
208 let req: *u8 = sys_mmap(MBG_CALL_BODY_BYTES + MBG_REQ_HEADROOM_BYTES)
209 var ro: i64 = mbg_cat(req, 0, "POST /mcp HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: " as *u8)
210 ro = mbg_catn(req, ro, bo)
211 ro = mbg_cat(req, ro, "\r\nConnection: close\r\n\r\n" as *u8)
212 var ci: i64 = 0
213 while ci < bo { req[ro + ci] = body[ci]; ci = ci + 1 }
214 ro = ro + bo
215 let t0: i64 = sys_now_ms()
216 var wo: i64 = 0
217 var wrun: i64 = 1
218 while wrun == 1 { if wo >= ro { wrun = 0 } else { let w: i64 = sys_write(fd, ((req as i64) + wo) as *u8, ro - wo); if w <= 0 { wrun = 0 } else { wo = wo + w } } }
219 var total: i64 = 0
220 var rrun: i64 = 1
221 while rrun == 1 {
222 if total >= outcap - 1 { rrun = 0 } else {
223 let r: i64 = sys_read(fd, ((out as i64) + total) as *u8, outcap - 1 - total)
224 if r <= 0 { rrun = 0 } else { total = total + r; if mbg_has(out, total, "}}}" as *u8) == 1 { rrun = 0 } }
225 }
226 }
227 out_ms[0] = sys_now_ms() - t0
228 out[total] = 0 as u8
229 out_fd[0] = fd
230 return total
231}
232// after the wall is released: read whatever arrives on the still-open socket within wait_s, then close it.
233func mbg_read_late(fd: i64, wait_s: i64, out: *u8, outcap: i64) -> i64 {
234 if fd < 0 { return 0 }
235 sys_set_socket_timeout(fd, wait_s)
236 var total: i64 = 0
237 var rrun: i64 = 1
238 while rrun == 1 {
239 if total >= outcap - 1 { rrun = 0 } else {
240 let r: i64 = sys_read(fd, ((out as i64) + total) as *u8, outcap - 1 - total)
241 if r <= 0 { rrun = 0 } else { total = total + r }
242 }
243 }
244 sys_close(fd)
245 out[total] = 0 as u8
246 return total
247}
248
249// POST a JSON-RPC tools/list body of ~nbytes to 127.0.0.1:port /mcp, with the `id` placed at the very END
250// (so only a FULL body read can echo it). Reads the whole response into out. Returns bytes read (0 on
251// connect failure -- the dead-port non-vacuity case).
252func mbg_post(port: i64, nbytes: i64, out: *u8, outcap: i64) -> i64 {
253 let addr: *u8 = sys_mmap(16)
254 nx_http_server_addr_loopback(addr, port)
255 let fd: i64 = sys_socket(2, 1, 0)
256 if fd < 0 { return 0 }
257 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 }
258 let mark: *u8 = sys_mmap(64); var mo: i64 = mbg_cat(mark, 0, "ENDMARK" as *u8); mo = mbg_catn(mark, mo, nbytes); mark[mo] = 0 as u8
259 let body: *u8 = sys_mmap(nbytes + MBG_REQ_HEADROOM_BYTES)
260 var bo: i64 = mbg_cat(body, 0, "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"pad\":\"" as *u8)
261 let tail: i64 = 8 + mo + 2 // '","id":"' + mark + '"}'
262 var fill: i64 = nbytes - bo - tail
263 if fill < 0 { fill = 0 }
264 var fi: i64 = 0
265 while fi < fill { body[bo + fi] = 120 as u8; fi = fi + 1 } // 'x'
266 bo = bo + fill
267 bo = mbg_cat(body, bo, "\",\"id\":\"" as *u8)
268 bo = mbg_cat(body, bo, mark)
269 bo = mbg_cat(body, bo, "\"}" as *u8)
270 let req: *u8 = sys_mmap(nbytes + MBG_REQ_HEADROOM_BYTES)
271 var ro: i64 = mbg_cat(req, 0, "POST /mcp HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: " as *u8)
272 ro = mbg_catn(req, ro, bo)
273 ro = mbg_cat(req, ro, "\r\nConnection: close\r\n\r\n" as *u8)
274 var ci: i64 = 0
275 while ci < bo { req[ro + ci] = body[ci]; ci = ci + 1 }
276 ro = ro + bo
277 var wo: i64 = 0
278 var wrun: i64 = 1
279 while wrun == 1 {
280 if wo >= ro { wrun = 0 } else {
281 let w: i64 = sys_write(fd, ((req as i64) + wo) as *u8, ro - wo)
282 if w <= 0 { wrun = 0 } else { wo = wo + w }
283 }
284 }
285 var total: i64 = 0
286 var rrun: i64 = 1
287 while rrun == 1 {
288 if total >= outcap { rrun = 0 } else {
289 let r: i64 = sys_read(fd, ((out as i64) + total) as *u8, outcap - total)
290 if r <= 0 { rrun = 0 } else { total = total + r }
291 }
292 }
293 sys_close(fd)
294 return total
295}
296
297func mbg_diag(tag: *u8, buf: *u8, n: i64) -> i64 {
298 sys_write(1, " [" as *u8, 3); sys_write(1, tag, mbg_slen(tag)); sys_write(1, "] bytes=" as *u8, 8)
299 let nb: *u8 = sys_mmap(32); let no: i64 = mbg_catn(nb, 0, n); sys_write(1, nb, no)
300 var head: i64 = n; if head > 120 { head = 120 }
301 sys_write(1, " head=" as *u8, 6); sys_write(1, buf, head); sys_write(1, "\n" as *u8, 1)
302 return 0
303}
304
305func main() -> i64 {
306 let ctr: *i64 = gv_ctr()
307 let out: *u8 = sys_mmap(MBG_RESP_CAP)
308 let mark150: *u8 = sys_mmap(32); let m1o: i64 = mbg_cat(mark150, 0, "ENDMARK" as *u8); let m1e: i64 = mbg_catn(mark150, m1o, MBG_FULL_BYTES); mark150[m1e] = 0 as u8
309
310 // T1 NON-VACUITY: POST to a dead port -> no bytes, no marker. If this "passes", every probe would.
311 let dn: i64 = mbg_post(MBG_DEADPORT, MBG_FULL_BYTES, out, MBG_RESP_CAP)
312 gv_check("neg-control-dead-port-yields-no-response (a probe that always answers proves nothing)" as *u8, dn <= 0, ctr)
313
314 // ---- SUBJECT = the STAGED (fixed) binary ----
315 let sp: i64 = mbg_spawn(MBG_STAGED, MBG_PORT_OK, MBG_SRVLOG_OK)
316 sys_sleep_ms(MBG_BIND_WAIT_MS)
317
318 // T2 THE FIX: a 150 KB body is read WHOLE -> the id at its END is echoed. (single-read daemon: never.)
319 let n2: i64 = mbg_post(MBG_PORT_OK, MBG_FULL_BYTES, out, MBG_RESP_CAP)
320 let echoed: i64 = mbg_has(out, n2, mark150)
321 mbg_diag("T2-staged-150k" as *u8, out, n2)
322 gv_check("staged-reads-WHOLE-150KB-body (id placed at byte ~149000 is echoed = full Content-Length read)" as *u8, echoed, ctr)
323
324 // T3 CEILING ANNOUNCE: a 1.3 MB body (> 1 MiB ceiling) is refused 413 naming BOTH the limit and received.
325 let n3: i64 = mbg_post(MBG_PORT_OK, MBG_OVER_BYTES, out, MBG_RESP_CAP)
326 let is413: i64 = mbg_has(out, n3, "413" as *u8)
327 let hasLimit: i64 = mbg_has(out, n3, "1048576" as *u8)
328 mbg_diag("T3-staged-1.3M" as *u8, out, n3)
329 gv_check("over-ceiling-body-ANNOUNCES-413 (never a silent {} / never a truncated parse)" as *u8, is413, ctr)
330 gv_check("413-NAMES-the-limit (1048576 present -- the reserve/ceiling, not a bare refusal)" as *u8, hasLimit, ctr)
331
332 // T4 no regression: a 1 KB body still gets a valid JSON-RPC response, never a {} / 413.
333 let n4: i64 = mbg_post(MBG_PORT_OK, MBG_SMALL_BYTES, out, MBG_RESP_CAP)
334 let okSmall: i64 = mbg_has(out, n4, "\"jsonrpc\"" as *u8)
335 mbg_diag("T4-staged-1k" as *u8, out, n4)
336 gv_check("small-1KB-body-still-served (valid jsonrpc response, no regression)" as *u8, okSmall, ctr)
337
338 mbg_kill(sp)
339
340 // T5 BITE: the BANKED PRE-FIX binary, SAME 150 KB probe -> the end marker is NOT echoed (it truncated
341 // the body at its single 64 KiB read, or 413'd it). Proves the tooth fires on the defect it guards.
342 let bp: i64 = mbg_spawn(MBG_PREFIX, MBG_PORT_BAD, MBG_SRVLOG_BAD)
343 sys_sleep_ms(MBG_BIND_WAIT_MS)
344 let n5: i64 = mbg_post(MBG_PORT_BAD, MBG_FULL_BYTES, out, MBG_RESP_CAP)
345 let echoedPre: i64 = mbg_has(out, n5, mark150)
346 mbg_diag("T5-prefix-150k" as *u8, out, n5)
347 var biteok: i64 = 0
348 if echoedPre == 0 { biteok = 1 }
349 gv_check("neg-control-PREFIX-binary-does-NOT-echo (the single-read daemon truncates the 150KB body)" as *u8, biteok, ctr)
350 mbg_kill(bp)
351
352 // ================= D2: REPLY BEFORE BOOKKEEPING (debt 1787505278) =================
353 // T6 THE FIX: staged daemon, FIFO wall in place -> JOB-STARTED is on the wire inside the fixture window.
354 let mk1: i64 = mbg_fixture(MBG_FIX1)
355 var fx1ok: i64 = 0
356 if mk1 == 0 { fx1ok = 1 }
357 if mk1 == MBG_EEXIST { fx1ok = 1 }
358 gv_check("fixture-reached: fixture tree + actlog FIFO wall created (mknodat ok or already there)" as *u8, fx1ok, ctr)
359 let p1: i64 = mbg_spawn_cwd(MBG_STAGED, MBG_PORT_P1, MBG_SRVLOG_P1, MBG_FIX1)
360 sys_sleep_ms(MBG_BIND_WAIT_MS)
361 let ems: *i64 = sys_mmap(16) as *i64
362 let efd: *i64 = sys_mmap(16) as *i64
363 let n6: i64 = mbg_post_call(MBG_PORT_P1, MBG_FX_SLEEP_MS, MBG_FX_WINDOW_MS / 1000, out, MBG_RESP_CAP, ems, efd)
364 let js6: i64 = mbg_has(out, n6, "JOB-STARTED" as *u8)
365 let b6: *u8 = sys_mmap(128)
366 var b6o: i64 = mbg_cat(b6, 0, " [T6-staged-promoted] reply_bytes=" as *u8); b6o = mbg_catn(b6, b6o, n6); b6o = mbg_cat(b6, b6o, " elapsed_ms=" as *u8); b6o = mbg_catn(b6, b6o, ems[0]); b6[b6o] = 10 as u8; sys_write(1, b6, b6o + 1)
367 mbg_diag("T6-staged-head" as *u8, out, n6)
368 var t6: i64 = 0
369 if js6 == 1 { if ems[0] < MBG_FX_WINDOW_MS { t6 = 1 } }
370 gv_check("staged-JOB-STARTED-on-the-wire-INSIDE-the-window (reply sent BEFORE the walled bookkeeping)" as *u8, t6, ctr)
371 if efd[0] >= 0 { sys_close(efd[0]) }
372 // release the wall, read the row the deferred bookkeeping wrote, and check its fields
373 let row: *u8 = sys_mmap(MBG_ROW_BYTES)
374 let rn: i64 = mbg_release_wall(MBG_FIX1, row, MBG_ROW_BYTES)
375 mbg_diag("T7-actlog-row" as *u8, row, rn)
376 var t7: i64 = 0
377 if mbg_has(row, rn, "lane=sync-promoted" as *u8) == 1 { if mbg_has(row, rn, " job=" as *u8) == 1 { if mbg_has(row, rn, " reply_ms=" as *u8) == 1 { t7 = 1 } } }
378 gv_check("deferred-bookkeeping-ran-AFTER-release: sync-promoted row carries job= and reply_ms=" as *u8, t7, ctr)
379 mbg_kill(p1)
380
381 // T8 NEG-CONTROL: the banked PRE-FIX binary behind the SAME wall -> the reply is NOT on the wire inside
382 // the window (its actlog ran before the reply was built); T9: it arrives only once the wall is released,
383 // proving the stall was the bookkeeping ORDER, not a dead daemon.
384 let mk2: i64 = mbg_fixture(MBG_FIX2)
385 let p2: i64 = mbg_spawn_cwd(MBG_PREFIX, MBG_PORT_P2, MBG_SRVLOG_P2, MBG_FIX2)
386 sys_sleep_ms(MBG_BIND_WAIT_MS)
387 let n8: i64 = mbg_post_call(MBG_PORT_P2, MBG_FX_SLEEP_MS, MBG_FX_WINDOW_MS / 1000, out, MBG_RESP_CAP, ems, efd)
388 let js8: i64 = mbg_has(out, n8, "JOB-STARTED" as *u8)
389 var b8o: i64 = mbg_cat(b6, 0, " [T8-prefix-promoted] reply_bytes=" as *u8); b8o = mbg_catn(b6, b8o, n8); b8o = mbg_cat(b6, b8o, " elapsed_ms=" as *u8); b8o = mbg_catn(b6, b8o, ems[0]); b6[b8o] = 10 as u8; sys_write(1, b6, b8o + 1)
390 var t8: i64 = 0
391 if js8 == 0 { t8 = 1 }
392 gv_check("neg-control-PREFIX-binary-MISSES-the-window (old ordering: actlog before reply, walled)" as *u8, t8, ctr)
393 let rn2: i64 = mbg_release_wall(MBG_FIX2, row, MBG_ROW_BYTES)
394 let n9: i64 = mbg_read_late(efd[0], MBG_FX_RELEASE_WAIT_S, out, MBG_RESP_CAP)
395 mbg_diag("T9-prefix-late" as *u8, out, n9)
396 gv_check("prefix-reply-ARRIVES-only-after-the-wall-is-released (the stall WAS the bookkeeping order, not a dead daemon)" as *u8, mbg_has(out, n9, "JOB-STARTED" as *u8), ctr)
397 mbg_kill(p2)
398
399 return gv_verdict("MCP-BODY-GATE" as *u8, ctr, "MCP transport reads the WHOLE Content-Length body; over-ceiling announces 413; a promoted call replies BEFORE its bookkeeping" as *u8)
400}