code wiki / (root) / nx_tool_exec_allow.nx

nx_tool_exec_allow.nx source

↩ module page · 480 lines · 26716 B

1// nx_tool_exec_allow.nx -- R1 of the executable-API rung: the EXECUTION allowlist that gates R0's raw 2// exec+capture primitive (nx_tool_run) so /mcp tools/call can only ever run VETTED, gate-GREEN organs. 3// This is the never-brick + no-confused-deputy boundary: a caller passes a tool NAME (already capability- 4// authorized upstream in ta_mcp_call); this layer maps that name -> an absolute ELF path ONLY if the name 5// matches an explicit allowlist row whose gate-status is the literal "GREEN". A caller can NEVER supply a 6// path, a "..", or a shell string -- those simply do not resolve. 7// 8// SEPARATE from nx_tool_registry.nx (the DISCOVERY registry that feeds /api/tools + tools/list): discovery 9// answers "what tools exist"; this answers "which of them may be EXECUTED, and from which exact ELF". Kept 10// apart on purpose -- listing a tool must never imply it is runnable. Data-driven (rule 11): the operator 11// curates `tool_allowlist.conf`, so vetting a tool for execution is a config edit + a gate run, not a code 12// change. Missing file / missing row / non-GREEN row -> refused (fail-closed). 13// Row format (TAB-separated; '#' comment lines and blank lines ignored): 14// <tool_name>\t<absolute_elf_path>\t<gate_status> 15// license_tier: ORIGINAL 16import "nx_tool_run.nx" // brings nx_syscalls (sys_read_file/sys_mmap/...) + tr_* transitively 17const TEA_MAGIC_4096: i64 = 4096 18const TEA_MAGIC_262144: i64 = 262144 19 20const TEA_CONF: *u8 = "tool_allowlist.conf" as *u8 // relative to the server CWD (same convention as cap_revoked.list) 21 22// resolve codes 23const TEA_OK: i64 = 1 // allowlisted AND gate-GREEN -> out_path filled, runnable 24const TEA_BLOCKED: i64 = 0 // allowlisted but gate-status != GREEN -> present, NOT runnable 25const TEA_NOTFOUND: i64 = 0 - 1 // no such tool in the allowlist 26const TEA_NOCONF: i64 = 0 - 2 // allowlist file absent/unreadable (fail-closed) 27 28// bytes [s,e) of buf equal the NUL-terminated key (exact length match)? -- for fixed literals (e.g. "GREEN"). 29func tea_field_eq(buf: *u8, s: i64, e: i64, key: *u8) -> i64 { 30 var i: i64 = s 31 var k: i64 = 0 32 while i < e { if buf[i] != key[k] { return 0 } i = i + 1; k = k + 1 } 33 if key[k] != (0 as u8) { return 0 } 34 return 1 35} 36 37// bytes [s,e) of buf equal name[0..nlen)? -- LENGTH-EXPLICIT on both sides, so it is correct whether `name` 38// is a NUL-terminated literal (gate) OR a non-terminated slice into an HTTP body (the live API path). This is 39// the comparison used to match the caller's tool name against an allowlist row. 40func tea_field_eq_n(buf: *u8, s: i64, e: i64, name: *u8, nlen: i64) -> i64 { 41 if e - s != nlen { return 0 } 42 var i: i64 = 0 43 while i < nlen { if buf[s + i] != name[i] { return 0 } i = i + 1 } 44 return 1 45} 46 47// tea_name_ok: defence-in-depth on the NAME before it ever indexes the allowlist -- reject empty, over-long, 48// or anything containing '/', NUL, or whitespace. (The allowlist match already makes traversal impossible; 49// this refuses obviously-hostile names early with a clean verdict.) 50func tea_name_ok(name: *u8, nlen: i64) -> i64 { 51 if nlen <= 0 { return 0 } 52 if nlen > 128 { return 0 } 53 var i: i64 = 0 54 while i < nlen { 55 let c: u8 = name[i] 56 if c == (0x2f as u8) { return 0 } // '/' 57 if c == (0x00 as u8) { return 0 } 58 if c == (0x20 as u8) { return 0 } // space 59 if c == (0x09 as u8) { return 0 } // tab 60 if c == (0x0a as u8) { return 0 } // newline 61 i = i + 1 62 } 63 return 1 64} 65 66// tea_resolve_from: like tea_resolve but reads the allowlist from an explicit `conf` path (testable; the 67// production wrapper tea_resolve pins conf = TEA_CONF). On a GREEN match, copy the ELF path (NUL-terminated) 68// into out_path (bounded by out_cap) and return TEA_OK. Otherwise return a negative/blocked code and leave 69// out_path an empty string. 70func tea_resolve_from(conf: *u8, name: *u8, nlen: i64, out_path: *u8, out_cap: i64) -> i64 { 71 out_path[0] = 0 as u8 72 if tea_name_ok(name, nlen) == 0 { return TEA_NOTFOUND } 73 let szp: *i64 = sys_mmap(16) as *i64 74 let buf: *u8 = sys_read_file(conf, szp) 75 if (buf as i64) == 0 { return TEA_NOCONF } 76 let n: i64 = szp[0] 77 var found: i64 = TEA_NOTFOUND 78 var ls: i64 = 0 79 var i: i64 = 0 80 while i <= n { 81 var eol: i64 = 0 82 if i == n { eol = 1 } else { if buf[i] == (0x0a as u8) { eol = 1 } } 83 if eol == 1 { 84 let le: i64 = i // line = buf[ls, le) 85 if le > ls { if buf[ls] != (0x23 as u8) { // skip blank + '#' comment 86 // field 0 = name: [ls, f0e) 87 var f0e: i64 = ls 88 while f0e < le { if buf[f0e] == (0x09 as u8) { break } f0e = f0e + 1 } 89 if f0e < le { if tea_field_eq_n(buf, ls, f0e, name, nlen) == 1 { 90 // field 1 = path: [p0, f1e) 91 let p0: i64 = f0e + 1 92 var f1e: i64 = p0 93 while f1e < le { if buf[f1e] == (0x09 as u8) { break } f1e = f1e + 1 } 94 // field 2 = status: [s0, le) 95 let s0: i64 = f1e + 1 96 if tea_field_eq(buf, s0, le, "GREEN" as *u8) == 1 { 97 if f1e-p0<=0 || f1e-p0>=out_cap { sys_munmap(buf,n+1); return TEA_BLOCKED } 98 var w: i64 = 0 99 var pp: i64 = p0 100 while pp < f1e { if w < out_cap - 1 { out_path[w] = buf[pp]; w = w + 1 } pp = pp + 1 } 101 out_path[w] = 0 as u8 102 return TEA_OK 103 } 104 found = TEA_BLOCKED 105 } } 106 } } 107 ls = i + 1 108 } 109 i = i + 1 110 } 111 return found 112} 113 114// tea_resolve: production wrapper -- pins conf = TEA_CONF ("tool_allowlist.conf" in the server CWD). 115func tea_resolve(name: *u8, nlen: i64, out_path: *u8, out_cap: i64) -> i64 { 116 return tea_resolve_from(TEA_CONF, name, nlen, out_path, out_cap) 117} 118 119// BOUNDED EXEC (seq1442). Every path below runs a vetted organ under a DEADLINE, because this module is 120// the tools-daemon's exec path: an organ that hangs here does not just fail its own call, it wedges the 121// daemon's tools/call for EVERY MCP client, and the client cannot cancel it. A refused-late call is 122// recoverable; a wedged daemon is not. 123// 124// 120s: long jobs are supposed to take the _async=1 detached job lane, so a SYNCHRONOUS tools/call that 125// has not finished in two minutes has already lost its client. Named, not buried -- and deliberately 126// generous so it can only ever catch a genuine hang, never a merely slow organ. 127// NEXT: this is one global number for ~531 tools of wildly different cost. The honest end state is a 128// per-tool budget carried as a 5th allowlist field, derived from what each organ actually does. 129const TEA_EXEC_TIMEOUT_MS: i64 = 120000 130 131// ---- LANE-SCOPED EXEC BUDGETS (root fix 2026-07-30, ws=sev-eater) ----------------------------- 132// MEASURED THIS SESSION, not inferred: the edge (sites.elf) abandons a proxied request at exactly 133// 15.02s, while a worker slot stayed held for the full 120s above. That is not one number being 134// wrong -- it is ONE GLOBAL TIMEOUT SERVING TWO LANES WITH OPPOSITE REQUIREMENTS. The synchronous 135// /mcp lane must give up INSIDE the edge window, because after it nobody is listening and the slot 136// is capacity spent on an answer that can never be delivered. The DETACHED async job lane must be 137// allowed to run long -- that is the entire reason it exists. Tuned for async, the sync lane 138// inherited a budget 8x its client's patience, so against a bounded 16-worker pool a trickle of 139// hung calls saturated the whole agent surface for every seat: 10 stuck handler pairs measured, 140// 4 of 8 requests 503ing, /proc showing the handler in pipe_wait behind an nx_mgmt_call parked in 141// sk_wait_data on a mgmt reply that never came. 142// LAW, already banked in this codebase for the reader/writer cap pair and it generalises: the two 143// ends of one pipe are a MATCHED PAIR. A timeout longer than the caller's patience is not safety, 144// it is capacity burned on nobody's behalf. 145// The budget is therefore a property of the LANE. TEA_EXEC_TIMEOUT_MS is left EXACTLY as it was for 146// the async/detached lane and all other existing callers (rule 19: no contract change); the sync 147// lane passes TEA_EXEC_TIMEOUT_SYNC_MS through the *_to entry points below. SYNC is DERIVED from 148// the window rather than typed as its own literal so the two can never drift apart (rule 11), and 149// it lands just inside so the DAEMON decides the failure and can answer before the edge gives up. 150const TEA_EDGE_WINDOW_MS: i64 = 15000 151const TEA_EXEC_TIMEOUT_SYNC_MS: i64 = TEA_EDGE_WINDOW_MS - 1000 152 153// tea_run_from: the composed R0+R1 call against an explicit conf -- resolve `name` through the allowlist, 154// then (only on TEA_OK) execute the vetted ELF via nx_tool_run's capture primitive with a single optional 155// arg. Returns the child exit code on success, or the negative TEA_* refusal code (so the API can map it to 156// a JSON-RPC error). *outlen receives captured byte count; *rc (if non-null) receives the resolve code. 157func tea_run_from(conf: *u8, name: *u8, nlen: i64, arg: *u8, out: *u8, out_cap: i64, outlen: *i64, rc: *i64) -> i64 { 158 let path: *u8 = sys_mmap(TEA_MAGIC_4096) 159 let code: i64 = tea_resolve_from(conf, name, nlen, path, TEA_MAGIC_4096) 160 if (rc as i64) != 0 { rc[0] = code } 161 if code != TEA_OK { if (outlen as i64) != 0 { outlen[0] = 0 } return code } 162 return tr_run1_to(path, arg, out, out_cap, outlen, TEA_EXEC_TIMEOUT_MS) 163} 164 165// tea_run: production wrapper -- pins conf = TEA_CONF. 166func tea_run(name: *u8, nlen: i64, arg: *u8, out: *u8, out_cap: i64, outlen: *i64, rc: *i64) -> i64 { 167 return tea_run_from(TEA_CONF, name, nlen, arg, out, out_cap, outlen, rc) 168} 169 170// tea_run_argv_from: like tea_run_from but executes with a FULL argv VECTOR (multi-arg tools/call). The caller 171// fills argv[1..] with the tool's positional args and NUL-terminates the vector; argv[0] is (re)set HERE to the 172// resolved absolute ELF path (the argv[0] convention). Resolution is unchanged -- only a GREEN-allowlisted NAME 173// resolves, so a caller still can never smuggle a path, "..", or shell string through argv[0]. Returns the child 174// exit code on TEA_OK, else the negative TEA_* refusal code. *outlen = captured bytes; *rc = the resolve code. 175func tea_run_argv_from(conf: *u8, name: *u8, nlen: i64, argv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64) -> i64 { 176 let path: *u8 = sys_mmap(TEA_MAGIC_4096) 177 let code: i64 = tea_resolve_from(conf, name, nlen, path, TEA_MAGIC_4096) 178 if (rc as i64) != 0 { rc[0] = code } 179 if code != TEA_OK { if (outlen as i64) != 0 { outlen[0] = 0 } return code } 180 argv[0] = path as i64 181 return tr_run_capture_to(path, argv, out, out_cap, outlen, TEA_EXEC_TIMEOUT_MS) 182} 183 184// tea_run_argv: production wrapper -- pins conf = TEA_CONF. 185func tea_run_argv(name: *u8, nlen: i64, argv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64) -> i64 { 186 return tea_run_argv_from(TEA_CONF, name, nlen, argv, out, out_cap, outlen, rc) 187} 188 189// ---- FIXED-ARG (pinned) execution --------------------------------------------------------------------------- 190// A GREEN allowlist row MAY carry a 4th TAB field = the tool's PINNED argv[1..] (space-separated). When present, 191// the caller's args are IGNORED and the tool runs with EXACTLY those fixed args. This safely exposes a multi-sub 192// control binary as a tool whose subcommand is FIXED: a cap for `nx_status` runs `nx_hostctl status` and can NEVER 193// run `nx_hostctl selfswap` -- the caller controls the tool NAME (cap-gated), never the argv. No 4th field -> the 194// caller's argv is used. Row: <name>\t<abs-elf>\t<GREEN>[\t<pinned args>] 195 196// Size the allocation from the opened registry, without a fixed corpus ceiling. 197func tea_read_conf(path: *u8, out_len: *i64) -> *u8 { 198 out_len[0]=0 199 let fd:i64=sys_openat_rd(path) 200 if fd<0 { return 0 as *u8 } 201 let size:i64=sys_lseek(fd,0,2) 202 if size<0 { sys_close(fd); return 0 as *u8 } 203 if sys_lseek(fd,0,0)!=0 { sys_close(fd); return 0 as *u8 } 204 let buf:*u8=sys_mmap(size+1) 205 var total:i64=0 206 while total<size { 207 let n:i64=sys_read(fd,buf+total,size-total) 208 if n<=0 { sys_close(fd); sys_munmap(buf,size+1); return 0 as *u8 } 209 total=total+n 210 } 211 // Registry writers replace atomically. An in-place size change is not a coherent snapshot. 212 let extra:i64=sys_read(fd,buf+size,1) 213 sys_close(fd) 214 if extra!=0 { sys_munmap(buf,size+1); return 0 as *u8 } 215 buf[size]=0 as u8; out_len[0]=size 216 return buf 217} 218 219// tokenize NUL-terminated `s` by spaces into argv[base..]; each token NUL-terminated into `scratch`. Returns count. 220func tea_tokenize(s: *u8, argv: *i64, base: i64, scratch: *u8, scap: i64) -> i64 { 221 var k: i64 = 0; var w: i64 = 0; var i: i64 = 0; var intok: i64 = 0; var done: i64 = 0 222 while done == 0 { 223 let c: i64 = s[i] as i64 224 if c == 0 { 225 if intok == 1 { if w < scap { scratch[w] = 0 as u8; w = w + 1 } } 226 done = 1 227 } else { 228 if c == 32 { 229 if intok == 1 { if w < scap { scratch[w] = 0 as u8; w = w + 1 } intok = 0 } 230 } else { 231 if intok == 0 { let st: i64 = (scratch as i64) + w; argv[base + k] = st; k = k + 1; intok = 1 } 232 if w < scap - 1 { scratch[w] = c as u8; w = w + 1 } 233 } 234 i = i + 1 235 } 236 } 237 return k 238} 239 240// Count and validate before writing either output buffer. Negative means no argv was constructed. 241func tea_tokenize_checked(s:*u8, argv:*i64, base:i64, argv_cap:i64, scratch:*u8, scap:i64)->i64 { 242 var count:i64=0; var bytes:i64=0; var inside:i64=0; var i:i64=0 243 while s[i]!=(0 as u8) { 244 if s[i]==(32 as u8) { 245 if inside==1 { bytes=bytes+1; inside=0 } 246 } else { 247 if inside==0 { count=count+1; inside=1 } 248 bytes=bytes+1 249 } 250 i=i+1 251 } 252 if inside==1 { bytes=bytes+1 } 253 if base<0 || argv_cap<=base || scap<0 { return 0-1 } 254 if count>=argv_cap-base || bytes>scap { return 0-1 } 255 let n:i64=tea_tokenize(s,argv,base,scratch,scap) 256 argv[base+n]=0 257 return n 258} 259 260// resolve a GREEN row -> path (field 1) AND its optional pinned args (field 3). out_haspin[0]=1 iff a non-empty 4th 261// field exists. Parses status as field 2 up to the NEXT tab (so a 4th field doesn't corrupt the GREEN match). 262func tea_resolve_pinned_from(conf: *u8, name: *u8, nlen: i64, out_path: *u8, out_cap: i64, out_pin: *u8, pin_cap: i64, out_haspin: *i64) -> i64 { 263 out_path[0] = 0 as u8; out_pin[0] = 0 as u8; out_haspin[0] = 0 264 if tea_name_ok(name, nlen) == 0 { return TEA_NOTFOUND } 265 let szp: *i64 = sys_mmap(16) as *i64 266 let buf: *u8 = tea_read_conf(conf, szp) 267 let n: i64 = szp[0] 268 sys_munmap(szp,16) 269 if (buf as i64) == 0 { return TEA_NOCONF } 270 var found: i64 = TEA_NOTFOUND 271 var ls: i64 = 0 272 var i: i64 = 0 273 while i <= n { 274 var eol: i64 = 0 275 if i == n { eol = 1 } else { if buf[i] == (0x0a as u8) { eol = 1 } } 276 if eol == 1 { 277 let le: i64 = i 278 if le > ls { if buf[ls] != (0x23 as u8) { 279 var f0e: i64 = ls 280 while f0e < le { if buf[f0e] == (0x09 as u8) { break } f0e = f0e + 1 } 281 if f0e < le { if tea_field_eq_n(buf, ls, f0e, name, nlen) == 1 { 282 let p0: i64 = f0e + 1 283 var f1e: i64 = p0 284 while f1e < le { if buf[f1e] == (0x09 as u8) { break } f1e = f1e + 1 } 285 let s0: i64 = f1e + 1 286 var s2e: i64 = s0 287 while s2e < le { if buf[s2e] == (0x09 as u8) { break } s2e = s2e + 1 } 288 if tea_field_eq(buf, s0, s2e, "GREEN" as *u8) == 1 { 289 if f1e-p0<=0 || f1e-p0>=out_cap { sys_munmap(buf,n+1); return TEA_BLOCKED } 290 if s2e<le { if le-s2e-1>=pin_cap { sys_munmap(buf,n+1); return TEA_BLOCKED } } 291 var w: i64 = 0 292 var pp: i64 = p0 293 while pp < f1e { if w < out_cap - 1 { out_path[w] = buf[pp]; w = w + 1 } pp = pp + 1 } 294 out_path[w] = 0 as u8 295 if s2e < le { 296 let p3: i64 = s2e + 1 297 var pw: i64 = 0 298 var qq: i64 = p3 299 while qq < le { if pw < pin_cap - 1 { out_pin[pw] = buf[qq]; pw = pw + 1 } qq = qq + 1 } 300 out_pin[pw] = 0 as u8 301 if pw > 0 { out_haspin[0] = 1 } 302 } 303 sys_munmap(buf,n+1) 304 return TEA_OK 305 } 306 found = TEA_BLOCKED 307 } } 308 } } 309 ls = i + 1 310 } 311 i = i + 1 312 } 313 sys_munmap(buf,n+1) 314 return found 315} 316 317// tea_run_pinned_from: pinning-aware exec. If the row pins args, build argv from them (caller args IGNORED); else 318// use the caller's argv (argv[0] set to the resolved path). Returns the child exit code / negative TEA_* code. 319// ★★★seq1611/1634 ROOT CAUSE: A DETACHED JOB MUST NOT INHERIT THE SYNCHRONOUS 320// REQUEST TIMEOUT. TEA_EXEC_TIMEOUT_MS (120s) is correct for a tools/call a 321// caller is waiting on, and wrong BY CONSTRUCTION for an async job whose entire 322// purpose is to outlive that window. The codewiki regen takes 3-6 minutes and 323// was killed at 2 minutes EVERY time, returning a negative rc that rendered as 324// an EMPTY exit slot -- which read like success to a careless poller. 325// tmo=0 selects the untimed path (tr_run_capture_to falls through). 326// ★The bound was not too SMALL; a fixed bound was the wrong SHAPE for this 327// caller -- the bounds law again: when a bound bites, change the architecture. 328func tea_run_pinned_from_to(conf: *u8, name: *u8, nlen: i64, callerargv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64, tmo: i64) -> i64 { 329 let path: *u8 = sys_mmap(TEA_MAGIC_4096) 330 let pin: *u8 = sys_mmap(TEA_MAGIC_4096) 331 let haspin: *i64 = sys_mmap(16) as *i64 332 let code: i64 = tea_resolve_pinned_from(conf, name, nlen, path, TEA_MAGIC_4096, pin, TEA_MAGIC_4096, haspin) 333 if (rc as i64) != 0 { rc[0] = code } 334 if code != TEA_OK { if (outlen as i64) != 0 { outlen[0] = 0 } return code } 335 if haspin[0] == 1 { 336 let pav: *i64 = sys_mmap(TEA_MAGIC_4096) as *i64 337 let psc: *u8 = sys_mmap(TEA_MAGIC_4096) 338 pav[0] = path as i64 339 let nt: i64 = tea_tokenize_checked(pin, pav, 1, TEA_MAGIC_4096/8, psc, TEA_MAGIC_4096) 340 if nt<0 { if (rc as i64)!=0 { rc[0]=TEA_BLOCKED }; if (outlen as i64)!=0 { outlen[0]=0 }; return TEA_BLOCKED } 341 pav[1 + nt] = 0 342 return tr_run_capture_to(path, pav, out, out_cap, outlen, tmo) 343 } 344 callerargv[0] = path as i64 345 return tr_run_capture_to(path, callerargv, out, out_cap, outlen, tmo) 346} 347// back-compat wrappers: every existing SYNCHRONOUS caller keeps the 120s 348// request timeout byte-for-byte; only a caller that asks for tmo gets another. 349func tea_run_pinned_from(conf: *u8, name: *u8, nlen: i64, callerargv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64) -> i64 { 350 return tea_run_pinned_from_to(conf, name, nlen, callerargv, out, out_cap, outlen, rc, TEA_EXEC_TIMEOUT_MS) 351} 352func tea_run_pinned_to(name: *u8, nlen: i64, callerargv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64, tmo: i64) -> i64 { 353 return tea_run_pinned_from_to(TEA_CONF, name, nlen, callerargv, out, out_cap, outlen, rc, tmo) 354} 355func tea_run_pinned(name: *u8, nlen: i64, callerargv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64) -> i64 { 356 return tea_run_pinned_from_to(TEA_CONF, name, nlen, callerargv, out, out_cap, outlen, rc, TEA_EXEC_TIMEOUT_MS) 357} 358 359// ---- DEADLINE EXECUTION: the PROMOTABLE sync lane (2026-08-22) ------------------------------------------ 360// Same resolve + pin logic as tea_run_pinned_from_to, dispatched through tr_run_capture_deadline: at the 361// deadline the worker is NOT killed -- the caller receives TR_PROMOTE plus the live pid and read end and 362// promotes the call onto the job lane. tea_run_pinned_from_to is byte-untouched (rule 19). 363func tea_run_pinned_deadline_from(conf: *u8, name: *u8, nlen: i64, callerargv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64, deadline_ms: i64, out_pid: *i64, out_rfd: *i64) -> i64 { 364 let path: *u8 = sys_mmap(TEA_MAGIC_4096) 365 let pin: *u8 = sys_mmap(TEA_MAGIC_4096) 366 let haspin: *i64 = sys_mmap(16) as *i64 367 let code: i64 = tea_resolve_pinned_from(conf, name, nlen, path, TEA_MAGIC_4096, pin, TEA_MAGIC_4096, haspin) 368 if (rc as i64) != 0 { rc[0] = code } 369 if code != TEA_OK { if (outlen as i64) != 0 { outlen[0] = 0 } return code } 370 if haspin[0] == 1 { 371 let pav: *i64 = sys_mmap(TEA_MAGIC_4096) as *i64 372 let psc: *u8 = sys_mmap(TEA_MAGIC_4096) 373 pav[0] = path as i64 374 let nt: i64 = tea_tokenize_checked(pin, pav, 1, TEA_MAGIC_4096/8, psc, TEA_MAGIC_4096) 375 if nt<0 { if (rc as i64)!=0 { rc[0]=TEA_BLOCKED }; if (outlen as i64)!=0 { outlen[0]=0 }; return TEA_BLOCKED } 376 pav[1 + nt] = 0 377 return tr_run_capture_deadline(path, pav, out, out_cap, outlen, deadline_ms, out_pid, out_rfd) 378 } 379 callerargv[0] = path as i64 380 return tr_run_capture_deadline(path, callerargv, out, out_cap, outlen, deadline_ms, out_pid, out_rfd) 381} 382func tea_run_pinned_deadline(name: *u8, nlen: i64, callerargv: *i64, out: *u8, out_cap: i64, outlen: *i64, rc: *i64, deadline_ms: i64, out_pid: *i64, out_rfd: *i64) -> i64 { 383 return tea_run_pinned_deadline_from(TEA_CONF, name, nlen, callerargv, out, out_cap, outlen, rc, deadline_ms, out_pid, out_rfd) 384} 385 386// ---- THE WINDOW PAIR, ONE OWNER (2026-08-22) ------------------------------------------------------------ 387// TEA_EDGE_WINDOW_MS above used to be the SECOND hand-typed copy of the edge's NX_SD2_BACKEND_TIMEOUT_S, 388// guarded by a comment. It is now the COMPILED FALLBACK only: the live value is read from 389// knowledge/edge_window.conf (the SSOT), and nx_sync_promote_gate refuses when that row and the edge's 390// compiled constant disagree -- the comment became an exit code. The reserve default is DERIVED from the 391// compiled pair (window - sync budget = 1000), never retyped as its own literal. 392const TEA_WINDOW_CONF: *u8 = "knowledge/edge_window.conf" as *u8 393const TEA_WINDOW_KEY: *u8 = "edge_window_ms" as *u8 394const TEA_RESERVE_KEY: *u8 = "reply_reserve_ms" as *u8 395const TEA_REPLY_RESERVE_DEFAULT_MS: i64 = TEA_EDGE_WINDOW_MS - TEA_EXEC_TIMEOUT_SYNC_MS 396const TEA_CONF_CAP: i64 = TEA_MAGIC_4096 // a two-row conf; a full read of it fits with 40x headroom 397const TEA_SRC_DEFAULT: i64 = 0 398const TEA_SRC_CONF: i64 = 1 399const TEA_NL: i64 = 10 400const TEA_D0: i64 = 48 401const TEA_D9: i64 = 57 402// ONE line-anchored conf reader, defined HERE. This is the SAME contract as rm_conf (nx_resmon_lib): 403// the first integer on the line that STARTS WITH key. It is NOT imported because nx_resmon_lib pulls 404// its whole closure into all 51 consumers of nx_tool_run; the two-row conf does not justify that. 405// One reader serves both keys, so there is no duplication WITHIN this file. 406func tea_conf_read(path: *u8, buf: *u8, cap: i64) -> i64 { 407 let fd: i64 = sys_openat_rd(path) 408 if fd < 0 { return 0 - 1 } 409 var tot: i64 = 0 410 var go: i64 = 1 411 while go == 1 { 412 let n: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot - 1) 413 if n <= 0 { go = 0 } else { tot = tot + n; if tot >= cap - 1 { go = 0 } } 414 } 415 sys_close(fd) 416 buf[tot] = 0 as u8 417 return tot 418} 419// first integer after `key` on the line that STARTS WITH key; dflt if key is absent or has no integer. 420func tea_conf_int(buf: *u8, n: i64, key: *u8, dflt: i64) -> i64 { 421 var kl: i64 = 0 422 while key[kl] != (0 as u8) { kl = kl + 1 } 423 var ls: i64 = 0 424 while ls < n { 425 var le: i64 = ls 426 while le < n { if buf[le] == (TEA_NL as u8) { break } le = le + 1 } 427 if le - ls >= kl { 428 var m: i64 = 1 429 var i: i64 = 0 430 while i < kl { if (buf[ls + i] as i64) != (key[i] as i64) { m = 0; i = kl } else { i = i + 1 } } 431 if m == 1 { 432 var p: i64 = ls + kl 433 var v: i64 = 0 434 var seen: i64 = 0 435 var run: i64 = 1 436 while run == 1 { if p >= le { run = 0 } else { 437 let c: i64 = buf[p] as i64 438 if c >= TEA_D0 { if c <= TEA_D9 { v = v * 10 + (c - TEA_D0); seen = 1 } else { if seen == 1 { run = 0 } } } else { if seen == 1 { run = 0 } } 439 p = p + 1 440 } } 441 if seen == 1 { return v } 442 } 443 } 444 ls = le + 1 445 } 446 return dflt 447} 448// out_src (if non-null): TEA_SRC_CONF when the row was read, TEA_SRC_DEFAULT when the compiled fallback 449// answered -- a fallback that is silent is a fallback nobody can see. 450func tea_edge_window_ms_from(conf: *u8, out_src: *i64) -> i64 { 451 if (out_src as i64) != 0 { out_src[0] = TEA_SRC_DEFAULT } 452 let cb: *u8 = sys_mmap(TEA_CONF_CAP) 453 let cn: i64 = tea_conf_read(conf, cb, TEA_CONF_CAP) 454 if cn <= 0 { return TEA_EDGE_WINDOW_MS } 455 let v: i64 = tea_conf_int(cb, cn, TEA_WINDOW_KEY, 0 - 1) 456 if v <= 0 { return TEA_EDGE_WINDOW_MS } 457 if (out_src as i64) != 0 { out_src[0] = TEA_SRC_CONF } 458 return v 459} 460func tea_edge_window_ms(out_src: *i64) -> i64 { return tea_edge_window_ms_from(TEA_WINDOW_CONF, out_src) } 461func tea_reply_reserve_ms_from(conf: *u8) -> i64 { 462 let cb: *u8 = sys_mmap(TEA_CONF_CAP) 463 let cn: i64 = tea_conf_read(conf, cb, TEA_CONF_CAP) 464 if cn <= 0 { return TEA_REPLY_RESERVE_DEFAULT_MS } 465 let v: i64 = tea_conf_int(cb, cn, TEA_RESERVE_KEY, 0 - 1) 466 if v < 0 { return TEA_REPLY_RESERVE_DEFAULT_MS } 467 return v 468} 469func tea_reply_reserve_ms() -> i64 { return tea_reply_reserve_ms_from(TEA_WINDOW_CONF) } 470// The sync PROMOTION deadline = window - reserve, derived live so the two ends cannot drift by a retype. 471// A reserve that swallows the whole window is a misconfiguration: refuse to produce a zero deadline, 472// because zero means UNBOUNDED to tr_run_capture_deadline and would silently re-create the hang. 473func tea_sync_promote_ms_from(conf: *u8, out_src: *i64) -> i64 { 474 let w: i64 = tea_edge_window_ms_from(conf, out_src) 475 let r: i64 = tea_reply_reserve_ms_from(conf) 476 var d: i64 = w - r 477 if d <= 0 { d = w } 478 return d 479} 480func tea_sync_promote_ms(out_src: *i64) -> i64 { return tea_sync_promote_ms_from(TEA_WINDOW_CONF, out_src) }