code wiki / _hdl_build / nx_wiki_publish.nx

nx_wiki_publish.nx source

↩ module page · 205 lines · 10237 B

1// nx_wiki_publish.nx -- A3: the ONE-CALL GUARDED PUBLISH WRAPPER (the enforcement point). EVERY page that 2// goes live MUST flow through pub_publish(); nothing reaches the served wiki doc-root unless the A2 guard 3// (nx_wiki_publish_guard pg_decide) returns PG_ALLOW. This is the wiki's publish PEP wrapped around the A2 4// PDP: same fail-closed / deny-by-default discipline as nx_access_pep around nx_access_wall. 5// 6// FLOW (composition, not reimplementation): 7// 1. read the page bytes from local_html_path. 8// 2. verdict = pg_decide(WAR_PREFIX, bytes, n, corpus...) -- the SOLE integrity authority (REUSED). 9// 3. REJECT -> return {status=PUB_REJECTED, code=<the exact PG_REJECT_* reason>, push_invoked=0}. 10// The push path is NEVER reached (fail-closed). This is THE safety property A3 exists to 11// guarantee: a guard-rejected page can never be streamed to the live site. 12// 4. ALLOW -> if do_push_flag == 0: return {status=PUB_ALLOW_NOPUSH, code=PG_ALLOW, push_invoked=0} 13// (the guard verdict is reported, but nothing is pushed -- lets the gate prove the ALLOW 14// path is reached WITHOUT touching the network). 15// -> if do_push_flag == 1: write the awpush.src/awpush.dst staging files for 16// /wiki/<slug>.html at the SERVED doc-root, then invoke the PROVEN push (nx_aw_push) as a 17// separate run, and return {status=PUB_PUBLISHED, bytes=n, push_invoked=1}. 18// 19// HOW THE PUSH IS INVOKED: nx_aw_push's streaming logic lives in its main() (it reads awpush.src/awpush.dst, 20// opens the vault, SSHes, streams) -- it is NOT an importable function. So A3 REUSES it the SAME way the 21// rest of the stack does: write the two staging files, then fork+execve _offc/nx_aw_push.elf and wait. We do 22// not re-implement vault auth or SSH (no rolled crypto, no copy-paste of the push spine). ADDITIVE-ONLY: the 23// dst command is `mkdir -p <wikidir>; cat > <wikidir>/<slug>.html` -- it writes exactly ONE page and never 24// touches any other file in the doc-root. 25// 26// corpus = parallel arrays (cs_ptr[i] = slug ptr, cs_len[i] = slug len, ncorpus) -- the SAME array idiom 27// pg_decide / nx_access_wall_gate use (NishiLang caps a call's arg count, so a set is passed as arrays). 28// Pure NishiLang, NO sql/.sh/.py/.js, no new .tsv/.conf. license_tier: ORIGINAL 29import "nx_wiki_publish_guard.nx" 30import "nx_syscalls.nx" 31const PUB_MAGIC_1024: i64 = 1024 32const PUB_MAGIC_2048: i64 = 2048 33 34// ===== structured result (distinct field names; no buffer+offset aliasing) ======================== 35// status values (sealed): the caller switches on these. 36const PUB_PUBLISHED: i64 = 2 // ALLOW + do_push_flag=1 + push invoked 37const PUB_ALLOW_NOPUSH: i64 = 1 // ALLOW + do_push_flag=0 (guard passed; nothing pushed) 38const PUB_REJECTED: i64 = 0 // guard REJECT -> fail-closed, push never reached 39 40struct PubResult { 41 status: i64, // one of PUB_* 42 code: i64, // the guard verdict: PG_ALLOW or the exact PG_REJECT_* reason 43 bytes: i64, // page size pushed (0 if not pushed) 44 push_invoked: i64, // 1 iff the push run was actually invoked, else 0 (THE safety witness) 45 push_rc: i64 // exit code of the push run (0 = ok), -1 if not invoked 46} 47 48// staging files the proven nx_aw_push reads (local /mnt/c paths -- persistent across WSL /tmp wipes). 49const PUB_AWPUSH_SRC: *u8 = "/mnt/c/Users/elder/AppData/Local/Temp/awpush.src" as *u8 50const PUB_AWPUSH_DST: *u8 = "/mnt/c/Users/elder/AppData/Local/Temp/awpush.dst" as *u8 51// the SERVED wiki doc-root on the NAS (the dst command must target THIS path). 52const PUB_WIKI_DIR: *u8 = "/volume1/homes/elderwesto/nishihost/sites/nishifamily/wiki" as *u8 53// the proven push organ (its main() does vault auth + SSH stream; invoked as a separate run). 54const PUB_AWPUSH_ELF: *u8 = "_offc/nx_aw_push.elf" as *u8 55// file mode 0644 for the staging files. 56const PUB_MODE_0644: i64 = 0x1a4 57 58func pub_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 59 60// append NUL-terminated s into dst at off; return new off (caller appends a 0 at the end). 61func pub_cat(dst: *u8, off: i64, s: *u8) -> i64 { 62 var o: i64 = off; var k: i64 = 0 63 while s[k] != (0 as u8) { dst[o] = s[k]; o = o + 1; k = k + 1 } 64 return o 65} 66 67// write the whole buffer buf[0..n] to path, truncating any prior content. Returns 0 ok, <0 on open fail. 68// (truncate => idempotent: re-staging the same publish overwrites the prior staging, never appends.) 69func pub_write_file(path: *u8, buf: *u8, n: i64) -> i64 { 70 let fd: i64 = sys_openat_wr(path, PUB_MODE_0644) 71 if fd < 0 { return 0 - 1 } 72 sys_write(fd, buf, n) 73 sys_close(fd) 74 return 0 75} 76 77// run an external program (path, up to two args) via fork+execve+wait. Returns its exit code (0..255), 78// or -1 if it could not be spawned. SAME idiom nx_aw_push itself uses to drive nx_machine_key/nx_vault. 79func pub_run(path: *u8, a1: *u8, a2: *u8) -> i64 { 80 let pid: i64 = sys_fork() 81 if pid == 0 { 82 let argv: *i64 = sys_mmap(64) as *i64 83 argv[0] = path as i64 84 var ai: i64 = 1 85 if (a1 as i64) != 0 { argv[ai] = a1 as i64; ai = ai + 1 } 86 if (a2 as i64) != 0 { argv[ai] = a2 as i64; ai = ai + 1 } 87 argv[ai] = 0 88 let envp: *i64 = sys_mmap(16) as *i64 89 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0 90 sys_execve(path, argv, envp) 91 sys_exit(127) 92 } 93 if pid < 0 { return 0 - 1 } 94 let st: *i64 = sys_mmap(16) as *i64 95 sys_wait4(pid, st, 0) 96 return (st[0] >> 8) & 0xff 97} 98 99// ===== THE ONE-CALL GUARDED PUBLISH =============================================================== 100// Returns the verdict in *out (PubResult). Writes NOTHING to the push path unless the guard ALLOWs AND 101// do_push_flag==1. corpus = the set of slugs published together (parallel arrays + count). prefix is the 102// archive store prefix for cite resolution (production callers pass WAR_PREFIX). 103func pub_publish_ex(prefix: *u8, local_html_path: *u8, target_slug: *u8, 104 cs_ptr: *i64, cs_len: *i64, ncorpus: i64, 105 do_push_flag: i64, out: *PubResult) -> i64 { 106 // defaults: deny-by-default posture -- assume rejected/not-pushed until proven otherwise. 107 out.status = PUB_REJECTED 108 out.code = PG_REJECT_PLACEHOLDER 109 out.bytes = 0 110 out.push_invoked = 0 111 out.push_rc = 0 - 1 112 113 // 1. read the page bytes. 114 let nbox: *i64 = sys_mmap(16) as *i64 115 let body: *u8 = sys_read_file(local_html_path, nbox) 116 if (body as i64) == 0 { 117 // cannot read the page -> fail-closed REJECT (treated as placeholder/empty), push never reached. 118 out.status = PUB_REJECTED 119 out.code = PG_REJECT_PLACEHOLDER 120 out.push_invoked = 0 121 return out.status 122 } 123 let n: i64 = nbox[0] 124 125 // 2. THE GUARD IS THE SOLE AUTHORITY (composed, not reimplemented). 126 let verdict: i64 = pg_decide(prefix, body, n, cs_ptr, cs_len, ncorpus) 127 out.code = verdict 128 129 // 3. fail-closed: any REJECT returns immediately and DOES NOT TOUCH the push path. 130 if pg_allowed(verdict) != 1 { 131 out.status = PUB_REJECTED 132 out.bytes = 0 133 out.push_invoked = 0 // <-- THE safety property: bad page never reaches push. 134 out.push_rc = 0 - 1 135 return out.status 136 } 137 138 // 4a. ALLOW but push suppressed (do_push_flag=0): report the ALLOW verdict, push nothing. 139 if do_push_flag == 0 { 140 out.status = PUB_ALLOW_NOPUSH 141 out.bytes = n 142 out.push_invoked = 0 143 out.push_rc = 0 - 1 144 return out.status 145 } 146 147 // 4b. ALLOW + push requested: stage the proven push, then invoke it as a separate run. 148 // awpush.src = the local file to stream (the page itself). 149 let srcbuf: *u8 = sys_mmap(PUB_MAGIC_1024) 150 var so: i64 = pub_cat(srcbuf, 0, local_html_path) 151 srcbuf[so] = 10 as u8; so = so + 1 // trailing newline (nx_aw_push trims it) 152 srcbuf[so] = 0 as u8 153 if pub_write_file(PUB_AWPUSH_SRC, srcbuf, so) != 0 { 154 // staging failed -> we have NOT pushed; report ALLOW-but-not-pushed honestly (push_invoked=0). 155 out.status = PUB_ALLOW_NOPUSH 156 out.bytes = n 157 out.push_invoked = 0 158 out.push_rc = 0 - 1 159 return out.status 160 } 161 162 // awpush.dst = remote command: ADDITIVE -- mkdir -p <wikidir>; cat > <wikidir>/<slug>.html 163 // (writes exactly this one page; never deletes or clobbers any other page). 164 let dstbuf: *u8 = sys_mmap(PUB_MAGIC_2048) 165 var d: i64 = 0 166 d = pub_cat(dstbuf, d, "mkdir -p " as *u8) 167 d = pub_cat(dstbuf, d, PUB_WIKI_DIR) 168 d = pub_cat(dstbuf, d, "; cat > " as *u8) 169 d = pub_cat(dstbuf, d, PUB_WIKI_DIR) 170 d = pub_cat(dstbuf, d, "/" as *u8) 171 d = pub_cat(dstbuf, d, target_slug) 172 d = pub_cat(dstbuf, d, ".html" as *u8) 173 dstbuf[d] = 10 as u8; d = d + 1 174 dstbuf[d] = 0 as u8 175 if pub_write_file(PUB_AWPUSH_DST, dstbuf, d) != 0 { 176 out.status = PUB_ALLOW_NOPUSH 177 out.bytes = n 178 out.push_invoked = 0 179 out.push_rc = 0 - 1 180 return out.status 181 } 182 183 // invoke the PROVEN push (separate run; reuses vault + SSH spine -- no rolled crypto here). 184 out.push_invoked = 1 // we are about to actually push. 185 let rc: i64 = pub_run(PUB_AWPUSH_ELF, 0 as *u8, 0 as *u8) 186 out.push_rc = rc 187 out.bytes = n 188 out.status = PUB_PUBLISHED // push run was invoked (rc reported in push_rc). 189 return out.status 190} 191 192// production convenience: prefix defaults to WAR_PREFIX (the live archive store). 193func pub_publish(local_html_path: *u8, target_slug: *u8, 194 cs_ptr: *i64, cs_len: *i64, ncorpus: i64, 195 do_push_flag: i64, out: *PubResult) -> i64 { 196 return pub_publish_ex(WAR_PREFIX, local_html_path, target_slug, cs_ptr, cs_len, ncorpus, do_push_flag, out) 197} 198 199// human-readable status name (for the live-proof runner / logs). 200func pub_status_name(s: i64) -> *u8 { 201 if s == PUB_PUBLISHED { return "PUBLISHED" as *u8 } 202 if s == PUB_ALLOW_NOPUSH { return "ALLOW_NOPUSH" as *u8 } 203 if s == PUB_REJECTED { return "REJECTED" as *u8 } 204 return "UNKNOWN" as *u8 205}