code wiki / (root) / nx_mcp_body_gate.nx

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}