code wiki / (root) / nx_toolreg_reconcile_lib.nx

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}