nx_tool_http_backend_candidate_t314.nx source
↩ module page · 284 lines · 13736 B
1// nx_tool_http_backend.nx -- HTTP-BACKEND execution mode for the sovereign agent tools API. A tools/call whose
2// tool name is declared in tool_backends.conf is an ALREADY-RUNNING sovereign HTTP service (search on :18456,
3// doc, ...), reached IN-PROCESS over LOOPBACK -- ONE general, data-driven mechanism, NOT a per-tool shim binary.
4// This is the second execution kind alongside fork-exec (nx_tool_exec_allow): a tool is EITHER a vetted ELF OR a
5// registered HTTP backend. SECURITY: host/port/path are fixed in the operator-curated registry; the caller supplies
6// ONLY the url-encoded param value (argv[1]) -> no SSRF, no path/header injection. FAIL-CLOSED: no row -> THB_NOROUTE
7// (the caller then falls back to fork-exec, which also fail-closes). Self-contained loopback client (only nx_syscalls;
8// sockaddr/parse-ip/read-loop replicate the proven nx_tool_run / nx_http_proxy patterns) -> minimal blast radius in
9// the crown-jewel daemon. Row (TAB): <name>\t<method>\t<host>\t<port>\t<path>\t<param>
10// license_tier: ORIGINAL
11import "nx_syscalls.nx"
12import "nx_fetch_deadline_candidate_t314.nx"
13import "nx_http_response_complete_candidate_t314.nx"
14import "nx_deadline_lib.nx"
15import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
16const TB_MAGIC_262144: i64 = 262144
17const TB_MAGIC_8192: i64 = 8192
18const TB_MAGIC_16384: i64 = 16384
19const TB_MAGIC_1048576: i64 = 1048576
20
21const TB_CONF: *u8 = "knowledge/tool_backends.conf" as *u8 // server CWD-relative (like tool_allowlist.conf)
22const THB_OK: i64 = 1 // fetched -> body in out[0..outlen)
23const THB_NOROUTE: i64 = 0 // tool not an HTTP backend -> caller falls back to fork-exec
24const THB_ERR: i64 = 0 - 1 // declared but malformed/unreachable
25
26func thb_hex(v: i64) -> i64 { if v < 10 { return 48 + v } return 55 + v } // 0-9 then A-F
27func thb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
28func thb_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 + n }
29func thb_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 }
30
31// idx-th TAB-field of line buf[ls,le) -> out2[0]=off,out2[1]=len; 1 if present.
32func thb_field(buf: *u8, ls: i64, le: i64, idx: i64, out2: *i64) -> i64 {
33 var fi: i64 = 0; var s: i64 = ls; var i: i64 = ls
34 while i <= le {
35 var sep: i64 = 0
36 if i == le { sep = 1 } else { if buf[i] == (9 as u8) { sep = 1 } }
37 if sep == 1 { if fi == idx { out2[0] = s; out2[1] = i - s; return 1 } fi = fi + 1; s = i + 1 }
38 i = i + 1
39 }
40 return 0
41}
42func thb_atoi(buf: *u8, off: i64, len: i64) -> i64 {
43 var v: i64 = 0; var i: i64 = 0
44 while i < len { let c: i64 = buf[off + i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 }
45 return v
46}
47// url-encode src[0..slen) into dst; RFC3986 unreserved kept, else %XX. Returns length. Forecloses injection.
48func thb_urlenc(src: *u8, slen: i64, dst: *u8) -> i64 {
49 var o: i64 = 0; var i: i64 = 0
50 while i < slen {
51 let c: i64 = src[i] as i64
52 var keep: i64 = 0
53 if c >= 48 { if c <= 57 { keep = 1 } }
54 if c >= 65 { if c <= 90 { keep = 1 } }
55 if c >= 97 { if c <= 122 { keep = 1 } }
56 if c == 45 { keep = 1 }
57 if c == 46 { keep = 1 }
58 if c == 95 { keep = 1 }
59 if c == 126 { keep = 1 }
60 if keep == 1 { dst[o] = c as u8; o = o + 1 } else {
61 let hi: i64 = thb_hex((c >> 4) & 15)
62 let lo: i64 = thb_hex(c & 15)
63 dst[o] = 37 as u8; o = o + 1 // '%'
64 dst[o] = hi as u8; o = o + 1
65 dst[o] = lo as u8; o = o + 1
66 }
67 i = i + 1
68 }
69 return o
70}
71
72// ---- minimal self-contained loopback HTTP client (only nx_syscalls) ----
73func thb_sockaddr(addr: *u8, port: i64, a: i64, b: i64, c: i64, d: i64) -> i64 {
74 addr[0] = 2 as u8; addr[1] = 0 as u8 // AF_INET
75 addr[2] = ((port / 256) & 255) as u8 // port hi (network byte order)
76 addr[3] = (port & 255) as u8 // port lo
77 addr[4] = a as u8; addr[5] = b as u8; addr[6] = c as u8; addr[7] = d as u8
78 var i: i64 = 8; while i < 16 { addr[i] = 0 as u8; i = i + 1 }
79 return 0
80}
81func thb_parse_ip(s:*u8,n:i64,out4:*i64)->i64 {
82 var oct:i64=0;var value:i64=0;var digits:i64=0;var i:i64=0
83 while i<n {
84 let c:i64=s[i] as i64
85 if c==46 { if digits==0 || oct>=3 { return 0 } out4[oct]=value;oct=oct+1;value=0;digits=0 }
86 else { if c<48 || c>57 { return 0 } value=value*10+c-48;if value>255 { return 0 } digits=digits+1 }
87 i=i+1
88 }
89 if oct!=3 || digits==0 { return 0 } out4[3]=value;return 1
90}
91// connect host[0..hostlen]:port, send req[0..reqn], drain response into out (cap). Bytes read, or <=0 on failure.
92struct NxHttpBackendEvidence {
93 state:i64
94 stage:i64
95 http_status:i64
96 framing:i64
97 sent_bytes:i64
98 received_bytes:i64
99 body_bytes:i64
100 begin_ms:i64
101 end_ms:i64
102 deadline_ms:i64
103}
104const THB_STAGE_CONNECT:i64=1
105const THB_STAGE_SEND:i64=2
106const THB_STAGE_READ:i64=3
107const THB_STAGE_FRAME:i64=4
108const THB_STAGE_STATUS:i64=5
109const THB_TIMEOUT:i64=0-2
110const THB_CAPACITY:i64=0-3
111const THB_FRAMING:i64=0-4
112const THB_UPSTREAM:i64=0-5
113func thb_evidence_init(e:*NxHttpBackendEvidence,deadline:i64)->i64 {
114 e.state=THB_ERR;e.stage=0;e.http_status=0;e.framing=0;e.sent_bytes=0;e.received_bytes=0;e.body_bytes=0
115 e.begin_ms=sys_now_ms();e.end_ms=e.begin_ms;e.deadline_ms=deadline;return 0
116}
117func thb_fetch_result(host:*u8,hostlen:i64,port:i64,req:*u8,reqn:i64,out:*u8,cap:i64,deadline:i64,e:*NxHttpBackendEvidence)->i64 {
118 thb_evidence_init(e,deadline)
119 if cap<0 || reqn<0 { return THB_ERR }
120 if port<1 || port>65535 { return THB_ERR }
121 if deadline<=e.begin_ms { e.state=THB_TIMEOUT;return e.state }
122 var a:i64=0;var b:i64=0;var c:i64=0;var d:i64=0
123 // Existing parser uses a four-word output; keep it in an owned, fallible allocation.
124 let ip:*i64=sys_mmap_try(4*__size_of(i64)) as *i64
125 if (ip as i64)<=0 { return THB_ERR }
126 let parsed:i64=thb_parse_ip(host,hostlen,ip)
127 if parsed==1 { a=ip[0];b=ip[1];c=ip[2];d=ip[3] }
128 sys_munmap_direct(ip as *u8,4*__size_of(i64))
129 if parsed!=1 { return THB_ERR }
130 let fd:i64=sys_socket(2,1,0)
131 if fd<0 { return THB_ERR }
132 let addr:*u8=sys_mmap_try(16)
133 if (addr as i64)<=0 { sys_close(fd);return THB_ERR }
134 thb_sockaddr(addr,port,a,b,c,d)
135 e.stage=THB_STAGE_CONNECT
136 let left:i64=dl_wait_ms(deadline,sys_now_ms(),NX_CONN_DEFAULT_MS)
137 var connected:i64=0-1
138 if left>0 { connected=nx_connect_bounded(fd,addr,16,left) }
139 sys_munmap_direct(addr,16)
140 if connected<0 { sys_close(fd);e.end_ms=sys_now_ms();if e.end_ms>=deadline { e.state=THB_TIMEOUT } return e.state }
141 let flags:i64=nx_fcntl(fd,NX_F_GETFL,0)
142 if flags<0 { sys_close(fd);return THB_ERR }
143 if nx_fcntl(fd,NX_F_SETFL,flags|NX_O_NONBLOCK)<0 { sys_close(fd);return THB_ERR }
144 e.stage=THB_STAGE_SEND
145 var sent:i64=0
146 let wr:i64=fb_send_until(fd,req,reqn,deadline,&sent)
147 e.sent_bytes=sent
148 if wr!=FB_OK { sys_close(fd);e.end_ms=sys_now_ms();if wr==FB_TIMEOUT { e.state=THB_TIMEOUT } return e.state }
149 e.stage=THB_STAGE_READ
150 var got:i64=0
151 let rd:i64=fb_receive_until(fd,out,cap,deadline,&got)
152 sys_close(fd);e.received_bytes=got;e.end_ms=sys_now_ms()
153 if rd==FB_TIMEOUT { e.state=THB_TIMEOUT;return e.state }
154 if rd==FB_OVERFLOW { e.state=THB_CAPACITY;return e.state }
155 if rd!=FB_OK { return e.state }
156 e.state=THB_OK;return e.state
157}
158func thb_fetch(host:*u8,hostlen:i64,port:i64,req:*u8,reqn:i64,out:*u8,cap:i64)->i64 {
159 var e:NxHttpBackendEvidence
160 let now:i64=sys_now_ms()
161 let budget:i64=dl_wait_ms(dl_scan_request(req,reqn),now,FB_DEFAULT_TIMEOUT_MS)
162 let r:i64=thb_fetch_result(host,hostlen,port,req,reqn,out,cap,now+budget,&e)
163 if r!=THB_OK { return THB_ERR }
164 return e.received_bytes
165}
166func thb_body_result(raw:*u8,n:i64,out:*u8,cap:i64,e:*NxHttpBackendEvidence)->i64 {
167 e.stage=THB_STAGE_FRAME
168 var code:i64=0;var bl:i64=0
169 let r:i64=nx_http_response_complete(raw,n,out,cap,&code,&bl)
170 e.framing=r;e.http_status=code;e.body_bytes=0
171 if r!=NX_HTTP_RESP_OK { e.state=THB_FRAMING;return e.state }
172 e.stage=THB_STAGE_STATUS
173 if code<200 || code>=300 { e.state=THB_UPSTREAM;return e.state }
174 e.body_bytes=bl;e.state=THB_OK;return e.state
175}
176
177// resolve `name` in `conf`; if it is an HTTP backend, fetch it with argv[1] as the url-encoded param value and
178// write the response BODY (after CRLFCRLF) into out. argv[1..] are the parsed args (argv[0] reserved), argc = count.
179// bounded config read: a config file never needs sys_read_file's 4GB reservation (rule 21). Reads up to 256KB into
180// a small mmap so calling this per-request doesn't accumulate 4GB reservations that starve fork() on a small VM.
181func thb_read_conf(path: *u8, out_len: *i64) -> *u8 {
182 out_len[0] = 0
183 let fd: i64 = sys_openat_rd(path)
184 if fd < 0 { return 0 as *u8 }
185 let cap: i64 = TB_MAGIC_262144
186 let buf: *u8 = sys_mmap(cap + 16)
187 var total: i64 = 0
188 var go: i64 = 1
189 while go == 1 {
190 if total >= cap { go = 0 } else {
191 let r: i64 = sys_read(fd, ((buf as i64) + total) as *u8, cap - total)
192 if r <= 0 { go = 0 } else { total = total + r }
193 }
194 }
195 sys_close(fd)
196 let term: *u8 = ((buf as i64) + total) as *u8
197 term[0] = 0 as u8
198 out_len[0] = total
199 return buf
200}
201
202func thb_dispatch_from_result(conf: *u8, name: *u8, nlen: i64, argv: *i64, argc: i64, out: *u8, cap: i64, outlen: *i64, deadline:i64, wire_cap:i64, ev:*NxHttpBackendEvidence) -> i64 {
203 outlen[0]=0;thb_evidence_init(ev,deadline)
204 if wire_cap<=0 || cap<0 { return THB_ERR }
205 if deadline<=sys_now_ms() { ev.state=THB_TIMEOUT;return THB_TIMEOUT }
206 let szp: *i64 = sys_mmap(16) as *i64
207 let buf: *u8 = thb_read_conf(conf, szp)
208 if (buf as i64) == 0 { return THB_NOROUTE }
209 let n: i64 = szp[0]
210 var f_host: i64 = 0; var l_host: i64 = 0; var v_port: i64 = 0
211 var f_path: i64 = 0; var l_path: i64 = 0; var f_param: i64 = 0; var l_param: i64 = 0
212 var found: i64 = 0
213 var ls: i64 = 0; var i: i64 = 0
214 while i <= n {
215 var eol: i64 = 0
216 if i == n { eol = 1 } else { if buf[i] == (10 as u8) { eol = 1 } }
217 if eol == 1 {
218 if found == 0 {
219 let le: i64 = i
220 if le > ls { if buf[ls] != (35 as u8) {
221 let f0: *i64 = sys_mmap(16) as *i64
222 if thb_field(buf, ls, le, 0, f0) == 1 {
223 if f0[1] == nlen {
224 var m: i64 = 1; var c: i64 = 0
225 while c < nlen { if buf[f0[0] + c] != name[c] { m = 0 } c = c + 1 }
226 if m == 1 {
227 let ff: *i64 = sys_mmap(16) as *i64
228 if thb_field(buf, ls, le, 2, ff) == 1 { f_host = ff[0]; l_host = ff[1] }
229 if thb_field(buf, ls, le, 3, ff) == 1 { v_port = thb_atoi(buf, ff[0], ff[1]) }
230 if thb_field(buf, ls, le, 4, ff) == 1 { f_path = ff[0]; l_path = ff[1] }
231 if thb_field(buf, ls, le, 5, ff) == 1 { f_param = ff[0]; l_param = ff[1] }
232 found = 1
233 }
234 }
235 }
236 } }
237 }
238 ls = i + 1
239 }
240 i = i + 1
241 }
242 if found == 0 { return THB_NOROUTE }
243 if l_host <= 0 { return THB_ERR }
244 if v_port <= 0 { return THB_ERR }
245 outlen[0]=0
246 var arg:*u8="" as *u8
247 if argc>=1 { arg=argv[1] as *u8 }
248 let an:i64=thb_slen(arg)
249 if an>(9223372036854775807-1)/3 { return THB_ERR }
250 let enc_cap:i64=an*3+1
251 let enc:*u8=sys_mmap_try(enc_cap)
252 if (enc as i64)<=0 { return THB_ERR }
253 let enclen:i64=thb_urlenc(arg,an,enc)
254 let suffix:*u8=" HTTP/1.0\r\nHost: nishifamily.com\r\nConnection: close\r\n\r\n" as *u8
255 let fixed:i64=thb_slen(suffix)+6+l_path+l_param
256 if enc_cap>9223372036854775807-fixed { sys_munmap_direct(enc,enc_cap);return THB_ERR }
257 let reqcap:i64=enc_cap+fixed
258 let req:*u8=sys_mmap_try(reqcap)
259 if (req as i64)<=0 { sys_munmap_direct(enc,enc_cap);return THB_ERR }
260 var o:i64=thb_cat(req,0,"GET " as *u8)
261 o=thb_catb(req,o,buf+f_path,l_path);o=thb_cat(req,o,"?" as *u8)
262 o=thb_catb(req,o,buf+f_param,l_param);o=thb_cat(req,o,"=" as *u8)
263 o=thb_catb(req,o,enc,enclen);o=thb_cat(req,o,suffix)
264 sys_munmap_direct(enc,enc_cap)
265 let resp:*u8=sys_mmap_try(wire_cap)
266 if (resp as i64)<=0 { sys_munmap_direct(req,reqcap);return THB_ERR }
267 let fr:i64=thb_fetch_result(buf+f_host,l_host,v_port,req,o,resp,wire_cap,deadline,ev)
268 sys_munmap_direct(req,reqcap)
269 var result:i64=fr
270 if fr==THB_OK { result=thb_body_result(resp,ev.received_bytes,out,cap,ev);if result==THB_OK { outlen[0]=ev.body_bytes } }
271 sys_munmap_direct(resp,wire_cap)
272 return result
273}
274
275func thb_dispatch(name: *u8, nlen: i64, argv: *i64, argc: i64, out: *u8, cap: i64, outlen: *i64) -> i64 {
276 return thb_dispatch_from(TB_CONF, name, nlen, argv, argc, out, cap, outlen)
277}
278
279func thb_dispatch_from(conf:*u8,name:*u8,nlen:i64,argv:*i64,argc:i64,out:*u8,cap:i64,outlen:*i64)->i64 {
280 var ev:NxHttpBackendEvidence
281 let rc:i64=thb_dispatch_from_result(conf,name,nlen,argv,argc,out,cap,outlen,sys_now_ms()+FB_DEFAULT_TIMEOUT_MS,TB_MAGIC_1048576,&ev)
282 if rc==THB_OK || rc==THB_NOROUTE { return rc }
283 return THB_ERR
284}