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}