nx_tool_http_backend.nx source
↩ module page · 204 lines · 10233 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_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
13const TB_MAGIC_262144: i64 = 262144
14const TB_MAGIC_8192: i64 = 8192
15const TB_MAGIC_16384: i64 = 16384
16const TB_MAGIC_1048576: i64 = 1048576
17
18const TB_CONF: *u8 = "knowledge/tool_backends.conf" as *u8 // server CWD-relative (like tool_allowlist.conf)
19const THB_OK: i64 = 1 // fetched -> body in out[0..outlen)
20const THB_NOROUTE: i64 = 0 // tool not an HTTP backend -> caller falls back to fork-exec
21const THB_ERR: i64 = 0 - 1 // declared but malformed/unreachable
22
23func thb_hex(v: i64) -> i64 { if v < 10 { return 48 + v } return 55 + v } // 0-9 then A-F
24func thb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
25func 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 }
26func 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 }
27
28// idx-th TAB-field of line buf[ls,le) -> out2[0]=off,out2[1]=len; 1 if present.
29func thb_field(buf: *u8, ls: i64, le: i64, idx: i64, out2: *i64) -> i64 {
30 var fi: i64 = 0; var s: i64 = ls; var i: i64 = ls
31 while i <= le {
32 var sep: i64 = 0
33 if i == le { sep = 1 } else { if buf[i] == (9 as u8) { sep = 1 } }
34 if sep == 1 { if fi == idx { out2[0] = s; out2[1] = i - s; return 1 } fi = fi + 1; s = i + 1 }
35 i = i + 1
36 }
37 return 0
38}
39func thb_atoi(buf: *u8, off: i64, len: i64) -> i64 {
40 var v: i64 = 0; var i: i64 = 0
41 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 }
42 return v
43}
44// url-encode src[0..slen) into dst; RFC3986 unreserved kept, else %XX. Returns length. Forecloses injection.
45func thb_urlenc(src: *u8, slen: i64, dst: *u8) -> i64 {
46 var o: i64 = 0; var i: i64 = 0
47 while i < slen {
48 let c: i64 = src[i] as i64
49 var keep: i64 = 0
50 if c >= 48 { if c <= 57 { keep = 1 } }
51 if c >= 65 { if c <= 90 { keep = 1 } }
52 if c >= 97 { if c <= 122 { keep = 1 } }
53 if c == 45 { keep = 1 }
54 if c == 46 { keep = 1 }
55 if c == 95 { keep = 1 }
56 if c == 126 { keep = 1 }
57 if keep == 1 { dst[o] = c as u8; o = o + 1 } else {
58 let hi: i64 = thb_hex((c >> 4) & 15)
59 let lo: i64 = thb_hex(c & 15)
60 dst[o] = 37 as u8; o = o + 1 // '%'
61 dst[o] = hi as u8; o = o + 1
62 dst[o] = lo as u8; o = o + 1
63 }
64 i = i + 1
65 }
66 return o
67}
68
69// ---- minimal self-contained loopback HTTP client (only nx_syscalls) ----
70func thb_sockaddr(addr: *u8, port: i64, a: i64, b: i64, c: i64, d: i64) -> i64 {
71 addr[0] = 2 as u8; addr[1] = 0 as u8 // AF_INET
72 addr[2] = ((port / 256) & 255) as u8 // port hi (network byte order)
73 addr[3] = (port & 255) as u8 // port lo
74 addr[4] = a as u8; addr[5] = b as u8; addr[6] = c as u8; addr[7] = d as u8
75 var i: i64 = 8; while i < 16 { addr[i] = 0 as u8; i = i + 1 }
76 return 0
77}
78func thb_parse_ip(s: *u8, slen: i64, out4: *i64) -> i64 {
79 var oct: i64 = 0; var val: i64 = 0; var i: i64 = 0
80 while i < slen {
81 let ch: i64 = s[i] as i64
82 if ch == 46 { if oct < 4 { out4[oct] = val } oct = oct + 1; val = 0 } else { if ch >= 48 { if ch <= 57 { val = val * 10 + (ch - 48) } } }
83 i = i + 1
84 }
85 if oct < 4 { out4[oct] = val; oct = oct + 1 }
86 if oct == 4 { return 1 }
87 return 0
88}
89// connect host[0..hostlen]:port, send req[0..reqn], drain response into out (cap). Bytes read, or <=0 on failure.
90func thb_fetch(host: *u8, hostlen: i64, port: i64, req: *u8, reqn: i64, out: *u8, cap: i64) -> i64 {
91 let ip: *i64 = sys_mmap(64) as *i64
92 if thb_parse_ip(host, hostlen, ip) != 1 { return 0 - 1 }
93 let fd: i64 = sys_socket(2, 1, 0) // AF_INET, SOCK_STREAM
94 if fd < 0 { return 0 - 1 }
95 let addr: *u8 = sys_mmap(16)
96 thb_sockaddr(addr, port, ip[0], ip[1], ip[2], ip[3])
97 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 }
98 var off: i64 = 0
99 while off < reqn { let w: i64 = sys_write(fd, ((req as i64) + off) as *u8, reqn - off); if w <= 0 { off = reqn } else { off = off + w } }
100 var total: i64 = 0; var go: i64 = 1
101 while go == 1 {
102 if total >= cap { go = 0 } else {
103 let r: i64 = sys_read(fd, ((out as i64) + total) as *u8, cap - total)
104 if r <= 0 { go = 0 } else { total = total + r }
105 }
106 }
107 sys_close(fd)
108 return total
109}
110
111// resolve `name` in `conf`; if it is an HTTP backend, fetch it with argv[1] as the url-encoded param value and
112// write the response BODY (after CRLFCRLF) into out. argv[1..] are the parsed args (argv[0] reserved), argc = count.
113// bounded config read: a config file never needs sys_read_file's 4GB reservation (rule 21). Reads up to 256KB into
114// a small mmap so calling this per-request doesn't accumulate 4GB reservations that starve fork() on a small VM.
115func thb_read_conf(path: *u8, out_len: *i64) -> *u8 {
116 out_len[0] = 0
117 let fd: i64 = sys_openat_rd(path)
118 if fd < 0 { return 0 as *u8 }
119 let cap: i64 = TB_MAGIC_262144
120 let buf: *u8 = sys_mmap(cap + 16)
121 var total: i64 = 0
122 var go: i64 = 1
123 while go == 1 {
124 if total >= cap { go = 0 } else {
125 let r: i64 = sys_read(fd, ((buf as i64) + total) as *u8, cap - total)
126 if r <= 0 { go = 0 } else { total = total + r }
127 }
128 }
129 sys_close(fd)
130 let term: *u8 = ((buf as i64) + total) as *u8
131 term[0] = 0 as u8
132 out_len[0] = total
133 return buf
134}
135
136func thb_dispatch_from(conf: *u8, name: *u8, nlen: i64, argv: *i64, argc: i64, out: *u8, cap: i64, outlen: *i64) -> i64 {
137 let szp: *i64 = sys_mmap(16) as *i64
138 let buf: *u8 = thb_read_conf(conf, szp)
139 if (buf as i64) == 0 { return THB_NOROUTE }
140 let n: i64 = szp[0]
141 var f_host: i64 = 0; var l_host: i64 = 0; var v_port: i64 = 0
142 var f_path: i64 = 0; var l_path: i64 = 0; var f_param: i64 = 0; var l_param: i64 = 0
143 var found: i64 = 0
144 var ls: i64 = 0; var i: i64 = 0
145 while i <= n {
146 var eol: i64 = 0
147 if i == n { eol = 1 } else { if buf[i] == (10 as u8) { eol = 1 } }
148 if eol == 1 {
149 if found == 0 {
150 let le: i64 = i
151 if le > ls { if buf[ls] != (35 as u8) {
152 let f0: *i64 = sys_mmap(16) as *i64
153 if thb_field(buf, ls, le, 0, f0) == 1 {
154 if f0[1] == nlen {
155 var m: i64 = 1; var c: i64 = 0
156 while c < nlen { if buf[f0[0] + c] != name[c] { m = 0 } c = c + 1 }
157 if m == 1 {
158 let ff: *i64 = sys_mmap(16) as *i64
159 if thb_field(buf, ls, le, 2, ff) == 1 { f_host = ff[0]; l_host = ff[1] }
160 if thb_field(buf, ls, le, 3, ff) == 1 { v_port = thb_atoi(buf, ff[0], ff[1]) }
161 if thb_field(buf, ls, le, 4, ff) == 1 { f_path = ff[0]; l_path = ff[1] }
162 if thb_field(buf, ls, le, 5, ff) == 1 { f_param = ff[0]; l_param = ff[1] }
163 found = 1
164 }
165 }
166 }
167 } }
168 }
169 ls = i + 1
170 }
171 i = i + 1
172 }
173 if found == 0 { return THB_NOROUTE }
174 if l_host <= 0 { return THB_ERR }
175 if v_port <= 0 { return THB_ERR }
176 let enc: *u8 = sys_mmap(TB_MAGIC_8192); var enclen: i64 = 0
177 if argc >= 1 { let a: *u8 = argv[1] as *u8; enclen = thb_urlenc(a, thb_slen(a), enc) }
178 let req: *u8 = sys_mmap(TB_MAGIC_16384)
179 var o: i64 = thb_cat(req, 0, "GET " as *u8)
180 o = thb_catb(req, o, ((buf as i64) + f_path) as *u8, l_path)
181 o = thb_catb(req, o, "?" as *u8, 1)
182 o = thb_catb(req, o, ((buf as i64) + f_param) as *u8, l_param)
183 o = thb_catb(req, o, "=" as *u8, 1)
184 o = thb_catb(req, o, enc, enclen)
185 o = thb_cat(req, o, " HTTP/1.0\r\nHost: nishifamily.com\r\nConnection: close\r\n\r\n" as *u8)
186 let rcap: i64 = TB_MAGIC_1048576
187 let resp: *u8 = sys_mmap(rcap)
188 let hostp: *u8 = ((buf as i64) + f_host) as *u8
189 let rn: i64 = thb_fetch(hostp, l_host, v_port, req, o, resp, rcap)
190 if rn <= 0 { return THB_ERR }
191 var bo: i64 = 0 - 1; var j: i64 = 0
192 while j + 3 < rn { if resp[j] == (13 as u8) { if resp[j + 1] == (10 as u8) { if resp[j + 2] == (13 as u8) { if resp[j + 3] == (10 as u8) { bo = j + 4; j = rn } } } } j = j + 1 }
193 var start: i64 = 0
194 if bo >= 0 { start = bo }
195 var blen: i64 = rn - start
196 if blen > cap { blen = cap }
197 var k: i64 = 0
198 while k < blen { out[k] = resp[start + k]; k = k + 1 }
199 outlen[0] = blen
200 return THB_OK
201}
202func thb_dispatch(name: *u8, nlen: i64, argv: *i64, argc: i64, out: *u8, cap: i64, outlen: *i64) -> i64 {
203 return thb_dispatch_from(TB_CONF, name, nlen, argv, argc, out, cap, outlen)
204}