code wiki / (root) / nx_content_get_client.nx

nx_content_get_client.nx source

↩ module page · 383 lines · 18328 B

1// nx_content_get_client.nx -- THE SOVEREIGN NAS->laptop DOWNLOAD CLIENT. Retires nx_content_get_client.py. 2// 3// WHY THIS EXISTS (operator standing order, restated 2026-09-04: "we shouldnt be using ps1 or py or 4// anything else in our build path only as external tooling being liar killers and validators etc"). 5// nx_content_get has been the sovereign SOURCE since 2026-09-03 and its only driver was a Python script 6// written on 2026-09-04 that shells out to a Python MCP wire. A SHIPPER IS THE BUILD PATH BY DEFINITION, 7// so it can never be one of the external oracles that order permits. This is the NishiLang driver. 8// Its outbound sibling nx_content_put_client.nx made exactly this move for the write direction and its 9// own first line says so; this file is the half that was missing. 10// 11// nx_content_get_client [dryrun] <remote_src> <local_dest> <capfile> <url> 12// 13// MEASURED COST OF THE GAP IT CLOSES (hostops debt 1788534921, 2026-09-04): with the NAS array degraded 14// and every NAS build refused, the laptop farm could compile and SHIP but could not BITE, because its 15// nx_gate_bite.nx was 39,842 bytes against the NAS 98,879 and nothing could refresh it without a human 16// copy or retyping bytes through an agent transcript. An outbound-only lane is a fallback that works 17// only until the thing you need is on the other side. 18// 19// THE PROTOCOL IS THE SERVER OWN AND IT IS STATELESS, WHICH CHANGES THIS CLIENT SHAPE: 20// begin <src> -> CG-BEGIN src= total_bytes= sha256= chunk_raw= nchunks= 21// chunk <src> <index> -> CG-CHUNK OK ... chunk_sha256= b64=<data> 22// There is no transfer id and no commit: in a get the bytes accumulate on the CLIENT, so there is no 23// server-side state to open, resume or close. The put client id-threading and commit-unknown handling 24// are therefore ABSENT rather than mirrored -- carrying them would be machinery with no invariant to 25// protect, which the server own header names as the asymmetry a symmetric copy gets wrong. 26// 27// NOTHING IS WRITTEN TO <local_dest> UNTIL THE WHOLE-FILE DIGEST MATCHES. 28// The server header states the exposure precisely: the SOURCE can change under a multi-call read, so 29// chunks reassembled from two generations give a franken-file that is byte-valid and semantically 30// nonsense. The per-chunk digest catches a CORRUPTED chunk and CANNOT catch a consistent read of a 31// CHANGED file; only the whole-file sha256 declared by begin can. So this client assembles in memory, 32// verifies, then writes ONCE. A partial or franken download must never be able to masquerade as a 33// refreshed source -- and a size check would not do, because a same-size rewrite passes it. 34// 35// THE CHUNK SIZE IS READ FROM THE SERVER RECEIPT, NEVER COMPUTED HERE. nx_content_get derives it from 36// the RESPONSE capture cap (163,840) rather than the request cap, which is the one term a symmetric copy 37// of the upload client would get wrong. Recomputing it here would install a second ruler that drifts the 38// moment the server reserve changes, and the drift would surface as truncated downloads. 39// 40// A SERVER DECISION IS NEVER RETRIED, AND IT HAS TWO VOCABULARIES. 41// Measured 2026-09-04 on the Python driver this retires: a capability denial arrives as a JSON-RPC 42// code -32001 carrying no CG- token, so a classifier testing only the ORGAN vocabulary read it as a 43// transport fault and retried SIX TIMES over about 30 seconds -- against an array the hostops lane had 44// filed as degraded and harmful to retry against. This client has NO retry loop at all: one call, one 45// answer. That is not a simplification, it is the correct shape for a stateless protocol whose failures 46// are either decisions (which will not change) or transport faults (which the caller can re-issue with 47// full knowledge, because an abandoned get leaks no server state). 48// 49// REFUSE RATHER THAN ESCAPE, on the sibling precedent. Every value on the wire comes from a known-safe 50// alphabet -- a path, decimal digits, and a capability token -- so a byte needing JSON escaping means the 51// input is wrong, not that an escaper is owed. A hand-rolled escaper fails silently and produces a 52// malformed request the server rejects for the wrong reason; a refusal names the offending value. 53// 54// exit: 0 ok | 2 usage | 3 local-io | 4 refused-unsafe-arg | 5 begin-failed | 6 chunk-failed | 7 digest-mismatch 55// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 56import "nx_syscalls.nx" 57import "nx_base64.nx" 58import "nx_sha256.nx" 59import "nx_https_post_lib.nx" 60import "nx_jobfollow_lib.nx" 61 62const CG_DIGEST: i64 = 32 63const CG_SHAHEX: i64 = 64 64const CG_RESP: i64 = 262144 65const CG_REQ: i64 = 262144 66const CG_NUMBUF: i64 = 32 67const CG_MODE_RW: i64 = 420 68// Poll budget for following a JOB-STARTED promotion: 40 x 3000ms = 120s, matched to the put twin so 69// the two clients cannot disagree about how long the job lane is allowed to take. 70const CG_POLLS: i64 = 40 71const CG_POLL_MS: i64 = 3000 72// Opens exactly THREE objects and ONE array, so the tail closes the bracket then three braces -- the same 73// arithmetic pairing the sibling gate asserts rather than trusts, after a first draft there carried a 74// DOUBLED params key whose four-brace tail balanced against itself and produced well-formed JSON of the 75// WRONG SHAPE, rejected by the server for a reason naming neither the client nor the real fault. 76const CG_PRE: *u8 = "{\x22jsonrpc\x22:\x222.0\x22,\x22id\x22:1,\x22method\x22:\x22tools/call\x22,\x22params\x22:{\x22name\x22:\x22nx_content_get\x22,\x22arguments\x22:{\x22argv\x22:[" 77const CG_TAIL: *u8 = "}}}" 78const CG_QUOTE: i64 = 34 79const CG_BACKSLASH: i64 = 92 80const CG_SPACE: i64 = 32 81const CG_DEL: i64 = 127 82 83func cg_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 84func cg_out(s: *u8) -> i64 { sys_write(1, s, cg_slen(s)); return 0 } 85func cg_eo(s: *u8) -> i64 { sys_write(2, s, cg_slen(s)); return 0 } 86func cg_num(v: i64) -> i64 { 87 let b: *u8 = sys_mmap(CG_NUMBUF) 88 var m: i64 = v 89 var i: i64 = CG_NUMBUF - 1 90 if m == 0 { i = i - 1; b[i] = 48 as u8 } 91 while m > 0 { i = i - 1; b[i] = (48 + (m % 10)) as u8; m = m / 10 } 92 sys_write(1, (b as i64 + i) as *u8, (CG_NUMBUF - 1) - i) 93 return 0 94} 95func cg_itoa(v: i64, out: *u8) -> i64 { 96 if v == 0 { out[0] = 48 as u8; return 1 } 97 let tmp: *u8 = sys_mmap(CG_NUMBUF) 98 var m: i64 = v 99 var i: i64 = 0 100 while m > 0 { tmp[i] = (48 + (m % 10)) as u8; m = m / 10; i = i + 1 } 101 var j: i64 = 0 102 while j < i { out[j] = tmp[i - 1 - j]; j = j + 1 } 103 return i 104} 105func cg_hex_into(d: *u8, out: *u8) -> i64 { 106 var i: i64 = 0 107 while i < CG_DIGEST { 108 let v: i64 = d[i] as i64 109 let hi: i64 = (v >> 4) & 15 110 let lo: i64 = v & 15 111 if hi < 10 { out[i * 2] = (48 + hi) as u8 } else { out[i * 2] = (87 + hi) as u8 } 112 if lo < 10 { out[i * 2 + 1] = (48 + lo) as u8 } else { out[i * 2 + 1] = (87 + lo) as u8 } 113 i = i + 1 114 } 115 out[CG_SHAHEX] = 0 as u8 116 return 0 117} 118func cg_json_safe(s: *u8, n: i64) -> i64 { 119 var i: i64 = 0 120 while i < n { 121 let c: i64 = s[i] as i64 122 if c == CG_QUOTE { return 0 } 123 if c == CG_BACKSLASH { return 0 } 124 if c < CG_SPACE { return 0 } 125 if c == CG_DEL { return 0 } 126 i = i + 1 127 } 128 return 1 129} 130// index just past needle, or -1. Exits on a FLAG, never by clobbering the cursor -- the loop-exit 131// sentinel defect this estate has recorded four times in one day. 132func cg_find(buf: *u8, n: i64, needle: *u8) -> i64 { 133 let m: i64 = cg_slen(needle) 134 if m == 0 { return 0 - 1 } 135 var i: i64 = 0 136 var hit: i64 = 0 - 1 137 while i + m <= n { 138 var j: i64 = 0 139 var same: i64 = 1 140 while j < m { if buf[i + j] != needle[j] { same = 0; j = m } else { j = j + 1 } } 141 if same == 1 { if hit < 0 { hit = i + m } } 142 i = i + 1 143 } 144 return hit 145} 146func cg_int_after(buf: *u8, n: i64, key: *u8) -> i64 { 147 let p: i64 = cg_find(buf, n, key) 148 if p < 0 { return 0 - 1 } 149 var i: i64 = p 150 var v: i64 = 0 151 var got: i64 = 0 152 var go: i64 = 1 153 while go == 1 { 154 if i >= n { go = 0 } else { 155 let c: i64 = buf[i] as i64 156 if c < 48 { go = 0 } else { 157 if c > 57 { go = 0 } else { v = v * 10 + (c - 48); got = 1; i = i + 1 } 158 } 159 } 160 } 161 if got == 0 { return 0 - 1 } 162 return v 163} 164// is c a base64 alphabet byte? The b64 value ends at the first byte that is not one. 165func cg_b64ch(c: i64) -> i64 { 166 if c >= 65 { if c <= 90 { return 1 } } 167 if c >= 97 { if c <= 122 { return 1 } } 168 if c >= 48 { if c <= 57 { return 1 } } 169 if c == 43 { return 1 } 170 if c == 47 { return 1 } 171 if c == 61 { return 1 } 172 return 0 173} 174func cg_put_arg(req: *u8, off: i64, val: *u8, vlen: i64, first: i64) -> i64 { 175 if cg_json_safe(val, vlen) == 0 { return 0 - 1 } 176 var o: i64 = off 177 if first == 0 { req[o] = 44 as u8; o = o + 1 } 178 req[o] = CG_QUOTE as u8; o = o + 1 179 var i: i64 = 0 180 while i < vlen { req[o + i] = val[i]; i = i + 1 } 181 o = o + vlen 182 req[o] = CG_QUOTE as u8; o = o + 1 183 return o 184} 185func cg_put_lit(req: *u8, off: i64, lit: *u8) -> i64 { 186 var o: i64 = off 187 let n: i64 = cg_slen(lit) 188 var i: i64 = 0 189 while i < n { req[o + i] = lit[i]; i = i + 1 } 190 return o + n 191} 192func cg_streq(a: *u8, b: *u8) -> i64 { 193 var i: i64 = 0 194 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 195 if b[i] != (0 as u8) { return 0 } 196 return 1 197} 198// A SERVER DECISION, IN EITHER VOCABULARY. The organ speaks CG-; the transport speaks -32001. Both are 199// decisions and neither changes on a re-issue. This client does not retry at all, so this exists to 200// REPORT the distinction to the caller rather than to gate a loop -- a caller that cannot tell a denial 201// from a dropped packet will retry the one that cannot possibly succeed. 202func cg_decided(buf: *u8, n: i64) -> i64 { 203 if cg_find(buf, n, "capability denied" as *u8) >= 0 { return 1 } 204 if cg_find(buf, n, "-32001" as *u8) >= 0 { return 1 } 205 if cg_find(buf, n, "CG-REFUSED" as *u8) >= 0 { return 1 } 206 return 0 207} 208 209// Does this argument carry a readcap= prefix? Exactly 8 bytes, compared explicitly rather than by a 210// shared prefix helper, because a loose match would swallow the expect= argument beside it. 211func cg_is_readcap(a: *u8) -> i64 { 212 let want: *u8 = "readcap=" as *u8 213 var i: i64 = 0 214 while i < 8 { 215 if a[i] == (0 as u8) { return 0 } 216 if a[i] != want[i] { return 0 } 217 i = i + 1 218 } 219 return 1 220} 221func main(argc: i64, argv: *i64) -> i64 { 222 // dryrun emits the BEGIN request this client WOULD send and exits without opening a socket, so the 223 // envelope is a decidable deterministic artifact a gate can fork. Same reasoning as the sibling. 224 var base: i64 = 0 225 if argc > 1 { if cg_streq(argv[1] as *u8, "dryrun" as *u8) == 1 { base = 1 } } 226 if argc < 5 + base { 227 cg_eo("usage: nx_content_get_client [dryrun] <remote_src> <local_dest> <capfile> <url>\n" as *u8) 228 return 2 229 } 230 let src: *u8 = argv[1 + base] as *u8 231 let dest: *u8 = argv[2 + base] as *u8 232 let capfile: *u8 = argv[3 + base] as *u8 233 let url: *u8 = argv[4 + base] as *u8 234 235 // OPTIONAL read capability, used ONLY to follow the edge JOB-STARTED promotion to its artifact. 236 // A SCAN, not a positional slot, so it composes with the existing optional expect= argument in 237 // either order and breaks no existing caller -- the same shape nx_gate_bite uses for its subject 238 // keyword, adopted here for the same reason. 239 var rcraw: *u8 = 0 as *u8 240 var rclen: i64 = 0 241 var ai: i64 = 1 242 while ai < argc { 243 let av: *u8 = argv[ai] as *u8 244 if cg_is_readcap(av) == 1 { 245 let rcbox: *i64 = sys_mmap(16) as *i64 246 rcraw = sys_read_file(((av as i64) + 8) as *u8, rcbox) 247 if rcraw as i64 == 0 { cg_eo("CC-IO: cannot read readcap file\n" as *u8); return 3 } 248 rclen = rcbox[0] 249 while rclen > 0 { 250 let c2: i64 = rcraw[rclen - 1] as i64 251 if c2 <= CG_SPACE { rclen = rclen - 1 } else { rclen = 0 - rclen } 252 } 253 if rclen < 0 { rclen = 0 - rclen } 254 } 255 ai = ai + 1 256 } 257 258 let capbox: *i64 = sys_mmap(16) as *i64 259 let capraw: *u8 = sys_read_file(capfile, capbox) 260 if capraw as i64 == 0 { cg_eo("CG-IO: cannot read cap file\n" as *u8); return 3 } 261 var caplen: i64 = capbox[0] 262 // trim trailing whitespace -- a cap with a stray newline is the silent-401 class 263 while caplen > 0 { 264 let c: i64 = capraw[caplen - 1] as i64 265 if c <= CG_SPACE { caplen = caplen - 1 } else { caplen = 0 - caplen } 266 } 267 if caplen < 0 { caplen = 0 - caplen } 268 if cg_json_safe(capraw, caplen) == 0 { 269 cg_eo("CG-REFUSED: the capability token carries a byte that would need JSON escaping -- refusing rather than escaping badly\n" as *u8) 270 return 4 271 } 272 273 let store_i: i64 = hf_store_load() 274 let resp: *u8 = sys_mmap(CG_RESP + 16) 275 let req: *u8 = sys_mmap(CG_REQ + 16) 276 let nb: *u8 = sys_mmap(CG_NUMBUF) 277 278 // ---- BEGIN ---- 279 var o: i64 = cg_put_lit(req, 0, CG_PRE) 280 o = cg_put_arg(req, o, "begin" as *u8, 5, 1) 281 o = cg_put_arg(req, o, src, cg_slen(src), 0) 282 if o < 0 { cg_eo("CG-REFUSED: remote src carries a byte needing JSON escaping\n" as *u8); return 4 } 283 o = cg_put_lit(req, o, "],\x22_cap\x22:" as *u8) 284 o = cg_put_arg(req, o, capraw, caplen, 1) 285 o = cg_put_lit(req, o, CG_TAIL) 286 if base == 1 { sys_write(1, req, o); sys_write(1, "\n" as *u8, 1); return 0 } 287 288 let n0: i64 = jf_post_follow(store_i, url, req, o, resp, CG_RESP, rcraw, rclen, CG_POLLS, CG_POLL_MS) 289 if n0 <= 0 { cg_eo("CG-BEGIN: post failed (transport) -- re-issue is safe, a get leaks no server state\n" as *u8); return 5 } 290 if cg_decided(resp, n0) == 1 { 291 cg_eo("CG-BEGIN: the SERVER DECIDED (refusal or capability denial). DO NOT RETRY -- the answer cannot change.\n" as *u8) 292 sys_write(2, resp, n0) 293 cg_eo("\n" as *u8) 294 return 5 295 } 296 let total: i64 = cg_int_after(resp, n0, "total_bytes=" as *u8) 297 let craw: i64 = cg_int_after(resp, n0, "chunk_raw=" as *u8) 298 let nch: i64 = cg_int_after(resp, n0, "nchunks=" as *u8) 299 let sp: i64 = cg_find(resp, n0, "sha256=" as *u8) 300 if total < 0 { cg_eo("CG-BEGIN: no total_bytes in receipt\n" as *u8); return 5 } 301 if craw <= 0 { cg_eo("CG-BEGIN: no chunk_raw in receipt (the server owns this number; it is never computed here)\n" as *u8); return 5 } 302 if nch <= 0 { cg_eo("CG-BEGIN: no nchunks in receipt\n" as *u8); return 5 } 303 if sp < 0 { cg_eo("CG-BEGIN: no sha256 in receipt -- without the whole-file digest a franken-read is undetectable, so this refuses rather than downloading blind\n" as *u8); return 5 } 304 let want: *u8 = sys_mmap(CG_SHAHEX + 8) 305 var wi: i64 = 0 306 while wi < CG_SHAHEX { want[wi] = resp[sp + wi]; wi = wi + 1 } 307 want[CG_SHAHEX] = 0 as u8 308 cg_out("CG-BEGIN src=" as *u8); cg_out(src) 309 cg_out(" bytes=" as *u8); cg_num(total) 310 cg_out(" chunk_raw=" as *u8); cg_num(craw) 311 cg_out(" nchunks=" as *u8); cg_num(nch) 312 cg_out(" sha256=" as *u8); cg_out(want); cg_out("\n" as *u8) 313 314 // ---- CHUNKS, assembled IN MEMORY. Nothing touches <local_dest> until the digest matches. ---- 315 let acc: *u8 = sys_mmap(total + 64) 316 var got: i64 = 0 317 var idx: i64 = 0 318 while idx < nch { 319 var co: i64 = cg_put_lit(req, 0, CG_PRE) 320 co = cg_put_arg(req, co, "chunk" as *u8, 5, 1) 321 co = cg_put_arg(req, co, src, cg_slen(src), 0) 322 let xl: i64 = cg_itoa(idx, nb) 323 co = cg_put_arg(req, co, nb, xl, 0) 324 co = cg_put_lit(req, co, "],\x22_cap\x22:" as *u8) 325 co = cg_put_arg(req, co, capraw, caplen, 1) 326 co = cg_put_lit(req, co, CG_TAIL) 327 let cn: i64 = jf_post_follow(store_i, url, req, co, resp, CG_RESP, rcraw, rclen, CG_POLLS, CG_POLL_MS) 328 if cn <= 0 { cg_eo("CG-CHUNK: post failed (transport) at index " as *u8); cg_num(idx); cg_eo("\n" as *u8); return 6 } 329 if cg_decided(resp, cn) == 1 { 330 cg_eo("CG-CHUNK: the SERVER DECIDED at index " as *u8) 331 cg_num(idx) 332 cg_eo(" -- DO NOT RETRY\n" as *u8) 333 return 6 334 } 335 let bp: i64 = cg_find(resp, cn, "b64=" as *u8) 336 if bp < 0 { cg_eo("CG-CHUNK: no b64 payload at index " as *u8); cg_num(idx); cg_eo("\n" as *u8); return 6 } 337 var be: i64 = bp 338 var scan: i64 = 1 339 while scan == 1 { 340 if be >= cn { scan = 0 } else { 341 if cg_b64ch(resp[be] as i64) == 1 { be = be + 1 } else { scan = 0 } 342 } 343 } 344 let raw: i64 = b64_decode((resp as i64 + bp) as *u8, be - bp, (acc as i64 + got) as *u8) 345 if raw <= 0 { cg_eo("CG-CHUNK: base64 decode produced nothing at index " as *u8); cg_num(idx); cg_eo("\n" as *u8); return 6 } 346 got = got + raw 347 idx = idx + 1 348 } 349 350 // ---- THE WHOLE-FILE DIGEST DECIDES. A size match is not an identity. ---- 351 if got != total { 352 cg_eo("CG-ASSEMBLY: reassembled " as *u8); cg_num(got) 353 cg_eo(" bytes against a declared " as *u8); cg_num(total) 354 cg_eo(" -- nothing written\n" as *u8) 355 return 7 356 } 357 let d: *u8 = sys_mmap(CG_DIGEST + 8) 358 sha256_digest(acc, got, d) 359 let hex: *u8 = sys_mmap(CG_SHAHEX + 8) 360 cg_hex_into(d, hex) 361 if cg_streq(hex, want) == 0 { 362 cg_eo("CG-DIGEST MISMATCH got=" as *u8) 363 sys_write(2, hex, CG_SHAHEX) 364 cg_eo(" want=" as *u8) 365 sys_write(2, want, CG_SHAHEX) 366 cg_eo("\n The source changed under the read, or a chunk is corrupt. NOTHING WAS WRITTEN.\n" as *u8) 367 return 7 368 } 369 let fd: i64 = sys_openat_wr(dest, CG_MODE_RW) 370 if fd < 0 { cg_eo("CG-IO: cannot open local dest for write\n" as *u8); return 3 } 371 var off: i64 = 0 372 while off < got { 373 let w: i64 = sys_write(fd, (acc as i64 + off) as *u8, got - off) 374 if w <= 0 { sys_close(fd); cg_eo("CG-IO: short write\n" as *u8); return 3 } 375 off = off + w 376 } 377 sys_close(fd) 378 cg_out("E2E OK: bytes=" as *u8); cg_num(got) 379 cg_out(" chunks=" as *u8); cg_num(nch) 380 cg_out(" sha256=" as *u8); cg_out(hex) 381 cg_out(" -> " as *u8); cg_out(dest); cg_out("\n" as *u8) 382 return 0 383}