nx_svc_budget_gate.nx source
↩ module page · 143 lines · 7676 B
1// nx_svc_budget_gate.nx -- REGRESSION PROOF for the self-retiring-service class (debt seq882, 2026-07-25).
2//
3// THE DEFECT IT LOCKS OUT: 44 sovereign services run `while served < budget { accept... } sys_exit(0)`. One
4// launched with a SMALL finite budget therefore RETIRES ITSELF -- and nx_hostctl's guard (nx_hostctl.nx:798)
5// reads the repeated exits as a crash-loop and BACKS OFF, so the surface goes genuinely DOWN while `status`
6// still prints UP. Measured live 2026-07-25: the gallery gateway, the wiki/hub/torrent/gen gateways, the main
7// OPAQUE login (:9091) AND the mgmt API itself were all launched with budget=5000. That is the root cause of
8// the operator-reported "gallery wont let me login" AND of the chronic mgmt/MCP transport flake.
9// nx_docportal_search_serve.nx:303 documents this SAME failure diagnosed on 2026-07-03 -- fixed there, never
10// swept. This gate exists so it can never silently return.
11//
12// WHY IT ASSERTS ON BYTES, NOT INTENT: "we fixed it" is author optimism, not evidence. Each tooth greps the
13// actual launch-command constant. T8 greps the LIVE DEPLOYED BINARY, not just source -- this session proved
14// that lie twice (a carve-out banked in memory as "landed in source" was NOT in source; a deployed gallery
15// thumbnailer predated its own fix by three weeks). Source-GREEN + binary-RED = NOT SHIPPED. T9/T10 are
16// MUTATION teeth: they fail if the exact bad literal reappears, so a revert cannot pass quietly.
17//
18// Named _svc_ not _daemon_ deliberately: md_promote_deny substring-blocks "daemon" so real daemons are forced
19// through the health-checked /api/deploy. This is a one-shot organ, so the honest name is the promotable one.
20//
21// Run from the nishihost CWD (the tools daemon fork-execs it there). license_tier: ORIGINAL No hw writes (Rule 26).
22import "nx_syscalls.nx"
23import "nx_gate_verdict.nx"
24
25const SBG_CAP: i64 = 1048576
26const SBG_MIN_BUDGET: i64 = 1000000
27
28func sbg_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
29func sbg_puts(s: *u8) -> i64 { sys_write(1, s, sbg_slen(s)); return 0 }
30func sbg_putn(v: i64) -> i64 {
31 let t: *u8 = sys_mmap(32)
32 var m: i64 = v
33 var k: i64 = 0
34 if m == 0 { t[0] = 48 as u8; k = 1 }
35 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
36 let o: *u8 = sys_mmap(32)
37 var w: i64 = 0
38 var q: i64 = k - 1
39 while q >= 0 { o[w] = t[q]; w = w + 1; q = q - 1 }
40 sys_write(1, o, w)
41 return 0
42}
43
44// bounded whole-file read into a caller buffer; returns bytes read (0 = unreadable/empty)
45func sbg_slurp(path: *u8, buf: *u8, cap: i64) -> i64 {
46 let fd: i64 = sys_openat_rd(path)
47 if fd < 0 { return 0 }
48 var total: i64 = 0
49 var n: i64 = sys_read(fd, buf, cap)
50 while n > 0 {
51 total = total + n
52 if total >= cap { n = 0 } else { n = sys_read(fd, (buf as i64 + total) as *u8, cap - total) }
53 }
54 sys_close(fd)
55 return total
56}
57
58func sbg_contains(hay: *u8, hn: i64, needle: *u8) -> i64 {
59 let nn: i64 = sbg_slen(needle)
60 if nn == 0 { return 1 }
61 var i: i64 = 0
62 while i + nn <= hn {
63 var k: i64 = 0
64 var ok: i64 = 1
65 while k < nn { if hay[i+k] != needle[k] { ok = 0; k = nn } else { k = k + 1 } }
66 if ok == 1 { return 1 }
67 i = i + 1
68 }
69 return 0
70}
71
72// PRESENT tooth: the launch constant must carry an unbounded budget.
73// D001-migrated 2026-08-03: gv_check carries the canonical count; pass[0] persists ONLY to feed the
74// legacy `T=10 PASS=n` line (kept as the public signature per the migrate-on-touch recipe).
75func sbg_tooth(buf: *u8, n: i64, needle: *u8, label: *u8, pass: *i64, ctr: *i64) -> i64 {
76 let hit: i64 = sbg_contains(buf, n, needle)
77 if hit == 1 { pass[0] = pass[0] + 1 }
78 gv_check(label, hit, ctr)
79 return hit
80}
81
82// MUTATION tooth: the exact regression literal must be ABSENT.
83func sbg_tooth_absent(buf: *u8, n: i64, needle: *u8, label: *u8, pass: *i64, ctr: *i64) -> i64 {
84 let hit: i64 = sbg_contains(buf, n, needle)
85 var ok: i64 = 0
86 if hit == 0 { ok = 1; pass[0] = pass[0] + 1 }
87 gv_check(label, ok, ctr)
88 return hit
89}
90
91func main(argc: i64, argv: *i64) -> i64 {
92 let pass: *i64 = sys_mmap(16) as *i64
93 pass[0] = 0
94 let total: i64 = 10
95 let ctr: *i64 = gv_ctr()
96
97 sbg_puts("nx_svc_budget_gate -- no supervised service may retire itself (min budget " as *u8)
98 sbg_putn(SBG_MIN_BUDGET)
99 sbg_puts(")\n" as *u8)
100
101 let src: *u8 = sys_mmap(SBG_CAP)
102 let sn: i64 = sbg_slurp("buildroot/runtime/_hdl_build/nx_hostctl.nx" as *u8, src, SBG_CAP)
103 if sn == 0 {
104 sbg_puts("FAIL cannot read nx_hostctl.nx source (run from the nishihost CWD)\n" as *u8)
105 sbg_puts("nx_svc_budget_gate T=10 PASS=0 verdict=RED\n" as *u8)
106 return 1
107 }
108
109 sbg_tooth(src, sn, "galx_gw_store 1000000000 18090" as *u8, "T1 gallery gateway :18190 unbounded" as *u8, pass, ctr)
110 sbg_tooth(src, sn, "mgmt_snap.json 1000000000 >" as *u8, "T2 mgmt API :18098 unbounded (the control plane)" as *u8, pass, ctr)
111 // ⚠NEEDLE DRIFT FIXED 2026-08-03: these teeth grep EXACT launch strings, and the commands legitimately
112 // evolved (redirects > -> >>, login gained wiki-pages + hr-log args) while the needles did not -- so the
113 // gate screamed FALSE RED on four services whose budgets were 1000000000 all along (verified by reading
114 // HC_*_CMD). Same class as the cron-watch head-read: an instrument bound to an outdated format ages into
115 // a liar. Needles now pin budget + the arg that FOLLOWS it (login: the wiki-pages path; hub/torrent/gen:
116 // their >> log names), so a redirect-style change can't fake a budget regression either way.
117 sbg_tooth(src, sn, "opaque_store.log 1000000000 19456 2 1 /mnt" as *u8, "T3 OPAQUE login :9091 unbounded" as *u8, pass, ctr)
118 sbg_tooth(src, sn, "nx_wiki_gw.elf 18791 keys store 1000000000" as *u8, "T4 wiki gateway :18791 unbounded" as *u8, pass, ctr)
119 sbg_tooth(src, sn, "opaque_store.log 1000000000 65536 3 4 >>/tmp/hub_gw.log" as *u8, "T5 hub gateway :18792 unbounded" as *u8, pass, ctr)
120 sbg_tooth(src, sn, "opaque_store.log 1000000000 65536 3 4 >>/tmp/torrentgw.log" as *u8, "T6 torrent gateway :18793 unbounded" as *u8, pass, ctr)
121 sbg_tooth(src, sn, "opaque_store.log 1000000000 18795 0 19456 2 1 86400 >>/tmp/gen_gw.log" as *u8, "T7 gen gateway :18794 unbounded" as *u8, pass, ctr)
122
123 // T8 -- SOURCE-vs-DEPLOYED DRIFT. A fix that lives only in source is an unverified intention.
124 let live: *u8 = sys_mmap(SBG_CAP)
125 let ln: i64 = sbg_slurp("nx_hostctl" as *u8, live, SBG_CAP)
126 if ln == 0 {
127 gv_check("T8 LIVE DEPLOYED binary carries the fix (not just source)" as *u8, 0, ctr)
128 sbg_puts("FAIL T8 cannot read the LIVE nx_hostctl binary\n" as *u8)
129 } else {
130 sbg_tooth(live, ln, "galx_gw_store 1000000000 18090" as *u8, "T8 LIVE DEPLOYED binary carries the fix (not just source)" as *u8, pass, ctr)
131 }
132
133 // T9/T10 -- MUTATION teeth: the exact regression literals must never reappear.
134 sbg_tooth_absent(src, sn, "galx_gw_store 5000 18090" as *u8, "T9 mutation: gallery budget=5000 ABSENT from source" as *u8, pass, ctr)
135 sbg_tooth_absent(src, sn, "mgmt_snap.json 5000 >" as *u8, "T10 mutation: mgmt API budget=5000 ABSENT from source" as *u8, pass, ctr)
136
137 sbg_puts("nx_svc_budget_gate T=" as *u8)
138 sbg_putn(total)
139 sbg_puts(" PASS=" as *u8)
140 sbg_putn(pass[0])
141 if pass[0] == total { sbg_puts(" verdict=GREEN\n" as *u8) } else { sbg_puts(" verdict=RED\n" as *u8) }
142 return gv_verdict("svc_budget" as *u8, ctr, "no supervised service may retire itself; needles pin budget 1000000000 in each live launch cmd" as *u8)
143}