code wiki / (root) / nx_api_edge.nx

nx_api_edge.nx source

↩ module page · 299 lines · 16999 B

1// nx_api_edge.nx -- the REAL s-class hardening edge for the live agent-facing API (nx_tools_api on :18096, 2// public as <domain>/api/tools + /mcp). WIRES the API-hardening capabilities that existed only as unwired 3// island primitives + a hardcoded-secret DRAFT gateway (nx_api_gateway) into ONE real HTTP middleware that 4// wraps ta_handle. NON-BREAKING by construction: it does NOT touch the cap-token execution auth (capt_verify 5// still guards tools/call) nor block the open read-only catalog -- it ADDS, per request: 6// * CORS deny-by-default (OPTIONS preflight -> 204; Access-Control-* only for allowlisted origins) 7// * security headers on every response (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS) 8// * a REAL token-bucket rate limit (monotonic-clock refill; 429 problem+json over budget) = DoS guard 9// * RFC 9457 application/problem+json for edge errors (429, 413) 10// This is the composition the apistack gates proved in isolation, now on the LIVE request path. 11// license_tier: ORIGINAL genealogy_id: international-research-sources/ietf/rfc_6454_cors + rfc_9457_problem 12import "nx_syscalls.nx" 13import "nx_tools_api.nx" // ta_handle + ta_reqline + ta_streq_n + ta_cat + ta_catn + ta_slen + ta_emit_lit 14const EDGE_MAGIC_1000000000: i64 = 1000000000 15const EDGE_MAGIC_1000000: i64 = 1000000 16 17const EDGE_RESP_CAP: i64 = 1048576 18// THE ONE REQUEST CEILING (2026-08-23). This was 65535, a hand copy of the serve loop's single-read WINDOW 19// ("matches the serve loop's read cap") -- two numbers for one envelope, and a read window mistaken for a 20// limit: every tools/call above 64 KiB answered 413 "the 64KiB edge limit", and the laptop wire rendered that 21// 413 as a bare {} so a whole-file source write read as "dropped". The serve loop now reads the WHOLE body by 22// Content-Length; the only ceiling left is this one, DERIVED: a request may be as large as the response 23// reserve this edge already declares (EDGE_RESP_CAP), memory the edge has already committed per request on 24// the way out. Above it the answer is 413 naming BOTH numbers; a body the peer cut short is 400 naming 25// got/need; a truncated request is never handed on as if it were the request. 26const EDGE_REQ_MAX: i64 = EDGE_RESP_CAP 27 28// Token bucket (per-process, monotonic-clock refill). Generous: 300 burst, 30/s sustained (1800/min) -- well 29// above any legit companion/UI rate, but caps a flood. Fixed-point milli-tokens to avoid fractional loss. 30const TB_CAP_MILLI: i64 = 300000 // 300 tokens * 1000 31const TB_RATE_PER_S: i64 = 30 // sustained refill tokens/second 32static tb_tokens_milli: i64 33static tb_last_ns: i64 34 35func edge_now_ns() -> i64 { 36 let ts: *i64 = sys_mmap(16) as *i64 37 __syscall(SYS_CLOCK_GETTIME, 1, ts as i64, 0, 0, 0, 0) // CLOCK_MONOTONIC 38 return ts[0] * EDGE_MAGIC_1000000000 + ts[1] 39} 40 41// Returns 1 if a token was available (allow), 0 if the bucket is empty (rate-limited). 42func edge_tb_allow() -> i64 { 43 let now: i64 = edge_now_ns() 44 if tb_last_ns == 0 { tb_last_ns = now; tb_tokens_milli = TB_CAP_MILLI } 45 let dt_ns: i64 = now - tb_last_ns 46 if dt_ns > 0 { 47 let refill_milli: i64 = (dt_ns * TB_RATE_PER_S) / EDGE_MAGIC_1000000 // (dt/1e9)*rate*1000 48 tb_tokens_milli = tb_tokens_milli + refill_milli 49 if tb_tokens_milli > TB_CAP_MILLI { tb_tokens_milli = TB_CAP_MILLI } 50 tb_last_ns = now 51 } 52 if tb_tokens_milli >= 1000 { tb_tokens_milli = tb_tokens_milli - 1000; return 1 } 53 return 0 54} 55 56func edge_cpy(dst: *u8, doff: i64, src: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { dst[doff + i] = src[i]; i = i + 1 } return doff + n } 57 58// Find a header value: scan for "\n" + name + ":" (headers are line-delimited); skip optional spaces; value runs 59// to the next \r or \n. Sets vo[0]=absolute offset into req, vo[1]=len. Returns 1 if found, 0 otherwise. 60// Header names are ASCII case-insensitive; values retain their original bytes. 61// Return occurrence count so security-sensitive callers can reject duplicates. 62func edge_header_lower(c: i64) -> i64 { 63 if c >= 65 { if c <= 90 { return c + 32 } } 64 return c 65} 66func edge_find_header(req: *u8, req_n: i64, name: *u8, name_len: i64, vo: *i64) -> i64 { 67 vo[0]=0; vo[1]=0 68 var start: i64 = 0 69 while start < req_n { if req[start] == (10 as u8) { start=start+1; break }; start=start+1 } 70 var count: i64 = 0 71 while start < req_n { 72 var end: i64 = start 73 while end < req_n { if req[end] == (10 as u8) { break }; end=end+1 } 74 var content_end: i64 = end 75 if content_end > start { if req[content_end-1] == (13 as u8) { content_end=content_end-1 } } 76 if content_end == start { return count } 77 if start+name_len < content_end { 78 var same: i64 = 1 79 var m: i64 = 0 80 while m < name_len { 81 if edge_header_lower(req[start+m] as i64) != edge_header_lower(name[m] as i64) { same=0 } 82 m=m+1 83 } 84 if same == 1 { if req[start+name_len] == (58 as u8) { 85 var p: i64 = start+name_len+1 86 while p < content_end { if req[p] != (32 as u8) { if req[p] != (9 as u8) { break } }; p=p+1 } 87 var v_end: i64 = content_end 88 while v_end > p { if req[v_end-1] != (32 as u8) { if req[v_end-1] != (9 as u8) { break } }; v_end=v_end-1 } 89 if count == 0 { vo[0]=p; vo[1]=v_end-p } 90 count=count+1 91 } } 92 } 93 start=end+1 94 } 95 return count 96} 97 98// Is the request Origin allowlisted? Deny-by-default. Ecosystem origins only. Sets vo to the origin bytes. 99func edge_origin_allowed(req: *u8, req_n: i64, vo: *i64) -> i64 { 100 if edge_find_header(req, req_n, "Origin" as *u8, 6, vo) != 1 { return 0 } 101 let o: *u8 = ((req as i64) + vo[0]) as *u8 102 let n: i64 = vo[1] 103 if ta_streq_n(o, n, "https://nishifamily.com" as *u8) == 1 { return 1 } 104 if ta_streq_n(o, n, "https://andelinwest.com" as *u8) == 1 { return 1 } 105 if ta_streq_n(o, n, "https://www.nishifamily.com" as *u8) == 1 { return 1 } 106 return 0 107} 108 109// Append the 4 always-on security headers (each ends with \r\n). 110func edge_sec_headers(out: *u8, o: i64) -> i64 { 111 o = ta_cat(out, o, "X-Content-Type-Options: nosniff\r\n" as *u8) 112 o = ta_cat(out, o, "X-Frame-Options: DENY\r\n" as *u8) 113 o = ta_cat(out, o, "Referrer-Policy: no-referrer\r\n" as *u8) 114 o = ta_cat(out, o, "Strict-Transport-Security: max-age=63072000; includeSubDomains\r\n" as *u8) 115 return o 116} 117 118// Inject security + (if allowed) CORS headers into an already-built response, right after its status line. 119func edge_inject(resp: *u8, resp_len: i64, out: *u8, req: *u8, req_n: i64) -> i64 { 120 // locate the end of the status line (first CRLF) 121 var p: i64 = 0 122 var found: i64 = 0 123 while p + 1 < resp_len { if resp[p] == (13 as u8) { if resp[p+1] == (10 as u8) { found = 1; break } } p = p + 1 } 124 if found == 0 { return edge_cpy(out, 0, resp, resp_len) } // malformed -> pass through untouched 125 var o: i64 = edge_cpy(out, 0, resp, p + 2) // status line incl CRLF 126 o = edge_sec_headers(out, o) 127 let vo: *i64 = sys_mmap(16) as *i64 128 if edge_origin_allowed(req, req_n, vo) == 1 { 129 o = ta_cat(out, o, "Access-Control-Allow-Origin: " as *u8) 130 o = edge_cpy(out, o, ((req as i64) + vo[0]) as *u8, vo[1]) 131 o = ta_cat(out, o, "\r\nAccess-Control-Allow-Credentials: true\r\nVary: Origin\r\n" as *u8) 132 } 133 // rest of the original response (headers tail + body) 134 o = edge_cpy(out, o, ((resp as i64) + p + 2) as *u8, resp_len - (p + 2)) 135 return o 136} 137 138// problem+json (RFC 9457) response builder. 139func edge_problem(out: *u8, status_line: *u8, ptype: *u8, title: *u8, status: i64, detail: *u8) -> i64 { 140 let jb: *u8 = sys_mmap(512) 141 var b: i64 = ta_cat(jb, 0, "{\"type\":\"" as *u8); b = ta_cat(jb, b, ptype) 142 b = ta_cat(jb, b, "\",\"title\":\"" as *u8); b = ta_cat(jb, b, title) 143 b = ta_cat(jb, b, "\",\"status\":" as *u8); b = ta_catn(jb, b, status) 144 b = ta_cat(jb, b, ",\"detail\":\"" as *u8); b = ta_cat(jb, b, detail); b = ta_cat(jb, b, "\"}" as *u8) 145 // build response with problem+json content type 146 var o: i64 = ta_cat(out, 0, "HTTP/1.1 " as *u8); o = ta_cat(out, o, status_line) 147 o = ta_cat(out, o, "\r\nContent-Type: application/problem+json\r\n" as *u8) 148 o = edge_sec_headers(out, o) 149 o = ta_cat(out, o, "Content-Length: " as *u8); o = ta_catn(out, o, b) 150 o = ta_cat(out, o, "\r\nConnection: close\r\n\r\n" as *u8) 151 o = edge_cpy(out, o, jb, b) 152 return o 153} 154 155// 413 naming both numbers: what the request declared and the ceiling it exceeded. 156func edge_problem_too_large(out: *u8, declared: i64, limit: i64) -> i64 { 157 let d: *u8 = sys_mmap(256) 158 var o: i64 = ta_cat(d, 0, "request of " as *u8); o = ta_catn(d, o, declared) 159 o = ta_cat(d, o, " bytes exceeds the edge limit of " as *u8); o = ta_catn(d, o, limit) 160 o = ta_cat(d, o, " bytes (EDGE_REQ_MAX, derived from the response reserve); body not read" as *u8) 161 return edge_problem(out, "413 Payload Too Large" as *u8, "about:blank" as *u8, "Payload Too Large" as *u8, 413, d) 162} 163// 400 naming got/need: the peer stopped sending before the bytes its own headers declared. 164func edge_problem_short_body(out: *u8, got: i64, need: i64) -> i64 { 165 let d: *u8 = sys_mmap(256) 166 var o: i64 = ta_cat(d, 0, "truncated request: received " as *u8); o = ta_catn(d, o, got) 167 o = ta_cat(d, o, " of the " as *u8); o = ta_catn(d, o, need) 168 o = ta_cat(d, o, " bytes declared (headers + Content-Length); not dispatched" as *u8) 169 return edge_problem(out, "400 Bad Request" as *u8, "about:blank" as *u8, "Bad Request" as *u8, 400, d) 170} 171 172// THE edge: hardened front for one request. Returns response length in `out` (cap EDGE_RESP_CAP). 173func nx_api_edge_handle(req: *u8, req_n: i64, out: *u8) -> i64 { 174 // 413: a request over the ONE ceiling -> reject cleanly, naming the numbers 175 if req_n > EDGE_REQ_MAX { return edge_problem_too_large(out, req_n, EDGE_REQ_MAX) } 176 177 let mo: *i64 = sys_mmap(16) as *i64 178 let po: *i64 = sys_mmap(16) as *i64 179 if ta_reqline(req, req_n, mo, po) == 0 { 180 return edge_problem(out, "400 Bad Request" as *u8, "about:blank" as *u8, "Bad Request" as *u8, 400, "malformed request line" as *u8) 181 } 182 let mp: *u8 = ((req as i64) + mo[0]) as *u8 183 let ml: i64 = mo[1] 184 185 // MCP origin enforcement happens before preflight or any tool dispatch. 186 let path: *u8 = ((req as i64)+po[0]) as *u8 187 var path_len: i64 = 0 188 while path_len < po[1] { if path[path_len] == (63 as u8) { break }; path_len=path_len+1 } 189 if ta_streq_n(path,path_len,"/mcp") == 1 { 190 let origin: *i64 = sys_mmap(16) as *i64 191 if edge_find_header(req,req_n,"Origin",6,origin) > 0 { 192 if edge_origin_allowed(req,req_n,origin) != 1 { 193 return edge_problem(out,"403 Forbidden","about:blank","Forbidden",403,"MCP origin is not uniquely allowlisted") 194 } 195 } 196 } 197 198 if ta_streq_n(path,path_len,"/mcp") == 1 { 199 let version: *i64 = sys_mmap(16) as *i64 200 let occurrences: i64 = edge_find_header(req,req_n,"MCP-Protocol-Version",20,version) 201 if occurrences > 1 { return edge_problem(out,"400 Bad Request","about:blank","Bad Request",400,"duplicate MCP protocol version") } 202 if occurrences == 1 { 203 let vp: *u8 = ((req as i64)+version[0]) as *u8 204 var supported: i64 = 0 205 if ta_streq_n(vp,version[1],"2024-11-05") == 1 { supported=1 } 206 if ta_streq_n(vp,version[1],"2025-03-26") == 1 { supported=1 } 207 if ta_streq_n(vp,version[1],"2025-06-18") == 1 { supported=1 } 208 if supported == 0 { return edge_problem(out,"400 Bad Request","about:blank","Bad Request",400,"unsupported MCP protocol version") } 209 } 210 } 211 212 // Other API routes retain their shared preflight behavior. 213 if ta_streq_n(mp, ml, "OPTIONS" as *u8) == 1 { 214 var o: i64 = ta_cat(out, 0, "HTTP/1.1 204 No Content\r\n" as *u8) 215 o = edge_sec_headers(out, o) 216 let vo: *i64 = sys_mmap(16) as *i64 217 if edge_origin_allowed(req, req_n, vo) == 1 { 218 o = ta_cat(out, o, "Access-Control-Allow-Origin: " as *u8) 219 o = edge_cpy(out, o, ((req as i64) + vo[0]) as *u8, vo[1]) 220 o = ta_cat(out, o, "\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type, X-Nishi-Cap, Authorization\r\nAccess-Control-Max-Age: 600\r\nVary: Origin\r\n" as *u8) 221 } 222 o = ta_cat(out, o, "Content-Length: 0\r\nConnection: close\r\n\r\n" as *u8) 223 return o 224 } 225 226 // Rate limit (real token bucket). Over budget -> 429 problem+json. 227 if edge_tb_allow() == 0 { 228 return edge_problem(out, "429 Too Many Requests" as *u8, "about:blank" as *u8, "Too Many Requests" as *u8, 429, "rate limit exceeded; retry shortly" as *u8) 229 } 230 231 // Dispatch to the real handler, then inject security + CORS headers into its response. 232 let inner: *u8 = sys_mmap(EDGE_RESP_CAP) 233 let rn: i64 = ta_handle(req, req_n, inner) 234 return edge_inject(inner, rn, out, req, req_n) 235} 236 237// ---- gate: real HTTP requests through the edge ---- 238func edge_w(s: *u8) -> i64 { sys_write(1, s, ta_slen(s)); return 0 } 239func edge_row(name: *u8, ok: i64) -> i64 { if ok == 1 { edge_w(" PASS " as *u8) } else { edge_w(" FAIL " as *u8) } edge_w(name); edge_w("\n" as *u8); return ok } 240// substring search: does haystack[0..hn] contain needle? 241func edge_has(hay: *u8, hn: i64, needle: *u8) -> i64 { 242 let nn: i64 = ta_slen(needle) 243 if nn == 0 { return 1 } 244 var i: i64 = 0 245 while i + nn <= hn { 246 var m: i64 = 0; var ok: i64 = 1 247 while m < nn { if hay[i + m] != needle[m] { ok = 0; m = nn } else { m = m + 1 } } 248 if ok == 1 { return 1 } 249 i = i + 1 250 } 251 return 0 252} 253 254func main() -> i64 { 255 edge_w("nx_api_edge gate (CORS deny-by-default + security headers + token-bucket rate-limit + problem+json)\n" as *u8) 256 let out: *u8 = sys_mmap(EDGE_RESP_CAP) 257 var pass: i64 = 0 258 259 // T1: GET /api/tools with an allowlisted Origin -> 200 + security headers + ACAO reflects the origin 260 let r1: *u8 = "GET /api/tools HTTP/1.1\r\nHost: nishifamily.com\r\nOrigin: https://nishifamily.com\r\nConnection: close\r\n\r\n" as *u8 261 let l1: i64 = nx_api_edge_handle(r1, ta_slen(r1), out) 262 var t1: i64 = 1 263 if edge_has(out, l1, "200 OK" as *u8) == 0 { t1 = 0 } 264 if edge_has(out, l1, "X-Content-Type-Options: nosniff" as *u8) == 0 { t1 = 0 } 265 if edge_has(out, l1, "Strict-Transport-Security:" as *u8) == 0 { t1 = 0 } 266 if edge_has(out, l1, "Access-Control-Allow-Origin: https://nishifamily.com" as *u8) == 0 { t1 = 0 } 267 pass = pass + edge_row("T1 GET /api/tools + allowed Origin -> 200, security headers, ACAO reflected" as *u8, t1) 268 269 // T2: same GET with a DISALLOWED Origin -> 200 + security headers but NO Access-Control-Allow-Origin (deny) 270 let r2: *u8 = "GET /api/tools HTTP/1.1\r\nHost: nishifamily.com\r\nOrigin: https://evil.example\r\nConnection: close\r\n\r\n" as *u8 271 let l2: i64 = nx_api_edge_handle(r2, ta_slen(r2), out) 272 var t2: i64 = 1 273 if edge_has(out, l2, "200 OK" as *u8) == 0 { t2 = 0 } 274 if edge_has(out, l2, "X-Content-Type-Options: nosniff" as *u8) == 0 { t2 = 0 } 275 if edge_has(out, l2, "Access-Control-Allow-Origin:" as *u8) == 1 { t2 = 0 } // must be ABSENT 276 pass = pass + edge_row("T2 disallowed Origin -> 200 + headers but NO ACAO (CORS deny-by-default)" as *u8, t2) 277 278 // T3: OPTIONS preflight from an allowed origin -> 204 + Access-Control-Allow-Methods 279 let r3: *u8 = "OPTIONS /mcp HTTP/1.1\r\nHost: nishifamily.com\r\nOrigin: https://andelinwest.com\r\nAccess-Control-Request-Method: POST\r\nConnection: close\r\n\r\n" as *u8 280 let l3: i64 = nx_api_edge_handle(r3, ta_slen(r3), out) 281 var t3: i64 = 1 282 if edge_has(out, l3, "204 No Content" as *u8) == 0 { t3 = 0 } 283 if edge_has(out, l3, "Access-Control-Allow-Methods: GET, POST, OPTIONS" as *u8) == 0 { t3 = 0 } 284 if edge_has(out, l3, "Access-Control-Allow-Origin: https://andelinwest.com" as *u8) == 0 { t3 = 0 } 285 pass = pass + edge_row("T3 OPTIONS preflight (allowed origin) -> 204 + Allow-Methods + ACAO" as *u8, t3) 286 287 // T4: drain the 300-token burst directly (rapid, ~0 refill), then a real request must get 429 problem+json. 288 var i: i64 = 0 289 while i < 320 { let _d: i64 = edge_tb_allow(); i = i + 1 } 290 let l4: i64 = nx_api_edge_handle(r1, ta_slen(r1), out) 291 var t4: i64 = 1 292 if edge_has(out, l4, "429 Too Many Requests" as *u8) == 0 { t4 = 0 } 293 if edge_has(out, l4, "application/problem+json" as *u8) == 0 { t4 = 0 } 294 pass = pass + edge_row("T4 drained token bucket -> request gets 429 application/problem+json" as *u8, t4) 295 296 if pass == 4 { edge_w("NX-API-EDGE GATE GREEN 4/4 (hardening composed on the live request path)\n" as *u8); sys_exit(0) } 297 edge_w("NX-API-EDGE GATE RED\n" as *u8); sys_exit(1) 298 return 1 299}