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) }