code wiki / _hdl_build / nx_routeguard_lib.nx
nx_routeguard_lib.nx source
↩ module page · 156 lines · 6585 B
1// nx_routeguard_lib.nx -- DEPLOY CONTRACT GUARD: does the candidate binary still serve every route
2// the live one does?
3//
4// THE DEFECT THIS EXISTS FOR (measured 2026-07-30, FIVE occurrences, one of them mine):
5// nx_mgmt_api has now lost /api/gate_run + /api/proc_kill from the LIVE binary five separate times
6// (21 routes -> 19). Mine happened because I built mgmt from a backdated tree and deployed a 528323-byte
7// artifact over a 575195-byte one -- a 47KB DECREASE that was visible at the time and that I did not
8// check. Every occurrence had the same shape: the source was fine, the BINARY silently lost surface,
9// and the deploy reported success. nx_route_diff already prints "route(s) vanished = deploy contract
10// regression" but is not wired into the deploy path, so nothing ever consulted it.
11//
12// WHY THIS GATE AND NOT LOAD-ADMISSION (self-correction, id=1785450386): my first instinct was to gate
13// deploys on host load the way /api/build is gated. That is WRONG and would deadlock the ecosystem --
14// the repair for a saturated host IS a deploy, so a load-gated deploy refuses the fix BY THE CONDITION
15// IT REPAIRS. Route loss, by contrast, is ALWAYS wrong regardless of load, so this guard can never
16// block a repair. That property is the whole reason this is the right guard.
17//
18// DERIVED, NOT DECLARED: the expected route set is EXTRACTED FROM THE LIVE BINARY, never hardcoded.
19// A baked list would be the same declared-denominator defect already filed (id=1785446417) -- it would
20// go stale the moment a route is legitimately added, and it would fail OPEN.
21//
22// ASYMMETRIC BY DESIGN: candidate ADDING routes is fine (that is a normal feature deploy). Only
23// DISAPPEARANCE is a defect. So this is a SUPERSET check, not an equality check.
24// license_tier: ORIGINAL Read-only. No hw writes (Rule 26).
25import "nx_syscalls.nx"
26
27const RG_BUF: i64 = 4194304 // per-binary read cap
28const RG_MAXR: i64 = 128 // max distinct routes tracked
29const RG_NAMEMAX: i64 = 64
30const RG_NEEDLE: i64 = 5 // strlen("/api/")
31
32func rg_isroute_ch(c: i64) -> i64 {
33 if c >= 97 { if c <= 122 { return 1 } }
34 if c >= 65 { if c <= 90 { return 1 } }
35 if c >= 48 { if c <= 57 { return 1 } }
36 if c == 95 { return 1 }
37 if c == 47 { return 1 }
38 if c == 46 { return 1 }
39 return 0
40}
41
42// read a whole file into a REUSED bounded buffer. returns bytes, or -1 if unopenable.
43func rg_read(path: *u8, buf: *u8, cap: i64) -> i64 {
44 let fd: i64 = sys_openat_rd(path)
45 if fd < 0 { return 0 - 1 }
46 var n: i64 = 0
47 var go: i64 = 1
48 while go == 1 {
49 let r: i64 = sys_read(fd, (buf as i64 + n) as *u8, cap - n)
50 if r <= 0 { go = 0 } else {
51 n = n + r
52 if n >= cap { go = 0 }
53 }
54 }
55 sys_close(fd)
56 return n
57}
58
59// Does buf[0..n) contain the needle AS A WHOLE ROUTE TOKEN?
60//
61// BOUNDARY-CHECKED, and that is not pedantry -- it was a real hole caught by this organ's own gate.
62// A plain substring scan says /api/healthcheck SATISFIES /api/health, so RENAMING a route would pass
63// the guard silently: the old name vanishes from the surface while the check reports 0 missing. That
64// is the same class as the @priv lexer needing a trailing boundary so @privv could not match @priv.
65// A route is only "still served" if the literal ENDS where the live one ends.
66func rg_contains(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 {
67 if nl <= 0 { return 0 }
68 var i: i64 = 0
69 let last: i64 = n - nl
70 while i <= last {
71 var j: i64 = 0
72 var ok: i64 = 1
73 while j < nl { if buf[i + j] != needle[j] { ok = 0; j = nl } else { j = j + 1 } }
74 if ok == 1 {
75 let e: i64 = i + nl
76 var bounded: i64 = 1
77 if e < n { if rg_isroute_ch(buf[e] as i64) == 1 { bounded = 0 } }
78 if bounded == 1 { return 1 }
79 }
80 i = i + 1
81 }
82 return 0
83}
84
85// already collected?
86func rg_seen(names: *u8, lens: *i64, cnt: i64, p: *u8, l: i64) -> i64 {
87 var i: i64 = 0
88 while i < cnt {
89 if lens[i] == l {
90 let base: i64 = i * RG_NAMEMAX
91 var k: i64 = 0
92 var same: i64 = 1
93 while k < l { if names[base + k] != p[k] { same = 0; k = l } else { k = k + 1 } }
94 if same == 1 { return 1 }
95 }
96 i = i + 1
97 }
98 return 0
99}
100
101// EXTRACT the route surface from a binary image: every distinct "/api/..." literal it carries.
102// Bounded by RG_MAXR; the caller REPORTS truncation rather than silently guarding a subset.
103func rg_extract(buf: *u8, n: i64, names: *u8, lens: *i64, trunc: *i64) -> i64 {
104 trunc[0] = 0
105 var cnt: i64 = 0
106 var i: i64 = 0
107 let last: i64 = n - RG_NEEDLE
108 while i <= last {
109 var hit: i64 = 0
110 if buf[i] == (47 as u8) { if buf[i+1] == (97 as u8) { if buf[i+2] == (112 as u8) {
111 if buf[i+3] == (105 as u8) { if buf[i+4] == (47 as u8) { hit = 1 } } } } }
112 if hit == 1 {
113 var end: i64 = i
114 var go: i64 = 1
115 while go == 1 {
116 if end >= n { go = 0 } else {
117 if rg_isroute_ch(buf[end] as i64) == 1 { end = end + 1 } else { go = 0 }
118 }
119 }
120 let l: i64 = end - i
121 if l > RG_NEEDLE {
122 if l < RG_NAMEMAX {
123 if rg_seen(names, lens, cnt, (buf as i64 + i) as *u8, l) == 0 {
124 if cnt < RG_MAXR {
125 let base: i64 = cnt * RG_NAMEMAX
126 var k: i64 = 0
127 while k < l { names[base + k] = buf[i + k]; k = k + 1 }
128 names[base + l] = 0 as u8
129 lens[cnt] = l
130 cnt = cnt + 1
131 } else { trunc[0] = trunc[0] + 1 }
132 }
133 }
134 }
135 i = end
136 } else { i = i + 1 }
137 }
138 return cnt
139}
140
141// THE VERDICT. Returns the number of routes present in LIVE but MISSING from CANDIDATE.
142// 0 = the candidate is a superset = safe to promote on this axis.
143// Missing route indices are written into miss[] (bounded by misscap).
144func rg_missing(lnames: *u8, llens: *i64, lcnt: i64, cbuf: *u8, cn: i64, miss: *i64, misscap: i64) -> i64 {
145 var m: i64 = 0
146 var i: i64 = 0
147 while i < lcnt {
148 let base: i64 = i * RG_NAMEMAX
149 if rg_contains(cbuf, cn, (lnames as i64 + base) as *u8, llens[i]) == 0 {
150 if m < misscap { miss[m] = i }
151 m = m + 1
152 }
153 i = i + 1
154 }
155 return m
156}