code wiki / _hdl_build / nx_cms_seo_gate.nx

nx_cms_seo_gate.nx source

↩ module page · 322 lines · 17731 B

1// nx_cms_seo_gate.nx -- CMS W2 GATE (re-runnable, evidence-driven): SEO head + sitemap + robots 2// (the Yoast class). Proves the TEAM-AUTHORED _pe_seohead head renders into the live page; values 3// are attribute-ESCAPED (no injection through a SEO field); empty SEO title/desc FALL BACK to the 4// headline/tagline; /sitemap.xml is well-formed and references the site URL; /robots.txt points at 5// the sitemap. Appends "CMSGATE row=nx_cms_seo ... verdict=PASS|FAIL". Exit 0 iff all rows pass. 6// license_tier: ORIGINAL 7import "nx_cms_store.nx" 8import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 9import "nx_sha256.nx" 10import "nx_syscalls.nx" 11import "_hdl_build/nx_kill_portable.nx" 12 13const SG_PORT: i64 = 8088 14const SG_SITE: *u8 = "/tmp/_cms_seo_site" 15 16func sg_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 17func sg_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 18func sg_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var k: i64=0; while s[k]!=(0 as u8){dst[o]=s[k];o=o+1;k=k+1} return o } 19func sg_catn(dst: *u8, off: i64, s: *u8, n: i64) -> i64 { var o: i64=off; var k: i64=0; while k<n{dst[o]=s[k];o=o+1;k=k+1} return o } 20func sg_index(hay: *u8, n: i64, needle: *u8) -> i64 { 21 let nl: i64 = sg_len(needle) 22 var i: i64 = 0 23 while i + nl <= n { 24 var q: i64 = 0 25 var ok: i64 = 1 26 while q < nl { if (hay[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 } 27 if ok == 1 { return i } 28 i = i + 1 29 } 30 return 0 - 1 31} 32func sg_has(hay: *u8, n: i64, needle: *u8) -> i64 { if sg_index(hay, n, needle) >= 0 { return 1 } return 0 } 33 34func sg_http(req: *u8, rl: i64, resp: *u8, cap: i64) -> i64 { 35 let fd: i64 = sys_socket(2, 1, 0) 36 if fd < 0 { return 0 - 1 } 37 let addr: *u8 = sys_mmap(16) 38 addr[0] = 2 as u8; addr[1] = 0 as u8 39 addr[2] = ((SG_PORT >> 8) & 0xff) as u8 40 addr[3] = (SG_PORT & 0xff) as u8 41 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8 42 var zi: i64 = 8 43 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 } 44 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 } 45 var off: i64 = 0 46 var go: i64 = 1 47 while go == 1 { 48 go = 0 49 if off < rl { let w: i64 = sys_write(fd, (req + off) as *u8, rl - off); if w > 0 { off = off + w; go = 1 } } 50 } 51 sys_set_socket_timeout(fd, 10) 52 var total: i64 = 0 53 go = 1 54 while go == 1 { 55 go = 0 56 if total < cap { let r: i64 = sys_read(fd, (resp + total) as *u8, cap - total); if r > 0 { total = total + r; go = 1 } } 57 } 58 sys_close(fd) 59 return total 60} 61func sg_post(req: *u8, path: *u8, cookie: *u8, body: *u8) -> i64 { 62 var o: i64 = sg_cat(req, 0, "POST " as *u8) 63 o = sg_cat(req, o, path) 64 o = sg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\nContent-Type: application/x-www-form-urlencoded\r\n" as *u8) 65 if (cookie[0] as i64) != 0 { o = sg_cat(req, o, "Cookie: nsess=" as *u8); o = sg_catn(req, o, cookie, 32); o = sg_cat(req, o, "\r\n" as *u8) } 66 o = sg_cat(req, o, "Content-Length: " as *u8) 67 let bl: i64 = sg_len(body) 68 let t: *u8 = sys_mmap(24) 69 var m: i64 = bl 70 var k: i64 = 0 71 if m == 0 { t[0] = 48 as u8; k = 1 } 72 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 73 var i: i64 = 0 74 while i < k { req[o] = t[k-1-i]; o = o + 1; i = i + 1 } 75 o = sg_cat(req, o, "\r\n\r\n" as *u8) 76 o = sg_cat(req, o, body) 77 return o 78} 79func sg_get(req: *u8, path: *u8, cookie: *u8) -> i64 { 80 var o: i64 = sg_cat(req, 0, "GET " as *u8) 81 o = sg_cat(req, o, path) 82 o = sg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\n" as *u8) 83 if (cookie[0] as i64) != 0 { o = sg_cat(req, o, "Cookie: nsess=" as *u8); o = sg_catn(req, o, cookie, 32); o = sg_cat(req, o, "\r\n" as *u8) } 84 o = sg_cat(req, o, "\r\n" as *u8) 85 return o 86} 87func sg_row(id: i64, ok: i64, what: *u8) -> i64 { 88 sg_w("SEOROW " as *u8) 89 let d: *u8 = sys_mmap(8); d[0] = (48 + id/10) as u8; d[1] = (48 + id%10) as u8; d[2] = 32 as u8; d[3] = 0 as u8 90 sg_w(d) 91 if ok == 1 { sg_w("PASS " as *u8) } 92 if ok == 0 { sg_w("FAIL " as *u8) } 93 sg_w(what); sg_w("\n" as *u8) 94 return ok 95} 96func sg_mkdir(path: *u8, mode: i64) -> i64 { let nb: *i64 = sys_mmap(8) as *i64; nb[0] = 258; return __syscall(nb[0], 0 - 100, path, mode, 0, 0, 0) } 97func sg_writefile(path: *u8, buf: *u8, n: i64) -> i64 { let fd: i64 = sys_openat_wr(path, 0x1a4); if fd < 0 { return 0 } sys_write(fd, buf, n); sys_close(fd); return 1 } 98 99func main() -> i64 { 100 sg_mkdir(SG_SITE, 0x1ed) 101 // template renders the team SEO head + the headline (so the h1 still proves content) 102 let tpl: *u8 = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">{{seo_head_html}}</head><body><h1>{{hero_title}}</h1></body></html>" as *u8 103 sg_writefile("/tmp/_cms_seo_site/template.html" as *u8, tpl, sg_len(tpl)) 104 // ★★SEED-SOURCE CHECK ADDED 2026-08-01 -- THE UNCHECKED COPY WAS THE WHOLE MYSTERY. 105 // This gate seeds its fixture by copying knowledge/cms/andelinwest/content.txt. That directory does 106 // NOT EXIST on this host (nx_fs ls knowledge/cms -> ABSENT), the copy's return was DISCARDED, so the 107 // site dir got no content.txt, ca_render_publish returned 0, the admin printed SEED-RENDER-FAIL and 108 // ALL EIGHT ROWS FAILED -- eight red lines describing eight capabilities, for one missing fixture 109 // file, with nothing anywhere naming it. 110 // ★AN UNCHECKED COPY TURNS A MISSING FIXTURE INTO A FALSE VERDICT ABOUT THE CODE UNDER TEST. 111 // The gate now REFUSES with the exact missing path instead of grading the subject on absent input: 112 // a gate must never report on a subject it failed to set up. 113 let seedsrc: *u8 = "knowledge/cms/andelinwest/content.txt" as *u8 114 let seeddst: *u8 = "/tmp/_cms_seo_site/content.txt" as *u8 115 // ★★I REINTRODUCED THE VERY DEFECT I WAS FIXING, ONE LAYER UP -- RECORDED BECAUSE IT IS THE 116 // INSTRUCTIVE PART. First fix keyed the fallback on "does the destination exist?" instead of "did 117 // the copy SUCCEED?", so a STALE FIXTURE from the previous run satisfied the check and silently 118 // overrode the corrected one: the gate reported the same 6/8 while running last run's data, and the 119 // "SEED:" line disappeared, which was the only visible tell. 120 // ★EXISTENCE IS NOT FRESHNESS. A /tmp artifact from a previous run is indistinguishable from a 121 // correct one by an existence test -- the same stale-/tmp trap that md5 caught during the seq1267 122 // caller-gap fix. Key the decision on the OPERATION'S RETURN VALUE (cst_copy gives 1/0 and the 123 // original code discarded it -- that discarded return WAS the root cause of this whole chain). 124 let copied: i64 = cst_copy(seedsrc, seeddst) 125 var seedchk: i64 = 0 - 1 126 if copied == 1 { seedchk = sys_openat_rd(seeddst) } 127 if seedchk < 0 { 128 // ★★MADE HERMETIC 2026-08-01. Refusing was the right FIRST move (it named the cause instead of 129 // grading the subject on absent input), but a gate that cannot run without an external site 130 // corpus is not a gate -- it is a gate PLUS a dependency nobody declared. This gate tests SEO 131 // RENDERING BEHAVIOUR, not the content of any particular site, so the fixture belongs to the 132 // gate. It writes its own minimal store (@key\nvalue\n records) and says so in the output. 133 // ★A TEST FIXTURE THAT LIVES OUTSIDE THE TEST IS A SILENT PREREQUISITE, AND IT FAILS ON THE 134 // FIRST HOST THAT NEVER HAD IT -- which is exactly how eight capability rows came to be red 135 // for one absent file. Deliberately carries hero_title and NO seo_title, because row 0 proves 136 // the title FALLS BACK to the headline when SEO fields are unset. 137 // hero_title MUST be exactly what row 0 asserts the <title> falls back to. My first fixture 138 // said "Andelin West" and row 0 failed -- MY error, not the subject's. ★WHEN YOU REPLACE A 139 // FIXTURE, THE ASSERTIONS BECOME ITS SPECIFICATION: read what the rows expect, do not invent 140 // plausible-looking data and then read the resulting red as a defect. 141 let fx: *u8 = "@hero_title\nAndelin West Law\n@hero_sub\nSeed fixture owned by nx_cms_seo_gate\n" as *u8 142 sg_writefile(seeddst, fx, sg_len(fx)) 143 seedchk = sys_openat_rd(seeddst) 144 if seedchk < 0 { 145 sg_w("CMSGATE row=nx_cms_seo SEED-WRITE-FAILED -- cannot create own fixture, NOT a subject failure\n" as *u8) 146 sys_exit(2) 147 } 148 sg_w("SEED: corpus absent, using gate-owned hermetic fixture (declared, not silent)\n" as *u8) 149 } 150 sys_close(seedchk) 151 sys_renameat("/tmp/_cms_seo_site/draft.txt" as *u8, "/tmp/_cms_seo_site/draft.stale" as *u8) 152 let dig: *u8 = sys_mmap(32) 153 sha256_digest("seo-gate-pw-1" as *u8, 13, dig) 154 let hexs: *u8 = sys_mmap(80) 155 let hxc: *u8 = "0123456789abcdef" as *u8 156 var i: i64 = 0 157 while i < 32 { hexs[i*2] = hxc[((dig[i] as i64) >> 4) & 15]; hexs[i*2+1] = hxc[(dig[i] as i64) & 15]; i = i + 1 } 158 sg_writefile("/tmp/_cms_seo_site/admin_pw.sha256" as *u8, hexs, 64) 159 160 let bpid: i64 = sys_fork() 161 if bpid == 0 { 162 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4) 163 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) } 164 let bargv: *i64 = sys_mmap(32) as *i64 165 bargv[0] = "_offc/nx_sov_build_run.elf" as *u8 as i64 166 bargv[1] = "nx_cms_admin" as *u8 as i64 167 bargv[2] = 0 168 let benvp: *i64 = sys_mmap(16) as *i64; benvp[0] = 0 169 sys_execve("_offc/nx_sov_build_run.elf" as *u8, bargv, benvp); sys_exit(127) 170 } 171 let bst: *i64 = sys_mmap(16) as *i64 172 sys_wait4(bpid, bst, 0) 173 // ★CALLER-GAP FIX 2026-08-01 (same class as seq1267, which fixed nx_hostctl + nx_restage and left 174 // these 11 cms gates behind): nx_sov_build_run EMITS buildroot/_build/<t>.sov.elf under the no-/tmp 175 // doctrine, while this gate still probed /tmp/<t>.sov.elf. PROVEN BY CONTROL before editing -- 176 // buildroot/_build/nx_cms_admin.sov.elf reads 208962B, /tmp/nx_cms_admin.sov.elf cannot be read. 177 // The gate was reporting ADMIN-BUILD-MISSING while the admin build was sitting there, built fine. 178 // ★A PRODUCER THAT MOVES ITS OUTPUT AND A CONSUMER THAT KEEPS PROBING THE OLD PATH REPORT 179 // "MISSING" AND "BROKEN" IN THE SAME WORDS -- and only the second one sends you to the wrong file. 180 // Prefer _build; fall back to /tmp so an older runner on another host still works. 181 var bchk: i64 = sys_openat_rd("buildroot/_build/nx_cms_admin.sov.elf" as *u8) 182 var badm: *u8 = "buildroot/_build/nx_cms_admin.sov.elf" as *u8 183 if bchk < 0 { bchk = sys_openat_rd("/tmp/nx_cms_admin.sov.elf" as *u8); badm = "/tmp/nx_cms_admin.sov.elf" as *u8 } 184 if bchk < 0 { sg_w("CMSGATE row=nx_cms_seo ADMIN-BUILD-MISSING (probed buildroot/_build AND /tmp)\n" as *u8); sys_exit(1) } 185 sys_close(bchk) 186 let pid: i64 = sys_fork() 187 if pid == 0 { 188 let lg: i64 = sys_openat_wr("/tmp/_cms_admin_seo_gate.log" as *u8, 0x1a4) 189 if lg >= 0 { sys_dup3(lg, 1, 0); sys_dup3(lg, 2, 0) } 190 let argvv: *i64 = sys_mmap(8*6) as *i64 191 argvv[0] = badm as i64 192 argvv[1] = "8088" as *u8 as i64 193 argvv[2] = SG_SITE as i64 194 argvv[3] = "99" as *u8 as i64 195 argvv[4] = 0 196 let envp: *i64 = sys_mmap(16) as *i64; envp[0] = 0 197 sys_execve(badm, argvv, envp) 198 sys_exit(127) 199 } 200 201 let req: *u8 = sys_mmap(262144) 202 let resp: *u8 = sys_mmap(524288) 203 let nocookie: *u8 = sys_mmap(8) 204 nocookie[0] = 0 as u8 205 let sess: *u8 = sys_mmap(64) 206 sess[0] = 0 as u8 207 let csrf: *u8 = sys_mmap(64) 208 csrf[0] = 0 as u8 209 210 // S0: boot + fallback head (no SEO fields yet) -> title falls back to hero_title 211 var rn: i64 = 0 - 1 212 var tries: i64 = 0 213 while tries < 50 { 214 let rl0: i64 = sg_get(req, "/" as *u8, nocookie) 215 rn = sg_http(req, rl0, resp, 524287) 216 if rn > 0 { tries = 50 } 217 if rn <= 0 { sys_sleep_ms(100); tries = tries + 1 } 218 } 219 var pass: i64 = 0 220 var rows: i64 = 0 221 var ok: i64 = 0 222 if rn > 0 { if sg_has(resp, rn, "<title>Andelin West Law</title>" as *u8) == 1 { if sg_has(resp, rn, "og:title" as *u8) == 1 { ok = 1 } } } 223 rows = rows + 1; pass = pass + sg_row(0, ok, "SEO title falls back to headline" as *u8) 224 225 // login + csrf 226 var rl: i64 = sg_post(req, "/admin/login" as *u8, nocookie, "pw=seo-gate-pw-1" as *u8) 227 rn = sg_http(req, rl, resp, 524287) 228 var have: i64 = 0 229 if rn > 0 { if sg_has(resp, rn, "303" as *u8) == 1 { 230 let sp: i64 = sg_index(resp, rn, "nsess=" as *u8) 231 if sp >= 0 { 232 var m2: i64 = 0 233 while m2 < 32 { sess[m2] = resp[sp+6+m2]; m2 = m2 + 1 } 234 sess[32] = 0 as u8 235 rl = sg_get(req, "/admin" as *u8, sess) 236 rn = sg_http(req, rl, resp, 524287) 237 let cp: i64 = sg_index(resp, rn, "name=\"csrf\" value=\"" as *u8) 238 if cp >= 0 { var m3: i64 = 0; while m3 < 32 { csrf[m3] = resp[cp+19+m3]; m3 = m3 + 1 } csrf[32] = 0 as u8; have = 1 } 239 } 240 } } 241 rows = rows + 1; pass = pass + sg_row(1, have, "login + editor shows SEO fields" as *u8) 242 ok = 0 243 if have == 1 { if sg_has(resp, rn, "name=\"seo_title\"" as *u8) == 1 { if sg_has(resp, rn, "name=\"site_url\"" as *u8) == 1 { ok = 1 } } } 244 rows = rows + 1; pass = pass + sg_row(2, ok, "editor carries seo_title + site_url inputs" as *u8) 245 246 // S3: save explicit SEO fields (desc has a <script> that MUST be escaped, not injected) 247 let body3: *u8 = sys_mmap(8192) 248 var b3: i64 = sg_cat(body3, 0, "csrf=" as *u8) 249 b3 = sg_catn(body3, b3, csrf, 32) 250 b3 = sg_cat(body3, b3, "&hero_title=Andelin+West+Law&seo_title=Andelin+West+Law+%E2%80%94+Trusted+Counsel&seo_desc=%3Cscript%3Ealert%284%29%3C%2Fscript%3EClient-first+legal+services.&og_image=https%3A%2F%2Fandelinwest.com%2Fog.png&site_url=https%3A%2F%2Fandelinwest.com" as *u8) 251 body3[b3] = 0 as u8 252 rl = sg_post(req, "/admin/save" as *u8, sess, body3) 253 rn = sg_http(req, rl, resp, 524287) 254 ok = 0 255 if rn > 0 { if sg_has(resp, rn, "303" as *u8) == 1 { ok = 1 } } 256 rows = rows + 1; pass = pass + sg_row(3, ok, "save SEO fields publishes" as *u8) 257 258 // S4: live page carries the explicit SEO title + description + og:image + canonical 259 rl = sg_get(req, "/" as *u8, nocookie) 260 rn = sg_http(req, rl, resp, 524287) 261 ok = 0 262 if rn > 0 { if sg_has(resp, rn, "Trusted Counsel" as *u8) == 1 { if sg_has(resp, rn, "og:image\" content=\"https://andelinwest.com/og.png\"" as *u8) == 1 { if sg_has(resp, rn, "rel=\"canonical\" href=\"https://andelinwest.com\"" as *u8) == 1 { ok = 1 } } } } 263 rows = rows + 1; pass = pass + sg_row(4, ok, "explicit SEO title/og:image/canonical render" as *u8) 264 265 // S5: the injected <script> in seo_desc is ESCAPED, not present as a tag 266 ok = 0 267 if rn > 0 { if sg_has(resp, rn, "Client-first legal services." as *u8) == 1 { if sg_has(resp, rn, "<script" as *u8) == 0 { if sg_has(resp, rn, "&lt;script" as *u8) == 1 { ok = 1 } } } } 268 rows = rows + 1; pass = pass + sg_row(5, ok, "SEO field XSS escaped not injected" as *u8) 269 270 // S6: /sitemap.xml well-formed + references the site URL home page 271 rl = sg_get(req, "/sitemap.xml" as *u8, nocookie) 272 rn = sg_http(req, rl, resp, 524287) 273 ok = 0 274 if rn > 0 { if sg_has(resp, rn, "application/xml" as *u8) == 1 { if sg_has(resp, rn, "<urlset" as *u8) == 1 { if sg_has(resp, rn, "<loc>https://andelinwest.com/</loc>" as *u8) == 1 { ok = 1 } } } } 275 rows = rows + 1; pass = pass + sg_row(6, ok, "sitemap.xml well-formed + home loc" as *u8) 276 277 // S7: /robots.txt references the sitemap 278 rl = sg_get(req, "/robots.txt" as *u8, nocookie) 279 rn = sg_http(req, rl, resp, 524287) 280 ok = 0 281 if rn > 0 { if sg_has(resp, rn, "text/plain" as *u8) == 1 { if sg_has(resp, rn, "Sitemap: https://andelinwest.com/sitemap.xml" as *u8) == 1 { if sg_has(resp, rn, "Allow: /" as *u8) == 1 { ok = 1 } } } } 282 rows = rows + 1; pass = pass + sg_row(7, ok, "robots.txt references the sitemap" as *u8) 283 284 // ---- teardown ---- 285 nxk_kill(pid, 9) 286 let stbuf: *i64 = sys_mmap(16) as *i64 287 sys_wait4(pid, stbuf, 0) 288 289 var verdict: *u8 = "FAIL" as *u8 290 if pass == rows { verdict = "PASS" as *u8 } 291 let line: *u8 = sys_mmap(512) 292 var lo: i64 = sg_cat(line, 0, "CMSGATE row=nx_cms_seo rows=" as *u8) 293 let t2: *u8 = sys_mmap(24) 294 var mm: i64 = rows 295 var kk: i64 = 0 296 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 297 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 298 var ii: i64 = 0 299 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 300 lo = sg_cat(line, lo, " pass=" as *u8) 301 mm = pass; kk = 0 302 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 303 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 304 ii = 0 305 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 306 lo = sg_cat(line, lo, " verdict=" as *u8) 307 lo = sg_cat(line, lo, verdict) 308 lo = sg_cat(line, lo, " epoch=" as *u8) 309 mm = sys_now_realtime_sec(); kk = 0 310 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 311 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 312 ii = 0 313 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 314 line[lo] = 10 as u8 315 lo = lo + 1 316 sys_write(1, line, lo) 317 let gf: i64 = sys_openat_append("knowledge/status/cms_gate.log" as *u8, 0x1a4) 318 if gf >= 0 { sys_write(gf, line, lo); sys_close(gf) } 319 if pass == rows { sys_exit(0) } 320 sys_exit(1) 321 return 1 322}