code wiki / (root) / nx_fsops_write_candidate_t190.nx

nx_fsops_write_candidate_t190.nx source

↩ module page · 578 lines · 35214 B

1// nx_fsops_write.nx -- WRITE-CLASS CLI of the consolidated fs tool (tools-api name: nx_fs_write). 2// 2026-07-30: `write` is now COMPARE-AND-SWAP gated (seq1379) -- see the block above fsw_usage. 3// Deliberately a SEPARATE binary from the read-only `nx_fs`: the exposure policy (knowledge/mcp/ 4// exposure_policy.txt) grades reads broad and writes cap-gated, so the two live under different 5// capability grants. One shared library (nx_fsops_lib) -- no logic duplicated. 6// write <path> <content> -> ATOMIC full-file write (tmp+fsync+rename) 7// edit <path> <old> <new> [all] -> exact-string replace, UNIQUENESS contract (Claude-Edit semantic) 8// append <path> <row> -> APPEND-ONLY (one O_APPEND write under flock; row must end in '\n'): 9// the journal/board verb -- no anchor, no expect token, nothing rewritten 10// exit: 0 ok | 2 usage | 3 absent | 4 io | 5 DENIED | 6 NOMATCH | 7 AMBIGUOUS (surfaced in MCP _meta) 11// DENY BY CONSTRUCTION (rule 26 + 12): secret material, OS device/firmware namespace (nx_os_fs seam), 12// the tool allowlist, + fs_write_deny.conf extras. license_tier: ORIGINAL 13import "nx_fsops_lib.nx" 14import "nx_apistack_idempotency_candidate_t190.nx" // id_lookup / id_record -- the estate's append-only Idempotency-Key ledger (2026-09-05, /compare/dataio DI4) 15 16const FSW_USAGE_RC: i64 = 2 17const FSW_ARG_VERB: i64 = 1 // argv slot of the verb 18const FSW_ARG_PATH: i64 = 2 // argv slot of the path 19const FSW_ARG_A: i64 = 3 // write: content | edit: old-string 20const FSW_ARG_B: i64 = 4 // edit: new-string 21const FSW_ARG_ALL: i64 = 5 // edit: optional literal "all" 22const FSW_ARGC_WRITE: i64 = 4 // write path content 23const FSW_ARGC_EDIT: i64 = 5 // edit path old new 24const FSW_ARGC_EDITALL: i64 = 6 // edit path old new all 25 26// seq1379 -- THE WORK-DESTROYER FIX. A whole-file `write` over an EXISTING file could silently BACKDATE it, 27// destroying another session's landed work with no error anywhere: on 2026-07-30 three canonical daemon 28// sources were reverted this way and FIVE eaten debts across THREE lanes re-opened. The `edit` verb next 29// door was never able to do that, because its anchor makes a losing race LOUD -- this brings `write` up to 30// the same standard instead of leaving the sharp edge on the more destructive verb. 31// CREATION IS ALWAYS FREE (absent file -> write). Overwriting an EXISTING file now demands explicit intent: 32// expect=<size> the byte size the caller believes is there -> refuse if reality differs (compare-and-swap) 33// expect=any "I intend to replace whatever is there" -> the deliberate escape hatch 34// Omitted on an existing file -> REFUSE and print the real size, so the caller can retry correctly. A 35// refusal costs one round trip; a silent clobber costs somebody their whole session. 36const FSW_RC_CLOBBER: i64 = 8 // distinct from ABSENT/DENIED/IO/NOMATCH/AMBIG so callers can branch on it 37const FSW_ARG_EXPECT: i64 = 4 // write: optional expect token 38const FSW_ARGC_WRITE_EXPECT: i64 = 5 39 40// Value part of a `key=value` token, or the whole token when there is no '='. 41// 42// ★seq1422 -- THE GUARD COULD NOT BE SATISFIED. Both checks ran on the ENTIRE 43// token: fsx_seq("expect=any","any") is false, and fsw_atoi("expect=5986") 44// finds no leading digit and yields 0, so a CORRECT expectation was refused -- 45// its own error message printed the two identical numbers back to the caller. 46// Every overwrite through this verb was therefore impossible. 47// 48// ★★THE REAL COST, and the reason this is a sev-8 not a typo: **a guard that 49// cannot be satisfied does not produce safety, it produces a BYPASS.** With 50// the sanctioned path refusing correct calls, sessions fell back to raw 51// scp/ssh -- which is exactly the unguarded third write path that silently 52// backdated source and destroyed shipped work (seq1439/seq1392). Parse the 53// VALUE, not the token. 54func fsw_argval(s: *u8) -> *u8 { 55 var i: i64 = 0 56 while s[i] != (0 as u8) { 57 if s[i] == (61 as u8) { return ((s as i64) + i + 1) as *u8 } 58 i = i + 1 59 } 60 return s 61} 62 63// current size of <path>, or -1 when absent/unreadable (absent = free to create) 64func fsw_cur_size(path: *u8) -> i64 { 65 let fd: i64 = sys_openat_rd(path) 66 if fd < 0 { return 0 - 1 } 67 let sz: i64 = sys_lseek(fd, 0, 2) 68 sys_close(fd) 69 return sz 70} 71 72// ---- seq1477: expect=<size> IS NOT A CONTENT HASH ------------------------------------------------------- 73// The size compare catches the LOUD races (append, delete, whole-file backdate -- what seq1379 was about) and 74// misses the QUIET ones, which in this tree are the COMMON ones: flipping a constant, swapping an equal-length 75// identifier, toggling a flag, substituting one same-length path. Those all preserve byte count and sail 76// through a size guard -- and they are exactly the edits most likely to be raced, because they are the small 77// ones siblings make constantly. A guard that passes the common case is not a guard. 78// FNV-1a/64 over the file's CONTENT. Not crypto and not claimed to be: this defends against CONCURRENT EDITS, 79// not an adversary, so a 64-bit non-cryptographic hash is the right cost -- and it needs no new import, which 80// matters because this organ sits under `runtime/` and cannot reach `_hdl_build/`. 81const FSW_FNV_OFF: i64 = 0xcbf29ce484222325 82const FSW_FNV_PRIME: i64 = 0x100000001b3 83func fsw_content_hash(path: *u8) -> i64 { 84 let szp: *i64 = sys_mmap(16) as *i64 85 let buf: *u8 = sys_read_file(path, szp) 86 if (buf as i64) == 0 { return 0 } 87 let n: i64 = szp[0] 88 var h: i64 = FSW_FNV_OFF 89 var i: i64 = 0 90 while i < n { h = (h ^ ((buf[i] as i64) & 0xff)) * FSW_FNV_PRIME; i = i + 1 } 91 return h 92} 93func fsw_hex_of(v: i64, out: *u8) -> i64 { 94 let dig: *u8 = "0123456789abcdef" as *u8 95 var i: i64 = 0 96 while i < 16 { out[i] = dig[(v >> ((15 - i) * 4)) & 15]; i = i + 1 } 97 out[16] = 0 as u8 98 return 16 99} 100func fsw_hex_val(s: *u8) -> i64 { 101 var v: i64 = 0 102 var i: i64 = 0 103 while s[i] != (0 as u8) { 104 let c: i64 = s[i] as i64 105 var d: i64 = 0 - 1 106 if c >= 48 { if c <= 57 { d = c - 48 } } 107 if c >= 97 { if c <= 102 { d = c - 87 } } 108 if c >= 65 { if c <= 70 { d = c - 55 } } 109 if d < 0 { return 0 } 110 v = (v << 4) | d 111 i = i + 1 112 } 113 return v 114} 115// Returns something the caller can compare against `cur`, so the existing `want == cur` test is unchanged: 116// on a CONTENT-HASH match it returns cur; on mismatch -1. A plain numeric expect keeps its old meaning. 117// Print the file's CURRENT content-hash. A helper (not an inline let) so both refusal sites can share one 118// identical line -- a caller that cannot LEARN the current hash can never retry with expect=h<hash>, which 119// would make the whole content-CAS unusable. The remedy has to travel with the refusal. 120func fsw_print_hash(path: *u8) -> i64 { 121 let hb: *u8 = sys_mmap(32) 122 fsw_hex_of(fsw_content_hash(path), hb) 123 fsx_puts(hb) 124 return 0 125} 126func fsw_expect_val(ex: *u8, path: *u8, cur: i64) -> i64 { 127 if ex[0] == (104 as u8) { 128 if fsw_hex_val(((ex as i64) + 1) as *u8) == fsw_content_hash(path) { return cur } 129 return 0 - 1 130 } 131 return fsw_atoi(ex) 132} 133func fsw_atoi(s: *u8) -> i64 { 134 var v: i64 = 0 135 var i: i64 = 0 136 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c < 48 { return 0 - 1 } if c > 57 { return 0 - 1 } v = v * 10 + (c - 48); i = i + 1 } 137 if i == 0 { return 0 - 1 } 138 return v 139} 140 141// ---- IDEMPOTENCY KEYS (2026-09-05, /compare/dataio DI4) -------------------------------------------------------- 142// MEASURED the day this landed: in ONE seat session nine writes returned Outcome-Unknown at the edge window (the 143// backend accepted the request and produced no response inside the window). Each had to be adjudicated by re-reading 144// the artifact, and two verb shapes cannot be re-issued safely at all: an INSERT-SELF-ANCHORED edit (its anchor 145// survives, so a re-issue applies it AGAIN) and an append (no anchor to lose). The IETF Idempotency-Key draft's 146// answer, done sovereign: the caller supplies key=<token>; the first APPLIED outcome is recorded in an append-only 147// ledger; a retry carrying the same key is REPLAYED -- the first outcome is printed and the mutation is NOT 148// re-executed -- so a retry after an unknown outcome is safe BY CONSTRUCTION. Composes the estate's ledger lib 149// (nx_apistack_idempotency: first-write-wins, append-only), never a second ruler. Only rc 0 is recorded: a refusal 150// changed nothing and must be judged afresh on retry. Without a key nothing changes (rule 19: additive). 151const FSW_IDEM_LEDGER: *u8 = "knowledge/status/fswrite_idem.jrnl" as *u8 // CWD-relative like every status file 152const FSW_IDEM_RESULT_CAP: i64 = 1024 153const FSW_KEY_MIN_CH: i64 = 33 // printable ASCII, no space: a key with a TAB or newline would corrupt the ledger row 154const FSW_KEY_MAX_CH: i64 = 126 155func fsw_prefix(s: *u8, p: *u8) -> i64 { var i: i64 = 0; while p[i] != (0 as u8) { if s[i] != p[i] { return 0 } i = i + 1 } return 1 } 156func fsw_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var oo: i64 = o; while s[i] != (0 as u8) { d[oo] = s[i]; oo = oo + 1; i = i + 1 } d[oo] = 0 as u8; return oo } 157func fsw_catn(d: *u8, o: i64, v: i64) -> i64 { 158 var m: i64 = v 159 var oo: i64 = o 160 if m < 0 { d[oo] = 45 as u8; oo = oo + 1; m = 0 - m } 161 let t: *u8 = sys_mmap(32) 162 var k: i64 = 0 163 if m == 0 { t[0] = 48 as u8; k = 1 } 164 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 165 while k > 0 { k = k - 1; d[oo] = t[k]; oo = oo + 1 } 166 d[oo] = 0 as u8 167 return oo 168} 169// the key=<token> value among the trailing args from slot `from`, or 0 when absent 170func fsw_keyarg(argc: i64, argv: *i64, from: i64) -> *u8 { 171 var i: i64 = from 172 while i < argc { 173 let t: *u8 = argv[i] as *u8 174 if fsw_prefix(t, "key=" as *u8) == 1 { return ((t as i64) + 4) as *u8 } 175 i = i + 1 176 } 177 return 0 as *u8 178} 179func fsw_key_ok(key: *u8) -> i64 { 180 var i: i64 = 0 181 while key[i] != (0 as u8) { 182 let c: i64 = key[i] as i64 183 if c < FSW_KEY_MIN_CH { return 0 } 184 if c > FSW_KEY_MAX_CH { return 0 } 185 i = i + 1 186 } 187 if i == 0 { return 0 } 188 return 1 189} 190// Fingerprint version1: eight ordered fields, each encoded as an eight-byte 191// little-endian length followed by exact bytes. Artifact inputs have already been loaded. 192const FSW_REQUEST_FIELDS:i64=8 193const FSW_LENGTH_BYTES:i64=8 194const FSW_REQUEST_HASH_BYTES:i64=32 195const FSW_REQUEST_HEX_BYTES:i64=64 196const FSW_RECONCILE_RC:i64=9 197const FSW_RECEIPT_RC:i64=10 198func fsw_request_hash(argc:i64,argv:*i64,hex:*u8)->i64{ 199 let fields:*i64=sys_mmap_try(FSW_REQUEST_FIELDS*8) as *i64 200 if (fields as i64)<=0{return ID_RESOURCE} 201 fields[0]="nishi.fswrite.request.v1" as i64;fields[1]=argv[1];fields[2]=argv[2];fields[3]=argv[3] 202 fields[4]="" as i64;fields[5]="absent" as i64;fields[6]="" as i64;fields[7]="0" as i64 203 let edit:i64=fsx_seq(argv[1] as *u8,"edit");let write:i64=fsx_seq(argv[1] as *u8,"write") 204 var from:i64=4;if edit==1{fields[4]=argv[4];from=5} 205 var i:i64=from 206 while i<argc{ 207 let t:*u8=argv[i] as *u8 208 if edit==1{ 209 if fsx_seq(t,"all")==1{fields[7]="1" as i64}else{ 210 let value:*u8=fsw_argval(t) 211 if (value as i64)!=(t as i64){if fsw_prefix(t,"key=")==0{fields[5]="present" as i64;fields[6]=value as i64}} 212 } 213 } 214 if write==1{if fsw_prefix(t,"expect=")==1{fields[5]="present" as i64;fields[6]=fsw_argval(t) as i64}} 215 i=i+1 216 } 217 var size:i64=FSW_REQUEST_FIELDS*FSW_LENGTH_BYTES;i=0 218 while i<FSW_REQUEST_FIELDS{ 219 let n:i64=vw_slen(fields[i] as *u8) 220 if n>ID_SIGNED_MAX-size{sys_munmap_direct(fields as *u8,FSW_REQUEST_FIELDS*8);return ID_CAPACITY} 221 size=size+n;i=i+1 222 } 223 let bytes:*u8=sys_mmap_try(size);let digest:*u8=sys_mmap_try(FSW_REQUEST_HASH_BYTES) 224 if (bytes as i64)<=0||(digest as i64)<=0{ 225 if (bytes as i64)>0{sys_munmap_direct(bytes,size)} 226 if (digest as i64)>0{sys_munmap_direct(digest,FSW_REQUEST_HASH_BYTES)} 227 sys_munmap_direct(fields as *u8,FSW_REQUEST_FIELDS*8);return ID_RESOURCE 228 } 229 var at:i64=0;i=0 230 while i<FSW_REQUEST_FIELDS{ 231 let p:*u8=fields[i] as *u8;let n:i64=vw_slen(p);var v:i64=n;var j:i64=0 232 while j<FSW_LENGTH_BYTES{bytes[at]=(v&255) as u8;v=v>>8;at=at+1;j=j+1} 233 j=0;while j<n{bytes[at]=p[j];at=at+1;j=j+1};i=i+1 234 } 235 let rc:i64=sha256_digest_checked_native(bytes,size,digest) 236 if rc==0{ 237 let digits:*u8="0123456789abcdef";i=0 238 while i<FSW_REQUEST_HASH_BYTES{let v:i64=digest[i] as i64;hex[i*2]=digits[v>>4];hex[i*2+1]=digits[v&15];i=i+1} 239 hex[FSW_REQUEST_HEX_BYTES]=0 as u8 240 } 241 sys_munmap_direct(bytes,size);sys_munmap_direct(digest,FSW_REQUEST_HASH_BYTES) 242 sys_munmap_direct(fields as *u8,FSW_REQUEST_FIELDS*8) 243 return rc 244} 245func fsw_evidence_decide(key:*u8,hex:*u8)->i64{ 246 let r:*NxIdEvidence=sys_mmap_try(__size_of(NxIdEvidence)) as *NxIdEvidence 247 if (r as i64)<=0{return FSW_RECONCILE_RC} 248 let state:i64=id_read_evidence(FSW_IDEM_LEDGER,key,vw_slen(key),r) 249 var answer:i64=FSW_RECONCILE_RC 250 if state==ID_NOT_FOUND{answer=0-1} 251 if state>=0{ 252 let prefix:*u8="rc=0 request_v=1 request_sha256=";let pn:i64=vw_slen(prefix) 253 var bound:i64=0 254 if r.result_bytes>pn+FSW_REQUEST_HEX_BYTES{ 255 var same:i64=1;var i:i64=0 256 while i<pn{if r.result[i]!=prefix[i]{same=0};i=i+1} 257 if r.result[pn+FSW_REQUEST_HEX_BYTES]!=32 as u8{same=0} 258 if same==1{bound=1;i=0;while i<FSW_REQUEST_HEX_BYTES{if r.result[pn+i]!=hex[i]{bound=2};i=i+1}} 259 } 260 if bound==1{ 261 fsx_puts("NX-FS-IDEM REPLAY request_fingerprint=matched first_outcome=");sys_write(1,r.result,r.result_bytes) 262 fsx_puts(" -- original retained outcome; mutation not re-executed; current file may include later edits.\n");answer=0 263 }else{ 264 if bound==2{fsx_puts("{\"action\":\"FSWRITE-IDEMPOTENCY\",\"state\":\"request-conflict\",\"applied\":false,\"next\":\"use a distinct key for a distinct request\"}\n")} 265 else{fsx_puts("{\"action\":\"FSWRITE-IDEMPOTENCY\",\"state\":\"legacy-unbound-evidence\",\"applied\":false,\"next\":\"reconcile original outcome; historical row retained\"}\n")} 266 } 267 }else{ 268 if state!=ID_NOT_FOUND{fsx_puts("{\"action\":\"FSWRITE-IDEMPOTENCY\",\"state\":\"evidence-unavailable\",\"applied\":false,\"next\":\"reconcile or repair retained ledger before retry\"}\n")} 269 } 270 id_evidence_close(r);sys_munmap_direct(r as *u8,__size_of(NxIdEvidence));return answer 271} 272func fsw_record_bound(key:*u8,hex:*u8,verb:*u8,path:*u8)->i64{ 273 let fixed:*u8="rc=0 request_v=1 request_sha256= verb= path= bytes= hash=h" 274 let vn:i64=vw_slen(verb);let pn:i64=vw_slen(path) 275 let base:i64=vw_slen(fixed)+FSW_REQUEST_HEX_BYTES+20+16+1 276 if vn>ID_SIGNED_MAX-base{return ID_CAPACITY} 277 if pn>ID_SIGNED_MAX-base-vn{return ID_CAPACITY} 278 let capacity:i64=base+vn+pn;let row:*u8=sys_mmap_try(capacity) 279 if (row as i64)<=0{return ID_RESOURCE} 280 var o:i64=fsw_cat(row,0,"rc=0 request_v=1 request_sha256=") 281 o=fsw_cat(row,o,hex);o=fsw_cat(row,o," verb=");o=fsw_cat(row,o,verb) 282 o=fsw_cat(row,o," path=");o=fsw_cat(row,o,path);o=fsw_cat(row,o," bytes=") 283 o=ccz_cat_num(row,o,fsw_cur_size(path));o=fsw_cat(row,o," hash=h") 284 fsw_hex_of(fsw_content_hash(path),row+o);o=o+16 285 let rc:i64=id_record(FSW_IDEM_LEDGER,key,vw_slen(key),row,o) 286 sys_munmap_direct(row,capacity);return rc 287} 288func fsw_dispatch(argc:i64,argv:*i64)->i64{ 289 if argc<FSW_ARGC_WRITE{return fsw_usage()} 290 let verb:*u8=argv[1] as *u8;let edit:i64=fsx_seq(verb,"edit") 291 if edit==1{if argc<FSW_ARGC_EDIT{return fsw_usage()}} 292 if edit==0&&fsx_seq(verb,"write")==0&&fsx_seq(verb,"append")==0{return fsw_usage()} 293 var from:i64=FSW_ARG_EXPECT;if edit==1{from=FSW_ARG_ALL} 294 let key:*u8=fsw_keyarg(argc,argv,from) 295 if (key as i64)==0{return fsw_execute(argc,argv)} 296 if fsw_key_ok(key)==0{fsx_puts("NX-FS-IDEM REFUSED invalid key; no mutation.\n");return FSW_USAGE_RC} 297 let hex:*u8=sys_mmap_try(FSW_REQUEST_HEX_BYTES+1) 298 if (hex as i64)<=0{return FSW_RECONCILE_RC} 299 if fsw_request_hash(argc,argv,hex)!=0{ 300 sys_munmap_direct(hex,FSW_REQUEST_HEX_BYTES+1) 301 fsx_puts("NX-FS-IDEM REFUSED request fingerprint unavailable; no mutation.\n");return FSW_RECONCILE_RC 302 } 303 let decision:i64=fsw_evidence_decide(key,hex) 304 if decision>=0{sys_munmap_direct(hex,FSW_REQUEST_HEX_BYTES+1);return decision} 305 let applied:i64=fsw_execute(argc,argv) 306 var result:i64=applied 307 if applied==0{ 308 if fsw_record_bound(key,hex,verb,argv[2] as *u8)!=1{ 309 fsx_puts("{\"action\":\"FSWRITE-IDEMPOTENCY\",\"state\":\"applied-receipt-failed\",\"applied\":true,\"next\":\"reconcile destination; do not blindly retry\"}\n");result=FSW_RECEIPT_RC 310 } 311 } 312 sys_munmap_direct(hex,FSW_REQUEST_HEX_BYTES+1);return result 313} 314 315func fsw_usage() -> i64 { 316 fsx_puts("artifact inputs: write-file <path> <payload-file> [expect=h<hash>|<size>|any] [key=<token>] | edit-files <path> <old-file> <new-file> [all] [expect=h<hash>|<size>|any] [key=<token>]\n Text is read from artifacts, never interpreted as launch arguments. Existing destination checks still apply. Embedded NUL and over-capacity inputs are refused before destination mutation.\n" as *u8) 317 fsx_puts("usage: nx_fs_write write <path> <content> [expect=h<hash>|<size>|any] | edit <path> <old> <new> [all] [expect=h<hash>|<size>|any] | append <path> <row-ending-in-newline> [every verb: key=<token>]\n key=<token> (2026-09-05): a retry with the SAME key and matching normalized request fingerprint replays its retained outcome; a different request or legacy unbound evidence refuses before mutation; replay requires a retained matching receipt. If mutation outcome is unknown and no receipt exists, reconcile the destination before retrying: a key alone does not close the mutation-to-journal crash window. A different key or no key behaves exactly as before; refusals are never recorded\n write REFUSES to overwrite an existing file without expect (seq1379: blind writes backdate other sessions' work); edit is anchored, and takes the same expect token for compare-and-swap (seq1456)\n PREFER expect=h<hash> (CONTENT compare-and-swap). expect=<size> is a SIZE-ONLY CAS and cannot see a same-size rewrite -- a flipped constant or an equal-length identifier, which is the commonest raced edit in this tree. Any refusal prints the file's current content-hash, so the retry is always one round trip away.\n TO REMOVE A FILE: use nx_retire_path retire <path> -- it RENAMES into knowledge/retired/ (never deletes, reversible, refuses traversal/shallow/dangerous names). There is NO delete verb here ON PURPOSE. Do NOT blank a file or comment it out to retire it: that leaves a landmine that still parses, still greps and can still shadow the real organ in a glob-driven build (measured 2026-07-31, ws=office).\n" as *u8) 318 return FSW_USAGE_RC 319} 320const FSW_DIR_MODE: i64 = 0x1ed // 0755 on any parent this creates -- same mode the rest of the tree uses 321const FSW_RC_DENIED_SENTINEL: i64 = 2 // fsx_write's DENIED return; it prints its own reason, we must not double-report 322 323// Create every missing parent directory of <path>, returning how many were created (0 = all present), 324// or -1 when the path is write-denied. 325// WHY: fsx_write's atomic open/rename returns a bare -3 when the parent is absent, and unlike its DENIED 326// path it printed NOTHING. MEASURED 2026-08-08: the identical 17-byte payload returned "NX-FS-WRITE OK 327// bytes=17" into an existing directory and a completely silent nothing into a new one -- so a 7-file site 328// publish reported success seven times and wrote zero files. A write verb that creates files is entitled to 329// create the directories those files live in; what it is NOT entitled to do is create them invisibly, so the 330// count is reported in the OK line. 331// The deny check runs FIRST and on the FULL path: asking to write a file inside a forbidden namespace must 332// never become a way to mkdir into it. 333func fsw_mkparents(path: *u8) -> i64 { 334 if fsx_write_denied(path) == 1 { return 0 - 1 } 335 let n: i64 = vw_slen(path) 336 if n >= FSX_PATH_CAP { return 0 - 1 } 337 let buf: *u8 = sys_mmap(FSX_PATH_CAP) 338 var i: i64 = 0 339 while i < n { buf[i] = path[i]; i = i + 1 } 340 buf[n] = 0 as u8 341 var made: i64 = 0 342 // start at 1 so a leading '/' is never itself a component to create 343 var j: i64 = 1 344 while j < n { 345 if (buf[j] as i64) == 47 { 346 buf[j] = 0 as u8 347 // mkdir returns negative for an already-existing directory; that is the common case, not an error, 348 // so only a genuine creation is counted. 349 if sys_mkdir(buf, FSW_DIR_MODE) >= 0 { made = made + 1 } 350 buf[j] = 47 as u8 351 } 352 j = j + 1 353 } 354 return made 355} 356 357// map a lib result (negative sentinel) to the CLI exit code 358func fsw_rc(r: i64) -> i64 { 359 if r >= 0 { return 0 } 360 if r == 0 - 1 { return FSX_RC_ABSENT } 361 if r == 0 - (2 as i64) { return FSX_RC_DENIED } 362 if r == 0 - (3 as i64) { return FSX_RC_IO } 363 if r == 0 - FSX_RC_NOMATCH { return FSX_RC_NOMATCH } 364 if r == 0 - FSX_RC_AMBIG { return FSX_RC_AMBIG } 365 if r == FSX_APP_EMPTY { return FSW_USAGE_RC } 366 if r == FSX_APP_NONL { return FSW_USAGE_RC } 367 return FSX_RC_IO 368} 369 370// Artifact-backed text payloads avoid process-launch quoting and argument-size limits. 371func fsw_payload_failure(path: *u8, stage: *u8, raw: i64, code: i64, next: *u8, status: *i64) -> *u8 { 372 status[0] = code 373 fsx_puts("NX-FS-PAYLOAD FAILURE operation=load-text stage=" as *u8); fsx_puts(stage) 374 fsx_puts(" path=" as *u8); fsx_puts(path) 375 fsx_puts(" native_result=" as *u8); fsx_putn(raw) 376 fsx_puts(" destination_mutated=0 next=" as *u8); fsx_puts(next); fsx_puts("\n" as *u8) 377 return 0 as *u8 378} 379func fsw_load_text(path: *u8, status: *i64) -> *u8 { 380 status[0] = FSX_RC_IO 381 if fsx_denied(path) == 1 { 382 return fsw_payload_failure(path, "authorize" as *u8, 0, FSX_RC_DENIED, "resolve-source-read-authorization" as *u8, status) 383 } 384 let fd: i64 = sys_openat_rd(path) 385 if fd < 0 { return fsw_payload_failure(path, "open" as *u8, fd, FSX_RC_IO, "inspect-source-path-and-native-open-error" as *u8, status) } 386 let buf: *u8 = sys_mmap(FSX_READ_CAP + 1) 387 if (buf as i64) <= 0 { 388 sys_close(fd) 389 return fsw_payload_failure(path, "allocate" as *u8, buf as i64, FSX_RC_IO, "resolve-payload-memory-allocation" as *u8, status) 390 } 391 var n: i64 = 0 392 while n < FSX_READ_CAP + 1 { 393 let r: i64 = sys_read(fd, buf+n, FSX_READ_CAP+1-n) 394 if r < 0 { 395 sys_close(fd) 396 return fsw_payload_failure(path, "read" as *u8, r, FSX_RC_IO, "inspect-source-read-error" as *u8, status) 397 } 398 if r == 0 { break } 399 n = n+r 400 } 401 let closed: i64 = sys_close(fd) 402 if closed < 0 { return fsw_payload_failure(path, "close" as *u8, closed, FSX_RC_IO, "resolve-source-handle-close" as *u8, status) } 403 if n > FSX_READ_CAP { return fsw_payload_failure(path, "capacity" as *u8, n, FSX_RC_IO, "resolve-writer-text-capacity-before-retry" as *u8, status) } 404 var i: i64 = 0 405 while i < n { 406 if buf[i] == (0 as u8) { return fsw_payload_failure(path, "embedded-NUL-offset" as *u8, i, FSW_USAGE_RC, "use-a-binary-artifact-operation-or-correct-text-source" as *u8, status) } 407 i = i+1 408 } 409 buf[n] = 0 as u8 410 status[0] = 0 411 return buf 412} 413func main(argc: i64, argv: *i64) -> i64 { 414 if argc < FSW_ARGC_WRITE { return fsw_usage() } 415 let verb: *u8 = argv[FSW_ARG_VERB] as *u8 416 let wf: i64 = fsx_seq(verb, "write-file" as *u8) 417 let ef: i64 = fsx_seq(verb, "edit-files" as *u8) 418 if wf == 0 && ef == 0 { return fsw_dispatch(argc,argv) } 419 if ef == 1 && argc < FSW_ARGC_EDIT { return fsw_usage() } 420 let av: *i64 = sys_mmap((argc+1)*8) as *i64 421 var i: i64 = 0 422 while i < argc { av[i] = argv[i]; i=i+1 } 423 av[argc] = 0 424 let status: *i64 = sys_mmap(8) as *i64 425 let a: *u8 = fsw_load_text(argv[FSW_ARG_A] as *u8, status) 426 if (a as i64) == 0 { return status[0] } 427 av[FSW_ARG_A] = a as i64 428 if ef == 1 { 429 let b: *u8 = fsw_load_text(argv[FSW_ARG_B] as *u8, status) 430 if (b as i64) == 0 { return status[0] } 431 av[FSW_ARG_B] = b as i64 432 av[FSW_ARG_VERB] = "edit" as *u8 as i64 433 } else { av[FSW_ARG_VERB] = "write" as *u8 as i64 } 434 return fsw_dispatch(argc,av) 435} 436func fsw_execute(argc: i64, argv: *i64) -> i64 { 437 if argc < FSW_ARGC_WRITE { return fsw_usage() } 438 let verb: *u8 = argv[FSW_ARG_VERB] as *u8 439 if fsx_seq(verb, "write" as *u8) == 1 { 440 let body: *u8 = argv[FSW_ARG_A] as *u8 441 // trailing args are order-free: expect=... and key=... may appear in either slot (a key must never be read as an expect) 442 var exs_w: *u8 = 0 as *u8 443 var wi: i64 = FSW_ARG_EXPECT 444 while wi < argc { let wt: *u8 = argv[wi] as *u8; if fsw_prefix(wt, "expect=" as *u8) == 1 { exs_w = fsw_argval(wt) } wi = wi + 1 } 445 // seq1379 compare-and-swap gate: creation free, overwrite must be intended 446 let cur: i64 = fsw_cur_size(argv[FSW_ARG_PATH] as *u8) 447 if cur >= 0 { 448 var okw: i64 = 0 449 if (exs_w as i64) != 0 { 450 let ex: *u8 = exs_w 451 if fsx_seq(ex, "any" as *u8) == 1 { okw = 1 } else { 452 let want: i64 = fsw_expect_val(ex, argv[FSW_ARG_PATH] as *u8, cur) 453 if want == cur { okw = 1 } else { 454 fsx_puts("NX-FS-WRITE REFUSED stale-expect: file is " as *u8); fsx_putn(cur) 455 fsx_puts(" bytes, you expected " as *u8); fsx_puts(ex) 456 fsx_puts(" -- it changed under you. CURRENT content-hash=h" as *u8); fsw_print_hash(argv[FSW_ARG_PATH] as *u8); fsx_puts(" (seq1477: pass expect=h<hash> for a CONTENT compare-and-swap; expect=<size> only catches SIZE changes, so a same-size rewrite sails through). RE-READ then retry (file unchanged)\n" as *u8) 457 return FSW_RC_CLOBBER 458 } 459 } 460 } 461 if okw == 0 { 462 fsx_puts("NX-FS-WRITE REFUSED would-clobber: " as *u8); fsx_puts(argv[FSW_ARG_PATH] as *u8) 463 fsx_puts(" already exists (" as *u8); fsx_putn(cur) 464 fsx_puts(" bytes) and no expect was given -- a blind whole-file write silently BACKDATES another session's work (seq1379). CURRENT content-hash=h" as *u8) 465 fsw_print_hash(argv[FSW_ARG_PATH] as *u8) 466 fsx_puts(" -- FIX, STRONGEST FIRST: pass expect=h<that hash> for a CONTENT compare-and-swap; or use `edit` (anchored, fails loud); or expect=" as *u8) 467 fsx_putn(cur) 468 fsx_puts(" which is a SIZE-ONLY CAS and lets a same-size rewrite sail straight through (a flipped constant, an equal-length identifier -- MEASURED 2026-08-06 as the commonest raced edit in this tree: a sibling moved const NXA_SMALL_MAX 256->128 in nx_syscalls.nx and BOTH trees still read 46652 bytes); or expect=any to replace deliberately. If you are trying to REMOVE this file, do NOT blank it and do NOT comment it out -- use nx_retire_path retire <path> (renames into knowledge/retired/, never deletes, reversible). A blanked or commented-out file is a LANDMINE: it still parses, still greps, and can still shadow the real organ in a glob-driven build. (file unchanged)\n" as *u8) 469 return FSW_RC_CLOBBER 470 } 471 } 472 // parents first, and the count is reported -- never created behind the caller's back. 473 let made: i64 = fsw_mkparents(argv[FSW_ARG_PATH] as *u8) 474 // (A trace lived here while hunting a "silent through MCP" symptom. The cause was NOT this organ and 475 // NOT the tools API: the allowlist maps the TOOL NAME nx_fs_write to the BINARY nx_fsops_write.elf, 476 // so every promote aimed at "nx_fs_write" updated a file nothing execs. The trace is removed now that 477 // it has done its job -- diagnostic output must not become part of the contract.) 478 let r: i64 = fsx_write(argv[FSW_ARG_PATH] as *u8, body, vw_slen(body)) 479 if r >= 0 { 480 fsx_puts("NX-FS-WRITE OK bytes=" as *u8); fsx_putn(r) 481 fsx_puts(" path=" as *u8); fsx_puts(argv[FSW_ARG_PATH] as *u8) 482 if made > 0 { fsx_puts(" mkdir=" as *u8); fsx_putn(made) } 483 fsx_puts("\n" as *u8) 484 } 485 // A FAILURE THAT PRINTS NOTHING IS RECORDED BY EVERY CALLER AS A SUCCESS. fsx_write's DENIED path 486 // prints its own reason; its IO path printed none, which is the whole defect. Never return a bare code. 487 if r < 0 { 488 if r != 0 - FSW_RC_DENIED_SENTINEL { 489 fsx_puts("NX-FS-WRITE FAILED path=" as *u8); fsx_puts(argv[FSW_ARG_PATH] as *u8) 490 fsx_puts(" -- the atomic write (tmp + fsync + rename) could not complete. Missing parents are created automatically, so the causes left are: a path at or over " as *u8) 491 fsx_putn(FSX_PATH_CAP) 492 fsx_puts(" bytes, a full or read-only filesystem, or directory permissions. FILE UNCHANGED.\n" as *u8) 493 } 494 } 495 let rcw: i64 = fsw_rc(r) 496 return rcw 497 } 498 if fsx_seq(verb, "append" as *u8) == 1 { 499 // 2026-09-02 -- THE COORDINATION VERB. Boards and journals are appended, never rewritten: no anchor 500 // to lose, no expect token to race, no read-modify-write window. One locked O_APPEND write per row. 501 // Missing parents are created and counted exactly as `write` does (deny check first, on the full path). 502 let abody: *u8 = argv[FSW_ARG_A] as *u8 503 let alen: i64 = vw_slen(abody) 504 let amade: i64 = fsw_mkparents(argv[FSW_ARG_PATH] as *u8) 505 let ra: i64 = fsx_append(argv[FSW_ARG_PATH] as *u8, abody, alen) 506 if ra >= 0 { 507 fsx_puts("NX-FS-APPEND OK bytes=" as *u8); fsx_putn(ra) 508 fsx_puts(" path=" as *u8); fsx_puts(argv[FSW_ARG_PATH] as *u8) 509 if ra > alen { fsx_puts(" healed_tail=1 (the file's last byte was not a newline; one was prepended inside the same locked write)" as *u8) } 510 if amade > 0 { fsx_puts(" mkdir=" as *u8); fsx_putn(amade) } 511 fsx_puts(" total=" as *u8); fsx_putn(fsw_cur_size(argv[FSW_ARG_PATH] as *u8)) 512 fsx_puts("\n" as *u8) 513 } 514 if ra == FSX_APP_EMPTY { fsx_puts("NX-FS-APPEND REFUSED empty: nothing to append (file unchanged)\n" as *u8) } 515 if ra == FSX_APP_NONL { fsx_puts("NX-FS-APPEND REFUSED no-trailing-newline: a row that does not terminate glues itself to the next seat's row -- end the text with a newline (file unchanged)\n" as *u8) } 516 if ra == 0 - (3 as i64) { 517 fsx_puts("NX-FS-APPEND FAILED path=" as *u8); fsx_puts(argv[FSW_ARG_PATH] as *u8) 518 fsx_puts(" -- open, lock or write failed (disk, permissions, or a path over the cap). Nothing was appended unless the write itself short-returned.\n" as *u8) 519 } 520 let rca: i64 = fsw_rc(ra) 521 return rca 522 } 523 if fsx_seq(verb, "edit" as *u8) == 1 { 524 if argc < FSW_ARGC_EDIT { return fsw_usage() } 525 // seq1456: `edit` accepts the SAME compare-and-swap token as `write`. 526 // Anchored editing already fails loud when its anchor is gone, but that 527 // does not cover an anchor that still matches in a file a sibling has 528 // rewritten around you. Trailing args are order-free: `all` and 529 // `expect=<size>|any` may appear in either slot, so no existing call 530 // shape changes. Absent expect keeps today's behaviour exactly. 531 var allf: i64 = 0 532 var exs: *u8 = 0 as *u8 533 var ai: i64 = FSW_ARG_ALL 534 while ai < argc { 535 let tok: *u8 = argv[ai] as *u8 536 if fsx_seq(tok, "all" as *u8) == 1 { allf = 1 } else { 537 let tv: *u8 = fsw_argval(tok) 538 if (tv as i64) != (tok as i64) { if fsw_prefix(tok, "key=" as *u8) == 0 { exs = tv } } 539 } 540 ai = ai + 1 541 } 542 if (exs as i64) != 0 { 543 let ecur: i64 = fsw_cur_size(argv[FSW_ARG_PATH] as *u8) 544 if ecur >= 0 { if fsx_seq(exs, "any" as *u8) == 0 { 545 let ewant: i64 = fsw_expect_val(exs, argv[FSW_ARG_PATH] as *u8, ecur) 546 if ewant != ecur { 547 fsx_puts("NX-FS-EDIT REFUSED stale-expect: file is " as *u8); fsx_putn(ecur) 548 fsx_puts(" bytes, you expected " as *u8); fsx_puts(exs) 549 fsx_puts(" -- it changed under you. CURRENT content-hash=h" as *u8); fsw_print_hash(argv[FSW_ARG_PATH] as *u8); fsx_puts(" (seq1477: pass expect=h<hash> for a CONTENT compare-and-swap; expect=<size> only catches SIZE changes, so a same-size rewrite sails through). RE-READ then retry (file unchanged)\n" as *u8) 550 return FSW_RC_CLOBBER 551 } 552 } } 553 } 554 let r2: i64 = fsx_edit(argv[FSW_ARG_PATH] as *u8, argv[FSW_ARG_A] as *u8, argv[FSW_ARG_B] as *u8, allf) 555 if r2 >= 0 { 556 fsx_puts("NX-FS-EDIT OK bytes=" as *u8); fsx_putn(r2) 557 fsx_puts(" path=" as *u8); fsx_puts(argv[FSW_ARG_PATH] as *u8) 558 // THE RECEIPT NOW DISCRIMINATES BY EDIT SHAPE, because OK alone cannot. Measured 2026-09-04: 559 // 339 permil of edits are self-anchored inserts for which the documented free retry SILENTLY 560 // does not hold, and seats re-issue them at the SAME rate as safe replaces because nothing 561 // told them apart. Deriving the kind here rather than asking the caller to declare it is what 562 // makes writer and reader unable to disagree. 563 if fsx_edit_self_anchored(argv[FSW_ARG_A] as *u8, argv[FSW_ARG_B] as *u8) == 1 { 564 fsx_puts(" kind=INSERT-SELF-ANCHORED retry=UNSAFE -- the replacement CONTAINS its own anchor, so the anchor SURVIVED this apply and is STILL UNIQUE. A re-issue returns OK and APPLIES IT AGAIN: the three-state table (OK=had-not-landed / NOMATCH=had-landed) DOES NOT HOLD for this edit. If the outcome was unknown (503 or no reply), RE-READ the file and diff -- do NOT re-issue. To make a retry self-adjudicating, INTERRUPT the anchor so the replacement cannot re-match it, or carry expect=h<hash>." as *u8) 565 } else { 566 fsx_puts(" kind=REPLACE retry=EXACT-SAFE -- the replacement destroys its own anchor, so a re-issue is self-adjudicating: OK means it had not landed, NOMATCH means it had." as *u8) 567 } 568 fsx_puts("\n" as *u8) 569 } 570 if r2 == 0 - FSX_RC_NOMATCH { fsx_puts("NX-FS-EDIT NOMATCH: old-string not found (file unchanged)\n" as *u8) } 571 if r2 == 0 - FSX_RC_AMBIG { fsx_puts("NX-FS-EDIT AMBIGUOUS: old-string occurs more than once; pass `all` or a longer unique context (file unchanged)\n" as *u8) } 572 if r2 == 0 - 1 { fsx_puts("NX-FS-EDIT ABSENT: cannot edit " as *u8); fsx_puts(argv[FSW_ARG_PATH] as *u8); fsx_puts(" -- file absent/unreadable (unchanged). FIX: confirm with `nx_fs ls <dir>`; create it via `nx_fs_write write <path> <content>`.\n" as *u8) } 573 if r2 == 0 - (3 as i64) { fsx_puts("NX-FS-EDIT IO: measured file read, allocation, size arithmetic or write failed. Re-read destination before retrying; check memory, disk and permissions.\n" as *u8) } 574 let rce: i64 = fsw_rc(r2) 575 return rce 576 } 577 return fsw_usage() 578}