nx_toolreg_reconcile_lib.nx source
↩ module page · 268 lines · 13861 B
1// nx_toolreg_reconcile_lib.nx -- DISCOVERY RECONCILER, library half. Closes the WIRE class forever:
2// every tool that is GREEN in tool_allowlist.conf (vetted + callable) but ABSENT from the discovery
3// registry (toolreg -> tools/list) gets registered using ITS AUTHOR'S OWN schema row from
4// knowledge/tool_schemas.conf -- no invented contracts. A tool with NO schema row is SKIPPED and
5// reported (fail-closed: never register a contract-less tool). Idempotent by construction (tool_get
6// first). Re-running after any future allowlist growth wires the newcomers -- the allowlist stays the
7// SSOT and discovery reconciles to it, so the built-not-discoverable bucket cannot silently regrow.
8// license_tier: ORIGINAL
9import "nx_tool_registry.nx"
10import "nx_vsz_watchdog_core.nx" // vw_read -- bounded file reads
11
12const RR_FILE_CAP: i64 = 262144 // allowlist/schemas read cap
13const RR_LINE_CAP: i64 = 4096 // one row (name/title/fields/desc)
14const RR_INV_CAP: i64 = 512 // generated invoke line
15const RR_TAB: i64 = 9 // '\t'
16const RR_NL: i64 = 10 // '\n'
17const RR_HASH: i64 = 35 // '#' (comment rows)
18const RR_C_REG: i64 = 0 // counts slot: newly registered
19const RR_C_ALREADY: i64 = 1 // counts slot: already discoverable
20const RR_C_NOSCHEMA: i64 = 2 // counts slot: skipped, no schema row (fail-closed)
21const RR_C_FAIL: i64 = 3 // counts slot: register call failed
22const RR_C_UPDATED: i64 = 4 // counts slot: stored description REWRITTEN from the author's schema
23const RR_COUNTS: i64 = 64 // counts buffer bytes
24const RR_COMMA: i64 = 44 // ',' join separator (name list)
25const RR_SPC: i64 = 32 // ' '
26const RR_SEP_LEN: i64 = 2 // ", " separator width
27// EXPOSURE POLICY (2026-07-17): tools deliberately kept OUT of tools/list discovery -- security-sensitive
28// (vault/secret CLIs), internal-only census metrics, or explicitly-not-an-inline-MCP-call organs. These are
29// callable-by-cap but intentionally un-advertised, so their absence from discovery is CORRECT, not drift.
30// One TAB-first-field name per line (# comments ok); absent/empty file -> no exemptions (fail-open).
31const RR_DENY_PATH: *u8 = "knowledge/mcp/exposure_deny.txt"
32
33func rr_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
34func rr_seq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 }
35// first TAB field of line[0..ll) -> out; returns len
36func rr_f0(line: *u8, ll: i64, out: *u8) -> i64 {
37 var i: i64 = 0
38 while i < ll { if line[i] == (RR_TAB as u8) { i = ll } else { out[i] = line[i]; i = i + 1 } }
39 var n: i64 = 0
40 while n < ll { if line[n] == (RR_TAB as u8) { out[n] = 0 as u8; return n } n = n + 1 }
41 out[ll] = 0 as u8
42 return ll
43}
44// LAST TAB field of line[0..ll) -> out (the schema desc column); returns len
45func rr_flast(line: *u8, ll: i64, out: *u8) -> i64 {
46 var s: i64 = 0
47 var i: i64 = 0
48 while i < ll { if line[i] == (RR_TAB as u8) { s = i + 1 } i = i + 1 }
49 var o: i64 = 0
50 while s < ll { out[o] = line[s]; o = o + 1; s = s + 1 }
51 out[o] = 0 as u8
52 return o
53}
54// field #1 of a TAB registry record -> out. The record is name\tdesc\tinvoke\tstatus, so field 1
55// is the DESCRIPTION consumers actually read in GET /api/tools and MCP tools/list. Needed so the
56// reconciler can COMPARE what is stored against what the author wrote (debt seq1526).
57func rr_rec_desc(rec: *u8, ln: i64, out: *u8) -> i64 {
58 var i: i64 = 0
59 var tabs: i64 = 0
60 var s: i64 = 0 - 1
61 var e: i64 = ln
62 while i < ln {
63 if rec[i] == (RR_TAB as u8) {
64 tabs = tabs + 1
65 if tabs == 1 { s = i + 1 } else { if tabs == 2 { e = i; i = ln } }
66 }
67 i = i + 1
68 }
69 if s < 0 { out[0] = 0 as u8; return 0 }
70 var o: i64 = 0
71 var k: i64 = s
72 while k < e { out[o] = rec[k]; o = o + 1; k = k + 1 }
73 out[o] = 0 as u8
74 return o
75}
76// find the schema row for `name` in schemas buf[0..n); copy its LAST field into desc. 1=found.
77func rr_schema_desc(buf: *u8, n: i64, name: *u8, desc: *u8) -> i64 {
78 let f0: *u8 = sys_mmap(RR_LINE_CAP)
79 var ls: i64 = 0
80 var i: i64 = 0
81 while i <= n {
82 var eol: i64 = 0
83 if i == n { eol = 1 } else { if buf[i] == (RR_NL as u8) { eol = 1 } }
84 if eol == 1 {
85 if i > ls { if buf[ls] != (RR_HASH as u8) {
86 rr_f0(((buf as i64 + ls) as *u8), i - ls, f0)
87 if rr_seq(f0, name) == 1 {
88 rr_flast(((buf as i64 + ls) as *u8), i - ls, desc)
89 return 1
90 }
91 } }
92 ls = i + 1
93 }
94 i = i + 1
95 }
96 return 0
97}
98// build "over /mcp: tools/call name=<t> arguments={argv:[...]} (cap granting <t>)" into inv
99func rr_invoke(name: *u8, inv: *u8) -> i64 {
100 var o: i64 = 0
101 let a: *u8 = "over /mcp: tools/call name=" as *u8
102 var i: i64 = 0
103 while a[i] != (0 as u8) { inv[o] = a[i]; o = o + 1; i = i + 1 }
104 i = 0
105 while name[i] != (0 as u8) { inv[o] = name[i]; o = o + 1; i = i + 1 }
106 let b: *u8 = " arguments={argv:[...]} with a capability granting the tool; args per the description" as *u8
107 i = 0
108 while b[i] != (0 as u8) { inv[o] = b[i]; o = o + 1; i = i + 1 }
109 inv[o] = 0 as u8
110 return o
111}
112// 1 if `name` is on the deny-advertise policy list (deliberately un-discoverable). First TAB field match;
113// a '#' comment or absent/empty deny buffer -> NOT denied (fail-open, so default behavior is unchanged).
114func rr_is_deny(name: *u8, db: *u8, dn: i64) -> i64 {
115 if dn <= 0 { return 0 }
116 let f0: *u8 = sys_mmap(RR_LINE_CAP)
117 var ls: i64 = 0
118 var i: i64 = 0
119 while i <= dn {
120 var eol: i64 = 0
121 if i == dn { eol = 1 } else { if db[i] == (RR_NL as u8) { eol = 1 } }
122 if eol == 1 {
123 if i > ls { if db[ls] != (RR_HASH as u8) {
124 rr_f0(((db as i64 + ls) as *u8), i - ls, f0)
125 if rr_seq(f0, name) == 1 { return 1 }
126 } }
127 ls = i + 1
128 }
129 i = i + 1
130 }
131 return 0
132}
133// RECONCILE/AUDIT under `prefix`: for each GREEN allowlist row missing from the registry, register with
134// the author's schema desc. counts: [reg, already, noschema, fail]. Returns rows examined.
135// dry==1 = AUDIT MODE: register NOTHING; RR_C_REG counts rows that WOULD register (the drift signal), so
136// a caller can assert drift=0 (reg + noschema == 0) without mutating -- the standing regression tooth.
137func rr_run(prefix: *u8, allow_path: *u8, schemas_path: *u8, counts: *i64, dry: i64) -> i64 {
138 counts[RR_C_REG] = 0
139 counts[RR_C_ALREADY] = 0
140 counts[RR_C_NOSCHEMA] = 0
141 counts[RR_C_FAIL] = 0
142 counts[RR_C_UPDATED] = 0
143 let ab: *u8 = sys_mmap(RR_FILE_CAP)
144 let an: i64 = vw_read(allow_path, ab, RR_FILE_CAP - 1)
145 if an <= 0 { return 0 - 1 }
146 let sb: *u8 = sys_mmap(RR_FILE_CAP)
147 let sn: i64 = vw_read(schemas_path, sb, RR_FILE_CAP - 1)
148 if sn <= 0 { return 0 - 1 }
149 let db: *u8 = sys_mmap(RR_FILE_CAP)
150 let dn: i64 = vw_read(RR_DENY_PATH, db, RR_FILE_CAP - 1) // optional exposure-deny policy; <=0 -> none
151 let name: *u8 = sys_mmap(RR_LINE_CAP)
152 let desc: *u8 = sys_mmap(RR_LINE_CAP)
153 let cur: *u8 = sys_mmap(RR_LINE_CAP)
154 let inv: *u8 = sys_mmap(RR_INV_CAP)
155 let po: *i64 = sys_mmap(16) as *i64
156 let lo: *i64 = sys_mmap(16) as *i64
157 var rows: i64 = 0
158 var ls: i64 = 0
159 var i: i64 = 0
160 while i <= an {
161 var eol: i64 = 0
162 if i == an { eol = 1 } else { if ab[i] == (RR_NL as u8) { eol = 1 } }
163 if eol == 1 {
164 if i > ls { if ab[ls] != (RR_HASH as u8) {
165 rows = rows + 1
166 rr_f0(((ab as i64 + ls) as *u8), i - ls, name)
167 if name[0] != (0 as u8) { if rr_is_deny(name, db, dn) == 0 {
168 if tool_get_pfx(prefix, name, po, lo) == 1 {
169 // UPSERT, NOT INSERT-ONLY (debt seq1526). This branch used to just count
170 // ALREADY and move on, so a description CORRECTED in tool_schemas.conf never
171 // reached consumers: the corpus and the registry silently disagreed and every
172 // stale or misleading tool contract was PERMANENT by construction. Compare
173 // stored-vs-authored and re-put on drift. A reconciler that cannot reconcile
174 // a CHANGE is a seeder. reg_put is keyed by name, so this replaces in place.
175 var updated: i64 = 0
176 if rr_schema_desc(sb, sn, name, desc) == 1 {
177 rr_rec_desc(po[0] as *u8, lo[0], cur)
178 if rr_seq(cur, desc) == 0 {
179 if dry == 1 { updated = 1 } else {
180 rr_invoke(name, inv)
181 let rc2: i64 = tool_register_pfx(prefix, name, desc,
182 inv, "GREEN (allowlisted+vetted; discovery row reconciled from the author's tool_schemas.conf entry by nx_toolreg_reconcile)" as *u8)
183 if rc2 == 0 { updated = 1 } else { counts[RR_C_FAIL] = counts[RR_C_FAIL] + 1 }
184 }
185 }
186 }
187 if updated == 1 { counts[RR_C_UPDATED] = counts[RR_C_UPDATED] + 1 }
188 else { counts[RR_C_ALREADY] = counts[RR_C_ALREADY] + 1 }
189 } else {
190 if rr_schema_desc(sb, sn, name, desc) == 1 {
191 if dry == 1 {
192 counts[RR_C_REG] = counts[RR_C_REG] + 1 // WOULD register (drift)
193 } else {
194 rr_invoke(name, inv)
195 let rc: i64 = tool_register_pfx(prefix, name, desc,
196 inv, "GREEN (allowlisted+vetted; discovery row reconciled from the author's tool_schemas.conf entry by nx_toolreg_reconcile)" as *u8)
197 if rc == 0 { counts[RR_C_REG] = counts[RR_C_REG] + 1 } else { counts[RR_C_FAIL] = counts[RR_C_FAIL] + 1 }
198 }
199 } else {
200 counts[RR_C_NOSCHEMA] = counts[RR_C_NOSCHEMA] + 1
201 }
202 }
203 } }
204 } }
205 ls = i + 1
206 }
207 i = i + 1
208 }
209 return rows
210}
211// back-compat: reconcile = the mutating run (dry=0).
212func rr_reconcile(prefix: *u8, allow_path: *u8, schemas_path: *u8, counts: *i64) -> i64 {
213 return rr_run(prefix, allow_path, schemas_path, counts, 0)
214}
215// DRIFT = tools that need a registry row (missing+has-schema) PLUS tools with no schema contract at all.
216// 0 = discovery is complete: every GREEN allowlisted tool is discoverable with a contract. Read-only.
217func rr_drift(prefix: *u8, allow_path: *u8, schemas_path: *u8, counts: *i64) -> i64 {
218 let rows: i64 = rr_run(prefix, allow_path, schemas_path, counts, 1)
219 if rows < 0 { return rows }
220 return counts[RR_C_REG] + counts[RR_C_NOSCHEMA]
221}
222// UNDISCOVERABLE RESIDUE: GREEN allowlist tools that are NEITHER in the discovery registry NOR have a
223// schema row -- the genuine debt reconcile cannot auto-fix (not discoverable AND nothing to register from).
224// A tool registered via its OWN organ (discoverable with an organ-supplied description) is NOT residue even
225// without a schemas.conf row -- it works. Comma-joins the residue names into `out`, returns the count. This
226// is what stays RED after a self-heal: a human must author a contract (or the owning register organ run).
227func rr_no_schema_names(prefix: *u8, allow_path: *u8, schemas_path: *u8, out: *u8, outcap: i64) -> i64 {
228 let ab: *u8 = sys_mmap(RR_FILE_CAP)
229 let an: i64 = vw_read(allow_path, ab, RR_FILE_CAP - 1)
230 if an <= 0 { out[0] = 0 as u8; return 0 - 1 }
231 let sb: *u8 = sys_mmap(RR_FILE_CAP)
232 let sn: i64 = vw_read(schemas_path, sb, RR_FILE_CAP - 1)
233 if sn <= 0 { out[0] = 0 as u8; return 0 - 1 }
234 let db: *u8 = sys_mmap(RR_FILE_CAP)
235 let dn: i64 = vw_read(RR_DENY_PATH, db, RR_FILE_CAP - 1) // optional exposure-deny policy; <=0 -> none
236 let name: *u8 = sys_mmap(RR_LINE_CAP)
237 let desc: *u8 = sys_mmap(RR_LINE_CAP)
238 let po: *i64 = sys_mmap(16) as *i64
239 let lo: *i64 = sys_mmap(16) as *i64
240 var cnt: i64 = 0
241 var o: i64 = 0
242 var ls: i64 = 0
243 var i: i64 = 0
244 while i <= an {
245 var eol: i64 = 0
246 if i == an { eol = 1 } else { if ab[i] == (RR_NL as u8) { eol = 1 } }
247 if eol == 1 {
248 if i > ls { if ab[ls] != (RR_HASH as u8) {
249 rr_f0(((ab as i64 + ls) as *u8), i - ls, name)
250 if name[0] != (0 as u8) { if rr_is_deny(name, db, dn) == 0 {
251 // residue iff BOTH: not discoverable (tool_get != 1 -> absent -1 or tombstoned 0) AND
252 // no schema (unfixable). A registered tool (==1) is discoverable -> never residue.
253 // Deny-listed (deliberately un-advertised by policy) tools are exempt -- not residue.
254 if tool_get_pfx(prefix, name, po, lo) != 1 { if rr_schema_desc(sb, sn, name, desc) == 0 {
255 if cnt > 0 { if o < outcap - RR_SEP_LEN { out[o] = RR_COMMA as u8; o = o + 1; out[o] = RR_SPC as u8; o = o + 1 } }
256 var j: i64 = 0
257 while name[j] != (0 as u8) { if o < outcap - 1 { out[o] = name[j]; o = o + 1 } j = j + 1 }
258 cnt = cnt + 1
259 } }
260 } }
261 } }
262 ls = i + 1
263 }
264 i = i + 1
265 }
266 out[o] = 0 as u8
267 return cnt
268}