code wiki / (root) / nx_research_fetch.nx

nx_research_fetch.nx source

↩ module page · 350 lines · 21259 B

1// nx_research_fetch.nx -- the Nishi RESEARCHER's sovereign web-fetch for the SPEED / BEAT-CUDA arc. Reuses the 2// proven sovereign TLS-1.3 stack (nx_https_fetch_follow: url->connect->TLS1.3 handshake validated vs the 3// Mozilla CA store->GET->redirect-follow->dechunk) -- 100% sovereign (own TLS, nx_cc->nxasm, no curl/wget). 4// Fetches a CITED source on efficient LLM inference + cross-vendor (CUDA-alternative) GPU compute, saves the 5// raw body to knowledge/fetched/ for analysis. The only non-Nishi inputs are the CA-root DATA + the fetched 6// page (exactly the researcher's contract). license_tier: ORIGINAL 7import "nx_syscalls.nx" 8import "nx_x509_trust_store.nx" 9import "nx_trust_store_load_from_certdata.nx" 10import "nx_https_fetch_follow.nx" 11const K_MAGIC_5381: i64 = 5381 12const RF_CERTDATA_CAP: i64 = 4194304 // Mozilla certdata.txt measures ~1MB; this is ITS reserve alone 13// THE NETWORK-BODY RESERVE -- the one legitimate bound (buffer-cap law): a response body's size is 14// genuinely unknowable in advance, so the bound is NAMED for this single purpose and sized for the 15// library mirror lane (packs carry images to ~22MB, PDFs to ~33MB, clips to ~92MB). A body that FILLS 16// the reserve ANNOUNCES and REFUSES below instead of saving as clean -- the old shared 4MiB cap 17// saved a cut PDF with status=200 and no marker (debt 1787063835). mmap commits pages lazily, so 18// unused headroom costs address space, never resident memory. 19const RF_BODY_CAP: i64 = 536870912 // RAISED 2026-09-14 (deliberately, per the refusal's own remedy) from 128 MiB for the BRIGHT leetcode documents parquet, which filled the 128 MiB reserve and was refused as maybe-cut; address space only, pages commit lazily 20const K_MAGIC_2000: i64 = 2000 21const K_MAGIC_2048: i64 = 2048 22const RF_PIPE: i64 = 124 23const RF_NL: i64 = 10 24const RF_PROV_PATH: *u8 = "knowledge/status/fetch_provenance.jrnl" 25// A provenance row is a fixed handful of short fields plus TWO paths: a url and a mirror path. Both are 26// already bounded elsewhere in this organ (rf_build_path writes into a 512-byte buffer), so this is 27// derived from those bounds rather than picked -- four times 512 leaves room for the epoch, the 64-hex 28// digest, the byte count and the hop count with the whole row still far inside one page. 29const RF_PROV_ROWCAP: i64 = 2048 30 31func rf_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 32func rf_putn(v: i64) -> i64 { 33 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 } 34 var m: i64 = v 35 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 36 let d: *u8 = sys_mmap(24); var k: i64 = 0 37 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 38 let o: *u8 = sys_mmap(24); var wi: i64 = 0 39 while wi < k { o[wi] = d[k - 1 - wi]; wi = wi + 1 } 40 sys_write(1, o, k) 41 return 0 42} 43 44// Bounded substring witness over the fetched body (n bytes, not NUL-terminated). 45func rf_has(hay: *u8, n: i64, needle: *u8) -> i64 { 46 var nl: i64 = 0 47 while needle[nl] != (0 as u8) { nl = nl + 1 } 48 if nl == 0 { return 0 } 49 var i: i64 = 0 50 while i + nl <= n { 51 var j: i64 = 0 52 while j < nl { if hay[i+j] != needle[j] { break } j = j + 1 } 53 if j == nl { return 1 } 54 i = i + 1 55 } 56 return 0 57} 58 59// Build knowledge/fetched/<name>: caller name (argv[2]) if given (sanitized), else rf_<djb2hex(url)>.raw. 60// FIXES the srch_latest.raw clobber (2026-07-03): every distinct URL now maps to a stable distinct file 61// (idempotent -- re-fetching a URL overwrites only its own file), so parallel research fetches coexist. 62func rf_hexnib(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } // 0-9 then a-f 63func rf_build_path(url: *u8, argc: i64, argv: *i64, out: *u8) -> i64 { 64 let pre: *u8 = "knowledge/fetched/\x00" 65 var o: i64 = 0 66 while pre[o] != (0 as u8) { out[o] = pre[o]; o = o + 1 } 67 if argc >= 3 { 68 let name: *u8 = argv[2] as *u8 69 var i: i64 = 0 70 while name[i] != (0 as u8) { 71 let c: i64 = name[i] as i64 72 var ok: i64 = 0 73 if c >= 48 { if c <= 57 { ok = 1 } } // 0-9 74 if c >= 65 { if c <= 90 { ok = 1 } } // A-Z 75 if c >= 97 { if c <= 122 { ok = 1 } } // a-z 76 if c == 46 { ok = 1 } // . 77 if c == 95 { ok = 1 } // _ 78 if c == 45 { ok = 1 } // - 79 if ok == 1 { out[o] = c as u8 } else { out[o] = 95 as u8 } 80 o = o + 1; i = i + 1 81 } 82 } else { 83 out[o] = 114 as u8; out[o+1] = 102 as u8; out[o+2] = 95 as u8; o = o + 3 // "rf_" 84 var h: i64 = K_MAGIC_5381 85 var i: i64 = 0 86 while url[i] != (0 as u8) { h = (((h << 5) + h) + (url[i] as i64)) & 0xFFFFFFFF; i = i + 1 } 87 var s: i64 = 28 88 while s >= 0 { out[o] = rf_hexnib((h >> s) & 0xF) as u8; o = o + 1; s = s - 4 } 89 let suf: *u8 = ".raw\x00" 90 var j: i64 = 0 91 while suf[j] != (0 as u8) { out[o] = suf[j]; o = o + 1; j = j + 1 } 92 } 93 out[o] = 0 as u8 94 return o 95} 96 97// ---- FETCH-TIME PROVENANCE (2026-08-20, lane E) ------------------------------------------------ 98// WHY THIS EXISTS. On 2026-08-18 four references were appended to charsim.refs from search-engine 99// summaries with ZERO pages opened; one of them carried a mirror that was a capture of a DIFFERENT url 100// fetched three days earlier, and nx_compare_refs_gate passed the file 9 of 9. It could not have done 101// otherwise: it proves a mirror EXISTS and that the pin EQUALS the filehash OF THAT MIRROR -- never that 102// the mirror is a capture of the url in the row. 103// ★★★★★★A PIN PROVES THE BYTES DID NOT CHANGE, NEVER THAT THEY ARE THE RIGHT DOCUMENT, AND A CITATION 104// REGISTER THAT CANNOT TELL THOSE APART WILL BLESS A FABRICATED PROVENANCE FOREVER. 105// The binding has to be made where the two facts are BOTH IN HAND, which is here and nowhere else: this 106// organ is the only place that knows the url it asked for AND the file it just wrote. A checker bolted on 107// afterwards can only ever re-inspect the file and re-assert what the row already claims. 108// ROW: prov|<epoch>|<requested-url>|<mirror-path>|<sha256 of the saved bytes>|<bytes>|<redirect-hops> 109// APPEND-ONLY by construction (sys_openat_append), so a later fetch of the same url adds a row and never 110// rewrites history -- the journal is evidence, and evidence that can be edited in place is testimony. 111// ⚠DECLARED IMPRECISION: the FINAL url after redirects is NOT recorded, only the requested one and the 112// hop count. Threading a final-url out-parameter would change nx_https_fetch_follow's signature at every 113// one of its call sites, which is a separate change with its own blast radius. The hole this closes is 114// "this mirror is a capture of some OTHER url"; the hole it leaves open is "the requested url redirected 115// somewhere I did not record". Naming it beats pretending the record is complete. 116func rf_hex32(dst: *u8, o: i64, digest: *u8) -> i64 { 117 var p: i64 = o 118 var i: i64 = 0 119 while i < 32 { 120 let b: i64 = digest[i] as i64 121 dst[p] = rf_hexnib((b >> 4) & 0xF) as u8 122 dst[p+1] = rf_hexnib(b & 0xF) as u8 123 p = p + 2 124 i = i + 1 125 } 126 return p 127} 128func rf_prov_cat(dst: *u8, o: i64, s: *u8) -> i64 { 129 var p: i64 = o 130 var i: i64 = 0 131 while s[i] != (0 as u8) { dst[p] = s[i]; p = p + 1; i = i + 1 } 132 return p 133} 134func rf_prov_num(dst: *u8, o: i64, v: i64) -> i64 { 135 if v == 0 { dst[o] = 48 as u8; return o + 1 } 136 var m: i64 = v 137 let d: *u8 = sys_mmap(24) 138 var k: i64 = 0 139 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 140 var p: i64 = o 141 var i: i64 = 0 142 while i < k { dst[p] = d[k-1-i]; p = p + 1; i = i + 1 } 143 return p 144} 145func rf_provenance(url: *u8, mirror: *u8, body: *u8, n: i64) -> i64 { 146 let digest: *u8 = sys_mmap(32) 147 sha256_digest(body, n, digest) 148 let row: *u8 = sys_mmap(RF_PROV_ROWCAP) 149 var p: i64 = rf_prov_cat(row, 0, "prov|" as *u8) 150 p = rf_prov_num(row, p, sys_now_realtime_sec()) 151 row[p] = RF_PIPE as u8; p = p + 1 152 p = rf_prov_cat(row, p, url) 153 row[p] = RF_PIPE as u8; p = p + 1 154 p = rf_prov_cat(row, p, mirror) 155 row[p] = RF_PIPE as u8; p = p + 1 156 p = rf_hex32(row, p, digest) 157 row[p] = RF_PIPE as u8; p = p + 1 158 p = rf_prov_num(row, p, n) 159 row[p] = RF_PIPE as u8; p = p + 1 160 p = rf_prov_num(row, p, nx_https_last_hops()) 161 row[p] = RF_NL as u8; p = p + 1 162 let jfd: i64 = sys_openat_append(RF_PROV_PATH as *u8, MODE_0644) 163 // A JOURNAL THAT CANNOT BE OPENED MUST SAY SO. Silently skipping the row would make an unprovenanced 164 // mirror indistinguishable from a provenanced one, which is the exact defect this whole function exists 165 // to end -- so the failure is announced and the fetch still succeeds (the body is real either way). 166 if jfd < 0 { rf_puts("RF-PROVENANCE-UNWRITTEN: cannot append "); rf_puts(RF_PROV_PATH as *u8); rf_puts(" -- this mirror will read as UNPROVEN to the refs gate\n"); return 0 - 1 } 167 sys_write(jfd, row, p) 168 sys_close(jfd) 169 rf_puts("PROVENANCE "); sys_write(1, row, p) 170 return 0 171} 172 173// --- TLS-1.3 SHORT-BODY LEDGER + ITS ARMING SWITCH ---------------------------------------------- 174// The TLS-1.2 leg has REFUSED a body short of its declared Content-Length since 2026-08-18 175// (RF-TRUNCATED, exit 6). The TLS-1.3 leg computed the same number and discarded it at three of its 176// four termination exits, so a cut body arrived here as a positive byte count with status 200 and was 177// saved -- and pinned -- as a clean mirror. nx_https_get_complete.nx now seals that outcome; this is 178// where it becomes visible to a human and countable by a census. 179const RF_TRUNC_NAMES: *u8 = "knowledge/status/fetch_truncation.names" 180const RF_TRUNC_ARM: *u8 = "knowledge/status/fetch_truncation_arm.conf" 181 182// ARMING IS DATA, NOT A RECOMPILE (Rules 11 and 17). The conf carries one row: `arm=0` or `arm=1`. 183// IT SHIPS AS 0 ON PURPOSE. A check that converts silent successes into estate-wide refusals on the 184// day it lands is a check everyone switches off, and the backlog it would refuse against PREDATES it. 185// WHAT ARMS IT, STATED SO IT CANNOT BECOME FOLKLORE: flip the row to arm=1 once 186// knowledge/status/refs_mirror_incomplete.txt -- the worklist nx_compare_refs_gate regenerates every 187// run, and whose ratchet knowledge/status/refs_mirror_ratchet.conf self-baselined at 23 on 2026-08-20 188// -- reaches ZERO rows. At that point the first refusal anybody meets is a NEW offender rather than 189// an inherited one, which is the difference between a ratchet and a permanently-red detector. 190// A MISSING OR UNREADABLE CONF READS AS 0, NEVER AS ARMED: for a guard that can stop a fetch, the 191// fail-safe direction is to keep fetching and keep talking. 192func rf_trunc_armed() -> i64 { 193 let lp: *i64 = sys_mmap(16) as *i64 194 lp[0] = 0 195 let b: *u8 = sys_read_file(RF_TRUNC_ARM as *u8, lp) 196 if (b as i64) <= 0 { return 0 } 197 if lp[0] <= 0 { return 0 } 198 if rf_has(b, lp[0], "arm=1" as *u8) == 1 { return 1 } 199 return 0 200} 201 202// NAME THE OFFENDER, NOT ONLY THE COUNT. A count without a worklist is not actionable, and the name is 203// already in hand at the instant the shortfall is measured -- throwing it away is what forces the next 204// reader to re-derive it. Append-only, one row per OBSERVATION (never per file), so a repeat offender 205// reads as a repeat instead of overwriting its own history. 206// Row: <epoch>|<mirror-path>|<expected>|<got> 207func rf_trunc_name(mirror: *u8, expected: i64, got: i64) -> i64 { 208 let row: *u8 = sys_mmap(RF_PROV_ROWCAP) 209 var p: i64 = 0 210 p = rf_prov_num(row, p, sys_now_realtime_sec()) 211 row[p] = RF_PIPE as u8; p = p + 1 212 p = rf_prov_cat(row, p, mirror) 213 row[p] = RF_PIPE as u8; p = p + 1 214 p = rf_prov_num(row, p, expected) 215 row[p] = RF_PIPE as u8; p = p + 1 216 p = rf_prov_num(row, p, got) 217 row[p] = RF_NL as u8; p = p + 1 218 let fd: i64 = sys_openat_append(RF_TRUNC_NAMES as *u8, MODE_0644) 219 if fd < 0 { rf_puts("RF-TRUNC-LEDGER-UNWRITTEN: cannot append "); rf_puts(RF_TRUNC_NAMES as *u8); rf_puts(" -- this offender is ANNOUNCED but NOT COUNTED, so the ratchet under-reports\n"); return 0 - 1 } 220 sys_write(fd, row, p) 221 sys_close(fd) 222 return 0 223} 224 225func main(argc: i64, argv: *i64) -> i64 { 226 // ARGV-DRIVEN (operator law: NO .txt staging-input files anywhere -- pass inputs as args). The URL is 227 // argv[1] (already NUL-terminated by the kernel). The Mozilla CA bundle is reference DATA, not a staging 228 // file, and is the researcher's one legitimate non-Nishi input. 229 if argc < 2 { rf_puts("usage: nx_research_fetch <url>\n"); return 3 } 230 let url: *u8 = argv[1] as *u8 231 let cpath: *u8 = "data/mozilla_certdata.txt\x00" 232 let r: i64 = nx_trust_store_load_from_certdata(cpath, 512, RF_CERTDATA_CAP) 233 if r <= 0 { rf_puts("RF: certdata load failed\n"); return 1 } 234 let store: *TrustStore = r as *TrustStore 235 rf_puts("CA roots="); rf_putn(trust_store_count(store)); rf_puts("\n") 236 rf_puts("FETCH "); rf_puts(url); rf_puts("\n") 237 let out: *u8 = sys_mmap(RF_BODY_CAP) 238 let status: *i64 = sys_mmap(8) as *i64 239 // RETRY-WITH-BACKOFF 2026-07-08: arxiv (and most paper hosts) rate-limit 240 // rapid repeated fetches with HTTP 429 -- the reason a batch of 2025/2026 241 // papers "wasn't getting through". 429 clears in a few seconds, so back off 242 // (2s/4s/8s) and retry. got: 0=trying 1=ok 2=gave-up. Success = body AND 243 // status < 400. 244 var n: i64 = 0 245 var got: i64 = 0 246 var attempt: i64 = 0 247 var backoff_ms: i64 = K_MAGIC_2000 248 while got == 0 { 249 n = nx_https_fetch_follow_best(url, store, out, RF_BODY_CAP, 6, status) 250 rf_puts("status="); rf_putn(status[0]); rf_puts(" body_bytes="); rf_putn(n); rf_puts("\n") 251 // 🔴 A CUT BODY IS NOT A TRANSIENT FAILURE AND MUST NEVER BE RETRIED INTO A SAVE (2026-08-20). 252 // The TLS-1.2 leg used to return a body truncated at its receive window with status=200, and this 253 // organ printed SAVED over it -- a FALSE PINNED CITATION, because a content-pin computed over a 254 // truncated body matches perfectly forever. ★★★★★★A PIN PROVES THE BYTES DID NOT CHANGE, NEVER 255 // THAT THEY ARE THE WHOLE DOCUMENT. The transport now refuses and names both numbers; this exits 256 // immediately rather than burning the retry ladder on a defect that is deterministic by construction. 257 if n == 0 - NX_FF_TRUNCATED { 258 rf_puts("RF-TRUNCATED: the transport refused a body short of its declared Content-Length (see the T12-TRUNCATED line above for expected= and got=). NOTHING WAS SAVED -- a partial document must never enter the citation register\n") 259 return 6 260 } 261 if n > 0 { if status[0] < 400 { got = 1 } } 262 if got == 0 { 263 // A DEFINITIVE STATUS EXITS THE LADDER AT ONCE (2026-08-25). 429 and 5xx are transient -- 264 // the resource exists and the server asks us to wait. 404/403/401/410 are FINAL: asking 265 // again cannot change the answer, and each pointless retry costs 2s+4s+8s AND three extra 266 // requests at an upstream that already answered clearly. MEASURED: a licence sweep probed 267 // four repositories and THREE returned a plain 404 on the first candidate, each burning 268 // the full ladder; a caller walking a 4-candidate ladder multiplies that by four. 269 // The precedent is already here -- the truncated-body case exits at once rather than 270 // "burning the retry ladder on a defect that is deterministic by construction". 271 // status[0] <= 0 means no HTTP answer parsed at all, which IS transient, so it falls through. 272 // 4xx ONLY, and 429 excluded. This block is reached whenever the fetch did NOT succeed, 273 // which includes a 200 that returned an EMPTY body -- and an empty body may well be 274 // transient, so a condition of "status > 0 and < 500" would wrongly retire it on the first 275 // try. Narrowing to 4xx keeps every non-4xx failure on the retry ladder exactly as before. 276 if status[0] >= 400 { if status[0] < 500 { if status[0] != 429 { 277 rf_puts("RF-DEFINITIVE: status="); rf_putn(status[0]) 278 rf_puts(" is final, not transient -- the resource is absent or forbidden. NOT retried, NOTHING saved; a caller walking a candidate ladder should try its next candidate now.\n") 279 return 2 280 } } } 281 attempt = attempt + 1 282 if attempt >= 4 { got = 2 } else { 283 rf_puts("RF: retry #"); rf_putn(attempt); rf_puts(" in "); rf_putn(backoff_ms); rf_puts("ms (rate-limit/transient)\n") 284 sys_sleep_ms(backoff_ms) 285 backoff_ms = backoff_ms * 2 286 } 287 } 288 } 289 if got != 1 { rf_puts("RF: fetch failed after retries (code "); rf_putn(n); rf_puts(")\n"); return 2 } 290 // TRUNCATION WITNESS: a body that fills the reserve to the brim is indistinguishable from a cut 291 // one, so it REFUSES loudly (never a clean SAVED for a maybe-partial artifact). False-refusal odds 292 // are one body in RF_BODY_CAP; a deliberate raise re-fetches and disambiguates. 293 if n >= RF_BODY_CAP { 294 rf_puts("RF-TRUNCATED: body filled the entire "); rf_putn(RF_BODY_CAP) 295 rf_puts("-byte reserve -- artifact may be cut, REFUSING to save it as clean (raise RF_BODY_CAP deliberately and re-fetch)\n") 296 return 5 297 } 298 299 // BOT-WALL WITNESS (2026-08-18, refs-lane finding): anti-bot interstitials arrive with 2xx/3xx status 300 // and a small plausible body, so "SAVED" blessed them as clean mirrors (measured on this estate: 301 // freedesktop's go-away meta-refresh challenge saved at 3.5KB status<400; PubMed's cookie 302 // interstitial at 5.5KB). Markers are TOOL-INTERNAL tokens read from real captured specimens 303 // (go-away's __goaway_ challenge params; PubMed's cookie-required-message CSS class), never prose 304 // words. FAIL-SAFE BY SHAPE: a hit still saves the body -- to <name>.botwall -- and ANNOUNCES the 305 // rule that fired, so a false positive costs a rename and never loses evidence, and every refusal 306 // names its rule (the deny-guard law). Cloudflare challenges arrive 403 and are already refused by 307 // the status<400 check above; a corpus-wide FP sweep came back UNPROVEN (nx_absent corpus_complete=0 308 // -- the fetched corpus outgrew the searcher's budget), which is WHY the preserve-on-refuse shape 309 // was chosen over a hard refusal. 310 var bw_rule: *u8 = 0 as *u8 311 if rf_has(out, n, "__goaway_" as *u8) == 1 { bw_rule = "goaway-meta-refresh-challenge" as *u8 } 312 if (bw_rule as i64) == 0 { if rf_has(out, n, "cookie-required-message" as *u8) == 1 { bw_rule = "pubmed-cookie-interstitial" as *u8 } } 313 let opath: *u8 = sys_mmap(512) 314 let ol: i64 = rf_build_path(url, argc, argv, opath) 315 if (bw_rule as i64) != 0 { 316 let sufb: *u8 = ".botwall" as *u8 317 var bi: i64 = 0 318 while sufb[bi] != (0 as u8) { opath[ol+bi] = sufb[bi]; bi = bi + 1 } 319 opath[ol+bi] = 0 as u8 320 rf_puts("RF-BOTWALL rule="); rf_puts(bw_rule); rf_puts(" -- an anti-bot interstitial, NOT the document: preserved at the .botwall path below, never cite it as a mirror\n") 321 } 322 // THE 1.3 LEG NOW ANSWERS THE QUESTION THE 1.2 LEG HAS ANSWERED SINCE 2026-08-18. UNJUDGEABLE is 323 // a real and common answer here (chunked and read-to-close responses declare no length at all), and 324 // it is NOT a complaint -- only TRUNCATED speaks. An axis that cannot see must abstain, not acquit. 325 let bst: i64 = nx_https_last_body_state() 326 if bst == NX_HTTPS_BODY_TRUNCATED { 327 let bexp: i64 = nx_https_last_body_expected() 328 let bgot: i64 = nx_https_last_body_got() 329 rf_puts("RF-BODY-TRUNCATED expected="); rf_putn(bexp) 330 rf_puts(" got="); rf_putn(bgot) 331 rf_puts(" short="); rf_putn(bexp - bgot) 332 rf_puts(" mirror="); rf_puts(opath) 333 rf_puts(" -- the peer declared a Content-Length this fetch did not reach. DO NOT PIN THIS AS A COMPLETE MIRROR: a content-pin computed over a cut body matches perfectly forever.\n") 334 rf_trunc_name(opath, bexp, bgot) 335 if rf_trunc_armed() == 1 { 336 rf_puts("RF-TRUNCATED: the truncation ratchet is ARMED (fetch_truncation_arm.conf arm=1) and this body is short -- REFUSING to save it as clean\n") 337 return 7 338 } 339 } 340 let fd: i64 = sys_openat_wr(opath, MODE_0644) 341 if fd < 0 { rf_puts("RF: save failed (dir missing?); first 2KB to stdout:\n"); var c: i64 = 0; while c < K_MAGIC_2048 { if c >= n { break } c = c + 1 } sys_write(1, out, c); rf_puts("\n"); return 0 } 342 sys_write(fd, out, n) 343 sys_close(fd) 344 // BIND THE MIRROR TO THE URL AT THE MOMENT BOTH ARE IN HAND. Written BEFORE the SAVED line so a 345 // reader who sees SAVED has already seen the provenance row, and SAVED stays the LAST line for the 346 // positional parsers that anchor on it. 347 rf_provenance(url, opath, out, n) 348 rf_puts("SAVED "); rf_puts(opath); rf_puts(" ("); rf_putn(n); rf_puts(" bytes) -- grep it to cite\n") 349 return 0 350}