code wiki / _hdl_build / nx_retire_path.nx

nx_retire_path.nx source

↩ module page · 524 lines · 32666 B

1// nx_retire_path.nx -- the missing sovereign primitive: take a file or directory OUT of a served tree 2// without ever deleting it. The estate had governance/atlas/debt sweeps but nothing that could retire a 3// path, so scratch artifacts accumulated on PUBLIC surfaces and the only "fix" anyone had was ssh rm. 4// 5// SAFE BY CONSTRUCTION (mirrors the established md_promote_deny substring-deny pattern rather than 6// inventing a new safety model): 7// * NEVER deletes -- sys_renameat only, so the bytes survive and the move is reversible (rule 13). 8// * moves INTO knowledge/retired/ (outside every docroot) so a retired path stops being served. 9// * REFUSES: any path containing ".." (traversal), a shallow path -- the rule COUNTS SLASHES and needs 10// at least 2, i.e. dir/subdir/file, so `knowledge/x.tsv` is REFUSED while `knowledge/status/x.tsv` is 11// allowed. (The message used to say "must be >=2 segments deep", which names the wrong unit and reads 12// as satisfied by the very path it rejects; corrected 2026-08-14 after it sent a caller hunting.) 13// so it can never retire a whole docroot or a hub-root file), and any path whose name carries a 14// dangerous substring (elf, key, cap, secret, opaque, .reg, .conf, daemon, serve, vault, mint). 15// * REFUSES a target that does not exist -- a no-op must never look like a success. 16// verbs: retire <path> | selftest 17// expect_exit: 0 license_tier: ORIGINAL 18import "nx_syscalls.nx" 19import "nx_emit_guard.nx" 20 21const RP_NAMEBUF: i64 = 1024 22 23func rt_say(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 24func rt_num(v: i64) -> i64 { if v == 0 { sys_write(1, "0" as *u8, 1); return 0 } var m: i64 = v; if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } let t: *u8 = sys_mmap(28); var k: i64 = 0; while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } while k > 0 { k = k - 1; sys_write(1, (((t as i64) + k) as *u8), 1) } return 0 } 25 26func rt_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 27// case-sensitive substring test (the deny list is lowercase and so are our paths) 28func rt_has(s: *u8, lit: *u8) -> i64 { 29 let sl: i64 = rt_len(s) 30 let ll: i64 = rt_len(lit) 31 if ll == 0 { return 0 } 32 var i: i64 = 0 33 while i + ll <= sl { 34 var j: i64 = 0 35 var ok: i64 = 1 36 while j < ll { if s[i + j] != lit[j] { ok = 0; j = ll } else { j = j + 1 } } 37 if ok == 1 { return 1 } 38 i = i + 1 39 } 40 return 0 41} 42// does s end with lit? (suffix test -- the staged-artifact exception below is a SUFFIX rule, never a 43// substring rule: a substring test would also match a LIVE path that merely mentions '.elf.new') 44func rt_ends(s: *u8, lit: *u8) -> i64 { 45 let sl: i64 = rt_len(s) 46 let ll: i64 = rt_len(lit) 47 if ll == 0 { return 0 } 48 if ll > sl { return 0 } 49 var i: i64 = 0 50 while i < ll { if s[sl - ll + i] != lit[i] { return 0 } i = i + 1 } 51 return 1 52} 53// SECRET-MATERIAL guards, applied to EVERY path including the staged-artifact exception. These are the 54// checks that protect CREDENTIALS; they are deliberately separate from the artifact-TYPE checks (.elf/ 55// .conf/.reg) and the depth rule, because those two protect LIVE-SERVING paths -- a distinction the 56// staged exception depends on. 57// ONE TABLE, EVERY READER. The deny tokens were written out THREE times: in rt_refuse_secret, in 58// rt_refuse, and as PROSE inside rt_reason's code-4 message. The prose copy drifted -- it listed 9 tokens 59// while the predicate checked 11, and the two it omitted (`daemon`, `serve`) are precisely the ones that 60// fired on a real caller. That caller read "protected name (.elf .key .cap .conf .reg secret opaque vault 61// mint)", found NONE of them in `buildroot/runtime/nx_servedrift.nx`, and reasonably wrote the guard up as 62// broken. It was not: `nx_servedrift` contains `serve`, and refusing it is CORRECT. 63// ★★★★★★A CORRECT REFUSAL WEARING AN INCOMPLETE REASON IS THE MOST EXPENSIVE FAILURE SHAPE THERE IS -- 64// IT SPENDS THE READER'S TRUST ON A DECISION THAT WAS RIGHT, AND TEACHES THEM TO ROUTE AROUND THE GUARD. 65// ★★★★★THE EARLIER DRY FIX WENT ONE LEVEL TOO SHALLOW: it derived the code->message MAP from the code and 66// left the MESSAGE reciting a hand-maintained copy of the predicate's own terms. Deriving the map buys 67// nothing while the leaf is still duplicated -- and this organ's header ALREADY required a refusal to name 68// WHICH rule fired rather than recite every rule it might have applied. Code 4 recited nine and named none. 69static rt_hit: *u8 70 71func rt_deny_n() -> i64 { return 11 } 72func rt_deny_tok(i: i64) -> *u8 { 73 if i == 0 { return ".elf" as *u8 } 74 if i == 1 { return ".key" as *u8 } 75 if i == 2 { return ".cap" as *u8 } 76 if i == 3 { return ".conf" as *u8 } 77 if i == 4 { return ".reg" as *u8 } 78 if i == 5 { return "secret" as *u8 } 79 if i == 6 { return "opaque" as *u8 } 80 if i == 7 { return "vault" as *u8 } 81 if i == 8 { return "mint" as *u8 } 82 if i == 9 { return "daemon" as *u8 } 83 if i == 10 { return "serve" as *u8 } 84 return "" as *u8 85} 86// The SECRET subset protects CREDENTIALS and applies to EVERY path INCLUDING the staged-artifact 87// exception; the rest protect LIVE-SERVING paths, which the staged exception deliberately bypasses. 88// Keeping that split as a COLUMN of one table, rather than as a second hand-written list, is what stops 89// the two from drifting apart again. 90func rt_deny_is_secret(i: i64) -> i64 { 91 if i == 1 { return 1 } 92 if i == 2 { return 1 } 93 if i == 5 { return 1 } 94 if i == 6 { return 1 } 95 if i == 7 { return 1 } 96 if i == 8 { return 1 } 97 return 0 98} 99func rt_refuse_secret(path: *u8) -> i64 { 100 var i: i64 = 0 101 while i < rt_deny_n() { 102 if rt_deny_is_secret(i) == 1 { 103 if rt_has(path, rt_deny_tok(i)) == 1 { rt_hit = rt_deny_tok(i); return 4 } 104 } 105 i = i + 1 106 } 107 return 0 108} 109// prefix test -- needed by the DIRECTORY lane below, which refuses served roots by prefix rather than by 110// depth, because for a directory the blast radius of a wrong call is a whole site rather than one file. 111func rt_starts(s: *u8, lit: *u8) -> i64 { 112 var i: i64 = 0 113 while lit[i] != (0 as u8) { 114 if s[i] != lit[i] { return 0 } 115 i = i + 1 116 } 117 return 1 118} 119func rt_slashes(s: *u8) -> i64 { 120 var c: i64 = 0 121 var i: i64 = 0 122 while s[i] != (0 as u8) { if s[i] == (47 as u8) { c = c + 1 } i = i + 1 } 123 return c 124} 125const RT_COMPARE_ROOT: *u8 = "sites/nishifamily/compare/" 126const RT_COMPARE_MIN_SLASHES: i64 = 4 127const RT_SLASH: i64 = 47 128// 0 = allowed, otherwise a refusal code naming the reason 129func rt_refuse(path: *u8) -> i64 { 130 if rt_len(path) < 3 { return 1 } 131 if rt_has(path, ".." as *u8) == 1 { return 2 } 132 // ---- STAGED-ARTIFACT EXCEPTION (2026-08-03, debt 1785771205) ---------------------------------- 133 // WHY IT IS SAFE, not a loosened guard: `<name>.elf.new` is a STAGED artifact. The LIVE binary is 134 // `<name>.elf` (or the extensionless `<name>`), so a `.elf.new` is BY CONSTRUCTION not serving any 135 // request -- retiring one can never stop a running service, which is exactly what the depth rule and 136 // the `.elf` name rule exist to prevent. Those two guards protect LIVE-SERVING paths; they were 137 // ALSO, as a side effect, making the estate's 512 stray staged artifacts unretirable -- including a 138 // 202,843 B `nx_hostctl.elf.new` sitting in the SUPERVISOR's deploy slot matching neither the live 139 // binary nor any source build. A guard that blocks the cleanup of the very hazard class it cannot 140 // see is protecting the wrong noun. 141 // SUFFIX, never substring: a live path that merely CONTAINS '.elf.new' is not exempted. 142 // Secret-material checks STILL APPLY (a `*.key.new` stays refused), and rename-not-delete means the 143 // bytes survive in knowledge/retired/ -- so a staged artifact retired in error is restorable. 144 if rt_ends(path, ".elf.new" as *u8) == 1 { return rt_refuse_secret(path) } 145 // ...and the BANKED twin. /api/build now renames an existing staged artifact to `<t>.sov.elf.new.prev` 146 // before recompiling (debt 1785772241) -- a remedy that CREATES artifacts must also create the means to 147 // sweep them, or the fix becomes the next litter source. Same reasoning as above: a banked staged copy 148 // is even further from live than the staged one, so it can never be serving anything. 149 if rt_ends(path, ".elf.new.prev" as *u8) == 1 { return rt_refuse_secret(path) } 150 // ---- SERVING-ROOT SOURCE-STRAY EXCEPTION (2026-08-17) ------------------------------------------ 151 // WHY IT IS SAFE, not a loosened guard: `runtime/<name>.nx` at exactly ONE slash is NishiLang SOURCE 152 // in the SERVING root. Source is never served and never executed from this tree -- the build lane is 153 // buildroot-anchored (nx_sov_build_run chdirs to buildroot/), so the only resolver that can ever see 154 // such a file is a CWD-rooted one: the shadow-resolution defect class adjudicated 2026-08-17 155 // (ep_src_path was made buildroot-anchored-by-construction for exactly this reason). Retiring one can 156 // never stop a running service; LEAVING one arms the trap where a nishihost-CWD caller silently 157 // compiles a different file than the build lane. Measured population when written: 8 stray sources, 158 // each byte-identical to its buildroot twin or an adjudicated probe stub. 159 // STATED IMPRECISION: unlike the staged lane this walk applies the FULL deny-token table (source can 160 // be named anything), so a stray whose NAME carries a protected token (e.g. nx_servedrift.nx) stays 161 // unretirable here -- over-refusal is the direction that costs a stray, never a service. 162 if rt_slashes(path) == 1 { if rt_starts(path, "runtime/" as *u8) == 1 { if rt_ends(path, ".nx" as *u8) == 1 { 163 var sd: i64 = 0 164 while sd < rt_deny_n() { 165 if rt_has(path, rt_deny_tok(sd)) == 1 { rt_hit = rt_deny_tok(sd); return 4 } 166 sd = sd + 1 167 } 168 return 0 169 } } } 170 // ---- COMPARE-BOARD PAGE EXCEPTION (2026-09-16) -------------------------------------------------- 171 // WHY IT IS SAFE, not a loosened guard: a file under sites/nishifamily/compare/<board>/ is a PAGE the /compare 172 // gateway reads from disk; no daemon runs from that tree, so retiring one can never stop a service. The <board> 173 // segment is a DOMAIN NAME from regen.list -- a data word -- and the token table read it as a material type: 174 // `cleanserve` carries `serve`, so a 60-day-old copy of an earlier radar page, sitting as a regular FILE exactly 175 // where the generator must create cleanserve/frontier/, could never be retired, and every full regen published 176 // RED on it (RECEIPT-TRANSACTION-FAIL syscall=-20, ENOTDIR). The FULL token table still applies to everything 177 // BELOW the board segment (compare/<board>/x.elf, compare/<board>/nx_media_serve stay refused), the SECRET subset 178 // still applies to the WHOLE path (a board segment carrying vault or mint stays refused), traversal was refused 179 // above, and only a file below a board enters: the board directory and the compare root carry too few slashes. 180 // STATED IMPRECISION: the prefix is the nishifamily compare docroot by name; a second compare docroot would need 181 // its own row, and until then it stays in the stricter general lane (over-refusal, never a service). 182 if rt_starts(path, RT_COMPARE_ROOT) == 1 { if rt_slashes(path) >= RT_COMPARE_MIN_SLASHES { 183 let csec: i64 = rt_refuse_secret(path) 184 if csec != 0 { return csec } 185 var bend: i64 = rt_len(RT_COMPARE_ROOT) 186 while path[bend] != (0 as u8) { if path[bend] == (RT_SLASH as u8) { break } bend = bend + 1 } 187 let below: *u8 = (path as i64 + bend + 1) as *u8 188 var cd: i64 = 0 189 while cd < rt_deny_n() { 190 if rt_has(below, rt_deny_tok(cd)) == 1 { rt_hit = rt_deny_tok(cd); return 4 } 191 cd = cd + 1 192 } 193 return 0 194 } } 195 if rt_slashes(path) < 2 { return 3 } 196 var d: i64 = 0 197 while d < rt_deny_n() { 198 if rt_has(path, rt_deny_tok(d)) == 1 { rt_hit = rt_deny_tok(d); return 4 } 199 d = d + 1 200 } 201 return 0 202} 203// ---- THE DIRECTORY LANE (2026-08-15) --------------------------------------------------------------- 204// WHY IT EXISTS: the file rule COUNTS SLASHES and needs >=2 (dir/subdir/file), which is right for files 205// and leaves a whole class unreachable -- an accidental orphan DIRECTORY is created at exactly 206// `knowledge/<name>`, one slash, so the organ that exists to clean up stray artifacts could not touch the 207// commonest stray shape. Measured this session: a 235-file 9.7 MB duplicate mirror sat at 208// knowledge/gfxmirror with no lane to retire it, and an unreferenced tree is not merely wasted disk -- 209// every reachability question asked over the tree silently consumes it as if it were a registry. 210// 211// WHY IT IS NOT A LOOSENED GUARD. It is strictly NARROWER than the file lane in the direction that 212// matters, and it borrows the estate's own established idiom for a removal that cannot be undone by 213// guessing: TWO NAMES. nx_toolreg's removal verb takes `<name> <by>` precisely so one name is never 214// enough, and the same reasoning applies with more force here, because a directory carries everything 215// beneath it. Every existing protection still applies unchanged -- traversal, the full 11-token protected 216// table, secret material, must-exist, and rename-never-delete. 217// * REFUSES a bare top-level name (zero slashes): `knowledge` can never be retired. 218// * REFUSES anything under a SERVED root by prefix. A directory under sites/ is a site or part of one, 219// and the cost of being wrong there is a whole surface going dark, so the directory lane simply does 220// not go there. That is a STATED limitation, not an oversight: retiring inside a docroot stays a 221// file-at-a-time operation where the existing >=2 rule already applies. 222// * REFUSES unless the confirmation name is byte-identical to the path. 223const RT_SERVED_ROOT: *u8 = "sites/" 224 225func rt_same(a: *u8, b: *u8) -> i64 { 226 var i: i64 = 0 227 while a[i] != (0 as u8) { 228 if a[i] != b[i] { return 0 } 229 i = i + 1 230 } 231 if b[i] != (0 as u8) { return 0 } 232 return 1 233} 234 235func rt_refuse_dir(path: *u8, confirm: *u8) -> i64 { 236 if rt_len(path) < 3 { return 1 } 237 if rt_has(path, ".." as *u8) == 1 { return 2 } 238 if rt_slashes(path) < 1 { return 6 } 239 if rt_starts(path, RT_SERVED_ROOT) == 1 { return 5 } 240 var d: i64 = 0 241 while d < rt_deny_n() { 242 if rt_has(path, rt_deny_tok(d)) == 1 { rt_hit = rt_deny_tok(d); return 4 } 243 d = d + 1 244 } 245 // the two-name check runs LAST so a caller who mistyped a PROTECTED path still learns which rule 246 // protects it, rather than being told only that their confirmation did not match. 247 if rt_same(path, confirm) == 0 { return 7 } 248 return 0 249} 250 251// flatten "a/b/c" -> "a_b_c" into dst 252func rt_flatten(dst: *u8, src: *u8) -> i64 { 253 var o: i64 = 0 254 var i: i64 = 0 255 while src[i] != (0 as u8) { 256 if src[i] == (47 as u8) { dst[o] = 95 as u8 } else { dst[o] = src[i] } 257 o = o + 1 258 i = i + 1 259 } 260 return o 261} 262func rt_cat(dst: *u8, off: i64, s: *u8) -> i64 { 263 var o: i64 = off 264 var i: i64 = 0 265 while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } 266 return o 267} 268 269// ONE PLACE NAMES EACH REFUSAL, AND THE MESSAGE READS IT. 270// The refusal used to print a hand-maintained "1=... 2=... 3=... 4=..." list beside rt_refuse's codes: 271// two parallel lists, so adding a code left the message silently lying, and correcting the WORDING (as was 272// done earlier today, after code 3 named the wrong unit and sent a caller hunting) does nothing to stop the 273// next drift. ★★★A MESSAGE MAINTAINED BESIDE THE PREDICATE IT DESCRIBES IS A DUPLICATE RULER; DERIVE IT 274// FROM THE CODE AND THERE IS ONE PLACE TO GET IT RIGHT. 275// It also prints ONLY the rule that actually fired -- the estate's own law that a refusal must say WHICH 276// rule refused, not recite every rule it might have applied. 277// ★UNKNOWN IS ITS OWN BUCKET and it accuses THIS ORGAN, not the caller's path: if rt_refuse ever returns a 278// code rt_reason does not name, the drift ANNOUNCES itself instead of mislabelling the refusal as one of 279// the codes it happens to sit near. 280func rt_reason(code: i64) -> *u8 { 281 if code == 1 { return "path too short to be a real artifact" as *u8 } 282 if code == 2 { return "traversal -- the path contains .." as *u8 } 283 if code == 3 { return "too shallow -- needs >=2 slashes (dir/subdir/file), so a file sitting directly inside a top-level dir is refused" as *u8 } 284 if code == 4 { return "protected name -- live/serving or secret material; the token that fired is named as token= below" as *u8 } 285 if code == 5 { return "directory lane refuses a SERVED root -- a directory under sites/ is a site or part of one, and retiring it would take a whole surface dark; retire inside a docroot one file at a time" as *u8 } 286 if code == 6 { return "directory lane needs >=1 slash -- a bare top-level name like knowledge can never be retired" as *u8 } 287 if code == 7 { return "confirmation name does not match the path -- retiredir takes the path TWICE on purpose, so one name is never enough to move a whole tree" as *u8 } 288 return "UNRECOGNISED CODE -- rt_refuse returned a code rt_reason does not name. That is a defect in THIS ORGAN, not in the path you gave it." as *u8 289} 290 291// returns 0 on success; prints the refusal and returns nonzero otherwise 292func rt_retire(path: *u8) -> i64 { 293 let why: i64 = rt_refuse(path) 294 if why != 0 { 295 rt_say("RETIRE-REFUSED code=" as *u8); rt_num(why) 296 rt_say(" path=" as *u8); rt_say(path) 297 rt_say(" reason=" as *u8); rt_say(rt_reason(why)) 298 // NAME THE RULE THAT FIRED, NOT THE SET IT BELONGS TO. ★UNKNOWN IS ITS OWN BUCKET and it accuses 299 // THIS ORGAN: a code 4 that reached here without recording a token is a defect in rt_refuse, and 300 // saying so is strictly better than printing a plausible token nobody verified fired. 301 if why == 4 { 302 rt_say(" token=" as *u8) 303 if (rt_hit as i64) == 0 { rt_say("UNRECORDED -- defect in THIS ORGAN: rt_refuse returned 4 without recording which token matched" as *u8) } else { rt_say(rt_hit) } 304 } 305 rt_say("\n" as *u8) 306 return why 307 } 308 if eg_have(path) == 0 { 309 // a no-op must never look like a success 310 rt_say("RETIRE-ABSENT path=" as *u8); rt_say(path); rt_say("\n" as *u8) 311 return 9 312 } 313 sys_mkdir("knowledge/retired" as *u8, 493) 314 let dst: *u8 = eg_buf(RP_NAMEBUF, "retire destination" as *u8) 315 var o: i64 = rt_cat(dst, 0, "knowledge/retired/" as *u8) 316 o = o + rt_flatten((((dst as i64) + o) as *u8), path) 317 o = rt_cat(dst, o, "-" as *u8) 318 // stamp so repeated retires of the same name never collide 319 var ts: i64 = sys_now_realtime_sec() 320 let tb: *u8 = eg_buf(32, "retire stamp" as *u8) 321 var k: i64 = 0 322 while ts > 0 { tb[k] = (48 + (ts % 10)) as u8; ts = ts / 10; k = k + 1 } 323 while k > 0 { k = k - 1; dst[o] = tb[k]; o = o + 1 } 324 dst[o] = 0 as u8 325 let rc: i64 = sys_renameat(path, dst) 326 if rc != 0 { 327 rt_say("RETIRE-FAIL rename rc=" as *u8); rt_num(rc) 328 rt_say(" path=" as *u8); rt_say(path); rt_say("\n" as *u8) 329 return 3 330 } 331 rt_say("{\"organ\":\"nx_retire_path\",\"v\":1,\"retired\":\"" as *u8); rt_say(path) 332 rt_say("\",\"to\":\"" as *u8); rt_say(dst) 333 rt_say("\",\"deleted\":0,\"reversible\":1}\n" as *u8) 334 return 0 335} 336 337// The directory twin of rt_retire. Same rename-never-delete move, same stamped destination, so a tree 338// retired in error is restored by renaming it back -- the bytes are never at risk. 339func rt_retiredir(path: *u8, confirm: *u8) -> i64 { 340 let why: i64 = rt_refuse_dir(path, confirm) 341 if why != 0 { 342 rt_say("RETIREDIR-REFUSED code=" as *u8); rt_num(why) 343 rt_say(" path=" as *u8); rt_say(path) 344 rt_say(" reason=" as *u8); rt_say(rt_reason(why)) 345 if why == 4 { 346 rt_say(" token=" as *u8) 347 if (rt_hit as i64) == 0 { rt_say("UNRECORDED -- defect in THIS ORGAN" as *u8) } else { rt_say(rt_hit) } 348 } 349 rt_say("\n" as *u8) 350 return why 351 } 352 if eg_have(path) == 0 { 353 rt_say("RETIREDIR-ABSENT path=" as *u8); rt_say(path); rt_say("\n" as *u8) 354 return 9 355 } 356 sys_mkdir("knowledge/retired" as *u8, 493) 357 let dst: *u8 = eg_buf(RP_NAMEBUF, "retiredir destination" as *u8) 358 var o: i64 = rt_cat(dst, 0, "knowledge/retired/" as *u8) 359 o = o + rt_flatten((((dst as i64) + o) as *u8), path) 360 o = rt_cat(dst, o, "-" as *u8) 361 var ts: i64 = sys_now_realtime_sec() 362 let tb: *u8 = eg_buf(32, "retiredir stamp" as *u8) 363 var k: i64 = 0 364 while ts > 0 { tb[k] = (48 + (ts % 10)) as u8; ts = ts / 10; k = k + 1 } 365 while k > 0 { k = k - 1; dst[o] = tb[k]; o = o + 1 } 366 dst[o] = 0 as u8 367 let rc: i64 = sys_renameat(path, dst) 368 if rc != 0 { 369 rt_say("RETIREDIR-FAIL rename rc=" as *u8); rt_num(rc) 370 rt_say(" path=" as *u8); rt_say(path); rt_say("\n" as *u8) 371 return 3 372 } 373 rt_say("{\x22organ\x22:\x22nx_retire_path\x22,\x22v\x22:1,\x22retireddir\x22:\x22" as *u8); rt_say(path) 374 rt_say("\x22,\x22to\x22:\x22" as *u8); rt_say(dst) 375 rt_say("\x22,\x22deleted\x22:0,\x22reversible\x22:1}\n" as *u8) 376 return 0 377} 378 379func main(argc: i64, argv: *i64) -> i64 { 380 if argc < 2 { 381 rt_say("usage: nx_retire_path {retire <path> | selftest}\n" as *u8) 382 rt_say(" moves a path into knowledge/retired/ (never deletes; bytes preserved, reversible)\n" as *u8) 383 sys_exit(2) 384 return 2 385 } 386 let verb: *u8 = argv[1] as *u8 387 if rt_has(verb, "selftest" as *u8) == 1 { 388 var fails: i64 = 0 389 // the refusals are the whole safety story, so they are what the selftest proves 390 if rt_refuse("nx_mgmt_api.elf" as *u8) == 0 { fails = fails + 1 } 391 if rt_refuse("tools_cap_secret.key" as *u8) == 0 { fails = fails + 1 } 392 if rt_refuse("sites/../../etc/passwd" as *u8) == 0 { fails = fails + 1 } 393 if rt_refuse("daemons.reg" as *u8) == 0 { fails = fails + 1 } 394 if rt_refuse("sites" as *u8) == 0 { fails = fails + 1 } 395 if rt_refuse("sites/nishifamily" as *u8) == 0 { fails = fails + 1 } 396 if rt_refuse("nx_sites_daemon_v2.elf" as *u8) == 0 { fails = fails + 1 } 397 // and a legitimate scratch path must be ALLOWED (a guard that refuses everything is useless) 398 if rt_refuse("sites/nishifamily/factory/gen-coffee/d1" as *u8) != 0 { fails = fails + 1 } 399 if rt_refuse("knowledge/status/uigen_tabprobe.txt" as *u8) != 0 { fails = fails + 1 } 400 // ---- STAGED-ARTIFACT EXCEPTION: it must ADMIT the hazard class and REFUSE every neighbour ---- 401 // ADMIT: root-level staged elfs (both staging shapes) -- the 512-strong stray class 402 if rt_refuse("nx_hostctl.elf.new" as *u8) != 0 { fails = fails + 1 } 403 if rt_refuse("nx_daemon_supervisor.sov.elf.new" as *u8) != 0 { fails = fails + 1 } 404 // ADMIT the BANKED twin that /api/build's new anti-clobber bank creates (debt 1785772241) 405 if rt_refuse("nx_tool_argecho.sov.elf.new.prev" as *u8) != 0 { fails = fails + 1 } 406 // REFUSE a LIVE .prev -- promote/deploy bank the RUNNING binary under this name, and that copy is 407 // the rollback source. Only the STAGED lineage (.elf.new.prev) is exempt, never `<name>.elf.prev`. 408 if rt_refuse("nx_mgmt_api.elf.prev" as *u8) == 0 { fails = fails + 1 } 409 // REFUSE: the LIVE binary of the same organ (the one-character difference that matters most) 410 if rt_refuse("nx_hostctl.elf" as *u8) == 0 { fails = fails + 1 } 411 // REFUSE: staged SECRET material -- the secret guards survive the exception 412 if rt_refuse("tools_cap_secret.key.new" as *u8) == 0 { fails = fails + 1 } 413 if rt_refuse("opaque_keys.bin.elf.new" as *u8) == 0 { fails = fails + 1 } 414 // REFUSE: traversal is never exempted, even wearing the staged suffix 415 if rt_refuse("../../etc/evil.elf.new" as *u8) == 0 { fails = fails + 1 } 416 // REFUSE: SUFFIX not substring -- a live path merely CONTAINING the marker is not exempt 417 if rt_refuse("nx_thing.elf.new.elf" as *u8) == 0 { fails = fails + 1 } 418 // ---- SOURCE-STRAY EXCEPTION (2026-08-17): ADMIT the shadow class, REFUSE every neighbour ---- 419 // ADMIT: a one-slash serving-root source -- the measured shadow class this lane exists for 420 if rt_refuse("runtime/nx_regdup.nx" as *u8) != 0 { fails = fails + 1 } 421 // REFUSE: same depth, not source -- an artifact wearing runtime/ is not exempted 422 if rt_refuse("runtime/nx_regdup.elf" as *u8) == 0 { fails = fails + 1 } 423 // REFUSE + NAME: the full token table survives this exception (unlike the staged lane) 424 if rt_refuse("runtime/nx_servedrift.nx" as *u8) != 4 { fails = fails + 1 } 425 if rt_has(rt_hit, "serve" as *u8) != 1 { fails = fails + 1 } 426 // REFUSE: the prefix is `runtime/` exactly -- a sibling dir cannot borrow the lane 427 if rt_refuse("myruntime/nx_regdup.nx" as *u8) == 0 { fails = fails + 1 } 428 // REFUSE: traversal is never exempted in any lane 429 if rt_refuse("runtime/../nx_regdup.nx" as *u8) == 0 { fails = fails + 1 } 430 // ---- COMPARE-BOARD PAGE EXCEPTION (2026-09-16): ADMIT a page below a board whose NAME carries a token ---- 431 // ADMIT: the exact stale page that held every full regen RED 432 if rt_refuse("sites/nishifamily/compare/cleanserve/frontier" as *u8) != 0 { fails = fails + 1 } 433 // REFUSE + NAME: the full table still applies below the board segment 434 if rt_refuse("sites/nishifamily/compare/cleanserve/nx_media_serve" as *u8) != 4 { fails = fails + 1 } 435 if rt_has(rt_hit, "serve" as *u8) != 1 { fails = fails + 1 } 436 if rt_refuse("sites/nishifamily/compare/search/nx_thing.elf" as *u8) != 4 { fails = fails + 1 } 437 // REFUSE: the secret subset still applies to the WHOLE path, board segment included 438 if rt_refuse("sites/nishifamily/compare/vaultboard/index.html" as *u8) != 4 { fails = fails + 1 } 439 if rt_has(rt_hit, "vault" as *u8) != 1 { fails = fails + 1 } 440 // REFUSE: the board directory itself never enters the lane (too few slashes, general table fires) 441 if rt_refuse("sites/nishifamily/compare/cleanserve" as *u8) != 4 { fails = fails + 1 } 442 // REFUSE: the prefix is exact -- a sibling tree and a lookalike root cannot borrow the lane 443 if rt_refuse("sites/nishifamily/cleanserve/frontier" as *u8) != 4 { fails = fails + 1 } 444 if rt_refuse("sites/nishifamily/compareX/cleanserve/frontier" as *u8) != 4 { fails = fails + 1 } 445 // REFUSE: traversal is never exempted in any lane 446 if rt_refuse("sites/nishifamily/compare/cleanserve/../../x" as *u8) != 2 { fails = fails + 1 } 447 // absent target must not report success 448 if rt_retire("sites/nishifamily/factory/definitely-not-here-xyz" as *u8) != 9 { fails = fails + 1 } 449 // and an absent STAGED artifact must also refuse (the exception must not fake a success) 450 if rt_retire("nx_definitely_not_staged_xyz.elf.new" as *u8) != 9 { fails = fails + 1 } 451 // ---- THE REFUSAL MUST NAME THE RULE THAT FIRED (regression, 2026-08-15) ------------------- 452 // The exact caller path that exposed the drift. It MUST refuse -- `nx_servedrift` contains `serve`. 453 if rt_refuse("buildroot/runtime/nx_servedrift.nx" as *u8) != 4 { fails = fails + 1 } 454 // ANTI-VACUITY: refusing is NOT enough. A guard that refuses correctly while naming the wrong rule 455 // passes every refuse-only test -- which is precisely how the 9-vs-11 drift survived. Assert the 456 // ORGAN NAMED THE TOKEN, not merely that it said no. 457 if rt_len(rt_hit) != 5 { fails = fails + 1 } 458 if rt_has(rt_hit, "serve" as *u8) != 1 { fails = fails + 1 } 459 // the OTHER token the stale message omitted -- proven reachable AND correctly named 460 if rt_refuse("sites/nishifamily/nx_daemon_notes.txt" as *u8) != 4 { fails = fails + 1 } 461 if rt_has(rt_hit, "daemon" as *u8) != 1 { fails = fails + 1 } 462 // neg-control-reason-naming: an ALLOWED path must still be allowed after all that token traffic, 463 // so the naming machinery cannot have turned the guard into one that refuses everything. 464 if rt_refuse("knowledge/status/uigen_tabprobe.txt" as *u8) != 0 { fails = fails + 1 } 465 // ---- THE DIRECTORY LANE (2026-08-15) ----------------------------------------------------- 466 // REFUSE a bare top-level name: the commonest catastrophic typo, and the one the file rule's 467 // slash count was really protecting against. 468 if rt_refuse_dir("knowledge" as *u8, "knowledge" as *u8) != 6 { fails = fails + 1 } 469 // REFUSE a SERVED root and anything under it -- by PREFIX, so depth cannot buy a way in. 470 if rt_refuse_dir("sites/nishifamily" as *u8, "sites/nishifamily" as *u8) != 5 { fails = fails + 1 } 471 if rt_refuse_dir("sites/nishifamily/world" as *u8, "sites/nishifamily/world" as *u8) != 5 { fails = fails + 1 } 472 // REFUSE traversal, which is never exempted in either lane 473 if rt_refuse_dir("knowledge/../../etc" as *u8, "knowledge/../../etc" as *u8) != 2 { fails = fails + 1 } 474 // REFUSE protected material -- the full token table still applies to directories 475 if rt_refuse_dir("knowledge/opaque_store" as *u8, "knowledge/opaque_store" as *u8) != 4 { fails = fails + 1 } 476 if rt_refuse_dir("knowledge/daemon_logs" as *u8, "knowledge/daemon_logs" as *u8) != 4 { fails = fails + 1 } 477 // ★THE TWO-NAME CONFIRMATION IS THE POINT: the SAME path that is allowed with a matching 478 // confirmation must be REFUSED when the confirmation differs by one character. Without this 479 // tooth the second argument would be decoration. 480 if rt_refuse_dir("knowledge/gfxmirror" as *u8, "knowledge/gfxmirro" as *u8) != 7 { fails = fails + 1 } 481 if rt_refuse_dir("knowledge/gfxmirror" as *u8, "knowledge/gfxmirrorx" as *u8) != 7 { fails = fails + 1 } 482 // neg-control-dir-allow: a guard that refuses everything passes every refusal tooth above, so a 483 // legitimate orphan directory with a MATCHING confirmation must be ALLOWED. 484 if rt_refuse_dir("knowledge/gfxmirror" as *u8, "knowledge/gfxmirror" as *u8) != 0 { fails = fails + 1 } 485 // an absent directory must not report success 486 if rt_retiredir("knowledge/definitely-not-here-xyz" as *u8, "knowledge/definitely-not-here-xyz" as *u8) != 9 { fails = fails + 1 } 487 // NO UNREACHABLE ROWS: every table entry must be a real token, so the table can never grow a row 488 // the predicate cannot fire on or the message cannot name. 489 var ti: i64 = 0 490 while ti < rt_deny_n() { 491 if rt_len(rt_deny_tok(ti)) == 0 { fails = fails + 1 } 492 ti = ti + 1 493 } 494 rt_say("NX-RETIRE-PATH selftest fails=" as *u8); rt_num(fails) 495 // ★THE VERDICT LINE DOES NOT RECITE COUNTS. The old one hand-maintained "12 protected paths / 4 496 // retirable / 2 absent" beside the teeth -- the SAME duplicate-ruler defect this session came here 497 // to fix, one line below the fix. Adding a tooth silently made it lie. 498 if fails == 0 { rt_say(" verdict=GREEN (all refusal, allow, staged-exception and reason-naming teeth passed)\n" as *u8); sys_exit(0); return 0 } 499 rt_say(" verdict=RED\n" as *u8) 500 sys_exit(1) 501 return 1 502 } 503 // EXACT verb match, never substring: `retire` is a prefix of `retiredir`, so a substring test would 504 // route every retiredir call into the file lane and refuse it for the wrong reason. 505 if rt_same(verb, "retiredir" as *u8) == 1 { 506 if argc < 4 { 507 rt_say("usage: nx_retire_path retiredir <path> <same-path-again>\n" as *u8) 508 rt_say(" the path is required TWICE on purpose -- one name is never enough to move a whole tree\n" as *u8) 509 sys_exit(2) 510 return 2 511 } 512 let drc: i64 = rt_retiredir(argv[2] as *u8, argv[3] as *u8) 513 sys_exit(drc) 514 return drc 515 } 516 if argc < 3 { 517 rt_say("usage: nx_retire_path {retire <path> | retiredir <path> <path> | selftest}\n" as *u8) 518 sys_exit(2) 519 return 2 520 } 521 let rc: i64 = rt_retire(argv[2] as *u8) 522 sys_exit(rc) 523 return rc 524}