nx_mcp_transport.nx source
↩ module page · 223 lines · 12353 B
1// nx_mcp_call.nx -- the SOVEREIGN MCP client: POST a JSON-RPC tools/call to /mcp over our own TLS-1.3 and PRINT
2// the response body. Fills the gap surfaced 2026-07-17 ([[reference-sovereign-mcp-call-client-gap-2026-07-17]]):
3// nx_mgmt_client's `call` sends x-www-form-urlencoded (built for /api/login) so it mis-frames a JSON tools/call
4// (status=0); this client sends Content-Type: application/json + Accept + X-Nishi-Cap and echoes the body.
5// ✅PROVEN 2026-07-17: `tools/call` WORKS end-to-end -- nishi_search returned real search hits, and
6// nx_dataplane_census returned 655 flat-file-debt rows (56KB, HTTP 200, isError:false) over sovereign TLS /mcp
7// with a least-authority cap. `initialize`/`tools/list`/`tools/call` all work; the sovereign JSON POST /mcp +
8// response-parse + body-print are complete. nx_mgmt_client CANNOT do this (it sends x-www-form-urlencoded -> the
9// daemon mis-frames the JSON -> status=0). That form-encoding -- NOT a daemon bug -- is why the doctrine's
10// "raw tools/call via nx_mgmt_client" path always failed. ⚠fork-exec tools are SLOW (census ~16s) so a client
11// needs patience; ⚠tools whose ELF FORKS CHILDREN (e.g. nx_status -> nx_hostctl sub-forks) return empty because
12// tr_run_capture (nx_tool_run.nx:21) dups the pipe to stdout/stderr but does NOT close the inherited request
13// SOCKET fd before execve -> forked grandchildren hold the connection open -> no clean close (a SEPARATE daemon
14// fix: add a close-fds-3..N loop in the child). Non-forking tools (search, census) work today.
15// tool_name "__initialize__" sends an initialize (open, no cap) probe. [[reference-sovereign-mcp-call-client-gap-2026-07-17]]
16// nx_mcp_call <base_url> <tool_name> <cap_file> [args_json_fragment]
17// e.g. nx_mcp_call https://nishifamily.com nx_dataplane_census /tmp/capfile
18// args_json_fragment (optional) is spliced into arguments after _cap, e.g. "argv":["a","b"]
19// REUSE (compose, don't rebuild TLS): copies nx_mgmt_client's PROVEN transport (build_request/send_drain/req)
20// verbatim -- renamed mc_* -- flipping ONLY the content-type. DRY debt: extract the shared transport into a
21// no-main lib later (nx_mgmt_client has a main(), so it can't be imported today). license_tier: ORIGINAL
22import "nx_acme_http.nx" // nx_http_response_parse/_alloc + nx_trust_store_load_from_certdata + TrustStore + full TLS-1.3 stack (transitive)
23import "nx_https_url_for_fetch.nx" // nx_https_url_for_fetch, NxUrl, nx_url_new, NxHttpsTarget, NX_HTTPS_URL_OK
24import "nx_https_url_connect.nx" // nx_https_url_connect, NX_HTTPS_CONNECT_OK
25import "nx_tls13_client_session_run.nx" // nx_tls13_client_session_run, TlsValidationContext
26import "nx_tls13_client_session.nx" // Tls13ClientSession, NX_TLS13_CSESSION_STATE_CONNECTED
27import "nx_tls13.nx" // NX_TLS13_CT_APPLICATION_DATA, NX_TLS13_CT_ALERT
28import "nx_tls13_record.nx" // nx_tls13_record_encrypt_v2/_decrypt_v2 + record header/tag consts
29import "nx_tls13_read_record_from_fd.nx" // nx_tls13_read_record_from_fd
30import "nx_csprng.nx" // nx_csprng_fill
31
32const MC_CONNECT_FAIL: i64 = 0 - 11
33const MC_HANDSHAKE_FAIL: i64 = 0 - 12
34
35func mc_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
36func mc_putn(v: i64) -> i64 {
37 let b: *u8 = sys_mmap(28); var x: i64 = v
38 if x < 0 { b[0] = 45 as u8; sys_write(1, b, 1); x = 0 - x }
39 if x == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 }
40 var d: i64 = 0; var y: i64 = x
41 while y > 0 { d = d + 1; y = y / 10 }
42 var i: i64 = d - 1; y = x
43 while i >= 0 { b[i] = (48 + (y % 10)) as u8; y = y / 10; i = i - 1 }
44 sys_write(1, b, d); return 0
45}
46func mc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
47func mc_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 }
48func mc_catb(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 + i }
49func mc_catn(d: *u8, o: i64, v: i64) -> i64 {
50 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0
51 if m == 0 { t[0] = 48 as u8; k = 1 }
52 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
53 var w: i64 = o; var i: i64 = 0
54 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 }
55 return w
56}
57
58func mc_write_n(fd: i64, buf: *u8, n: i64) -> i64 {
59 var off: i64 = 0
60 while off < n {
61 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off)
62 if w <= 0 { return 0 - 1 }
63 off = off + w
64 }
65 return 0
66}
67func mc_read_file(path: *u8, out: *u8, cap: i64) -> i64 {
68 let fd: i64 = sys_openat_rd(path)
69 if fd <= 0 { return 0 - 1 }
70 var off: i64 = 0
71 var go: i64 = 1
72 while go == 1 {
73 if off >= cap { go = 0 }
74 else { let r: i64 = sys_read(fd, ((out as i64) + off) as *u8, cap - off); if r <= 0 { go = 0 } else { off = off + r } }
75 }
76 sys_close(fd)
77 return off
78}
79func mc_rtrim_nl(buf: *u8, n: i64) -> i64 {
80 var m: i64 = n
81 var go: i64 = 1
82 while go == 1 {
83 if m <= 0 { go = 0 }
84 else { let c: i64 = buf[m - 1] as i64; if c == 10 { m = m - 1 } else { if c == 13 { m = m - 1 } else { go = 0 } } }
85 }
86 return m
87}
88func mc_join_url(base_url: *u8, suffix: *u8, out: *u8) -> i64 {
89 var o: i64 = mc_cat(out, 0, base_url)
90 if o > 0 { if out[o - 1] == (47 as u8) { o = o - 1 } }
91 if suffix[0] != (47 as u8) { out[o] = 47 as u8; o = o + 1 }
92 o = mc_cat(out, o, suffix)
93 out[o] = 0 as u8
94 return o
95}
96
97// Build a full HTTP/1.1 request (origin-form target). Content-Type parameterized -> we pass application/json.
98func mc_build_request(method: *u8, method_len: i64, path: *u8, path_len: i64, host: *u8, host_len: i64, cap: *u8, cap_len: i64, ct: *u8, ct_len: i64, body: *u8, body_len: i64, out: *u8) -> i64 {
99 var o: i64 = 0
100 o = mc_catb(out, o, method, method_len)
101 out[o] = 0x20 as u8; o = o + 1
102 o = mc_catb(out, o, path, path_len)
103 o = mc_cat(out, o, " HTTP/1.1\r\nHost: " as *u8)
104 o = mc_catb(out, o, host, host_len)
105 o = mc_cat(out, o, "\r\n" as *u8)
106 o = mc_cat(out, o, "Accept: application/json, text/event-stream\r\n" as *u8) // MCP Streamable-HTTP transport requires it
107 if cap_len > 0 { // primary auth path (Claude Code presents the cap this way)
108 o = mc_cat(out, o, "X-Nishi-Cap: " as *u8)
109 o = mc_catb(out, o, cap, cap_len)
110 o = mc_cat(out, o, "\r\n" as *u8)
111 }
112 if body_len > 0 {
113 o = mc_cat(out, o, "Content-Type: " as *u8)
114 o = mc_catb(out, o, ct, ct_len)
115 o = mc_cat(out, o, "\r\nContent-Length: " as *u8)
116 o = mc_catn(out, o, body_len)
117 o = mc_cat(out, o, "\r\n" as *u8)
118 }
119 o = mc_cat(out, o, "Connection: close\r\n\r\n" as *u8)
120 if body_len > 0 { o = mc_catb(out, o, body, body_len) }
121 return o
122}
123
124// A complete Content-Length response does not require waiting for the peer's idle-close timer.
125// Zero means headers incomplete; -1 retains close-delimited behavior; -2 is an invalid/oversized frame.
126func mc_http_expected(src: *u8, n: i64, capacity: i64) -> i64 {
127 if nx_http_resp_find_crlfcrlf(src, n, 0) < 0 { return 0 }
128 let parsed: *i64 = nx_http_resp_alloc()
129 if nx_http_response_parse(src, n, parsed) != NX_HTTP_RESP_OK { return 0 - 2 }
130 if parsed[8] != NX_HTTP_BODY_CONTENT_LENGTH { return 0 - 1 }
131 if parsed[7] < 0 { return 0 - 2 }
132 if parsed[6] > capacity { return 0 - 2 }
133 if parsed[7] > capacity - parsed[6] { return 0 - 2 }
134 return parsed[6] + parsed[7]
135}
136
137func mc_received_result(acc: i64, expected: i64) -> i64 {
138 if expected == 0 { return 0 - 6 }
139 if expected > 0 { if acc < expected { return 0 - 6 } }
140 return acc
141}
142
143// Send a PRE-BUILT request over a CONNECTED TLS-1.3 session, then drain the full response into out. Verbatim
144// copy of nx_mgmt_client's proven mcl_send_drain (RFC 8446 <=16KB record fragmentation + decrypt loop).
145func mc_send_drain(s: *Tls13ClientSession, fd: i64, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 {
146 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { return 0 - 1 }
147 let rec_buf: *u8 = sys_mmap(16384 + 64)
148 var snd_off: i64 = 0
149 var first_frag: i64 = 1
150 while first_frag == 1 { first_frag = 0
151 var frag: i64 = req_len - snd_off
152 if frag > 16384 { frag = 16384 }
153 let header_out: *u8 = rec_buf
154 let ct_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8
155 let tag_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN + frag + 1) as *u8
156 let enc_v: i64 = nx_tls13_record_encrypt_v2(s.cipher_suite, s.client_app_traffic_key, s.client_app_iv, s.client_app_seq, ((req as i64) + snd_off) as *u8, frag, NX_TLS13_CT_APPLICATION_DATA, 0, header_out, ct_out, tag_out)
157 s.client_app_seq = s.client_app_seq + 1
158 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - 2 }
159 let total: i64 = NX_TLS13_RECORD_HEADER_LEN + frag + 1 + NX_TLS13_RECORD_TAG_LEN
160 if mc_write_n(fd, rec_buf, total) < 0 { return 0 - 3 }
161 snd_off = snd_off + frag
162 if snd_off < req_len { first_frag = 1 }
163 }
164 var acc: i64 = 0
165 var expected: i64 = 0
166 let rec_in: *u8 = sys_mmap(16645)
167 let plain: *u8 = sys_mmap(16645)
168 let cttype: *i64 = sys_mmap(16) as *i64
169 let ptlen: *i64 = sys_mmap(16) as *i64
170 while acc < out_cap {
171 let rin: i64 = nx_tls13_read_record_from_fd(fd, rec_in, 16645)
172 if rin < 0 { return mc_received_result(acc, expected) }
173 let ctlen: i64 = rin - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN
174 let rin_ct: *u8 = ((rec_in as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8
175 let rin_tag: *u8 = ((rec_in as i64) + rin - NX_TLS13_RECORD_TAG_LEN) as *u8
176 let dv: i64 = nx_tls13_record_decrypt_v2(s.cipher_suite, s.server_app_traffic_key, s.server_app_iv, s.server_app_seq, rec_in, rin_ct, ctlen, rin_tag, plain, cttype, ptlen)
177 s.server_app_seq = s.server_app_seq + 1
178 if dv != NX_TLS13_REC_VERDICT_OK { return 0 - 7 }
179 if cttype[0] == NX_TLS13_CT_ALERT { return mc_received_result(acc, expected) }
180 if cttype[0] == NX_TLS13_CT_APPLICATION_DATA {
181 let tc: i64 = ptlen[0]
182 if acc + tc > out_cap { return 0 - 5 }
183 var i: i64 = 0
184 while i < tc { out[acc + i] = plain[i]; i = i + 1 }
185 acc = acc + tc
186 if expected == 0 { expected = mc_http_expected(out, acc, out_cap) }
187 if expected == (0 - 2) { return 0 - 5 }
188 if expected > 0 { if acc >= expected { return expected } }
189 }
190 }
191 return 0 - 5
192}
193
194// Connect to full_url's host:443, validated TLS-1.3 handshake, send req, drain response. Verbatim copy of mcl_req.
195func mc_req_timed(store: *TrustStore, full_url: *u8, target: *NxHttpsTarget, req: *u8, req_len: i64, out: *u8, out_cap: i64, timeout_secs: i64) -> i64 {
196 let now: i64 = sys_now_realtime_sec()
197 let fd_p: *i64 = sys_mmap(16) as *i64
198 if nx_https_url_connect(target, full_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return MC_CONNECT_FAIL }
199 let fd: i64 = fd_p[0]
200 if timeout_secs > 0 { sys_set_socket_timeout(fd, timeout_secs) }
201 let host: *u8 = ((full_url as i64) + target.url.host_off) as *u8
202 let hlen: i64 = target.url.host_len
203 let cr: *u8 = sys_mmap(32)
204 let priv: *u8 = sys_mmap(32)
205 nx_csprng_fill(cr, 32)
206 nx_csprng_fill(priv, 32)
207 let vc: *TlsValidationContext = sys_mmap(128) as *TlsValidationContext
208 vc.store = store
209 vc.sni_host = host
210 vc.sni_host_len = hlen
211 vc.now_epoch = now
212 let sr: i64 = nx_tls13_client_session_run(fd, host, hlen, cr, priv, vc)
213 if sr <= 0 { sys_close(fd); return MC_HANDSHAKE_FAIL }
214 let session: *Tls13ClientSession = sr as *Tls13ClientSession
215 let n: i64 = mc_send_drain(session, fd, req, req_len, out, out_cap)
216 sys_close(fd)
217 return n
218}
219
220// Preserve the original CLI API and its existing connection timeout.
221func mc_req(store: *TrustStore, full_url: *u8, target: *NxHttpsTarget, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 {
222 return mc_req_timed(store, full_url, target, req, req_len, out, out_cap, 0)
223}