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}