code wiki / wiki / nx_wiki_queue.nx

nx_wiki_queue.nx source

↩ module page · 303 lines · 14228 B

1// nx_wiki_queue.nx -- the sovereign partnership work-queue. 2// 3// This closes the "trigger Claude partnership work" loop: a visitor submits 4// a task on a component article (POST /wiki/request), it is appended to an 5// append-only Nishi journal on disk, and GET /wiki/queue renders the live 6// log -- the durable, sovereign record of requested + completed work that a 7// Claude session drains and documents. All NishiLang, written + read by 8// Nishi I/O (sys_openat_append / sys_write / sys_read_file). No shell, no 9// TSV-glue, no external lib. 10// 11// NEW CAPABILITY vs the rest of the wiki: this is RENDERED AT REQUEST TIME 12// (it reads the live journal), so it runs in the daemon's forked child -- 13// where per-request allocation is fine (the child exits and the OS reclaims 14// it), unlike the parent loop where everything must be hoisted. 15// 16// SECURITY (Cardinal 12, defensive at boundary): the task text is untrusted 17// public input. On write it is sanitized (control bytes + the field 18// separator replaced with spaces, length-capped); on render it is 19// HTML-escaped via the wiki render primitive. The record format uses the 20// 0x1F field separator (never present in sanitized text) so parsing is 21// unambiguous. 22// 23// COMPOSED BY: wiki/nx_wiki_routes (GET /wiki/queue, POST /wiki/request) and 24// bin/nx_sites_daemon (same, dispatched in the child). 25// 26// Status: V1. 2026-05-30. 27 28import "nx_syscalls.nx" 29import "wiki/nx_wiki_doc_render.nx" 30 31// ===== Verdicts ================================================= 32const NX_WQ_OK: i64 = 0 33const NX_WQ_ERR: i64 = 3000 34 35// ===== Config (M7) ================================================= 36const NX_WQ_LOG_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/wiki_workqueue.log" as *u8 37const NX_WQ_FS: i64 = 0x1F // field separator 38const NX_WQ_REC: i64 = 0x0A // record separator (newline) 39const NX_WQ_MAX_TASK: i64 = 600 // cap on the task field 40const NX_WQ_MAX_SLUG: i64 = 96 41const NX_WQ_RENDER_CAP: i64 = 131072 42const NX_WQ_SCRATCH_CAP: i64 = 4096 43const NX_WQ_LINE_CAP: i64 = 1024 44const NX_WQ_PUTZ_CAP: i64 = 1048576 45const NX_WQ_STRLEN_CAP: i64 = 65536 46const NX_WQ_MAX_RENDER_RECS: i64 = 80 // bound the rendered list (TLS record budget) 47 48// ===== Small helpers ================================================= 49func nx_wq_strlen(s: *u8) -> i64 { 50 if (s as i64) == 0 { return 0 } 51 var n: i64 = 0 52 while n < NX_WQ_STRLEN_CAP { 53 if s[n] == (0 as u8) { return n } 54 n = n + 1 55 } 56 return NX_WQ_STRLEN_CAP 57} 58 59func nx_wq_put_z(dst: *u8, off: *i64, cap: i64, s: *u8) -> i64 { 60 if (s as i64) == 0 { return 0 - NX_WQ_ERR } 61 var n: i64 = 0 62 while n < NX_WQ_PUTZ_CAP { 63 if s[n] == (0 as u8) { 64 if off[0] + n > cap { return 0 - NX_WQ_ERR } 65 var i: i64 = 0 66 while i < n { dst[off[0] + i] = s[i]; i = i + 1 } 67 off[0] = off[0] + n 68 return NX_WQ_OK 69 } 70 n = n + 1 71 } 72 return 0 - NX_WQ_ERR 73} 74 75// Copy src->dst replacing control bytes (<0x20) and the field separator 76// with a space; cap at max. Returns bytes written. (M5/M12.) 77func nx_wq_sanitize(dst: *u8, max: i64, src: *u8, src_n: i64) -> i64 { 78 var w: i64 = 0 79 var i: i64 = 0 80 while i < src_n { 81 if w >= max { i = src_n } else { 82 let b: i64 = src[i] as i64 83 if b < 0x20 { dst[w] = 0x20 as u8 } else { 84 if b == NX_WQ_FS { dst[w] = 0x20 as u8 } else { dst[w] = src[i] } 85 } 86 w = w + 1 87 i = i + 1 88 } 89 } 90 return w 91} 92 93func nx_wq_hexval(c: i64) -> i64 { 94 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } } // 0-9 95 if c >= 0x61 { if c <= 0x66 { return c - 0x61 + 10 } } // a-f 96 if c >= 0x41 { if c <= 0x46 { return c - 0x41 + 10 } } // A-F 97 return 0 - 1 98} 99 100// URL-decode a x-www-form-urlencoded value: '+' -> space, %XX -> byte. 101func nx_wq_urldecode(src: *u8, src_n: i64, out: *u8, out_cap: i64) -> i64 { 102 var w: i64 = 0 103 var i: i64 = 0 104 while i < src_n { 105 if w >= out_cap { i = src_n } else { 106 let c: i64 = src[i] as i64 107 if c == 0x2B { out[w] = 0x20 as u8; w = w + 1; i = i + 1 } else { 108 if c == 0x25 { 109 if i + 2 < src_n { 110 let hi: i64 = nx_wq_hexval(src[i + 1] as i64) 111 let lo: i64 = nx_wq_hexval(src[i + 2] as i64) 112 if hi >= 0 { if lo >= 0 { 113 out[w] = ((hi * 16) + lo) as u8; w = w + 1; i = i + 3 114 } } 115 if hi < 0 { out[w] = src[i]; w = w + 1; i = i + 1 } 116 if hi >= 0 { if lo < 0 { out[w] = src[i]; w = w + 1; i = i + 1 } } 117 } else { out[w] = src[i]; w = w + 1; i = i + 1 } 118 } else { out[w] = src[i]; w = w + 1; i = i + 1 } 119 } 120 } 121 } 122 return w 123} 124 125// Find "key=" in body; URL-decode its value (until '&' or end) into out. 126// Returns decoded length, or 0 if not found. (First match; the form has 127// only slug+task so this is unambiguous.) 128func nx_wq_form_field(body: *u8, body_n: i64, key: *u8, key_n: i64, 129 out: *u8, out_cap: i64) -> i64 { 130 if body_n < key_n + 1 { return 0 } 131 var i: i64 = 0 132 let last: i64 = body_n - (key_n + 1) 133 while i <= last { 134 var matched: i64 = 1 135 var j: i64 = 0 136 while j < key_n { 137 if body[i + j] != key[j] { matched = 0; j = key_n } else { j = j + 1 } 138 } 139 if matched == 1 { if body[i + key_n] == (0x3D as u8) { 140 let vstart: i64 = i + key_n + 1 141 var e: i64 = vstart 142 while e < body_n { 143 if body[e] == (0x26 as u8) { return nx_wq_urldecode((body as i64 + vstart) as *u8, e - vstart, out, out_cap) } 144 e = e + 1 145 } 146 return nx_wq_urldecode((body as i64 + vstart) as *u8, body_n - vstart, out, out_cap) 147 } } 148 i = i + 1 149 } 150 return 0 151} 152 153// Append a record: kind <FS> slug <FS> sanitized-text <REC>. O_APPEND -> 154// atomic across concurrent forked children (record < PIPE_BUF). Graceful: 155// a failed open simply drops the record (never blocks serving). 156func nx_wq_append(kind: *u8, kind_n: i64, slug: *u8, slug_n: i64, 157 text: *u8, text_n: i64) -> i64 { 158 let fd: i64 = sys_openat_append(NX_WQ_LOG_PATH, 420) 159 if fd < 0 { return 0 - NX_WQ_ERR } 160 let line: *u8 = sys_mmap(NX_WQ_LINE_CAP) 161 var o: i64 = 0 162 var i: i64 = 0 163 while i < kind_n { if o < NX_WQ_LINE_CAP { line[o] = kind[i]; o = o + 1 }; i = i + 1 } 164 if o < NX_WQ_LINE_CAP { line[o] = NX_WQ_FS as u8; o = o + 1 } 165 o = o + nx_wq_sanitize((line as i64 + o) as *u8, NX_WQ_MAX_SLUG, slug, slug_n) 166 if o < NX_WQ_LINE_CAP { line[o] = NX_WQ_FS as u8; o = o + 1 } 167 o = o + nx_wq_sanitize((line as i64 + o) as *u8, NX_WQ_MAX_TASK, text, text_n) 168 if o < NX_WQ_LINE_CAP { line[o] = NX_WQ_REC as u8; o = o + 1 } 169 sys_write(fd, line, o) 170 sys_close(fd) 171 return NX_WQ_OK 172} 173 174// ===== HTTP wrap ================================================= 175func nx_wq_finish_http(ctx: *NxWikiDocCtx, resp_buf: *u8, resp_cap: i64, out_resp_n: *i64) -> i64 { 176 let body_len: i64 = ctx.out_off 177 let off: *i64 = (sys_mmap(8)) as *i64 178 off[0] = 0 179 let rc1: i64 = nx_wq_put_z(resp_buf, off, resp_cap, 180 "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " as *u8) 181 if rc1 != NX_WQ_OK { return rc1 } 182 var bl: i64 = body_len 183 if bl == 0 { if off[0] >= resp_cap { return 0 - NX_WQ_ERR } resp_buf[off[0]] = 0x30 as u8; off[0] = off[0] + 1 } 184 if bl > 0 { 185 let tmp: *u8 = sys_mmap(32) 186 var k: i64 = 0 187 while bl > 0 { if k >= 32 { return 0 - NX_WQ_ERR } tmp[k] = (0x30 + (bl % 10)) as u8; bl = bl / 10; k = k + 1 } 188 var j: i64 = k - 1 189 while j >= 0 { if off[0] >= resp_cap { return 0 - NX_WQ_ERR } resp_buf[off[0]] = tmp[j]; off[0] = off[0] + 1; j = j - 1 } 190 } 191 let rc2: i64 = nx_wq_put_z(resp_buf, off, resp_cap, "\r\n\r\n" as *u8) 192 if rc2 != NX_WQ_OK { return rc2 } 193 if off[0] + body_len > resp_cap { return 0 - NX_WQ_ERR } 194 var bi: i64 = 0 195 while bi < body_len { resp_buf[off[0] + bi] = ctx.out_buf[bi]; bi = bi + 1 } 196 off[0] = off[0] + body_len 197 out_resp_n[0] = off[0] 198 return NX_WQ_OK 199} 200 201// ===== POST /wiki/request -> append + 302 to /wiki/queue ================================================= 202func nx_wiki_request_handle(body: *u8, body_n: i64, 203 resp_buf: *u8, resp_cap: i64, out_resp_n: *i64) -> i64 { 204 if (resp_buf as i64) == 0 { return 0 - NX_WQ_ERR } 205 out_resp_n[0] = 0 206 let slug: *u8 = sys_mmap(128) 207 var slug_n: i64 = nx_wq_form_field(body, body_n, "slug" as *u8, 4, slug, NX_WQ_MAX_SLUG) 208 if slug_n <= 0 { 209 let g: *u8 = "general" as *u8 210 var gi: i64 = 0 211 while gi < 7 { slug[gi] = g[gi]; gi = gi + 1 } 212 slug_n = 7 213 } 214 let task: *u8 = sys_mmap(NX_WQ_MAX_TASK + 16) 215 let task_n: i64 = nx_wq_form_field(body, body_n, "task" as *u8, 4, task, NX_WQ_MAX_TASK) 216 if task_n > 0 { nx_wq_append("REQ" as *u8, 3, slug, slug_n, task, task_n) } 217 // POST-redirect-GET so a refresh doesn't re-submit; shows the updated queue. 218 let off: *i64 = (sys_mmap(8)) as *i64 219 off[0] = 0 220 let rc: i64 = nx_wq_put_z(resp_buf, off, resp_cap, 221 "HTTP/1.1 303 See Other\r\nLocation: /wiki/queue\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n" as *u8) 222 if rc != NX_WQ_OK { return rc } 223 out_resp_n[0] = off[0] 224 return NX_WQ_OK 225} 226 227// ===== GET /wiki/queue -> read journal + render live ================================================= 228func nx_wq_emit_row(ctx: *NxWikiDocCtx, data: *u8, ks: i64, ke: i64, 229 ss: i64, se: i64, ts: i64, te: i64) -> i64 { 230 var rc: i64 = nx_wiki_doc_write_z(ctx, "<tr><td>" as *u8); if rc != NX_WIKI_DOC_OK { return rc } 231 rc = nx_wiki_doc_write_escaped(ctx, (data as i64 + ks) as *u8, ke - ks); if rc != NX_WIKI_DOC_OK { return rc } 232 rc = nx_wiki_doc_write_z(ctx, "</td><td>" as *u8); if rc != NX_WIKI_DOC_OK { return rc } 233 rc = nx_wiki_doc_write_escaped(ctx, (data as i64 + ss) as *u8, se - ss); if rc != NX_WIKI_DOC_OK { return rc } 234 rc = nx_wiki_doc_write_z(ctx, "</td><td>" as *u8); if rc != NX_WIKI_DOC_OK { return rc } 235 rc = nx_wiki_doc_write_escaped(ctx, (data as i64 + ts) as *u8, te - ts); if rc != NX_WIKI_DOC_OK { return rc } 236 return nx_wiki_doc_write_z(ctx, "</td></tr>" as *u8) 237} 238 239func nx_wiki_queue_handle(resp_buf: *u8, resp_cap: i64, out_resp_n: *i64) -> i64 { 240 if (resp_buf as i64) == 0 { return 0 - NX_WQ_ERR } 241 out_resp_n[0] = 0 242 let render_buf: *u8 = sys_mmap(NX_WQ_RENDER_CAP) 243 let scratch: *u8 = sys_mmap(NX_WQ_SCRATCH_CAP) 244 let ctx: *NxWikiDocCtx = (sys_mmap(512)) as *NxWikiDocCtx 245 let srcph: *u8 = "work-queue" as *u8 246 let rc_ci: i64 = nx_wiki_doc_ctx_init(ctx, render_buf, NX_WQ_RENDER_CAP, 247 srcph, nx_wq_strlen(srcph), 248 0 as *u8, 0, 0, scratch, NX_WQ_SCRATCH_CAP) 249 if rc_ci != NX_WIKI_DOC_OK { return rc_ci } 250 let title: *u8 = "Partnership Work Queue" as *u8 251 let rc_h: i64 = nx_wiki_doc_write_header(ctx, title, nx_wq_strlen(title)) 252 if rc_h != NX_WIKI_DOC_OK { return rc_h } 253 var rc: i64 = nx_wiki_doc_write_z(ctx, "<h1>Partnership Work Queue</h1><p class=\"status\">The sovereign, append-only record of human&harr;Claude work. Submit a task from any <a href=\"/wiki/components\">component article</a>; a Claude session drains the queue and appends what it built (local + global functionality). Served bits-up by the Nishi daemon.</p><form method=\"POST\" action=\"/wiki/request\"><p>Component: <input name=\"slug\" value=\"general\" size=\"16\"> &nbsp; Task: <input name=\"task\" size=\"48\" placeholder=\"e.g. wire the live crawl front-half\"> <button type=\"submit\">Request work</button></p></form>" as *u8) 254 if rc != NX_WIKI_DOC_OK { return rc } 255 // Read the journal. 256 let len_box: *i64 = (sys_mmap(8)) as *i64 257 len_box[0] = 0 258 let data: *u8 = sys_read_file(NX_WQ_LOG_PATH, len_box) 259 var data_n: i64 = 0 260 if (data as i64) != 0 { data_n = len_box[0] } 261 if data_n <= 0 { 262 rc = nx_wiki_doc_write_z(ctx, "<p class=\"informative\">No entries yet &mdash; be the first to request work above.</p>" as *u8) 263 if rc != NX_WIKI_DOC_OK { return rc } 264 } else { 265 rc = nx_wiki_doc_write_z(ctx, "<table><thead><tr><th>Kind</th><th>Component</th><th>Task / work</th></tr></thead><tbody>" as *u8) 266 if rc != NX_WIKI_DOC_OK { return rc } 267 // Parse records (split on REC); within each, two FS split 3 fields. 268 var i: i64 = 0 269 var recs: i64 = 0 270 while i < data_n { 271 if recs >= NX_WQ_MAX_RENDER_RECS { i = data_n } else { 272 let ks: i64 = i 273 // record end = first REC byte at/after ks (or data_n) 274 var rec_end: i64 = data_n 275 var p: i64 = ks 276 while p < data_n { 277 if data[p] == (NX_WQ_REC as u8) { rec_end = p; p = data_n } else { p = p + 1 } 278 } 279 // the two field separators within [ks, rec_end) 280 var fs1: i64 = 0 - 1 281 var fs2: i64 = 0 - 1 282 var q: i64 = ks 283 while q < rec_end { 284 if data[q] == (NX_WQ_FS as u8) { 285 if fs1 < 0 { fs1 = q } else { if fs2 < 0 { fs2 = q } } 286 } 287 q = q + 1 288 } 289 if fs1 > ks { if fs2 > fs1 { 290 let r: i64 = nx_wq_emit_row(ctx, data, ks, fs1, fs1 + 1, fs2, fs2 + 1, rec_end) 291 if r != NX_WIKI_DOC_OK { return r } 292 recs = recs + 1 293 } } 294 i = rec_end + 1 295 } 296 } 297 rc = nx_wiki_doc_write_z(ctx, "</tbody></table>" as *u8) 298 if rc != NX_WIKI_DOC_OK { return rc } 299 } 300 let rc_f: i64 = nx_wiki_doc_write_footer(ctx) 301 if rc_f != NX_WIKI_DOC_OK { return rc_f } 302 return nx_wq_finish_http(ctx, resp_buf, resp_cap, out_resp_n) 303}