code wiki / (root) / nx_service.nx

nx_service.nx source

↩ module page · 333 lines · 10559 B

1// nx_service.nx -- SERVICE BASE rung. The shared harness every domain organ 2// inherits to become an MCP tool + HTTP-drivable API endpoint: a JSON 3// response-builder object, a versioned envelope, structured errors, and a 4// verb dispatcher. This is the base class of the whole lab-science / 5// go-to-market service family -- write it once, and every facade over the 6// existing library modules is a thin skin on top. 7// 8// WHY A BASE AND NOT A COPY PER SERVICE. Seventeen library organs already 9// exist as pure functions. Turning each into a standalone tool by hand would 10// duplicate the envelope, the escaping, the error shape and the dispatch 11// seventeen times -- the exact copy-paste this codebase forbids. Instead a 12// facade composes: it owns a verb table and handlers, and delegates all of 13// the wire format to NxJson + the nsvc_* envelope here. Rule 15 (DRY through 14// a shared library), rule 6 (OOP: NxJson is an object with methods), rule 12 15// (structured errors at the boundary), rule 19 (a VERSIONED contract). 16// 17// AGENTIC + WORKFLOW READY BY CONSTRUCTION. Every reply is machine-readable 18// JSON with a stable envelope, every handler is a PURE stateless function of 19// its arguments, and every facade exposes a `describe` verb that emits its 20// own verb catalog -- the service-level analog of MCP tools/list, so an agent 21// can discover a facade's capabilities without a human in the loop. Stateless 22// + structured + self-describing is exactly what a pipeline stage or an LLM 23// tool call consumes. 24// 25// SCALES BY BEING STATELESS. No handler holds state between calls; the whole 26// object is a per-request scratch buffer. N concurrent callers are N 27// independent invocations, so horizontal scale is free -- fork more workers. 28// 29// TRUNCATION IS MARKED, NEVER SILENT. The response buffer is bounded; an 30// overflow sets ov=1 and the envelope reports "truncated":true rather than 31// emitting invalid JSON. A parser must never receive a lie. 32// 33// genealogy_id: service_infrastructure + nishi_sovereign_api 34 35import "nx_syscalls.nx" 36import "nx_grounding.nx" 37const NSVC_MAGIC_4096: i64 = 4096 38 39// ===== Versioned contract ============================================= 40 41const NSVC_API_VERSION: i64 = 1 42 43// ===== Structured error codes (machine-readable + HTTP-ish status) ==== 44 45const NSVC_OK: i64 = 0 46const NSVC_ERR_UNKNOWN_VERB: i64 = 1 // 404 47const NSVC_ERR_BAD_ARGS: i64 = 2 // 400 48const NSVC_ERR_NOT_FOUND: i64 = 3 // 404 49const NSVC_ERR_REFUSED: i64 = 4 // 422 (fail-closed domain refusal) 50const NSVC_ERR_INTERNAL: i64 = 5 // 500 51 52func nsvc_err_status(code: i64) -> i64 { 53 if code == NSVC_ERR_UNKNOWN_VERB { return 404 } 54 if code == NSVC_ERR_BAD_ARGS { return 400 } 55 if code == NSVC_ERR_NOT_FOUND { return 404 } 56 if code == NSVC_ERR_REFUSED { return 422 } 57 if code == NSVC_ERR_INTERNAL { return 500 } 58 return 200 59} 60 61func nsvc_err_slug(code: i64) -> *u8 { 62 if code == NSVC_ERR_UNKNOWN_VERB { return "UNKNOWN_VERB" as *u8 } 63 if code == NSVC_ERR_BAD_ARGS { return "BAD_ARGS" as *u8 } 64 if code == NSVC_ERR_NOT_FOUND { return "NOT_FOUND" as *u8 } 65 if code == NSVC_ERR_REFUSED { return "REFUSED" as *u8 } 66 if code == NSVC_ERR_INTERNAL { return "INTERNAL" as *u8 } 67 return "OK" as *u8 68} 69 70// ===== NxJson: the response-builder object ============================ 71// 72// An object: a buffer, a write cursor, a capacity, and an overflow flag. 73// The nj_* functions are its methods; each takes the object as its first 74// argument (the receiver). Appends are bounds-checked, so no method can run 75// off the end of the buffer. 76 77struct NxJson { 78 buf: *u8, 79 off: i64, 80 cap: i64, 81 ov: i64, 82} 83 84func nx_json_new(cap: i64) -> *NxJson { 85 let j: *NxJson = (sys_mmap(32)) as *NxJson 86 var c: i64 = cap 87 if c <= 0 { c = NSVC_MAGIC_4096 } 88 j.buf = sys_mmap(c) 89 j.off = 0 90 j.cap = c 91 j.ov = 0 92 return j 93} 94 95// Append one byte, bounds-checked. Sets ov on overflow rather than writing 96// past the buffer. 97func nj_putc(j: *NxJson, ch: i64) -> i64 { 98 if j.off >= j.cap { 99 j.ov = 1 100 return 0 101 } 102 let b: *u8 = j.buf 103 b[j.off] = ch as u8 104 j.off = j.off + 1 105 return 0 106} 107 108// Append a NUL-terminated raw string (already valid JSON or a bare token). 109func nj_puts(j: *NxJson, s: *u8) -> i64 { 110 var i: i64 = 0 111 while s[i] != (0 as u8) { 112 nj_putc(j, s[i] as i64) 113 i = i + 1 114 } 115 return 0 116} 117 118// Append a signed integer in decimal. 119func nj_puti(j: *NxJson, v: i64) -> i64 { 120 var m: i64 = v 121 if m < 0 { 122 nj_putc(j, 45) 123 m = 0 - m 124 } 125 if m == 0 { 126 nj_putc(j, 48) 127 return 0 128 } 129 let tmp: *u8 = sys_mmap(24) 130 var k: i64 = 0 131 while m > 0 { 132 tmp[k] = (48 + (m % 10)) as u8 133 m = m / 10 134 k = k + 1 135 } 136 while k > 0 { 137 k = k - 1 138 nj_putc(j, tmp[k] as i64) 139 } 140 return 0 141} 142 143// Append a JSON-escaped, double-quoted string. Escapes the characters that 144// would otherwise break a parser (quote, backslash, control chars). Defensive 145// at the boundary: untrusted text (a claim sentence, an ingredient name) 146// cannot inject structure. 147func nj_putstr(j: *NxJson, s: *u8) -> i64 { 148 nj_putc(j, 34) 149 var i: i64 = 0 150 while s[i] != (0 as u8) { 151 let c: i64 = s[i] as i64 152 if c == 34 { nj_putc(j, 92); nj_putc(j, 34) } 153 if c == 92 { nj_putc(j, 92); nj_putc(j, 92) } 154 if c == 10 { nj_putc(j, 92); nj_putc(j, 110) } 155 if c == 9 { nj_putc(j, 92); nj_putc(j, 116) } 156 if c == 13 { nj_putc(j, 92); nj_putc(j, 114) } 157 if c >= 32 { 158 if c != 34 { if c != 92 { nj_putc(j, c) } } 159 } 160 i = i + 1 161 } 162 nj_putc(j, 34) 163 return 0 164} 165 166// A "key": prefix. Comma handling is the caller's job via nj_comma so the 167// object stays flat and predictable. 168func nj_key(j: *NxJson, k: *u8) -> i64 { 169 nj_putstr(j, k) 170 nj_putc(j, 58) 171 return 0 172} 173 174func nj_comma(j: *NxJson) -> i64 { 175 nj_putc(j, 44) 176 return 0 177} 178 179// key:int and key:"str" convenience pairs. 180func nj_kv_int(j: *NxJson, k: *u8, v: i64) -> i64 { 181 nj_key(j, k) 182 nj_puti(j, v) 183 return 0 184} 185 186func nj_kv_str(j: *NxJson, k: *u8, v: *u8) -> i64 { 187 nj_key(j, k) 188 nj_putstr(j, v) 189 return 0 190} 191 192func nj_kv_bool(j: *NxJson, k: *u8, v: i64) -> i64 { 193 nj_key(j, k) 194 if v == 0 { nj_puts(j, "false" as *u8) } else { nj_puts(j, "true" as *u8) } 195 return 0 196} 197 198// Write the finished buffer to stdout with a trailing newline. 199func nj_flush(j: *NxJson) -> i64 { 200 sys_write(1, j.buf, j.off) 201 sys_write(1, "\n" as *u8, 1) 202 return 0 203} 204 205func nj_len(j: *NxJson) -> i64 { 206 return j.off 207} 208 209// ===== The envelope =================================================== 210// 211// { "v":1, "svc":"<name>", "verb":"<verb>", "ok":true, "data":{ ... }, 212// "truncated":false } 213// or 214// { "v":1, "svc":..., "verb":..., "ok":false, 215// "error":{ "code":"SLUG", "status":404, "detail":"..." } } 216// 217// nsvc_ok_open writes everything up to and including `"data":{`; the handler 218// appends the data fields; nsvc_ok_close finishes with `}` plus the 219// truncation flag and the closing brace. 220 221func nsvc_ok_open(j: *NxJson, svc: *u8, verb: *u8) -> i64 { 222 nj_putc(j, 123) 223 nj_kv_int(j, "v" as *u8, NSVC_API_VERSION) 224 nj_comma(j) 225 nj_kv_str(j, "svc" as *u8, svc) 226 nj_comma(j) 227 nj_kv_str(j, "verb" as *u8, verb) 228 nj_comma(j) 229 nj_kv_bool(j, "ok" as *u8, 1) 230 nj_comma(j) 231 nj_key(j, "data" as *u8) 232 nj_putc(j, 123) 233 return 0 234} 235 236func nsvc_ok_close(j: *NxJson) -> i64 { 237 nj_putc(j, 125) 238 nj_comma(j) 239 nj_kv_bool(j, "truncated" as *u8, j.ov) 240 nj_putc(j, 125) 241 return 0 242} 243 244// Emit the grounding trailer as the LAST fields of the data object, so every 245// answer self-declares how much to trust it (anti-over-optimism, in the wire 246// format). Call this right BEFORE nsvc_ok_close, after the last data field: 247// ..."x":1<CALL>} -> ..."x":1,"grounding":"ASSERTED","verified":false, 248// "grounding_note":"..."},... 249// tier is a GND_* value; the trailer names it, says whether it counts as 250// verified (only VALIDATED/ANCHORED do), and explains what the tier means. 251func nsvc_ground(j: *NxJson, tier: i64) -> i64 { 252 nj_comma(j) 253 nj_kv_str(j, "grounding" as *u8, gnd_name(tier)) 254 nj_comma(j) 255 nj_kv_bool(j, "verified" as *u8, gnd_is_verified(tier)) 256 nj_comma(j) 257 nj_kv_str(j, "grounding_note" as *u8, gnd_meaning(tier)) 258 return 0 259} 260 261// The common case: emit the grounding trailer then close. A handler ends with 262// nsvc_ok_close_g(j, GND_<tier>) instead of nsvc_ok_close(j). 263func nsvc_ok_close_g(j: *NxJson, tier: i64) -> i64 { 264 nsvc_ground(j, tier) 265 nsvc_ok_close(j) 266 return 0 267} 268 269// The whole error envelope in one call. 270func nsvc_error(j: *NxJson, svc: *u8, verb: *u8, code: i64, detail: *u8) -> i64 { 271 nj_putc(j, 123) 272 nj_kv_int(j, "v" as *u8, NSVC_API_VERSION) 273 nj_comma(j) 274 nj_kv_str(j, "svc" as *u8, svc) 275 nj_comma(j) 276 nj_kv_str(j, "verb" as *u8, verb) 277 nj_comma(j) 278 nj_kv_bool(j, "ok" as *u8, 0) 279 nj_comma(j) 280 nj_key(j, "error" as *u8) 281 nj_putc(j, 123) 282 nj_kv_str(j, "code" as *u8, nsvc_err_slug(code)) 283 nj_comma(j) 284 nj_kv_int(j, "status" as *u8, nsvc_err_status(code)) 285 nj_comma(j) 286 nj_kv_str(j, "detail" as *u8, detail) 287 nj_putc(j, 125) 288 nj_putc(j, 125) 289 return 0 290} 291 292// ===== Argv + dispatch helpers ======================================== 293 294// argv[k] as *u8, or a NUL sentinel if out of range. Bounds-safe so a 295// missing argument can never dereference garbage. 296func nsvc_arg(argc: i64, argv: *i64, k: i64) -> *u8 { 297 if k < 0 { return "\x00" as *u8 } 298 if k >= argc { return "\x00" as *u8 } 299 return argv[k] as *u8 300} 301 302func nsvc_streq(a: *u8, b: *u8) -> i64 { 303 var i: i64 = 0 304 while 1 == 1 { 305 if a[i] != b[i] { return 0 } 306 if a[i] == (0 as u8) { return 1 } 307 i = i + 1 308 } 309 return 1 310} 311 312// Signed integer parse. Returns 0 on empty; a leading '-' negates. Trailing 313// junk stops contributing (a domain handler validates ranges itself). 314func nsvc_atoi(s: *u8) -> i64 { 315 var i: i64 = 0 316 var neg: i64 = 0 317 var v: i64 = 0 318 if s[0] == (45 as u8) { neg = 1; i = 1 } 319 while s[i] != (0 as u8) { 320 let c: i64 = s[i] as i64 321 if c >= 48 { 322 if c <= 57 { v = v * 10 + (c - 48) } 323 } 324 i = i + 1 325 } 326 if neg == 1 { return 0 - v } 327 return v 328} 329 330func nsvc_arg_int(argc: i64, argv: *i64, k: i64) -> i64 { 331 let s: *u8 = nsvc_arg(argc, argv, k) 332 return nsvc_atoi(s) 333}