nx_mcp_pending_http_t174.nx source
↩ module page · 393 lines · 19566 B
1// MCP-specific identity ownership over the generic process pool.
2import "nx_mcp_control.nx"
3import "nx_request_pool.nx"
4
5struct NxMcpPending {
6 pid: i64,
7 kind: i64,
8 method: i64,
9 data: *u8,
10 length: i64,
11 allocated: i64,
12}
13// mp_spawn returns this (not a bare -1) when the id is ALREADY in flight, so the
14// caller can answer that ONE request with a correlated error and keep serving the
15// rest instead of tearing the session down. Still < 0, so mp_spawn's existing
16// "< 0 means not spawned" contract (and its gate) are unchanged.
17const MP_SPAWN_DUP: i64 = 0 - 2
18// Parent-side recovery error codes (JSON-RPC server range / internal). A worker
19// that died WITHOUT self-reporting (a crash, or the SIGALRM deadline killing it)
20// is turned into one of these correlated errors by mp_recover_pump.
21const MP_CODE_DEADLINE: i64 = 0 - 32012
22const MP_CODE_WORKER: i64 = 0 - 32603
23const MP_SIG_ALARM: i64 = 14
24// Capacity is an admitted execution budget, not an instruction to block the
25// control channel. Only requests can receive a correlated refusal response.
26func mp_admit(p: *NxRequestPool, body: *u8, control: *NxMcpControl, output: i64) -> i64 {
27 if p.active < p.capacity { return 1 }
28 if control.id_kind == 0 { return -1 }
29 let prefix: *u8="{\"jsonrpc\":\"2.0\",\"id\":" as *u8
30 let suffix: *u8=",\"error\":{\"code\":-32000,\"message\":\"Connector execution capacity is occupied\",\"data\":{\"owner\":\"nx_mcp_stdio\",\"stage\":\"admission\",\"cause\":\"admitted_workers_in_use\",\"dispatched\":false,\"next\":\"Wait for an outstanding request to finish or cancel it before retrying\"}}}\n" as *u8
31 var off: i64=control.id_off
32 var count: i64=control.id_len
33 if control.id_kind == NX_JSON_STRING { off=off-1; count=count+2 }
34 if rp_write(output,prefix,mr_len(prefix)) != 0 { return -1 }
35 if rp_write(output,body+off,count) != 0 { return -1 }
36 if rp_write(output,suffix,mr_len(suffix)) != 0 { return -1 }
37 return 0
38}
39func mp_at(rows: *u8, slot: i64) -> *NxMcpPending {
40 return (rows+slot*__size_of(NxMcpPending)) as *NxMcpPending
41}
42func mp_clear(row: *NxMcpPending) -> i64 {
43 if row.allocated > 0 { sys_munmap(row.data,row.allocated) }
44 row.pid=0; row.kind=0; row.method=0; row.data=0 as *u8; row.length=0; row.allocated=0
45 return 0
46}
47func mp_sweep(p: *NxRequestPool, rows: *u8) -> i64 {
48 var i: i64=0
49 while i<p.capacity {
50 let row: *NxMcpPending=mp_at(rows,i)
51 if row.pid != 0 { if row.pid != rp_worker(p,i).pid { mp_clear(row) } }
52 i=i+1
53 }
54 return 0
55}
56func mp_normalize(kind: i64, src: *u8, n: i64, dst: *u8) -> i64 {
57 if kind == NX_JSON_STRING { return jx_decode_span(src,n,dst,n+1) }
58 if kind != NX_JSON_NUMBER { return -1 }
59 var i: i64=0
60 // JSON -0 and 0 denote the same integer identity.
61 if n == 2 { if src[0] == (45 as u8) { if src[1] == (48 as u8) { dst[0]=48 as u8; return 1 } } }
62 while i<n { dst[i]=src[i]; i=i+1 }
63 return n
64}
65func mp_find(p: *NxRequestPool, rows: *u8, kind: i64, data: *u8, n: i64) -> i64 {
66 mp_sweep(p,rows)
67 var slot: i64=0
68 while slot<p.capacity {
69 let row: *NxMcpPending=mp_at(rows,slot)
70 if row.pid != 0 { if row.kind == kind { if row.length == n {
71 var equal: i64=1
72 var i: i64=0
73 while i<n { if row.data[i] != data[i] { equal=0 }; i=i+1 }
74 if equal != 0 { return slot }
75 } } }
76 slot=slot+1
77 }
78 return -1
79}
80func mp_spawn(p: *NxRequestPool, rows: *u8, body: *u8, control: *NxMcpControl) -> i64 {
81 if control.id_kind == 0 { return rp_fork(p) }
82 let allocated: i64=control.id_len+1
83 let data: *u8=sys_mmap(allocated)
84 let n: i64=mp_normalize(control.id_kind,body+control.id_off,control.id_len,data)
85 if n<0 { sys_munmap(data,allocated); return -1 }
86 if mp_find(p,rows,control.id_kind,data,n)>=0 { sys_munmap(data,allocated); return MP_SPAWN_DUP }
87 let pid: i64=rp_fork(p)
88 if pid==0 { return 0 }
89 if pid<0 { sys_munmap(data,allocated); return pid }
90 var slot: i64=0
91 while slot<p.capacity {
92 if rp_worker(p,slot).pid==pid {
93 let row: *NxMcpPending=mp_at(rows,slot)
94 mp_clear(row)
95 row.pid=pid; row.kind=control.id_kind; row.method=control.method
96 row.data=data; row.length=n; row.allocated=allocated
97 return pid
98 }
99 slot=slot+1
100 }
101 sys_munmap(data,allocated)
102 return -1
103}
104// Returns 1 for confirmed local worker cancellation, 0 for no active target,
105// -1 for failed/prohibited cancellation. This does not cancel a remote job.
106func mp_cancel(p: *NxRequestPool, rows: *u8, body: *u8, control: *NxMcpControl) -> i64 {
107 let allocated: i64=control.cancel_len+1
108 let data: *u8=sys_mmap(allocated)
109 let n: i64=mp_normalize(control.cancel_kind,body+control.cancel_off,control.cancel_len,data)
110 if n<0 { sys_munmap(data,allocated); return -1 }
111 let slot: i64=mp_find(p,rows,control.cancel_kind,data,n)
112 sys_munmap(data,allocated)
113 if slot<0 { return 0 }
114 let row: *NxMcpPending=mp_at(rows,slot)
115 if row.method==MCT_INITIALIZE { return -1 }
116 if rp_cancel_owned(p,slot,row.pid)!=0 { return -1 }
117 mp_clear(row)
118 return 1
119}
120
121// Extract a top-level "id" and its COMPLETE value from a (possibly truncated) request prefix -- the
122// bytes we DID retain before an oversized or malformed line. Returns 1 and fills kind/off/len (string
123// content EXCLUDES the quotes; number is its digit span with an optional leading '-'); returns 0 when
124// no whole top-level id is present (nested only, absent, or the value is cut short by the truncation).
125// Lives here (not in the connector's main-bearing file) so the gate can test it directly.
126func mo_prefix_id(src: *u8, n: i64, kind: *i64, off: *i64, len: *i64) -> i64 {
127 var depth: i64 = 0
128 var quoted: i64 = 0
129 var escaped: i64 = 0
130 var start: i64 = 0
131 var i: i64 = 0
132 while i < n {
133 let c: i64 = src[i] as i64
134 if quoted == 1 {
135 if escaped == 1 { escaped = 0 }
136 else { if c == 92 { escaped = 1 }
137 else { if c == 34 {
138 quoted = 0
139 if depth == 1 { if i - start == 2 {
140 if src[start] == (105 as u8) { if src[start + 1] == (100 as u8) {
141 var j: i64 = i + 1
142 while j < n { if src[j] == (32 as u8) { j = j + 1 } else { break } }
143 if j < n { if src[j] == (58 as u8) {
144 j = j + 1
145 while j < n { if src[j] == (32 as u8) { j = j + 1 } else { break } }
146 if j >= n { return 0 }
147 let vc: i64 = src[j] as i64
148 if vc == 34 {
149 let vstart: i64 = j + 1
150 var k: i64 = vstart
151 var esc: i64 = 0
152 var closed: i64 = 0
153 while k < n {
154 let cc: i64 = src[k] as i64
155 if esc == 1 { esc = 0 }
156 else { if cc == 92 { esc = 1 } else { if cc == 34 { closed = 1; break } } }
157 k = k + 1
158 }
159 if closed == 0 { return 0 }
160 *kind = NX_JSON_STRING
161 *off = vstart
162 *len = k - vstart
163 return 1
164 }
165 var k2: i64 = j
166 if src[k2] == (45 as u8) { k2 = k2 + 1 }
167 let dstart: i64 = k2
168 while k2 < n { if nx_ascii_is_digit(src[k2]) == 1 { k2 = k2 + 1 } else { break } }
169 if k2 == dstart { return 0 }
170 if k2 >= n { return 0 }
171 *kind = NX_JSON_NUMBER
172 *off = j
173 *len = k2 - j
174 return 1
175 } }
176 } }
177 } }
178 } } }
179 } else {
180 if c == 34 { quoted = 1; start = i + 1 }
181 else { if c == 123 { depth = depth + 1 }
182 else { if c == 125 { depth = depth - 1 } } }
183 }
184 i = i + 1
185 }
186 return 0
187}
188
189// ---- CORRELATED JSON-RPC ERROR EMISSION -------------------------------------
190// One place builds the JSON-RPC 2.0 error envelope so the request CHILD (rich
191// soft-failure errors, original id span) and the PARENT recovery (a crashed
192// worker, decoded id from its pending row) produce the SAME shape. Self-contained
193// cat helpers: nx_mcp_pending sees neither the transport's mc_* nor the tools API.
194func mpe_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
195func mpe_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p }
196func mpe_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 }
197func mpe_catn(d: *u8, o: i64, v: i64) -> i64 {
198 var w: i64 = o
199 var x: i64 = v
200 if x < 0 { d[w] = 45 as u8; w = w + 1; x = 0 - x }
201 let t: *u8 = sys_mmap(24)
202 var k: i64 = 0
203 if x == 0 { t[0] = 48 as u8; k = 1 }
204 while x > 0 { t[k] = (48 + (x % 10)) as u8; x = x / 10; k = k + 1 }
205 var i: i64 = 0
206 while i < k { d[w] = t[k-1-i]; w = w + 1; i = i + 1 }
207 sys_munmap(t, 24)
208 return w
209}
210// JSON-escape a DECODED id string span (parent path): a raw quote, backslash, or
211// control byte in a decoded id would break the envelope, so escape them. The
212// child path passes the ORIGINAL span verbatim (already valid JSON) and skips this.
213func mpe_cat_esc(d: *u8, o: i64, s: *u8, n: i64) -> i64 {
214 var w: i64 = o
215 var i: i64 = 0
216 while i < n {
217 let c: i64 = s[i] as i64
218 if c == 34 { d[w] = 92 as u8; d[w+1] = 34 as u8; w = w + 2 }
219 else { if c == 92 { d[w] = 92 as u8; d[w+1] = 92 as u8; w = w + 2 }
220 else { if c < 32 {
221 d[w] = 92 as u8; d[w+1] = 117 as u8; d[w+2] = 48 as u8; d[w+3] = 48 as u8
222 let hi: i64 = (c >> 4) & 15
223 let lo: i64 = c & 15
224 var hc: i64 = 48 + hi
225 if hi > 9 { hc = 87 + hi }
226 var lc: i64 = 48 + lo
227 if lo > 9 { lc = 87 + lo }
228 d[w+4] = hc as u8; d[w+5] = lc as u8; w = w + 6
229 } else { d[w] = c as u8; w = w + 1 } } }
230 i = i + 1
231 }
232 return w
233}
234// Write a correlated JSON-RPC 2.0 error to fd for request id (id_kind/id_ptr/id_len).
235// escape=0: id_ptr is the ORIGINAL request id span (string content or number
236// digits) -- emitted verbatim. escape=1: id_ptr is a DECODED id (pending row) --
237// string content is re-escaped. id_kind==0 (a notification) writes NOTHING and
238// returns 0: a notification never gets a response. Returns 0 on success (or on
239// nothing-to-write), non-zero when the protocol fd write failed (broken output).
240func mpe_write_error_detail(fd: i64, id_kind: i64, id_ptr: *u8, id_len: i64, escape: i64, code: i64, stage: *u8, cause: *u8, measured: i64, next: *u8, detail:*u8, detail_n:i64) -> i64 {
241 if id_kind == 0 { return 0 }
242 let cap: i64 = id_len*6 + mpe_slen(stage) + mpe_slen(cause)*2 + mpe_slen(next) + 512 + detail_n
243 let d: *u8 = sys_mmap(cap)
244 var o: i64 = mpe_cat(d, 0, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8)
245 if id_kind == NX_JSON_STRING {
246 d[o] = 34 as u8; o = o + 1
247 if escape == 0 { o = mpe_catb(d, o, id_ptr, id_len) } else { o = mpe_cat_esc(d, o, id_ptr, id_len) }
248 d[o] = 34 as u8; o = o + 1
249 } else {
250 o = mpe_catb(d, o, id_ptr, id_len)
251 }
252 o = mpe_cat(d, o, ",\"error\":{\"code\":" as *u8)
253 o = mpe_catn(d, o, code)
254 o = mpe_cat(d, o, ",\"message\":\"" as *u8)
255 o = mpe_cat(d, o, cause)
256 o = mpe_cat(d, o, "\",\"data\":{\"owner\":\"nx_mcp_stdio\",\"stage\":\"" as *u8)
257 o = mpe_cat(d, o, stage)
258 o = mpe_cat(d, o, "\",\"cause\":\"" as *u8)
259 o = mpe_cat(d, o, cause)
260 o = mpe_cat(d, o, "\",\"measured\":" as *u8)
261 o = mpe_catn(d, o, measured)
262 o = mpe_cat(d, o, ",\"next\":\"" as *u8)
263 o = mpe_cat(d, o, next)
264 o = mpe_cat(d, o, "\"" as *u8)
265 if (detail as i64)>0&&detail_n>0{o=mpe_cat(d,o,",\"http\":");o=mpe_catb(d,o,detail,detail_n)}
266 o = mpe_cat(d, o, "}}}\n" as *u8)
267 let rc: i64 = rp_write(fd, d, o)
268 sys_munmap(d, cap)
269 return rc
270}
271
272// ---- mp_recover_pump: A PER-REQUEST WORKER FAILURE IS ONE ERROR, NOT AN OUTAGE
273// Wrap rp_pump so a child that failed WITHOUT self-reporting (a crash, or the
274// SIGALRM deadline that killed it) becomes a CORRELATED error for that one request
275// id and the connector keeps serving every other request. Only a genuine fatal --
276// poll failure, a broken protocol fd (rp_pump last_error -3), or a reap fault --
277// returns -1. Success/progress (1 stdin-ready, 0 progress) is passed through;
278// a recovered per-request failure returns 0 (progress, keep going).
279func mp_recover_pump(p: *NxRequestPool, rows: *u8, accept_input: i64, protocol_fd: i64, timeout_secs: i64) -> i64 {
280 let r: i64 = rp_pump(p, accept_input, protocol_fd)
281 if r >= 0 { mp_sweep(p, rows); return r }
282 // r == -1. A known failed slot AND not a broken-output-fd fatal => recoverable.
283 let slot: i64 = p.failed_slot
284 if slot < 0 { return 0 - 1 }
285 if p.last_error == 0 - 3 { return 0 - 1 }
286 // Over-budget leaves the child alive; reap it before reusing the slot. A
287 // non-zero exit already released it (rp_worker(p,slot).pid == 0), so skip.
288 if rp_worker(p, slot).pid != 0 {
289 if rp_cancel_owned(p, slot, rp_worker(p, slot).pid) != 0 { return 0 - 1 }
290 }
291 let row: *NxMcpPending = mp_at(rows, slot)
292 var wrc: i64 = 0
293 if row.pid != 0 {
294 var code: i64 = MP_CODE_WORKER
295 var stage: *u8 = "worker" as *u8
296 var cause: *u8 = "the request worker terminated before delivering a response" as *u8
297 var measured: i64 = p.last_error
298 if wait_term_signal(p.last_error) == MP_SIG_ALARM {
299 code = MP_CODE_DEADLINE
300 stage = "deadline" as *u8
301 cause = "the request exceeded its per-request time budget" as *u8
302 measured = timeout_secs
303 }
304 wrc = mpe_write_error(protocol_fd, row.kind, row.data, row.length, 1, code, stage, cause, measured, "check service health and byte budgets; verify any write outcome before retrying" as *u8)
305 mp_clear(row)
306 }
307 p.failed_slot = 0 - 1
308 mp_sweep(p, rows)
309 if wrc != 0 { return 0 - 1 }
310 return 0
311}
312
313// Selective HTTP evidence for per-request failures. Raw response bodies, cookies,
314// authorization headers and free-form upstream detail never enter protocol errors.
315import "nx_http_response_parse.nx"
316import "nx_mcp_route.nx"
317import "nx_sha256.nx"
318func mpe_http_header(src:*u8,n:i64,p:*i64,key:*u8,offset:*i64,length:*i64)->i64 {
319 var pos:i64=p[4];var count:i64=0
320 var no:i64=0;var nl:i64=0;var vo:i64=0;var vl:i64=0;var next:i64=0
321 while pos<p[6]-2{
322 if nx_http_resp_parse_header_line(src,n,pos,&no,&nl,&vo,&vl,&next)!=NX_HTTP_RESP_OK{return -1}
323 if next<=pos{return -1}
324 if nx_http_resp_cieq(src+no,nl,key,mpe_slen(key))==1{
325 var i:i64=0;while i<vl{let c:i64=src[vo+i] as i64;if c<32||c>126{return -1};i=i+1}
326 count=count+1;*offset=vo;*length=vl
327 };pos=next
328 }
329 return count
330}
331func mpe_http_field(d:*u8,o0:i64,src:*u8,n:i64,p:*i64,name:*u8,label:*u8)->i64 {
332 var off:i64=0;var len:i64=0;let count:i64=mpe_http_header(src,n,p,name,&off,&len)
333 var o:i64=mpe_cat(d,o0,",\"");o=mpe_cat(d,o,label);o=mpe_cat(d,o,"\":")
334 if count==1{o=mpe_cat(d,o,"\"");o=mpe_cat_esc(d,o,src+off,len);o=mpe_cat(d,o,"\"")}else{o=mpe_cat(d,o,"null")}
335 o=mpe_cat(d,o,",\"");o=mpe_cat(d,o,label);o=mpe_cat(d,o,"_observation\":\"")
336 var state:*u8="absent";if count==1{state="present"};if count>1{state="ambiguous-duplicate"};if count<0{state="malformed"}
337 o=mpe_cat(d,o,state);return mpe_cat(d,o,"\"")
338}
339// Returned object is validated/escaped here and borrowed by the error emitter.
340// Allocations live only in the existing one-request worker; no persistent collector.
341func mpe_http_evidence(src:*u8,n:i64,p:*i64,out_len:*i64)->*u8 {
342 *out_len=0
343 if n<=0||n>(NX_RA_SIZE_MAX-4096)/6||p[6]<0||p[6]>n{return 0 as *u8}
344 let cap:i64=n*6+4096;let d:*u8=sys_mmap(cap);if (d as i64)<=0{return 0 as *u8}
345 let bn:i64=n-p[6];var o:i64=mpe_cat(d,0,"{\"schema\":\"nishi.mcp-http-observation.v1\",\"status\":")
346 o=mpe_catn(d,o,p[1]);o=mpe_cat(d,o,",\"observed_at_unix\":");o=mpe_catn(d,o,sys_now_realtime_sec())
347 o=mpe_http_field(d,o,src,n,p,"Retry-After","retry_after")
348 o=mpe_http_field(d,o,src,n,p,"X-Request-Id","request_id")
349 o=mpe_http_field(d,o,src,n,p,"X-Nishi-Request-Id","nishi_request_id")
350 o=mpe_http_field(d,o,src,n,p,"Traceparent","traceparent")
351 o=mpe_http_field(d,o,src,n,p,"Content-Type","content_type")
352 o=mpe_cat(d,o,",\"observed_body_bytes\":");o=mpe_catn(d,o,bn)
353 o=mpe_cat(d,o,",\"declared_body_bytes\":")
354 if p[8]==NX_HTTP_BODY_CONTENT_LENGTH{o=mpe_catn(d,o,p[7])}else{o=mpe_cat(d,o,"null")}
355 var complete:i64=0
356 if p[8]==NX_HTTP_BODY_CONTENT_LENGTH&&p[7]==bn{complete=1}
357 o=mpe_cat(d,o,",\"body_framing\":\"")
358 var framing:*u8="unverified-transfer";if p[8]==NX_HTTP_BODY_CONTENT_LENGTH{framing="length-mismatch";if complete==1{framing="content-length-complete"}}
359 o=mpe_cat(d,o,framing);o=mpe_cat(d,o,"\",\"observed_body_sha256\":")
360 let hash:*u8=sys_mmap(SHA256_DIGEST_BYTES)
361 if (hash as i64)>0&&sha256_digest_checked_native(src+p[6],bn,hash)==0{
362 let hex:*u8="0123456789abcdef";o=mpe_cat(d,o,"\"");var i:i64=0
363 while i<SHA256_DIGEST_BYTES{let b:i64=hash[i] as i64;d[o]=hex[b>>4];d[o+1]=hex[b&15];o=o+2;i=i+1}
364 o=mpe_cat(d,o,"\"")
365 }else{o=mpe_cat(d,o,"null")}
366 if (hash as i64)>0{sys_munmap(hash,SHA256_DIGEST_BYTES)}
367 var stage:*u8=0 as *u8;var retry:*u8=0 as *u8
368 if complete==1{
369 var parsed:i64=0;let root:*NxValue=nx_value_parse_json(src+p[6],bn,&parsed)
370 if parsed==NX_VAL_PARSE_OK{
371 var bad:i64=0;let sv:*NxValue=mr_member(root,"stage",&bad);let rv:*NxValue=mr_member(root,"retry",&bad)
372 if bad==0{
373 if sv!=(0 as *NxValue){if sv.kind==NX_VAL_STRING{
374 if mr_equal(sv.str_ptr,sv.str_len,"connect")==1{stage="connect"}
375 if mr_equal(sv.str_ptr,sv.str_len,"read")==1{stage="read"}
376 if mr_equal(sv.str_ptr,sv.str_len,"deadline")==1{stage="deadline"}
377 if mr_equal(sv.str_ptr,sv.str_len,"read-after-accept")==1{stage="read-after-accept"}
378 }}
379 if rv!=(0 as *NxValue){if rv.kind==NX_VAL_STRING{if mr_equal(rv.str_ptr,rv.str_len,"unsafe")==1{retry="unsafe"}}}
380 }
381 }
382 }
383 o=mpe_cat(d,o,",\"upstream_stage\":")
384 if (stage as i64)>0{o=mpe_cat(d,o,"\"");o=mpe_cat(d,o,stage);o=mpe_cat(d,o,"\"")}else{o=mpe_cat(d,o,"null")}
385 o=mpe_cat(d,o,",\"upstream_retry\":")
386 if (retry as i64)>0{o=mpe_cat(d,o,"\"");o=mpe_cat(d,o,retry);o=mpe_cat(d,o,"\"")}else{o=mpe_cat(d,o,"null")}
387 o=mpe_cat(d,o,",\"request_outcome\":\"unknown\",\"automatic_retry\":false,\"body_retention\":\"digest-only; original body not retained\",\"correlation\":\"outer JSON-RPC id preserved; optional upstream headers are declarations\"}")
388 *out_len=o;return d
389}
390
391func mpe_write_error(fd:i64,id_kind:i64,id_ptr:*u8,id_len:i64,escape:i64,code:i64,stage:*u8,cause:*u8,measured:i64,next:*u8)->i64 {
392 return mpe_write_error_detail(fd,id_kind,id_ptr,id_len,escape,code,stage,cause,measured,next,0 as *u8,0)
393}