code wiki / (root) / nx_jrnl_archive_lib.nx

nx_jrnl_archive_lib.nx source

↩ module page · 175 lines · 8796 B

1// nx_jrnl_archive_lib.nx -- THE SANCTIONED "ARCHIVE IT DELIBERATELY" VERB for an append-only 2// status journal that has reached its budget (2026-08-23, lane L). 3// 4// WHY THIS EXISTS. nx_compare_regen refuses receipts once knowledge/status/comparepub.jrnl passes its 5// budget and tells the reader to "archive it deliberately" -- and no organ in the estate could do that 6// (nx_spendgate: name free, no incumbent). Rotation was rejected by design: a silent rotator is a 7// duplicate ruler beside nx_sizeguard and a SHRINKING journal is exactly what nx_jrnlguard calls a 8// clobber. So the archive is a VERB a seat runs on purpose, and it is preserve-before-remove BY 9// CONSTRUCTION: the live journal is reset ONLY after the archived copy has been re-read from disk and 10// hashed equal to the original. A write that fails, a verify that mismatches, or a journal that 11// cannot be read leaves the live file byte-for-byte untouched. 12// 13// Contract (ja_archive): 0 OK archived+reset | 1 OK nothing-to-archive (empty journal, no file made) 14// | 3 cannot read journal | 4 cannot create archive dir / write archive | 5 VERIFY MISMATCH (archive 15// kept, live NOT reset) | 6 reset failed (archive verified, live unchanged). 16// Every outcome prints one NX-JRNL-ARCHIVE line naming src, bytes, sha256, the archive path, and 17// verified=/reset= so the announcement IS the audit row. The archive name is <dir>/<base>.<epoch>. 18// jrnlguard: the estate's twelve jg_*.base baselines do not cover comparepub.jrnl (measured 19// 2026-08-23); a journal that IS baselined must be re-snapshotted after an archive (jg's own rule). 20import "nx_syscalls.nx" 21import "nx_sha256.nx" 22 23const JA_DIGEST_BYTES: i64 = 32 24const JA_ARCH_MODE: i64 = 0x1a4 // rw-r--r-- for the archived copy 25const JA_DIR_MODE: i64 = 0x1ed // rwxr-xr-x for the archive dir 26const JA_PATH_MAX: i64 = 4096 // PATH_MAX: the kernel bound on a path, the only constant a path needs 27 28func ja_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 29func ja_puts(s: *u8) -> i64 { sys_write(1, s, ja_slen(s)); return 0 } 30func ja_pn(v: i64) -> i64 { 31 let t: *u8 = sys_mmap(24) 32 var x: i64 = v 33 var k: i64 = 0 34 if x < 0 { t[0] = 45 as u8; sys_write(1, t, 1); x = 0 - x } 35 if x == 0 { t[0] = 48 as u8; k = 1 } 36 while x > 0 { t[k] = (48 + (x % 10)) as u8; x = x / 10; k = k + 1 } 37 let o: *u8 = sys_mmap(24) 38 var i: i64 = 0 39 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 40 sys_write(1, o, k) 41 return 0 42} 43func ja_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } d[p] = 0 as u8; return p } 44func ja_catn(d: *u8, o: i64, v: i64) -> i64 { 45 let t: *u8 = sys_mmap(24) 46 var x: i64 = v 47 var k: i64 = 0 48 if x == 0 { t[0] = 48 as u8; k = 1 } 49 while x > 0 { t[k] = (48 + (x % 10)) as u8; x = x / 10; k = k + 1 } 50 var p: i64 = o 51 var i: i64 = 0 52 while i < k { d[p] = t[k - 1 - i]; p = p + 1; i = i + 1 } 53 d[p] = 0 as u8 54 return p 55} 56func ja_hex(d: *u8, out: *u8) -> i64 { 57 var i: i64 = 0 58 while i < JA_DIGEST_BYTES { 59 let v: i64 = d[i] as i64 60 let hi: i64 = (v >> 4) & 15 61 let lo: i64 = v & 15 62 if hi < 10 { out[i*2] = (48 + hi) as u8 } else { out[i*2] = (87 + hi) as u8 } 63 if lo < 10 { out[i*2+1] = (48 + lo) as u8 } else { out[i*2+1] = (87 + lo) as u8 } 64 i = i + 1 65 } 66 out[JA_DIGEST_BYTES*2] = 0 as u8 67 return 0 68} 69// basename of a path (the text after the last '/'), and its directory (everything before it, or ".") 70func ja_basename(p: *u8) -> *u8 { 71 var i: i64 = 0 72 var last: i64 = 0 - 1 73 while p[i] != (0 as u8) { if p[i] == (47 as u8) { last = i } i = i + 1 } 74 return (p as i64 + last + 1) as *u8 75} 76func ja_dirname(p: *u8, out: *u8) -> i64 { 77 var i: i64 = 0 78 var last: i64 = 0 - 1 79 while p[i] != (0 as u8) { if p[i] == (47 as u8) { last = i } i = i + 1 } 80 if last < 0 { out[0] = 46 as u8; out[1] = 0 as u8; return 1 } 81 var k: i64 = 0 82 while k < last { out[k] = p[k]; k = k + 1 } 83 out[k] = 0 as u8 84 return k 85} 86// write ALL of buf to path (create/truncate), fsync, close. 0 ok, -1 open failed, -2 short write. 87func ja_write_all(path: *u8, buf: *u8, n: i64, mode: i64) -> i64 { 88 let fd: i64 = sys_openat_wr(path, mode) 89 if fd < 0 { return 0 - 1 } 90 var off: i64 = 0 91 while off < n { 92 let r: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off) 93 if r <= 0 { sys_close(fd); return 0 - 2 } 94 off = off + r 95 } 96 sys_fsync(fd) 97 sys_close(fd) 98 return 0 99} 100// rep (may be 0): rep[0]=bytes rep[1]=verified rep[2]=reset -- the announce line as numbers a gate can read 101func ja_announce(src: *u8, bytes: i64, hex: *u8, arch: *u8, verified: i64, reset: i64, rc: i64, why: *u8, rep: *i64) -> i64 { 102 if (rep as i64) != 0 { rep[0] = bytes; rep[1] = verified; rep[2] = reset } 103 ja_puts("NX-JRNL-ARCHIVE src=" as *u8); ja_puts(src) 104 ja_puts(" bytes=" as *u8); ja_pn(bytes) 105 ja_puts(" sha256=" as *u8); ja_puts(hex) 106 ja_puts(" archived=" as *u8); ja_puts(arch) 107 ja_puts(" verified=" as *u8); ja_pn(verified) 108 ja_puts(" reset=" as *u8); ja_pn(reset) 109 ja_puts(" rc=" as *u8); ja_pn(rc) 110 ja_puts(" " as *u8); ja_puts(why); ja_puts("\n" as *u8) 111 return rc 112} 113// THE VERB. archive_dir may be 0: then <dirname(journal)>/archive. epoch names the archive. 114func ja_archive(journal: *u8, archive_dir: *u8, epoch: i64, rep: *i64) -> i64 { 115 let hex: *u8 = sys_mmap(JA_DIGEST_BYTES * 2 + 8) 116 hex[0] = 45 as u8; hex[1] = 0 as u8 117 let ln: *i64 = sys_mmap(16) as *i64 118 let src: *u8 = sys_read_file(journal, ln) 119 if (src as i64) == 0 { return ja_announce(journal, 0 - 1, hex, "-" as *u8, 0, 0, 3, "CANNOT-READ-JOURNAL (nothing touched)" as *u8, rep) } 120 let n: i64 = ln[0] 121 if n == 0 { return ja_announce(journal, 0, hex, "-" as *u8, 0, 0, 1, "EMPTY (nothing to archive, nothing touched)" as *u8, rep) } 122 let d0: *u8 = sys_mmap(JA_DIGEST_BYTES + 8) 123 sha256_digest(src, n, d0) 124 ja_hex(d0, hex) 125 // archive dir 126 let dir: *u8 = sys_mmap(JA_PATH_MAX) 127 if (archive_dir as i64) != 0 { ja_cat(dir, 0, archive_dir) } else { 128 var dl: i64 = ja_dirname(journal, dir) 129 dl = ja_cat(dir, dl, "/archive" as *u8) 130 } 131 sys_mkdir(dir, JA_DIR_MODE) // EEXIST is fine; a real failure surfaces at the write below 132 let arch: *u8 = sys_mmap(JA_PATH_MAX) 133 var ao: i64 = ja_cat(arch, 0, dir) 134 ao = ja_cat(arch, ao, "/" as *u8) 135 ao = ja_cat(arch, ao, ja_basename(journal)) 136 ao = ja_cat(arch, ao, "." as *u8) 137 ao = ja_catn(arch, ao, epoch) 138 let wr: i64 = ja_write_all(arch, src, n, JA_ARCH_MODE) 139 if wr != 0 { return ja_announce(journal, n, hex, arch, 0, 0, 4, "ARCHIVE-WRITE-FAILED (live journal untouched)" as *u8, rep) } 140 // VERIFY BY RE-READING THE ARCHIVE FROM DISK, never by trusting the write 141 let ln2: *i64 = sys_mmap(16) as *i64 142 let back: *u8 = sys_read_file(arch, ln2) 143 var ok: i64 = 0 144 if (back as i64) != 0 { if ln2[0] == n { 145 let d1: *u8 = sys_mmap(JA_DIGEST_BYTES + 8) 146 sha256_digest(back, n, d1) 147 ok = 1 148 var i: i64 = 0 149 while i < JA_DIGEST_BYTES { if d0[i] != d1[i] { ok = 0 } i = i + 1 } 150 } } 151 if ok == 0 { return ja_announce(journal, n, hex, arch, 0, 0, 5, "VERIFY-MISMATCH (archive kept for inspection, live journal NOT reset)" as *u8, rep) } 152 // reset the live journal: create/truncate to zero bytes. The archive is already verified on disk. 153 let rfd: i64 = sys_openat_wr(journal, JA_ARCH_MODE) 154 if rfd < 0 { return ja_announce(journal, n, hex, arch, 1, 0, 6, "RESET-FAILED (archive verified, live journal unchanged)" as *u8, rep) } 155 sys_fsync(rfd) 156 sys_close(rfd) 157 return ja_announce(journal, n, hex, arch, 1, 1, 0, "OK (hash-verified copy, live journal reset to 0 bytes)" as *u8, rep) } 158 159// Shared append/archive protocol: acquire stable journal+".lock" before opening the journal. 160// Added for checked receipt writers. Legacy ja_archive above does NOT use this lock and remains unsafe to run concurrently; this change does not disable its deployed ELF. 161func ja_lock(journal: *u8) -> i64 { 162 if ja_slen(journal) + 6 >= JA_PATH_MAX { return 0 - 36 } 163 let path: *u8 = sys_mmap_shared(JA_PATH_MAX) 164 if (path as i64) <= 0 { return path as i64 } 165 let off: i64 = ja_cat(path, 0, journal) 166 ja_cat(path, off, ".lock" as *u8) 167 let fd: i64 = sys_openat_lock(path) 168 let released: i64 = sys_munmap(path, JA_PATH_MAX) 169 if released != 0 { if fd >= 0 { sys_close(fd) }; return released } 170 if fd < 0 { return fd } 171 let rc: i64 = sys_flock(fd, SYS_LOCK_EX | SYS_LOCK_NB) 172 if rc != 0 { sys_close(fd); return rc } 173 return fd 174} 175func ja_unlock(fd: i64) -> i64 { return sys_close(fd) }