nx_mcp_body_gate.nx
buildroot/runtime/nx_mcp_body_gate.nx
about
nx_mcp_body_gate.nx -- LANE K 2026-08-23. Referee for the MCP-wire body-read root fix in
nx_tools_api_serve.nx (tsv_serve_one). THE DEFECT: the daemon read the whole request with ONE
sys_read into a 64 KiB buffer, so a body over ~64 KiB (or one arriving in >1 TCP segment) was parsed
as a FRAGMENT -- trailing argv/fields silently vanished (measured: a 131072 B write landed as 42045 B)
and every source push had to be split into anchored hunks. THE FIX: tsv_serve_one composes the estate's
own nx_http_server_read_request (the Content-Length read loop the daemon never used) + a conf-driven
named reserve (tools_req_reserve.conf, one owner via EDGE_REQ_MAX) + a 413 that ANNOUNCES limit+received.
IT RUNS, IT DOES NOT MERELY COMPILE: it starts the SUBJECT binary on a throwaway port and POSTs REAL
bodies to /mcp over a real socket. The proof that the WHOLE body was read is that the JSON-RPC `id` --
placed at the very END of a 150 KB body -- is ECHOED in the response; a single-read daemon never sees it.
T_BITE spawns the BANKED PRE-FIX binary and shows the SAME probe does NOT echo the end marker -- the
non-vacuity law made executable: the tooth fires on the bad binary and is silent on the good one.
license_tier: ORIGINAL
dependencies 4 imports · 2 importers
imports: nx_http_server.nxnx_connect.nxnx_gate_verdict.nxnx_tools_api.nx
imported by: nx_job_connected_gate_t183.nxnx_route_period_gate_t184.nx
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 20 | const MBG_STAGED: *u8 = "/volume1/homes/elderwesto/nishihost/nx_tools_api_serve.sov.elf.new" as *u8 |
| 21 | const MBG_PREFIX: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/bank/nx_tools_api_serve.elf.pre-bodycap-d3b23e08" as *u8 |
| 22 | const MBG_PORT_OK: i64 = 18993 |
| 23 | const MBG_PORT_BAD: i64 = 18994 |
| 24 | const MBG_DEADPORT: i64 = 18995 |
| 25 | const MBG_SRVLOG_OK: *u8 = "/tmp/nx_mcp_body_srv_ok.out" as *u8 |
| 26 | const MBG_SRVLOG_BAD: *u8 = "/tmp/nx_mcp_body_srv_bad.out" as *u8 |
| 27 | const MBG_RESP_CAP: i64 = 1048576 |
| 28 | const MBG_FULL_BYTES: i64 = 150000 // > 64 KiB single-read window, < the 1 MiB ceiling: MUST fully read |
| 29 | const MBG_SMALL_BYTES: i64 = 1024 // trivially fits: the fix must not regress small bodies |
| 30 | const MBG_OVER_BYTES: i64 = 1300000 // > EDGE_REQ_MAX (1 MiB): must 413 naming limit + received |
| 31 | const MBG_CEIL: i64 = 1048576 // EDGE_REQ_MAX / default reserve (edge plaincap-derived) |
| 32 | const MBG_BIND_WAIT_MS: i64 = 4000 |
| 42 | const MBG_FIX1: *u8 = "/tmp/nx_mcp_body_gate_fx1" as *u8 |
| 43 | const MBG_FIX2: *u8 = "/tmp/nx_mcp_body_gate_fx2" as *u8 |
| 44 | const MBG_PORT_P1: i64 = 18996 |
| 45 | const MBG_PORT_P2: i64 = 18997 |
| 46 | const MBG_SRVLOG_P1: *u8 = "/tmp/nx_mcp_body_srv_p1.out" as *u8 |
| 47 | const MBG_SRVLOG_P2: *u8 = "/tmp/nx_mcp_body_srv_p2.out" as *u8 |
| 48 | const MBG_FX_WINDOW_MS: i64 = 3000 // fixture edge window (conf row); the promote decision = window - reserve |
| 49 | const MBG_FX_RESERVE_MS: i64 = 1000 |
| 50 | const MBG_FX_SLEEP_MS: i64 = 4500 // > window: the worker is genuinely mid-run at the decision |
| 51 | const MBG_FX_RELEASE_WAIT_S: i64 = 10 // after releasing the wall, how long a late reply may take |
| 52 | const MBG_SYS_OPENAT: i64 = 257 // x86-64 openat (not an rv64 key: passes the translator through) |
| 53 | const MBG_SYS_MKNODAT: i64 = 259 // x86-64 mknodat (the incumbent nx_autorun_daemon uses exactly this) |
| 54 | const MBG_AT_FDCWD: i64 = 0 - 100 |
| 55 | const MBG_S_IFIFO_0644: i64 = 4516 // S_IFIFO (0o010000 = 4096) | 0644 (420) |
| 56 | const MBG_O_RDONLY_NONBLOCK: i64 = 2048 // O_RDONLY(0) | O_NONBLOCK(0x800): release the wall without ever blocking the gate |
| 57 | const MBG_DIRMODE: i64 = 493 // 0755 |
| 58 | const MBG_CAP_TTL_S: i64 = 3600 |
| 59 | const MBG_EEXIST: i64 = 0 - 17 |
| 61 | const MBG_CAP_TOKEN_BYTES: i64 = 1024 // a minted cap: allow~exp~nonce.sig, base64url -- far under 1 KiB |
| 62 | const MBG_CALL_BODY_BYTES: i64 = 4096 // the tools/call JSON envelope + one short argv + the cap |
| 63 | const MBG_REQ_HEADROOM_BYTES: i64 = 8192 // HTTP request line + headers on top of any body (a handful of lines) |
| 64 | const MBG_ROW_BYTES: i64 = 4096 // one actlog row drained from the FIFO wall |
functions
| 67 | func mbg_pid() -> i64 { return __syscall(172, 0, 0, 0, 0, 0, 0) } called by 1: mbg_post_call |
| 68 | func mbg_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 69 | func 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 } |
| 70 | func mbg_catn(d: *u8, o: i64, v: i64) -> i64 |
| 78 | func mbg_has(buf: *u8, n: i64, pat: *u8) -> i64 |
| 92 | func mbg_spawn(elf: *u8, port: i64, logf: *u8) -> i64 |
| 111 | func mbg_kill(pid: i64) -> i64 |
| 126 | func mbg_spawn_cwd(elf: *u8, port: i64, logf: *u8, cwd: *u8) -> i64 |
| 145 | func mbg_writefile(path: *u8, content: *u8) -> i64 |
| 151 | func 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 } |
| 153 | func mbg_fixture(root: *u8) -> i64 |
| 170 | func mbg_release_wall(root: *u8, out: *u8, cap: i64) -> i64 |
| 189 | func mbg_post_call(port: i64, sleep_ms: i64, wait_s: i64, out: *u8, outcap: i64, out_ms: *i64, out_fd: *i64) -> i64 called by 2: mainmain calls 7: nx_http_server_addr_loopbacknx_connect_boundedmbg_slenmbg_pidmbg_catmbg_catn+1 |
| 233 | func mbg_read_late(fd: i64, wait_s: i64, out: *u8, outcap: i64) -> i64 called by 1: main |
| 252 | func mbg_post(port: i64, nbytes: i64, out: *u8, outcap: i64) -> i64 |
| 297 | func mbg_diag(tag: *u8, buf: *u8, n: i64) -> i64 |
| 305 | func main() -> i64 |