code wiki / _hdl_build / nx_api_contract.nx

nx_api_contract.nx source

↩ module page · 230 lines · 11974 B

1// nx_api_contract.nx -- API-CONTRACT REGRESSION GUARD (operator: "build the capabilities to prevent things 2// like this" -- a sibling mgmt-API redeploy silently DROPPED /api/promote 404, breaking organ deploys for 3// EVERY session; enforces CLAUDE rule-19 API contract stability). Data-driven: a canonical manifest of 4// critical routes (knowledge/registry/api_critical_routes.tsv) is the CONTRACT; the cron probe 5// (nx_api_contract_probe.sh, via sovereign nx_https_get) writes the live VERDICT to knowledge/status/ 6// api_contract.log. THIS organ is the agent/MCP surface: it reports the contract + the last supervised 7// verdict so a session can check contract integrity BEFORE trusting a deploy. 8// nx_api_contract check [manifest] [verdictlog] (JSON: routes + last verdict + status) 9// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 10import "nx_syscalls.nx" 11const AC_MAGIC_4096: i64 = 4096 12 13const AC_CAP: i64 = 262144 14const AC_TAB: i64 = 9 15const AC_NL: i64 = 10 16const AC_HASH: i64 = 35 17const AC_STDERR: i64 = 2 18const AC_SPAN: i64 = 16 19const AC_EXIT_USAGE: i64 = 2 20 21func ac_werr(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(AC_STDERR, s, n); return 0 } 22func ac_vlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 23func ac_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } return o } 24func ac_catn(d: *u8, o: i64, v: i64) -> i64 { let t: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 } var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } var i: i64 = 0; while i < k { d[o] = t[k-1-i]; o = o + 1; i = i + 1 } return o } 25func ac_cat_esc(d: *u8, o: i64, q: *u8, s: i64, e: i64) -> i64 { var i: i64 = s; while i < e { var c: i64 = q[i] as i64; if c == 34 { c = 39 } if c == 92 { c = 47 } if c < 32 { c = 32 } d[o] = c as u8; o = o + 1; i = i + 1 } return o } 26func ac_read(path: *u8, buf: *u8, cap: i64) -> i64 { 27 let fd: i64 = sys_openat_rd(path) 28 if fd < 0 { return 0 - 1 } 29 var n: i64 = 0 30 var go: i64 = 1 31 while go == 1 { let r: i64 = sys_read(fd, ((buf as i64) + n) as *u8, cap - n); if r <= 0 { go = 0 } else { n = n + r } if n >= cap { go = 0 } } 32 sys_close(fd) 33 return n 34} 35func ac_starts(buf: *u8, n: i64, lit: *u8) -> i64 { var i: i64 = 0; while lit[i] != (0 as u8) { if i >= n { return 0 } if buf[i] != lit[i] { return 0 } i = i + 1 } return 1 } 36func ac_le(q: *u8, i: i64, n: i64) -> i64 { var e: i64 = i; var s: i64 = 1; while s == 1 { if e >= n { s = 0 } else { if q[e] == (AC_NL as u8) { s = 0 } else { e = e + 1 } } } return e } 37func ac_col(q: *u8, ls: i64, le: i64, c: i64, out: *i64) -> i64 { 38 var col: i64 = 0 39 var p: i64 = ls 40 while col < c { 41 var s: i64 = 1 42 while s == 1 { if p >= le { return 0 } if q[p] == (AC_TAB as u8) { s = 0 } else { p = p + 1 } } 43 p = p + 1 44 col = col + 1 45 } 46 var e: i64 = p 47 var s2: i64 = 1 48 while s2 == 1 { if e >= le { s2 = 0 } else { if q[e] == (AC_TAB as u8) { s2 = 0 } else { e = e + 1 } } } 49 out[0] = p 50 out[1] = e 51 return 1 52} 53func ac_is_word(c: i64) -> i64 { if c >= 97 { if c <= 122 { return 1 } } if c == 95 { return 1 } return 0 } 54// substring match where <needle> is a COMPLETE /api/[a-z_]+ token (next char not a word char) -- mirrors the 55// cron probe's `grep -oE '/api/[a-z_]+' | grep -Fxq` semantics, so a manifest prefix like /api/tools matches 56// "/api/tools/register" (next char '/') while /api/promote does NOT falsely match /api/promote_content alone. 57func ac_contains_token(hay: *u8, hn: i64, needle: *u8) -> i64 { 58 let nl: i64 = ac_vlen(needle) 59 if nl == 0 { return 0 } 60 var i: i64 = 0 61 while i + nl <= hn { 62 var m: i64 = 1 63 var j: i64 = 0 64 while j < nl { if hay[i+j] != needle[j] { m = 0; j = nl } else { j = j + 1 } } 65 if m == 1 { 66 var nxt: i64 = 0 67 if i + nl < hn { nxt = hay[i+nl] as i64 } 68 if ac_is_word(nxt) == 0 { return 1 } 69 } 70 i = i + 1 71 } 72 return 0 73} 74// fork+exec ./nx_https_get.elf <url> <connect-override> capturing stdout+stderr to outfile. Empty envp (the 75// fetch tool needs none). Same idiom as nx_deploy_lib dep_run_capture; runs on the NAS (tools-daemon cwd). 76func ac_run_capture(elf: *u8, a1: *u8, a2: *u8, outfile: *u8) -> i64 { 77 let pid: i64 = sys_fork() 78 if pid == 0 { 79 let fd: i64 = sys_openat_wr(outfile, 0x1a4) 80 if fd >= 0 { sys_dup3(fd, 1, 0); sys_dup3(fd, 2, 0) } 81 let argv: *i64 = sys_mmap(40) as *i64 82 let envp: *i64 = sys_mmap(16) as *i64; envp[0] = 0 83 argv[0] = elf as i64; argv[1] = a1 as i64; argv[2] = a2 as i64; argv[3] = 0 84 sys_execve(elf, argv, envp) 85 sys_exit(127) 86 } 87 let st: *i64 = sys_mmap(16) as *i64 88 sys_wait4(pid, st, 0) 89 if (st[0] % 128) != 0 { return 0 - 1 } 90 return (st[0] >> 8) & 0xff 91} 92func ac_write_log(path: *u8, s: *u8, n: i64) -> i64 { 93 let fd: i64 = sys_openat_wr(path, 0x1a4) 94 if fd < 0 { return 0 } 95 sys_write(fd, s, n) 96 sys_close(fd) 97 return 1 98} 99// PROBE: the ON-DEMAND / INLINE live route-contract check (seq162 mandatory-inline half; read-only, ZERO 100// rollback authority = safe). FAIL-STATIC: an empty fetch or a response lacking the /api/health sentinel -> 101// VERDICT=UNKNOWN (never trust a failed sensor). Else diff the manifest routes vs the live /api and emit+log 102// GREEN/REGRESSION. A deployer runs this right after /api/deploy for a zero-lag verdict (vs the 10-min cron). 103func ac_do_probe(mpath: *u8, vpath: *u8) -> i64 { 104 let cap: *u8 = sys_mmap(AC_CAP) 105 ac_run_capture("./nx_https_get.elf" as *u8, "https://nishifamily.com/api" as *u8, "127.0.0.1:8443" as *u8, "/tmp/nx_ac_probe.out" as *u8) 106 let rn: i64 = ac_read("/tmp/nx_ac_probe.out" as *u8, cap, AC_CAP) 107 let tsb: *i64 = sys_mmap(16) as *i64 108 sys_clock_gettime_real(tsb) 109 let out: *u8 = sys_mmap(AC_CAP) 110 var o: i64 = 0 111 var trust: i64 = 1 112 if rn <= 0 { trust = 0 } 113 if trust == 1 { if ac_contains_token(cap, rn, "/api/health" as *u8) == 0 { trust = 0 } } 114 if trust == 0 { 115 let ub: *u8 = sys_mmap(AC_MAGIC_4096) 116 var uo: i64 = ac_cat(ub, 0, "VERDICT=UNKNOWN api-contract probe-fetch-failed (empty or no /api/health sentinel; fail-static, no diff) ts=" as *u8) 117 uo = ac_catn(ub, uo, tsb[0]); uo = ac_cat(ub, uo, "\n" as *u8) 118 ac_write_log(vpath, ub, uo) 119 o = ac_cat(out, 0, "{\"verb\":\"probe\",\"verdict\":\"UNKNOWN\",\"reason\":\"probe-fetch-failed-fail-static\"}\n" as *u8) 120 sys_write(1, out, o); sys_exit(0); return 0 121 } 122 let man: *u8 = sys_mmap(AC_CAP) 123 let mn: i64 = ac_read(mpath, man, AC_CAP) 124 let c0: *i64 = sys_mmap(AC_SPAN) as *i64 125 var total: i64 = 0 126 var missing: i64 = 0 127 o = ac_cat(out, o, "{\"verb\":\"probe\",\"missing\":[" as *u8) 128 var i: i64 = 0 129 while i < mn { 130 let le: i64 = ac_le(man, i, mn) 131 if le > i { if (man[i] as i64) != AC_HASH { 132 if ac_col(man, i, le, 0, c0) == 1 { 133 let rp: *u8 = sys_mmap(128) 134 var k: i64 = 0 135 var p: i64 = c0[0] 136 while p < c0[1] { rp[k] = man[p]; k = k + 1; p = p + 1 } 137 rp[k] = 0 as u8 138 total = total + 1 139 if ac_contains_token(cap, rn, rp) == 0 { 140 if missing > 0 { o = ac_cat(out, o, "," as *u8) } 141 o = ac_cat(out, o, "\"" as *u8) 142 o = ac_cat_esc(out, o, man, c0[0], c0[1]) 143 o = ac_cat(out, o, "\"" as *u8) 144 missing = missing + 1 145 } 146 } 147 } } 148 i = le + 1 149 } 150 o = ac_cat(out, o, "],\"routes_total\":" as *u8) 151 o = ac_catn(out, o, total) 152 o = ac_cat(out, o, ",\"missing_count\":" as *u8) 153 o = ac_catn(out, o, missing) 154 o = ac_cat(out, o, ",\"verdict\":\"" as *u8) 155 let lb: *u8 = sys_mmap(AC_MAGIC_4096) 156 var lo: i64 = 0 157 if missing == 0 { 158 o = ac_cat(out, o, "GREEN\"}\n" as *u8) 159 lo = ac_cat(lb, 0, "VERDICT=GREEN api-contract " as *u8) 160 lo = ac_catn(lb, lo, total); lo = ac_cat(lb, lo, "/" as *u8); lo = ac_catn(lb, lo, total) 161 lo = ac_cat(lb, lo, " critical routes present (on-demand probe) ts=" as *u8) 162 } else { 163 o = ac_cat(out, o, "REGRESSION\"}\n" as *u8) 164 lo = ac_cat(lb, 0, "VERDICT=REGRESSION api-contract " as *u8) 165 lo = ac_catn(lb, lo, total - missing); lo = ac_cat(lb, lo, "/" as *u8); lo = ac_catn(lb, lo, total) 166 lo = ac_cat(lb, lo, " present MISSING=" as *u8); lo = ac_catn(lb, lo, missing) 167 lo = ac_cat(lb, lo, " (on-demand probe) ts=" as *u8) 168 } 169 lo = ac_catn(lb, lo, tsb[0]); lo = ac_cat(lb, lo, "\n" as *u8) 170 ac_write_log(vpath, lb, lo) 171 sys_write(1, out, o); sys_exit(0); return 0 172} 173func main(argc: i64, argv: *i64) -> i64 { 174 var verb: *u8 = "check" as *u8 175 if argc > 1 { verb = argv[1] as *u8 } 176 if ac_starts(verb, ac_vlen(verb), "probe" as *u8) == 1 { 177 var mp: *u8 = "knowledge/registry/api_critical_routes.tsv" as *u8 178 var vp: *u8 = "knowledge/status/api_contract.log" as *u8 179 if argc > 2 { mp = argv[2] as *u8 } 180 if argc > 3 { vp = argv[3] as *u8 } 181 return ac_do_probe(mp, vp) 182 } 183 if ac_starts(verb, ac_vlen(verb), "check" as *u8) == 0 { ac_werr("usage: nx_api_contract check|probe [manifest] [verdictlog]\n" as *u8); sys_exit(AC_EXIT_USAGE); return AC_EXIT_USAGE } 184 var mpath: *u8 = "knowledge/registry/api_critical_routes.tsv" as *u8 185 var vpath: *u8 = "knowledge/status/api_contract.log" as *u8 186 if argc > 2 { mpath = argv[2] as *u8 } 187 if argc > 3 { vpath = argv[3] as *u8 } 188 let man: *u8 = sys_mmap(AC_CAP) 189 let mn: i64 = ac_read(mpath, man, AC_CAP) 190 let vbuf: *u8 = sys_mmap(AC_CAP) 191 let vn: i64 = ac_read(vpath, vbuf, AC_CAP) 192 let out: *u8 = sys_mmap(AC_CAP) 193 var o: i64 = 0 194 o = ac_cat(out, o, "{\"verb\":\"check\",\"contract\":[" as *u8) 195 let c0: *i64 = sys_mmap(AC_SPAN) as *i64 196 let c1: *i64 = sys_mmap(AC_SPAN) as *i64 197 var routes: i64 = 0 198 var i: i64 = 0 199 while i < mn { 200 let le: i64 = ac_le(man, i, mn) 201 if le > i { if (man[i] as i64) != AC_HASH { 202 if ac_col(man, i, le, 0, c0) == 1 { 203 if routes > 0 { o = ac_cat(out, o, "," as *u8) } 204 o = ac_cat(out, o, "{\"route\":\"" as *u8) 205 o = ac_cat_esc(out, o, man, c0[0], c0[1]) 206 o = ac_cat(out, o, "\",\"why\":\"" as *u8) 207 if ac_col(man, i, le, 1, c1) == 1 { o = ac_cat_esc(out, o, man, c1[0], c1[1]) } 208 o = ac_cat(out, o, "\"}" as *u8) 209 routes = routes + 1 210 } 211 } } 212 i = le + 1 213 } 214 o = ac_cat(out, o, "],\"routes_count\":" as *u8) 215 o = ac_catn(out, o, routes) 216 // last supervised verdict (from the cron probe) 217 o = ac_cat(out, o, ",\"last_verdict\":\"" as *u8) 218 var green: i64 = 0 219 if ac_starts(vbuf, vn, "VERDICT=GREEN" as *u8) == 1 { green = 1; o = ac_cat(out, o, "GREEN" as *u8) } else { if vn > 0 { o = ac_cat(out, o, "REGRESSION" as *u8) } else { o = ac_cat(out, o, "UNKNOWN" as *u8) } } 220 o = ac_cat(out, o, "\",\"verdict_line\":\"" as *u8) 221 // the verdict line, up to newline, escaped 222 var vle: i64 = 0 223 if vn > 0 { vle = ac_le(vbuf, 0, vn); o = ac_cat_esc(out, o, vbuf, 0, vle) } 224 o = ac_cat(out, o, "\",\"status\":\"" as *u8) 225 if green == 1 { o = ac_cat(out, o, "GREEN" as *u8) } else { if vn > 0 { o = ac_cat(out, o, "REGRESSION" as *u8) } else { o = ac_cat(out, o, "UNMONITORED" as *u8) } } 226 o = ac_cat(out, o, "\",\"envelope\":\"critical-route manifest + verdict log each read bounded at AC_CAP 256KiB; a larger file reads TRUNCATED and a route could be judged missing -- declared per the scale-law (F846), never silent\"}\n" as *u8) 227 sys_write(1, out, o) 228 sys_exit(0) 229 return 0 230}