code wiki / (root) / nx_tools_api.nx

nx_tools_api.nx source

↩ module page · 1510 lines · 97345 B

1// nx_tools_api.nx -- R0 of the sovereign AGENT-FACING API: expose the nx_tool_registry over HTTP. 2// TWO surfaces from ONE registry (the ecosystem's capability SSOT), benchmark = MCP, and BEYOND it: 3// GET /api/tools -> {"tools":[{name,description,invoke,status}]} -- SUPERSET: every tool advertises its 4// gate-`status` (proven/not), a provenance field MCP's tools/list has no concept of. 5// POST /mcp -> JSON-RPC 2.0 (initialize + tools/list) -- MCP-COMPATIBLE so ANY MCP client (Claude 6// included) discovers Nishi's tools with zero adapter. 7// PURE FUNCTION ta_handle_pfx(prefix, req, req_n, out) -> out_n (bytes in, bytes out, NO socket) so the gate 8// drives it in-process (nx_tools_api_gate), exactly like ma_handle. Composes nx_tool_registry (data) + nx_tabrec 9// (tr_field, transitive). No new transport -- wired behind the sovereign edge at deploy. license_tier: ORIGINAL 10import "nx_tool_registry.nx" // tool_list_pfx / tool_get_pfx (+ transitive nx_tabrec: tr_field) 11import "nx_cap_token.nx" // capt_verify / CAPT_OK -- capability-scoped invocation (R2: the beyond-MCP security exceed) 12import "nx_tool_exec_allow.nx" // tea_run / TEA_OK -- R2 EXECUTION: real tools/call runs a vetted GREEN organ (+ transitive nx_tool_run) 13import "nx_tool_http_backend.nx" // thb_dispatch / THB_OK -- 2nd execution kind: a registered HTTP backend (search/doc) fetched in-process over loopback 14import "sha256.nx" // sha256_digest -- PKCE S256 code-challenge verification (OAuth 2.1 AS) 15const TA_MAGIC_65536: i64 = 65536 16// seq1301 (2026-07-30): sync tools/call capture cap. DERIVED: worst-case ta_json_esc expansion is 6x 17// (\uXXXX per control byte), jb response buffer = 1MiB => cap = 160KiB keeps 6x escape at 960KiB < jb. 18// The old 64KiB cap clipped organ stdout SILENTLY (an 80KiB source read came back as exactly 65536 19// bytes, no marker -- nearly shipped a truncated daemon). Truncation is now ALSO flagged in _meta. 20const TA_CAPTURE_CAP: i64 = 163840 21const TA_MAGIC_8192: i64 = 8192 22const TA_MAGIC_1048576: i64 = 1048576 23const TA_MAGIC_9999999999: i64 = 9999999999 24const TA_MAGIC_2048: i64 = 2048 25const TA_MAGIC_4096: i64 = 4096 26const TA_MAGIC_1469598: i64 = 1469598 27const TA_MAGIC_16384: i64 = 16384 28const TA_MAGIC_4294967295: i64 = 4294967295 29const TA_MAGIC_3600: i64 = 3600 30const TA_MAGIC_88161: i64 = 88161 31 32const TA_TOOLPFX: *u8 = "knowledge/toolreg-" as *u8 // production registry prefix (mirrors TOOL_PREFIX) 33const TA_MCP_PAGE: i64 = 64 // MCP tools/list page size (cursor pagination); clients walk nextCursor. 34 // 8 made a ~500-tool roster cost ~63 round-trips per session initialize -- 35 // with any per-request store cost that compounds into minutes (2026-07-30 36 // outage). 64 keeps a page well under the 1MiB response buffer. 37const TA_ALL_LIMIT: i64 = 100000 // /api/tools emits ALL tools (effectively-unbounded page) 38// HMAC secret for capability tokens. WARN: PLACEHOLDER for the gate + first increment -- production MUST source this 39// from the sovereign vault (nx_vault), NEVER a compiled constant (a baked secret is forgeable by anyone reading the binary). 40const TA_CAP_SECRET: *u8 = "nishi-tools-cap-hmac-secret-v1-REPLACE-FROM-VAULT" as *u8 41// PROD secret: raw bytes in this keyfile (relative to the server CWD = the NAS nishihost dir), 0600, provisioned once. 42// Loaded per tools/call; falls back to the constant above ONLY when absent (dev / in-process gates). This retires the 43// baked secret in production while keeping the gates deterministic. 44const TA_CAP_KEYFILE: *u8 = "tools_cap_secret.key" as *u8 45 46// ---- byte helpers (self-contained) ------------------------------------------------------------------ 47func ta_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 48func ta_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i } 49// async-job file landing (rung 24): O_CREAT|O_TRUNC write, full-drain loop. 50// ---- INVOCATION LEDGER (2026-08-04, debt 1785878018) ------------------------------------------ 51// THE DEFECT THIS CLOSES: nx_catalog / nx_wirecensus grade an organ's INVOKED axis from 52// knowledge/status/actlog.jrnl -- but nothing ever wrote a tools/call there. Only organs that 53// self-log, plus harvest ingests, appeared. MEASURED: nx_agreebound_gate, nx_writebench and 54// nx_writebench_gate were each driven over the LIVE /mcp surface (HTTP 200, full output, 55// _meta.exit_code 0) and all three still graded REGISTERED-DARK(S4) "NEVER RUN", while nx_catalog 56// itself had ZERO rows despite many runs. So the banked headline "768 registered / 274 invoked / 57// 183 DARK" was measuring SELF-LOGGING ADOPTION, and an MCP-only tool could never be cleared -- 58// the guard reported a gap that no amount of using the tools could close. 59// ★★★★★A DETECTOR'S COVERAGE IS TWO SETS: WHAT IT MATCHES AND WHERE IT LOOKS. This one looked in a 60// log its own subject never wrote to. The daemon that dispatches the call is the only place that 61// KNOWS a call happened, so the record belongs here. 62// 63// NEVER-BRICK / never-degrade: append-only, best-effort, fail-silent. A missing dir, a full disk or 64// a refused open must NEVER affect the tool result -- logging observes the call, it does not gate 65// it. Row shape matches the existing journal: epoch \t actor \t tool \t verb \t status \t note. 66// (ta_actlog itself is defined below ta_catb/ta_catn, which it uses.) 67 68func ta_job_put(path: *u8, buf: *u8, n: i64) -> i64 { 69 let fd: i64 = sys_openat_wr(path, 420) 70 if fd < 0 { return 0 - 1 } 71 var w: i64 = 0 72 while w < n { 73 let r: i64 = sys_write(fd, ((buf as i64) + w) as *u8, n - w) 74 if r <= 0 { w = n } else { w = w + r } 75 } 76 sys_close(fd) 77 return 0 78} 79func ta_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { d[o + i] = s[i]; i = i + 1 } return o + n } 80func ta_catn(d: *u8, o: i64, v: i64) -> i64 { 81 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 82 if m == 0 { t[0] = 48 as u8; k = 1 } 83 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 84 var w: i64 = o; var i: i64 = 0 85 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 } 86 return w 87} 88func ta_actlog(tool: *u8, tlen: i64, lane: *u8, status: *u8, ec: i64, bytes: i64) -> i64 { 89 var fd: i64 = sys_openat_append("knowledge/status/actlog.jrnl" as *u8, 420) 90 if fd < 0 { fd = sys_openat_append("/volume1/homes/elderwesto/nishihost/knowledge/status/actlog.jrnl" as *u8, 420) } 91 if fd < 0 { return 0 - 1 } 92 let ln: *u8 = sys_mmap(512) 93 var o: i64 = ta_catn(ln, 0, sys_now_realtime_sec()) 94 o = ta_cat(ln, o, "\tmcp\t" as *u8) 95 o = ta_catb(ln, o, tool, tlen) 96 o = ta_cat(ln, o, "\tcall\t" as *u8) 97 o = ta_cat(ln, o, status) 98 o = ta_cat(ln, o, "\ttools/call lane=" as *u8) 99 o = ta_cat(ln, o, lane) 100 o = ta_cat(ln, o, " exit=" as *u8) 101 o = ta_catn(ln, o, ec) 102 o = ta_cat(ln, o, " bytes=" as *u8) 103 o = ta_catn(ln, o, bytes) 104 o = ta_cat(ln, o, "\n" as *u8) 105 var w: i64 = 0 106 while w < o { 107 let r: i64 = sys_write(fd, ((ln as i64) + w) as *u8, o - w) 108 if r <= 0 { w = o } else { w = w + r } 109 } 110 sys_close(fd) 111 return 0 112} 113 114func ta_put1(d: *u8, o: i64, a: i64) -> i64 { d[o] = a as u8; return o + 1 } 115func ta_put2(d: *u8, o: i64, a: i64, b: i64) -> i64 { d[o] = a as u8; d[o + 1] = b as u8; return o + 2 } 116func ta_hexd(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } // 0-9 then a-f 117 118// ---- cap-signing secret: ONE resolver (DRY across tools/call + cap issue/revoke + the status probe) ---------- 119// Resolve the HMAC signing secret EXACTLY as the server does everywhere: prefer the vault-provisioned keyfile 120// (production), else fall back to the dev/gate placeholder constant. `keyfile` is explicit so a gate can drive it 121// against a temp path without touching the server CWD; ta_load_cap_secret pins the production TA_CAP_KEYFILE. 122func ta_load_cap_secret_from(keyfile: *u8, outlen: *i64) -> *u8 { 123 let ksz: *i64 = sys_mmap(16) as *i64 124 let kf: *u8 = sys_read_file(keyfile, ksz) 125 if (kf as i64) != 0 { if ksz[0] > 0 { outlen[0] = ksz[0]; return kf } } 126 let pn: i64 = ta_slen(TA_CAP_SECRET) // hoist the call OUT of the array store (nx_cc: call-in-array-store -> corrupt/SEGV) 127 outlen[0] = pn 128 return TA_CAP_SECRET 129} 130func ta_load_cap_secret(outlen: *i64) -> *u8 { return ta_load_cap_secret_from(TA_CAP_KEYFILE, outlen) } 131 132// 1 if a REAL secret is loaded (a non-empty keyfile whose bytes are NOT the forgeable baked placeholder), else 0. 133// Reveals only a boolean config-health fact -- NEVER the secret. Also catches the footgun of a keyfile that literally 134// contains the placeholder string. 135func ta_cap_provisioned_from(keyfile: *u8) -> i64 { 136 let sl: *i64 = sys_mmap(16) as *i64 137 let s: *u8 = ta_load_cap_secret_from(keyfile, sl) 138 let plc: *u8 = TA_CAP_SECRET // hoist the const ptr to a local before indexing (nx_cc: CONST[i] indexing is broken) 139 let pn: i64 = ta_slen(plc) 140 if sl[0] != pn { return 1 } 141 var i: i64 = 0 142 while i < pn { if s[i] != plc[i] { return 1 } i = i + 1 } 143 return 0 144} 145func ta_cap_provisioned() -> i64 { return ta_cap_provisioned_from(TA_CAP_KEYFILE) } 146 147// a[0..an) equals NUL-terminated b ? (exact) -- used for method/path slice compares. 148func ta_streq_n(a: *u8, an: i64, b: *u8) -> i64 { 149 var i: i64 = 0 150 while i < an { if b[i] == (0 as u8) { return 0 } if a[i] != b[i] { return 0 } i = i + 1 } 151 if b[i] != (0 as u8) { return 0 } 152 return 1 153} 154 155// append s[0..n) into d at o as a JSON-safe string body (RFC 8259: escape " \ and control chars). Returns new o. 156// THE defensive boundary: a description carrying a raw '"' would corrupt the JSON otherwise (silent corruption). 157func ta_json_esc(d: *u8, o: i64, s: *u8, n: i64) -> i64 { 158 var i: i64 = 0 159 while i < n { 160 let c: i64 = (s[i] as i64) & 0xff 161 if c == 34 { o = ta_put2(d, o, 92, 34) } 162 else { if c == 92 { o = ta_put2(d, o, 92, 92) } 163 else { if c == 10 { o = ta_put2(d, o, 92, 110) } 164 else { if c == 13 { o = ta_put2(d, o, 92, 114) } 165 else { if c == 9 { o = ta_put2(d, o, 92, 116) } 166 else { if c < 32 { 167 o = ta_put2(d, o, 92, 117); o = ta_put2(d, o, 48, 48) 168 o = ta_put1(d, o, ta_hexd((c >> 4) & 0xf)); o = ta_put1(d, o, ta_hexd(c & 0xf)) 169 } else { o = ta_put1(d, o, c) } } } } } } 170 i = i + 1 171 } 172 return o 173} 174 175// first offset of NUL-term `needle` in hay[0..hn), or -1. 176func ta_indexof(hay: *u8, hn: i64, needle: *u8) -> i64 { 177 let nl: i64 = ta_slen(needle) 178 if nl == 0 { return 0 } 179 var i: i64 = 0 180 while i + nl <= hn { 181 var m: i64 = 1; var j: i64 = 0 182 while j < nl { if hay[i + j] != needle[j] { m = 0 } j = j + 1 } 183 if m == 1 { return i } 184 i = i + 1 185 } 186 return 0 - 1 187} 188 189// ---- HTTP request shredding ------------------------------------------------------------------------- 190// body offset = just past the CRLFCRLF header terminator (req_n if none -> empty body). 191func ta_body_off(req: *u8, req_n: i64) -> i64 { 192 var i: i64 = 0 193 while i + 3 < req_n { 194 if req[i] == (13 as u8) { if req[i + 1] == (10 as u8) { if req[i + 2] == (13 as u8) { if req[i + 3] == (10 as u8) { return i + 4 } } } } 195 i = i + 1 196 } 197 return req_n 198} 199// parse request line "METHOD SP PATH SP VERSION": mo[0/1]=method off/len, po[0/1]=path off/len. 1 ok / 0 malformed. 200func ta_reqline(req: *u8, req_n: i64, mo: *i64, po: *i64) -> i64 { 201 var i: i64 = 0 202 var sp1: i64 = 0 - 1 203 while i < req_n { if sp1 < 0 { if req[i] == (32 as u8) { sp1 = i } } i = i + 1 } 204 if sp1 < 0 { return 0 } 205 mo[0] = 0; mo[1] = sp1 206 let k: i64 = sp1 + 1 207 var sp2: i64 = 0 - 1 208 i = k 209 while i < req_n { if sp2 < 0 { if req[i] == (32 as u8) { sp2 = i } } i = i + 1 } 210 po[0] = k 211 if sp2 < 0 { po[1] = req_n - k } else { po[1] = sp2 - k } 212 return 1 213} 214 215// ---- JSON body field pluck (single-responsibility, fail-closed) ------------------------------------- 216// string VALUE content of "<keyq>":"content" -> out2[0]=off,out2[1]=len; 1 found / 0 absent. keyq includes quotes. 217func ta_json_str(body: *u8, n: i64, keyq: *u8, out2: *i64) -> i64 { 218 let p: i64 = ta_indexof(body, n, keyq) 219 if p < 0 { return 0 } 220 var i: i64 = p + ta_slen(keyq) 221 var q1: i64 = 0 - 1 222 while i < n { if q1 < 0 { if body[i] == (34 as u8) { q1 = i } } i = i + 1 } 223 if q1 < 0 { return 0 } 224 var q2: i64 = 0 - 1 225 i = q1 + 1 226 while i < n { if q2 < 0 { if body[i] == (34 as u8) { q2 = i } } i = i + 1 } 227 if q2 < 0 { return 0 } 228 out2[0] = q1 + 1; out2[1] = q2 - (q1 + 1) 229 return 1 230} 231// RAW value token after "<keyq>": (number OR quoted string, echoed verbatim for JSON-RPC id). 1 found / 0 absent. 232func ta_json_raw(body: *u8, n: i64, keyq: *u8, out2: *i64) -> i64 { 233 let p: i64 = ta_indexof(body, n, keyq) 234 if p < 0 { return 0 } 235 var i: i64 = p + ta_slen(keyq) 236 var col: i64 = 0 - 1 237 while i < n { if col < 0 { if body[i] == (58 as u8) { col = i } } i = i + 1 } 238 if col < 0 { return 0 } 239 var s: i64 = col + 1 240 var go: i64 = 1 241 while go == 1 { if s >= n { go = 0 } else { let c: i64 = body[s] as i64; if c == 32 { s = s + 1 } else { if c == 9 { s = s + 1 } else { go = 0 } } } } 242 var e: i64 = s 243 var go2: i64 = 1 244 while go2 == 1 { 245 if e >= n { go2 = 0 } else { 246 let c: i64 = body[e] as i64 247 if c == 44 { go2 = 0 } else { if c == 125 { go2 = 0 } else { if c == 32 { go2 = 0 } else { if c == 13 { go2 = 0 } else { if c == 10 { go2 = 0 } else { e = e + 1 } } } } } 248 } 249 } 250 out2[0] = s; out2[1] = e - s 251 return 1 252} 253// string-ARRAY value: parse "<keyq>":[ "s0","s1",... ] -> write each element (JSON-unescaped, NUL-terminated) 254// into the bump buffer `scratch` (capacity scap) and store its pointer (as i64) in out[base+k]. Returns the count 255// k (>=0); 0 if the key is absent, the value is not an array, or the array is empty. Stops at ']', at `maxn` 256// items, or when scratch would overflow (fail-closed -- keeps what fit). This is what turns a tools/call 257// params.arguments.argv into a native argv so MULTI-ARG organs (nx_mgmt_client <url> call GET <path> <tok>) are 258// callable over MCP, not just no-arg ones. Escapes handled: \" \\ \/ \n \t \r ; any other \x copies x verbatim. 259func ta_json_str_array(body: *u8, n: i64, keyq: *u8, out: *i64, base: i64, maxn: i64, scratch: *u8, scap: i64) -> i64 { 260 let p: i64 = ta_indexof(body, n, keyq) 261 if p < 0 { return 0 } 262 var i: i64 = p + ta_slen(keyq) 263 // skip ':' and whitespace up to the opening '[' 264 var go: i64 = 1 265 while go == 1 { 266 if i >= n { return 0 } 267 let c: i64 = body[i] as i64 268 if c == 58 { i = i + 1 } else { if c == 32 { i = i + 1 } else { if c == 9 { i = i + 1 } else { if c == 13 { i = i + 1 } else { if c == 10 { i = i + 1 } else { go = 0 } } } } } 269 } 270 if (body[i] as i64) != 91 { return 0 } // must be '[' 271 i = i + 1 272 var k: i64 = 0 273 var w: i64 = 0 // scratch write cursor 274 var scan: i64 = 1 275 while scan == 1 { 276 if i >= n { scan = 0 } else { 277 let c: i64 = body[i] as i64 278 if c == 93 { scan = 0 } else { // ']' -> end of array 279 if c == 34 { // '"' -> parse one string element 280 if k >= maxn { scan = 0 } else { 281 let start: i64 = w 282 i = i + 1 283 var instr: i64 = 1 284 while instr == 1 { 285 if i >= n { instr = 0; scan = 0 } else { 286 let d: i64 = body[i] as i64 287 if d == 34 { instr = 0; i = i + 1 } else { // closing quote 288 if d == 92 { // backslash escape 289 i = i + 1 290 if i < n { 291 let e: i64 = body[i] as i64 292 var oc: i64 = e 293 if e == 110 { oc = 10 } else { if e == 116 { oc = 9 } else { if e == 114 { oc = 13 } else { oc = e } } } 294 if w < scap - 1 { scratch[w] = oc as u8; w = w + 1 } 295 i = i + 1 296 } 297 } else { 298 if w < scap - 1 { scratch[w] = d as u8; w = w + 1 } 299 i = i + 1 300 } 301 } 302 } 303 } 304 if w < scap { scratch[w] = 0 as u8; w = w + 1 } // NUL-terminate this arg 305 let ptr: i64 = (scratch as i64) + start 306 out[base + k] = ptr 307 k = k + 1 308 } 309 } else { i = i + 1 } // skip commas / whitespace between elements 310 } 311 } 312 } 313 return k 314} 315 316// ---- response assembly (all JSON) ------------------------------------------------------------------- 317func ta_emit(out: *u8, status: *u8, body: *u8, body_n: i64) -> i64 { 318 var o: i64 = ta_cat(out, 0, status) 319 o = ta_cat(out, o, "\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8) 320 o = ta_catn(out, o, body_n) 321 o = ta_cat(out, o, "\r\n\r\n" as *u8) 322 o = ta_catb(out, o, body, body_n) 323 return o 324} 325func ta_emit_lit(out: *u8, status: *u8, lit: *u8) -> i64 { return ta_emit(out, status, lit, ta_slen(lit)) } 326 327// ---- R1 rich MCP tool schemas (data-driven, benchmark = MCP 2025-06-18 tools spec) -------------------- 328// Per-tool title + annotations (readOnly/destructive/idempotent/openWorld safety hints) + outputSchema + the 329// TYPED argv CONTRACT, sourced from a TAB-separated config so vetting a tool's profile is a data edit (rule 11), 330// not a code change. 331// Row: <name>\t<title>\t<ro>\t<destr>\t<idem>\t<openworld>\t<output-desc>[\t<argv-contract>] 332// col8 argv-contract (07-17, the tool-schema SOTA gap): the tool's exact argv grammar (verbs/args), emitted as 333// the inputSchema argv description so an agent reads the CONTRACT from tools/list instead of a generic blurb. 334// Absent col8 -> the generic argv description (back-compat). Conf is read per call = contract edits are LIVE. 335// UNKNOWN tool (no row) -> MCP-safe defaults: NOT read-only, POSSIBLY destructive, NOT idempotent, open-world 336// (so an agent treats an un-annotated tool as the cautious case). '#' comment + blank lines ignored. 337const TA_SCHEMACONF: *u8 = "knowledge/tool_schemas.conf" as *u8 // server CWD-relative (like tool_allowlist.conf) 338 339func ta_bool_json(v: i64) -> *u8 { if v == 1 { return "true" as *u8 } return "false" as *u8 } 340 341// idx-th TAB-field of line buf[ls,le) -> out2[0]=off,out2[1]=len; 1 if present, 0 if fewer fields. 342func ta_tsv_field(buf: *u8, ls: i64, le: i64, idx: i64, out2: *i64) -> i64 { 343 var fi: i64 = 0 344 var s: i64 = ls 345 var i: i64 = ls 346 while i <= le { 347 var sep: i64 = 0 348 if i == le { sep = 1 } else { if buf[i] == (9 as u8) { sep = 1 } } 349 if sep == 1 { 350 if fi == idx { out2[0] = s; out2[1] = i - s; return 1 } 351 fi = fi + 1 352 s = i + 1 353 } 354 i = i + 1 355 } 356 return 0 357} 358 359// emit ,"title":.. ,"annotations":{..} ,"outputSchema":{..} for `name` from `conf`. Testable (_from); the 360// production wrapper pins TA_SCHEMACONF. Always emits annotations (defaults if no row) so every MCP tool carries 361// safety hints. Appends to body at offset bin; returns the new offset. 362func ta_emit_mcp_schema_from(conf: *u8, name: *u8, nlen: i64, body: *u8, bin: i64) -> i64 { 363 let szp0: *i64 = sys_mmap(16) as *i64 364 let buf0: *u8 = sys_read_file(conf, szp0) 365 var n0: i64 = 0 366 if (buf0 as i64) != 0 { n0 = szp0[0] } 367 return ta_emit_mcp_schema_buf(buf0, n0, name, nlen, body, bin) 368} 369// Buffer-based core: the schema conf is read ONCE PER REQUEST by the caller, not once per TOOL. 370// At page size 64 a per-tool re-read meant 64 x ~125KB file reads for a single tools/list -- the 371// dominant per-page cost once the toolreg plane was compacted (2026-07-30). 372func ta_emit_mcp_schema_buf(buf: *u8, bufn: i64, name: *u8, nlen: i64, body: *u8, bin: i64) -> i64 { 373 var b: i64 = bin 374 var ro: i64 = 0; var de: i64 = 1; var idp: i64 = 0; var ow: i64 = 1 // MCP-safe defaults for an unknown tool 375 var has_title: i64 = 0; var tt_s: i64 = 0; var tt_l: i64 = 0 376 var has_out: i64 = 0; var od_s: i64 = 0; var od_l: i64 = 0 377 var has_av: i64 = 0; var av_s: i64 = 0; var av_l: i64 = 0 // col8: typed argv contract 378 if (buf as i64) != 0 { 379 let n: i64 = bufn 380 var ls: i64 = 0; var i: i64 = 0; var done: i64 = 0 381 while i <= n { 382 var eol: i64 = 0 383 if i == n { eol = 1 } else { if buf[i] == (10 as u8) { eol = 1 } } 384 if eol == 1 { 385 if done == 0 { 386 let le: i64 = i 387 if le > ls { if buf[ls] != (35 as u8) { 388 let f0: *i64 = sys_mmap(16) as *i64 389 if ta_tsv_field(buf, ls, le, 0, f0) == 1 { 390 if f0[1] == nlen { 391 var m: i64 = 1; var c: i64 = 0 392 while c < nlen { if buf[f0[0] + c] != name[c] { m = 0 } c = c + 1 } 393 if m == 1 { 394 let ff: *i64 = sys_mmap(16) as *i64 395 if ta_tsv_field(buf, ls, le, 1, ff) == 1 { has_title = 1; tt_s = ff[0]; tt_l = ff[1] } 396 if ta_tsv_field(buf, ls, le, 2, ff) == 1 { if buf[ff[0]] == (49 as u8) { ro = 1 } else { ro = 0 } } 397 if ta_tsv_field(buf, ls, le, 3, ff) == 1 { if buf[ff[0]] == (49 as u8) { de = 1 } else { de = 0 } } 398 if ta_tsv_field(buf, ls, le, 4, ff) == 1 { if buf[ff[0]] == (49 as u8) { idp = 1 } else { idp = 0 } } 399 if ta_tsv_field(buf, ls, le, 5, ff) == 1 { if buf[ff[0]] == (49 as u8) { ow = 1 } else { ow = 0 } } 400 if ta_tsv_field(buf, ls, le, 6, ff) == 1 { has_out = 1; od_s = ff[0]; od_l = ff[1] } 401 if ta_tsv_field(buf, ls, le, 7, ff) == 1 { if ff[1] > 0 { has_av = 1; av_s = ff[0]; av_l = ff[1] } } 402 done = 1 403 } 404 } 405 } 406 } } 407 } 408 ls = i + 1 409 } 410 i = i + 1 411 } 412 } 413 // inputSchema: argv array typed by the tool's own contract (col8) when declared; generic otherwise. 414 b = ta_cat(body, b, ",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"argv\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"" as *u8) 415 if has_av == 1 { 416 b = ta_json_esc(body, b, ((buf as i64) + av_s) as *u8, av_l) 417 } else { 418 b = ta_cat(body, b, "positional CLI args passed to the organ (argv[1..])" as *u8) 419 } 420 b = ta_cat(body, b, "\"}}}" as *u8) 421 if has_title == 1 { 422 b = ta_cat(body, b, ",\"title\":\"" as *u8) 423 b = ta_json_esc(body, b, ((buf as i64) + tt_s) as *u8, tt_l) 424 b = ta_cat(body, b, "\"" as *u8) 425 } 426 b = ta_cat(body, b, ",\"annotations\":{\"readOnlyHint\":" as *u8) 427 b = ta_cat(body, b, ta_bool_json(ro)) 428 b = ta_cat(body, b, ",\"destructiveHint\":" as *u8) 429 b = ta_cat(body, b, ta_bool_json(de)) 430 b = ta_cat(body, b, ",\"idempotentHint\":" as *u8) 431 b = ta_cat(body, b, ta_bool_json(idp)) 432 b = ta_cat(body, b, ",\"openWorldHint\":" as *u8) 433 b = ta_cat(body, b, ta_bool_json(ow)) 434 b = ta_cat(body, b, "}" as *u8) 435 if has_out == 1 { 436 b = ta_cat(body, b, ",\"outputSchema\":{\"type\":\"object\",\"description\":\"" as *u8) 437 b = ta_json_esc(body, b, ((buf as i64) + od_s) as *u8, od_l) 438 b = ta_cat(body, b, "\"}" as *u8) 439 } 440 return b 441} 442func ta_emit_mcp_schema(name: *u8, nlen: i64, body: *u8, bin: i64) -> i64 { return ta_emit_mcp_schema_from(TA_SCHEMACONF, name, nlen, body, bin) } 443 444// ---- the registry -> JSON tools array (shared by REST + MCP; mcp=1 emits inputSchema, mcp=0 emits invoke/status) -- 445func ta_emit_field(body: *u8, b: i64, rec: *u8, rl: i64, idx: i64) -> i64 { 446 let f2: *i64 = sys_mmap(16) as *i64 447 if tr_field(rec, 0, rl, idx, f2) == 1 { return ta_json_esc(body, b, ((rec as i64) + f2[0]) as *u8, f2[1]) } 448 return b 449} 450func ta_emit_tools(prefix: *u8, body: *u8, bin: i64, mcp: i64, offset: i64, limit: i64, out_more: *i64) -> i64 { 451 var b: i64 = bin 452 var tidx: i64 = 0 // pagination: index across ALL tools; emit only [offset, offset+limit); flag out_more if beyond 453 let idxbuf: *u8 = sys_mmap(TA_MAGIC_65536) 454 let il: i64 = tool_list_pfx(prefix, idxbuf, TA_MAGIC_65536) 455 var scb: *u8 = 0 as *u8 456 var scn: i64 = 0 457 if mcp == 1 { 458 let sszp: *i64 = sys_mmap(16) as *i64 459 scb = sys_read_file(TA_SCHEMACONF, sszp) 460 if (scb as i64) != 0 { scn = sszp[0] } 461 } 462 let namez: *u8 = sys_mmap(512) 463 let po: *i64 = sys_mmap(16) as *i64 464 let lo: *i64 = sys_mmap(16) as *i64 465 var ls: i64 = 0 466 var i: i64 = 0 467 var first: i64 = 1 468 while i <= il { 469 var eol: i64 = 0 470 if i == il { eol = 1 } else { if idxbuf[i] == (10 as u8) { eol = 1 } } 471 if eol == 1 { 472 let nl: i64 = i - ls 473 if nl > 0 { 474 var c: i64 = 0 475 while c < nl { namez[c] = idxbuf[ls + c]; c = c + 1 } 476 namez[nl] = 0 as u8 477 if tool_get_pfx(prefix, namez, po, lo) == 1 { 478 if tidx >= offset { if tidx < offset + limit { 479 let rec: *u8 = po[0] as *u8 480 let rl: i64 = lo[0] 481 if first == 0 { b = ta_cat(body, b, "," as *u8) } 482 first = 0 483 b = ta_cat(body, b, "{\"name\":\"" as *u8) 484 b = ta_emit_field(body, b, rec, rl, 0) 485 b = ta_cat(body, b, "\",\"description\":\"" as *u8) 486 b = ta_emit_field(body, b, rec, rl, 1) 487 if mcp == 1 { 488 b = ta_cat(body, b, "\"" as *u8) 489 b = ta_emit_mcp_schema_buf(scb, scn, namez, nl, body, b) // conf read once per request above 490 b = ta_cat(body, b, "}" as *u8) 491 } else { 492 b = ta_cat(body, b, "\",\"invoke\":\"" as *u8) 493 b = ta_emit_field(body, b, rec, rl, 2) 494 b = ta_cat(body, b, "\",\"status\":\"" as *u8) 495 b = ta_emit_field(body, b, rec, rl, 3) 496 b = ta_cat(body, b, "\"}" as *u8) 497 } 498 } else { if (out_more as i64) != 0 { out_more[0] = 1 } } } 499 tidx = tidx + 1 500 } 501 } 502 ls = i + 1 503 } 504 i = i + 1 505 } 506 return b 507} 508 509// ---- route handlers --------------------------------------------------------------------------------- 510func ta_index(out: *u8) -> i64 { 511 let body: *u8 = "{\"api\":\"nishi-tools\",\"version\":1,\"surfaces\":{\"rest\":\"GET /api/tools\",\"mcp\":\"POST /mcp (JSON-RPC 2.0: initialize, tools/list)\"},\"beyond_mcp\":\"each tool carries a gate-status MCP tools/list has no field for\"}" as *u8 512 return ta_emit_lit(out, "HTTP/1.1 200 OK" as *u8, body) 513} 514func ta_emit_page(out: *u8, html: *u8, n: i64) -> i64 { 515 var o: i64 = ta_cat(out, 0, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8) 516 o = ta_catn(out, o, n) 517 o = ta_cat(out, o, "\r\n\r\n" as *u8) 518 o = ta_catb(out, o, html, n) 519 return o 520} 521// GET /tools -- a self-contained HUMAN page (NO double-quotes in the HTML so it is a clean NishiLang literal): it 522// fetches /api/tools client-side + renders one card per tool (XSS-escaped). THE login-level UI surface for the 523// capability ([[feedback-tools-must-surface-in-ui]]). 524func ta_html_tools(out: *u8) -> i64 { 525 let h: *u8 = sys_mmap(TA_MAGIC_8192) 526 var o: i64 = 0 527 o = ta_cat(h, o, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><title>Nishi Tools</title>" as *u8) 528 o = ta_cat(h, o, "<style>body{font-family:-apple-system,Segoe UI,sans-serif;max-width:900px;margin:4vh auto;padding:0 20px;color:#1c1c1e}h1{font-size:1.4rem;margin-bottom:.2rem}.sub{color:#666;margin:.1rem 0 1.1rem}.card{border:1px solid #e2e2e6;border-radius:10px;padding:12px 16px;margin:12px 0;background:#fbfbfd}.card h3{margin:.1rem 0 .3rem;font-family:ui-monospace,monospace}.inv{font-family:ui-monospace,monospace;font-size:.82rem;color:#555;background:#f2f2f7;padding:6px 8px;border-radius:6px;margin:.4rem 0;overflow-x:auto}.badge{display:inline-block;font-size:.72rem;padding:2px 8px;border-radius:99px;background:#e6f5ec;color:#0a6;font-weight:600}code{font-family:ui-monospace,monospace}.mcp{background:#eef;border:1px solid #dde;border-radius:8px;padding:10px 14px;font-size:.86rem;margin:1rem 0}</style>" as *u8) 529 o = ta_cat(h, o, "<h1>Nishi Tools</h1><p class=sub>Sovereign agent-facing registry &mdash; MCP-compatible + superset.</p><div class=mcp><b>MCP endpoint:</b> <code>POST /mcp</code> (JSON-RPC 2.0: <code>initialize</code>, <code>tools/list</code>) &mdash; any MCP client connects with zero adapter. &nbsp;<b>REST:</b> <code>GET /api/tools</code>. &nbsp;<b>Beyond MCP:</b> every tool carries a gate-<code>status</code>.</div><div id=t>loading&hellip;</div>" as *u8) 530 o = ta_cat(h, o, "<script>function esc(s){return String(s).split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;')}fetch('/api/tools').then(function(r){return r.json()}).then(function(j){var a=j.tools||[],b='';for(var i=0;i<a.length;i++){var t=a[i];b+='<div class=card><h3>'+esc(t.name)+' <span class=badge>'+esc(t.status||'')+'</span></h3><div>'+esc(t.description||'')+'</div><div class=inv>'+esc(t.invoke||'')+'</div></div>'}document.getElementById('t').innerHTML=b||'<i>no tools registered</i>'}).catch(function(){document.getElementById('t').innerHTML='<i>failed to load /api/tools</i>'})</script>" as *u8) 531 return ta_emit_page(out, h, o) 532} 533func ta_tools(prefix: *u8, out: *u8) -> i64 { 534 let jb: *u8 = sys_mmap(TA_MAGIC_1048576) 535 var b: i64 = ta_cat(jb, 0, "{\"api\":\"nishi-tools\",\"version\":1,\"tools\":[" as *u8) 536 b = ta_emit_tools(prefix, jb, b, 0, 0, TA_ALL_LIMIT, 0 as *i64) 537 b = ta_cat(jb, b, "]}" as *u8) 538 return ta_emit(out, "HTTP/1.1 200 OK" as *u8, jb, b) 539} 540// extract an HTTP header value into out2 (off,len). `key` includes the trailing colon, e.g. "X-Nishi-Cap:". 541// Searches only the header region (before the CRLFCRLF), trims leading spaces, stops at CR/LF. 1 found / 0 absent. 542func ta_header(req: *u8, req_n: i64, key: *u8, out2: *i64) -> i64 { 543 let hend: i64 = ta_body_off(req, req_n) 544 let p: i64 = ta_indexof(req, hend, key) 545 if p < 0 { return 0 } 546 var vs: i64 = p + ta_slen(key) 547 var go: i64 = 1 548 while go == 1 { if vs < hend { if req[vs] == (0x20 as u8) { vs = vs + 1 } else { go = 0 } } else { go = 0 } } 549 var ve: i64 = vs 550 var go2: i64 = 1 551 while go2 == 1 { if ve < hend { let c: i64 = req[ve] as i64; if c == 13 { go2 = 0 } else { if c == 10 { go2 = 0 } else { ve = ve + 1 } } } else { go2 = 0 } } 552 out2[0] = vs; out2[1] = ve - vs 553 return 1 554} 555 556// ---- ALTERNATE PRESENTER: the capability on the REQUEST-LINE QUERY STRING (?cap=<token>) ------------------ 557// WHY THIS EXISTS (measured 2026-07-30; debt seq1235). An MCP client that does not deliver its configured 558// X-Nishi-Cap header leaves EVERY tools/call denied for a WHOLE SESSION. The deny is indistinguishable from 559// -- this tool does not exist -- so the session falls back to raw ssh/PowerShell and never revisits: that is 560// 5,776 of 13,003 measured tool calls (44pct). Custom request headers are the most commonly dropped part of an 561// HTTP client's config; the URL is the ONE thing every client provably sends, because it is how the request 562// reached us at all. Binding the cap to the URL makes a session-starts-denied state impossible BY CONSTRUCTION 563// instead of by remembering to restart a client so a header re-loads. 564// SAME OBJECT-CAPABILITY, different presenter: the token is still HMAC-verified, expiry-checked, revocation- 565// checked and least-authority below. This changes WHERE the token is read from, never WHETHER it is proven -- 566// exactly the relationship the Authorization: Bearer presenter already has to the native header. 567// PRECEDENCE IS LOWEST ON PURPOSE (see ta_mcp_call): an explicit per-call _cap always wins, so the URL carries 568// the session BASELINE (least authority) and a deliberate escalation still overrides it. 569// Present ONLY a read/least-authority cap this way: URLs are logged by proxies far more readily than headers. 570// Key match is EXACT (len 3 + c,a,p) so a lookalike param like recap= or capx= can never authorize. 571func ta_query_cap(req: *u8, req_n: i64, out2: *i64) -> i64 { 572 let mo: *i64 = sys_mmap(16) as *i64 573 let po: *i64 = sys_mmap(16) as *i64 574 if ta_reqline(req, req_n, mo, po) == 0 { return 0 } 575 let poff: i64 = po[0] 576 let plen: i64 = po[1] 577 var qi: i64 = 0 578 var qs: i64 = 0 - 1 579 while qi < plen { if qs < 0 { if req[poff + qi] == (63 as u8) { qs = qi + 1 } } qi = qi + 1 } 580 if qs < 0 { return 0 } 581 // walk k=v pairs separated by '&'; accept the FIRST pair whose key is exactly cap (len 3). 582 var s: i64 = qs 583 while s < plen { 584 var e: i64 = s 585 var go: i64 = 1 586 while go == 1 { if e >= plen { go = 0 } else { if req[poff + e] == (38 as u8) { go = 0 } else { e = e + 1 } } } 587 var eq: i64 = 0 - 1 588 var k: i64 = s 589 while k < e { if eq < 0 { if req[poff + k] == (61 as u8) { eq = k } } k = k + 1 } 590 if eq - s == 3 { 591 if req[poff + s] == (99 as u8) { if req[poff + s + 1] == (97 as u8) { if req[poff + s + 2] == (112 as u8) { 592 if e - eq - 1 > 0 { out2[0] = poff + eq + 1; out2[1] = e - eq - 1; return 1 } 593 } } } 594 } 595 s = e + 1 596 } 597 return 0 598} 599// Liveness marker for the query-string presenter: returns 1 so a gate can assert the presenter is COMPILED IN 600// rather than merely declared. Asserted by nx_tools_api_gate T14. 601func ta_query_cap_available() -> i64 { 602 return 1 603} 604 605// R2: capability-scoped tools/call. Requires an X-Nishi-Cap capability token that GRANTS the requested tool (authority- 606// in-the-token, verified with NO ambient identity -> confused-deputy is structurally impossible, unlike MCP's OAuth 607// scopes). No capability, or one that doesn't grant this tool, or a forged/expired one -> JSON-RPC -32001. On success: 608// ---- REVOCATION denylist: kill a leaked capability BEFORE its expiry (completes issue->delegate->verify->REVOKE) ---- 609const TA_REVOKED_LIST: *u8 = "cap_revoked.list" as *u8 // newline-separated decimal nonces, relative to the server CWD 610func ta_openat_append(path: *u8) -> i64 { return __syscall(257, AT_FDCWD, path, 0x441, 0x1a4, 0, 0) } // O_WRONLY|O_CREAT|O_APPEND, 0644 611func ta_nonce_revoked(nonce: i64) -> i64 { 612 if nonce < 0 { return 0 } 613 let szp: *i64 = sys_mmap(16) as *i64 614 let buf: *u8 = sys_read_file(TA_REVOKED_LIST, szp) 615 if (buf as i64) == 0 { return 0 } 616 let n: i64 = szp[0] 617 let dec: *u8 = sys_mmap(32); let dl: i64 = ta_catn(dec, 0, nonce) 618 var s: i64 = 0; var i: i64 = 0 619 while i <= n { 620 var eol: i64 = 0 621 if i == n { eol = 1 } else { if buf[i] == (0x0a as u8) { eol = 1 } } 622 if eol == 1 { 623 if i - s == dl { var m: i64 = 1; var c: i64 = 0; while c < dl { if buf[s + c] != dec[c] { m = 0 } c = c + 1 } if m == 1 { return 1 } } 624 s = i + 1 625 } 626 i = i + 1 627 } 628 return 0 629} 630func ta_append_revoked(nonce: i64) -> i64 { 631 let dec: *u8 = sys_mmap(48); var dl: i64 = ta_catn(dec, 0, nonce); dec[dl] = 0x0a as u8; dl = dl + 1 632 let fd: i64 = ta_openat_append(TA_REVOKED_LIST) 633 if fd < 0 { return 0 - 1 } 634 sys_write(fd, dec, dl); sys_close(fd) 635 return 0 636} 637 638// a SAFE STUB result (increment 1 proves the AUTHORIZATION; real tool execution is a separate guarded surface). Appends 639// the ",result"/",error" tail to jb and returns the new offset. 640// seq1293: exact-line membership test (CR-tolerant) for async_only_tools.conf -- substring matching 641// would let "nx_fs" shadow "nx_fs_write", so lines must match the WHOLE tool name. 642func ta_name_in_lines(buf: *u8, n: i64, nm: *u8, nl: i64) -> i64 { 643 var i: i64 = 0 644 while i < n { 645 var e: i64 = i 646 var s: i64 = 1 647 while s == 1 { if e >= n { s = 0 } else { if buf[e] == (10 as u8) { s = 0 } else { e = e + 1 } } } 648 var le: i64 = e 649 if le > i { if buf[le-1] == (13 as u8) { le = le - 1 } } 650 if le - i == nl { 651 var j: i64 = 0 652 var ok: i64 = 1 653 while j < nl { if buf[i+j] != nm[j] { ok = 0; j = nl } else { j = j + 1 } } 654 if ok == 1 { return 1 } 655 } 656 i = e + 1 657 } 658 return 0 659} 660func ta_mcp_call(prefix: *u8, req: *u8, req_n: i64, body: *u8, bn: i64, jb: *u8, bin: i64) -> i64 { 661 var b: i64 = bin 662 let nm2: *i64 = sys_mmap(16) as *i64 663 if ta_json_str(body, bn, "\"name\"" as *u8, nm2) != 1 { 664 return ta_cat(jb, b, ",\"error\":{\"code\":-32602,\"message\":\"missing params.name\"}}" as *u8) 665 } 666 let nmp: *u8 = ((body as i64) + nm2[0]) as *u8 667 let nml: i64 = nm2[1] 668 // load the cap-signing secret: prefer the vault-provisioned keyfile (production); else the dev/gate constant. 669 let slb: *i64 = sys_mmap(16) as *i64 670 let secret: *u8 = ta_load_cap_secret(slb) 671 let secretlen: i64 = slb[0] 672 let cap2: *i64 = sys_mmap(16) as *i64 673 var granted: i64 = 0 - 9 // sentinel: no capability presented 674 var capp: *u8 = 0 as *u8 675 var capl: i64 = 0 676 // ---- PRESENTER SELECTION: ANY-GRANTS (root fix 2026-07-30) ---------------------------------------------- 677 // WAS a FIRST-PRESENT-WINS chain: if the X-Nishi-Cap header was present AT ALL, params._cap was never read -- 678 // it sat inside the else. That contradicted the contract this file states in TWO places (ta_query_cap: "an 679 // explicit per-call _cap always wins"; the deny text below: "params._cap ... per-call override, wins over all 680 // others") and it made a session whose header carries a least-authority READ cap STRUCTURALLY unable to 681 // escalate for even one call: the deliberate override was silently ignored and the caller got a -32001 682 // indistinguishable from -- no such tool -- which is the exact trigger for the measured 44pct ssh/PowerShell 683 // fallback that the ?cap= presenter was built to kill. Same session-starts-denied lockout, different door. 684 // NOW: collect every presenter, try them in documented precedence order, and let the FIRST ONE THAT ACTUALLY 685 // GRANTS THIS TOOL authorize the call. This CANNOT widen authority -- each candidate is still HMAC-verified, 686 // expiry-checked, revocation-checked and least-authority-checked against THIS tool name -- so choosing WHICH 687 // proven token authorizes can never grant what no presented token carries. It deletes exactly one state: a 688 // caller holding a valid, granting capability refused because a weaker one happened to arrive first. 689 // Slots in precedence order: 0 body params._cap (per-call override) | 1 X-Nishi-Cap | 2 Bearer | 3 ?cap= 690 let ps: *i64 = sys_mmap(64) as *i64 691 var pz: i64 = 0 692 while pz < 8 { ps[pz] = 0; pz = pz + 1 } 693 let pcb: *i64 = sys_mmap(16) as *i64 694 if ta_json_str(body, bn, "\"_cap\"" as *u8, pcb) == 1 { ps[0] = (body as i64) + pcb[0]; ps[1] = pcb[1] } 695 if ta_header(req, req_n, "X-Nishi-Cap:" as *u8, cap2) == 1 { ps[2] = (req as i64) + cap2[0]; ps[3] = cap2[1] } 696 // OAuth 2.1 interop (R2): a STOCK MCP client presents the cap as `Authorization: Bearer <cap>` (RFC6750 697 // header method) -- an ALTERNATE PRESENTER for the SAME object-capability, no ambient scope, still 698 // HMAC-verified below, no confused-deputy. Scheme match is case-insensitive per RFC7235. 699 if ta_header(req, req_n, "Authorization:" as *u8, cap2) == 1 { 700 let bav: *u8 = ((req as i64) + cap2[0]) as *u8 701 let bavl: i64 = cap2[1] 702 if bavl > 7 { 703 let pbl: *u8 = "bearer " as *u8 704 var bj: i64 = 0 705 var bok: i64 = 1 706 while bj < 7 { 707 var bc: i64 = bav[bj] as i64 708 if bc >= 65 { if bc <= 90 { bc = bc + 32 } } 709 if bc != (pbl[bj] as i64) { bok = 0 } 710 bj = bj + 1 711 } 712 if bok == 1 { ps[4] = (bav as i64) + 7; ps[5] = bavl - 7 } 713 } 714 } 715 // ?cap=<token> on the request line: the session BASELINE, and the one presenter every HTTP client provably 716 // sends. Lowest precedence by construction -- see ta_query_cap for why this presenter exists. 717 if ta_query_cap(req, req_n, cap2) == 1 { ps[6] = (req as i64) + cap2[0]; ps[7] = cap2[1] } 718 var pk: i64 = 0 719 while pk < 4 { 720 let pcp: i64 = ps[pk + pk] 721 let pcl: i64 = ps[pk + pk + 1] 722 if pcl > 0 { 723 // remember that SOMETHING was presented: drives data.presented and WHICH deny text the caller gets 724 // (no-capability and wrong-capability have OPPOSITE remedies). 725 if capl <= 0 { capp = pcp as *u8; capl = pcl } 726 var pg: i64 = capt_verify(secret, secretlen, pcp as *u8, pcl, nmp, nml, sys_now_realtime_sec()) 727 // revocation: even a valid, unexpired, tool-granting cap is refused if its nonce is on the denylist 728 // (kill leaked caps). Checked PER PRESENTER so a revoked one cannot mask a live one behind it. 729 if pg == CAPT_OK { if ta_nonce_revoked(capt_nonce_of(pcp as *u8, pcl)) == 1 { pg = CAPT_DENY_REVOKED } } 730 if pg == CAPT_OK { capp = pcp as *u8; capl = pcl; granted = CAPT_OK; pk = 4 } 731 else { if granted == (0 - 9) { granted = pg } } 732 } 733 pk = pk + 1 734 } 735 if granted == CAPT_OK { 736 // ---- R2 EXECUTE: the capability said WHO may call; the execution allowlist (nx_tool_exec_allow) says 737 // WHICH vetted, gate-GREEN ELF actually runs (defense in depth, fail-closed). Only a GREEN-allowlisted 738 // tool is forked+captured; anything else is authorized-but-not-runnable. never-brick: name-only, no path, 739 // no /bin/sh -- a bad/absent row can only refuse. Capture is bounded (64KB) so the escaped result always 740 // fits the 1MB jb buffer. Args come from params.arguments.argv (parsed just below into a native argv). 741 let xcap: i64 = TA_CAPTURE_CAP 742 let xout: *u8 = sys_mmap(xcap) 743 let xlen: *i64 = sys_mmap(16) as *i64 744 let xrc: *i64 = sys_mmap(16) as *i64 745 // R2 ARGS: parse optional params.arguments.argv (JSON string array) into a native argv vector so 746 // multi-arg organs are callable over MCP. av[0] is reserved for the resolved ELF path (filled by 747 // tea_run_argv); parsed args land at av[1..]. Absent/empty argv -> av = [path, 0] = argless, i.e. 748 // byte-for-byte the prior behavior, so already-working no-arg tools are unaffected (back-compat). 749 let av: *i64 = sys_mmap(TA_MAGIC_8192) as *i64 750 let sb: *u8 = sys_mmap(TA_MAGIC_65536) 751 let ac: i64 = ta_json_str_array(body, bn, "\"argv\"" as *u8, av, 1, 250, sb, TA_MAGIC_65536) 752 av[1 + ac] = 0 753 // ---- HTTP-BACKEND dispatch (data-driven, fail-closed, SSRF-safe): a tool declared in tool_backends.conf 754 // is an already-running sovereign HTTP service reached IN-PROCESS over loopback -- no shim binary. Tried 755 // BEFORE fork-exec; THB_NOROUTE (not a backend) falls straight through to the fork-exec path below, 756 // byte-for-byte unchanged for organ tools (never-brick preserving). 757 let hbcap: i64 = TA_MAGIC_1048576 758 let hbout: *u8 = sys_mmap(hbcap) 759 let hblen: *i64 = sys_mmap(16) as *i64 760 let hbrc: i64 = thb_dispatch(nmp, nml, av, ac, hbout, hbcap, hblen) 761 if hbrc == THB_OK { 762 ta_actlog(nmp, nml, "http" as *u8, "ok" as *u8, 0, hblen[0]) 763 b = ta_cat(jb, b, ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"" as *u8) 764 b = ta_json_esc(jb, b, hbout, hblen[0]) 765 b = ta_cat(jb, b, "\"}],\"isError\":false,\"_meta\":{\"backend\":\"http\",\"bytes\":" as *u8) 766 b = ta_catn(jb, b, hblen[0]) 767 b = ta_cat(jb, b, "}}}" as *u8) 768 return b 769 } 770 if hbrc == THB_ERR { 771 b = ta_cat(jb, b, ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"tool '" as *u8) 772 b = ta_json_esc(jb, b, nmp, nml) 773 b = ta_cat(jb, b, "' HTTP backend is declared but unreachable\"}],\"isError\":true}}" as *u8) 774 return b 775 } 776 // ---- ASYNC JOB LANE (rung 24; eats seq1273, defuses the seq1261 777 // trigger): a long organ run cannot live inside the synchronous 778 // response window -- the edge read-timeout drops the response and the 779 // daemon then dies writing to the dead socket (the crash-loop class). 780 // "_async":"1" anywhere in the body forks the SAME cap+allowlist-gated 781 // pinned dispatch DETACHED (double-fork, no zombies): stdout lands in 782 // _jobs/job_<id>.out, then _jobs/job_<id>.done lands ATOMICALLY 783 // (tmp+rename, written LAST = the completion marker). The call returns 784 // the job id IMMEDIATELY; poll with the already-registered nx_fs 785 // (read _jobs/job_<id>.done then .out) -- no new poll surface. 786 let asyb: *i64 = sys_mmap(16) as *i64 787 var isasync: i64 = 0 788 if ta_json_str(body, bn, "\"_async\"" as *u8, asyb) == 1 { if body[asyb[0]] == (49 as u8) { isasync = 1 } } 789 // ---- ASYNC-ONLY ENFORCEMENT (seq1293, incident 2026-07-29): the DAEMON owns the envelope, 790 // so the daemon decides -- a tool listed in async_only_tools.conf (one exact name per line, 791 // hot-read per call like the cap) is FORCED onto the job lane even when the caller forgot 792 // _async. The 2026-07-29 outage trigger was exactly a heavy sweep running sync, outrunning 793 // the edge window, and being retried. Absent/empty conf = byte-identical prior behavior. 794 if isasync == 0 { 795 let aosz: *i64 = sys_mmap(16) as *i64 796 let aob: *u8 = sys_read_file("async_only_tools.conf" as *u8, aosz) 797 if (aob as i64) != 0 { if aosz[0] > 0 { 798 if ta_name_in_lines(aob, aosz[0], nmp, nml) == 1 { isasync = 1 } 799 } } 800 } 801 if isasync == 1 { 802 sys_mkdir("_jobs" as *u8, 493) 803 var jid: i64 = sys_now_realtime_sec() 804 let jpath: *u8 = sys_mmap(256) 805 let jout2: *u8 = sys_mmap(256) 806 let jtmp: *u8 = sys_mmap(256) 807 var jp: i64 = 0 808 var probe: i64 = 0 809 while probe < 100 { 810 jp = ta_cat(jpath, 0, "_jobs/job_" as *u8) 811 jp = ta_catn(jpath, jp, jid) 812 jp = ta_cat(jpath, jp, ".done" as *u8) 813 jpath[jp] = 0 as u8 814 let pfd: i64 = sys_openat_rd(jpath) 815 if pfd < 0 { probe = 100 } else { sys_close(pfd); jid = jid + 1; probe = probe + 1 } 816 } 817 var jo: i64 = ta_cat(jout2, 0, "_jobs/job_" as *u8) 818 jo = ta_catn(jout2, jo, jid) 819 jo = ta_cat(jout2, jo, ".out" as *u8) 820 jout2[jo] = 0 as u8 821 var jt: i64 = ta_cat(jtmp, 0, "_jobs/job_" as *u8) 822 jt = ta_catn(jtmp, jt, jid) 823 jt = ta_cat(jtmp, jt, ".tmp" as *u8) 824 jtmp[jt] = 0 as u8 825 let pid1: i64 = sys_fork() 826 if pid1 == 0 { 827 // middle child: shed every inherited fd above stderr (listen + 828 // conn sockets included) so a detached job can never pin the 829 // daemon's ports; then detach the worker and exit at once. 830 var fdc: i64 = 3 831 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 } 832 // ★LEAVE THE DAEMON'S SESSION AND PROCESS GROUP (seq1611). 833 // Closing fds and double-forking reparents the worker to init but 834 // leaves it in the DAEMON'S process group, so any group-directed 835 // signal -- a supervisor cycling the daemon, a request-child sweep 836 // -- kills it mid-run. MEASURED: short jobs recorded exit=0 and 837 // finished, while the multi-minute regen died after its first two 838 // output lines EVERY time, with an EMPTY exit slot (= terminated by 839 // a signal, not an exit). A detached job must own its own session, 840 // exactly as hc_spawn_tapi already does for the daemon itself. 841 nx_setsid() 842 let pid2: i64 = sys_fork() 843 if pid2 != 0 { sys_exit(0) } 844 // seq1301: job output lands on DISK (no json-escape bound applies) -> full 1MiB capture, 845 // and a clipped capture is MARKED inside the .out itself so a poller can never mistake 846 // a truncated result for a complete one. 847 let jbuf: *u8 = sys_mmap(TA_MAGIC_1048576) 848 let jlen: *i64 = sys_mmap(16) as *i64 849 let jrc: *i64 = sys_mmap(16) as *i64 850 // tmo=0: NO TIMEOUT. A detached job exists precisely to outlive 851 // the synchronous window; inheriting the 120s request timeout 852 // killed every multi-minute regen at the 2-minute mark (seq1634). 853 let jec: i64 = tea_run_pinned_to(nmp, nml, av, jbuf, TA_MAGIC_1048576, jlen, jrc, 0) 854 if jlen[0] >= TA_MAGIC_1048576 { 855 let tm: *u8 = "\n[NX-JOB CAPTURE-TRUNCATED at 1048576 bytes -- output incomplete]\n" as *u8 856 var tl: i64 = 0 857 while tm[tl] != (0 as u8) { tl = tl + 1 } 858 var cw: i64 = 0 859 while cw < tl { if jlen[0] - tl + cw >= 0 { jbuf[jlen[0] - tl + cw] = tm[cw] } cw = cw + 1 } 860 } 861 ta_job_put(jout2, jbuf, jlen[0]) 862 let dbuf: *u8 = sys_mmap(256) 863 var db: i64 = ta_cat(dbuf, 0, "state=DONE rc=" as *u8) 864 db = ta_catn(dbuf, db, jrc[0]) 865 db = ta_cat(dbuf, db, " exit=" as *u8) 866 db = ta_catn(dbuf, db, jec) 867 db = ta_cat(dbuf, db, " bytes=" as *u8) 868 db = ta_catn(dbuf, db, jlen[0]) 869 db = ta_cat(dbuf, db, "\n" as *u8) 870 ta_job_put(jtmp, dbuf, db) 871 sys_renameat(jtmp, jpath) 872 sys_exit(0) 873 return 0 874 } 875 if pid1 > 0 { 876 let jst: *i64 = sys_mmap(16) as *i64 877 sys_wait4(pid1, jst, 0) 878 } 879 ta_actlog(nmp, nml, "async" as *u8, "started" as *u8, 0, jid) 880 b = ta_cat(jb, b, ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"JOB-STARTED id=" as *u8) 881 b = ta_catn(jb, b, jid) 882 b = ta_cat(jb, b, " tool=" as *u8) 883 b = ta_json_esc(jb, b, nmp, nml) 884 b = ta_cat(jb, b, " poll: nx_fs read _jobs/job_<id>.done (absent = RUNNING) then _jobs/job_<id>.out\"}],\"isError\":false,\"_meta\":{\"job\":" as *u8) 885 b = ta_catn(jb, b, jid) 886 b = ta_cat(jb, b, ",\"state\":\"RUNNING\"}}}" as *u8) 887 return b 888 } 889 // SYNCHRONOUS LANE (root fix 2026-07-30, ws=sev-eater). A client is waiting behind the 890 // edge's MEASURED 15.02s window, so this call gets the SYNC budget -- not the 120s default 891 // that tea_run_pinned carries for the async lane. Holding a worker slot past the window 892 // cannot deliver an answer to anyone; it only starves the bounded 16-worker pool, which is 893 // exactly how a trickle of hung calls took the whole agent surface down for every seat 894 // (measured: handler in pipe_wait behind an nx_mgmt_call parked in sk_wait_data, 10 stuck 895 // handler pairs, 4 of 8 requests 503ing). This is the SYNC half of the seq1611/1634 fix: 896 // that arc correctly gave the DETACHED lane an unbounded budget; the synchronous lane still 897 // needed one BOUNDED BY THE CALLER'S PATIENCE. Same primitive, opposite direction. 898 let ec: i64 = tea_run_pinned_to(nmp, nml, av, xout, xcap, xlen, xrc, TEA_EXEC_TIMEOUT_SYNC_MS) 899 if xrc[0] == TEA_OK { 900 ta_actlog(nmp, nml, "sync" as *u8, "ok" as *u8, ec, xlen[0]) 901 b = ta_cat(jb, b, ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"" as *u8) 902 b = ta_json_esc(jb, b, xout, xlen[0]) 903 // seq1301 put capture_truncated in _meta -- correct, and it is STILL emitted below. But MCP 904 // CLIENTS DO NOT SURFACE _meta: they render result.content[].text ONLY. So the one consumer who 905 // acts on a truncated result never saw the warning, and the sole symptom was ABSENCE of the 906 // tool's own trailing footer -- a negative signal no caller knows to look for. Say it IN THE 907 // TEXT, where every client actually reads. Guarded by the same condition, so untruncated output 908 // stays byte-identical to before (rule 19). No backslash escapes here on purpose. 909 if xlen[0] >= xcap { 910 b = ta_cat(jb, b, " [NX-TRUNCATED OUTPUT-IS-PARTIAL capture_cap=" as *u8) 911 b = ta_catn(jb, b, xcap) 912 b = ta_cat(jb, b, " -- narrow the query or re-run with _async=1]" as *u8) 913 } 914 b = ta_cat(jb, b, "\"}],\"isError\":false,\"_meta\":{\"exit_code\":" as *u8) 915 b = ta_catn(jb, b, ec) 916 b = ta_cat(jb, b, ",\"bytes\":" as *u8) 917 b = ta_catn(jb, b, xlen[0]) 918 // seq1301: a full capture buffer means the organ's stdout was (or may have been) CLIPPED -- 919 // silence here is how a truncated source nearly shipped. Say so, machine-readably. 920 if xlen[0] >= xcap { 921 b = ta_cat(jb, b, ",\"capture_truncated\":1,\"capture_cap\":" as *u8) 922 b = ta_catn(jb, b, xcap) 923 } 924 b = ta_cat(jb, b, "}}}" as *u8) 925 return b 926 } 927 // capability-valid but NOT on the GREEN execution allowlist -> clean TOOL-level error (not a protocol 928 // error). rc magnitude: 0=blocked(present,non-GREEN) 1=not-in-allowlist 2=no-allowlist-file. 929 b = ta_cat(jb, b, ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"tool '" as *u8) 930 b = ta_json_esc(jb, b, nmp, nml) 931 b = ta_cat(jb, b, "' is capability-authorized but not on the GREEN execution allowlist (rc=" as *u8) 932 b = ta_catn(jb, b, 0 - xrc[0]) 933 b = ta_cat(jb, b, ")\"}],\"isError\":true}}" as *u8) 934 return b 935 } 936 // ACTIONABLE DENY (rule 18: say WHAT happened, WHY it matters, WHAT to do). The old text was a bare 937 // capability-denied-is-required, which a caller cannot distinguish from -- this tool does not exist -- 938 // measured as the trigger for the ssh/PowerShell fallback cascade (debt seq1235). The two cases have 939 // OPPOSITE remedies, so they get OPPOSITE messages, and the presented flag states which one you are in. 940 b = ta_cat(jb, b, ",\"error\":{\"code\":-32001,\"message\":\"capability denied for tool '" as *u8) 941 b = ta_json_esc(jb, b, nmp, nml) 942 if capl <= 0 { 943 b = ta_cat(jb, b, "': NO capability was presented. This tool EXISTS and is callable -- this is an auth gap, NOT a missing tool, so do NOT fall back to shell/ssh. Present one of: ?cap=<token> on the MCP URL (session baseline, survives clients that drop custom headers), X-Nishi-Cap: <token>, Authorization: Bearer <token>, or params._cap in the call arguments (per-call override, wins over all others). Mint one with POST /api/cap/mint allow=<tool>&exp=<epoch>&nonce=<unique>&confirm=yes presented with an admin cap." as *u8) 944 } else { 945 b = ta_cat(jb, b, "': the capability you DID present does not grant this tool, or is expired, forged, or revoked. Check its allow-list covers this exact tool name; mint a scoped one with POST /api/cap/mint allow=<tool>&exp=<epoch>&nonce=<unique>&confirm=yes; confirm the server's signing key with GET /api/cap/status. The tool itself exists -- do NOT fall back to shell/ssh." as *u8) 946 } 947 b = ta_cat(jb, b, "\",\"data\":{\"reason\":" as *u8) 948 b = ta_catn(jb, b, 0 - granted) 949 b = ta_cat(jb, b, ",\"presented\":" as *u8) 950 if capl > 0 { b = ta_cat(jb, b, "1" as *u8) } else { b = ta_cat(jb, b, "0" as *u8) } 951 b = ta_cat(jb, b, ",\"tool\":\"" as *u8) 952 b = ta_json_esc(jb, b, nmp, nml) 953 b = ta_cat(jb, b, "\",\"presenters\":[\"query:?cap=\",\"header:X-Nishi-Cap\",\"header:Authorization Bearer\",\"body:params._cap\"]" as *u8) 954 b = ta_cat(jb, b, "}}}" as *u8) 955 return b 956} 957 958// R3: MCP resources/read -- serve READABLE content (not tool execution). Two resources: the LIVE ecosystem maturity 959// grade (forks the allowlisted rollup, escapes its output) and the operating doctrine (embedded). Public read-only 960// (non-sensitive, no side effects); resource not found -> -32002. Composes with the nx_ecosystem_maturity_rollup tool. 961func ta_mcp_resread(body: *u8, bn: i64, jb: *u8, b0: i64) -> i64 { 962 var b: i64 = b0 963 let u2: *i64 = sys_mmap(16) as *i64 964 if ta_json_str(body, bn, "\"uri\"" as *u8, u2) == 0 { 965 return ta_cat(jb, b, ",\"error\":{\"code\":-32602,\"message\":\"resources/read requires params.uri\"}}" as *u8) 966 } 967 let up: *u8 = ((body as i64) + u2[0]) as *u8 968 let ul: i64 = u2[1] 969 if ta_indexof(up, ul, "ecosystem-maturity" as *u8) >= 0 { 970 let xcap: i64 = TA_MAGIC_65536 971 let xout: *u8 = sys_mmap(xcap) 972 let xlen: *i64 = sys_mmap(16) as *i64 973 let xrc: *i64 = sys_mmap(16) as *i64 974 let av: *i64 = sys_mmap(64) as *i64 975 av[1] = 0 976 tea_run_pinned("nx_ecosystem_maturity_rollup" as *u8, 28, av, xout, xcap, xlen, xrc) 977 b = ta_cat(jb, b, ",\"result\":{\"contents\":[{\"uri\":\"nishi://sota/ecosystem-maturity\",\"mimeType\":\"text/plain\",\"text\":\"" as *u8) 978 if xrc[0] == TEA_OK { b = ta_json_esc(jb, b, xout, xlen[0]) } else { b = ta_cat(jb, b, "(maturity rollup unavailable)" as *u8) } 979 return ta_cat(jb, b, "\"}]}}" as *u8) 980 } 981 if ta_indexof(up, ul, "doctrine/operating" as *u8) >= 0 { 982 b = ta_cat(jb, b, ",\"result\":{\"contents\":[{\"uri\":\"nishi://doctrine/operating\",\"mimeType\":\"text/plain\",\"text\":\"" as *u8) 983 b = ta_cat(jb, b, "Operate Nishi over sovereign MCP. Tools: nishi_search/nishi_doc (search); nx_status/nx_services/nx_health (status); nx_mgmt (admin: build/deploy/reconcile/restart); nx_ecosystem_maturity_rollup (live grade). Auth: ?cap=token on the MCP URL (session baseline -- use this if your client drops custom headers), X-Nishi-Cap header, Authorization: Bearer cap, or params._cap (per-call, wins over the rest). Build+deploy on the NAS via /api/build + /api/deploy (never-brick, auto-rollback). One-command edit-to-build: nx_ship target tokenfile srcdir." as *u8) 984 return ta_cat(jb, b, "\"}]}}" as *u8) 985 } 986 return ta_cat(jb, b, ",\"error\":{\"code\":-32002,\"message\":\"resource not found\"}}" as *u8) 987} 988// R3: MCP prompts/get -- return a reusable prompt template, arguments filled. `operate-nishi` primes an agent on the 989// sovereign toolset; `ship-target` (arg: target) is a build+deploy prompt. unknown -> -32602. 990func ta_mcp_promget(body: *u8, bn: i64, jb: *u8, b0: i64) -> i64 { 991 var b: i64 = b0 992 let n2: *i64 = sys_mmap(16) as *i64 993 if ta_json_str(body, bn, "\"name\"" as *u8, n2) == 0 { 994 return ta_cat(jb, b, ",\"error\":{\"code\":-32602,\"message\":\"prompts/get requires params.name\"}}" as *u8) 995 } 996 let np: *u8 = ((body as i64) + n2[0]) as *u8 997 let nl: i64 = n2[1] 998 if ta_streq_n(np, nl, "operate-nishi" as *u8) == 1 { 999 b = ta_cat(jb, b, ",\"result\":{\"description\":\"Prime an agent to operate the Nishi ecosystem over sovereign MCP\",\"messages\":[{\"role\":\"user\",\"content\":{\"type\":\"text\",\"text\":\"" as *u8) 1000 b = ta_cat(jb, b, "You operate the Nishi sovereign ecosystem over MCP; prefer these tools over shell. Search: nishi_search/nishi_doc. Status: nx_status/nx_services/nx_health. Admin build/deploy/reconcile/restart (never-brick): nx_mgmt. Live SOTA grade: nx_ecosystem_maturity_rollup. One-command edit-to-build: nx_ship. Read the resource nishi://doctrine/operating first." as *u8) 1001 return ta_cat(jb, b, "\"}}]}}" as *u8) 1002 } 1003 if ta_streq_n(np, nl, "ship-target" as *u8) == 1 { 1004 let t2: *i64 = sys_mmap(16) as *i64 1005 var tp: *u8 = "TARGET" as *u8 1006 var tl: i64 = 6 1007 if ta_json_str(body, bn, "\"target\"" as *u8, t2) == 1 { tp = ((body as i64) + t2[0]) as *u8; tl = t2[1] } 1008 b = ta_cat(jb, b, ",\"result\":{\"description\":\"Build and deploy a target on the NAS over sovereign APIs\",\"messages\":[{\"role\":\"user\",\"content\":{\"type\":\"text\",\"text\":\"Build and deploy the target '" as *u8) 1009 b = ta_json_esc(jb, b, tp, tl) 1010 b = ta_cat(jb, b, "' on the NAS with ZERO shell: sync+compile via nx_ship (or nx_mgmt POST /api/build target=" as *u8) 1011 b = ta_json_esc(jb, b, tp, tl) 1012 b = ta_cat(jb, b, "), then promote via nx_mgmt POST /api/deploy (never-brick, auto-rollback), and verify with nx_health.\"}}]}}" as *u8) 1013 return b 1014 } 1015 return ta_cat(jb, b, ",\"error\":{\"code\":-32602,\"message\":\"unknown prompt\"}}" as *u8) 1016} 1017func ta_mcp(prefix: *u8, req: *u8, req_n: i64, out: *u8) -> i64 { 1018 let bo: i64 = ta_body_off(req, req_n) 1019 let body: *u8 = ((req as i64) + bo) as *u8 1020 let bn: i64 = req_n - bo 1021 let idbuf: *u8 = sys_mmap(128) 1022 var idlen: i64 = 0 1023 let i2: *i64 = sys_mmap(16) as *i64 1024 if ta_json_raw(body, bn, "\"id\"" as *u8, i2) == 1 { 1025 var c: i64 = 0 1026 while c < i2[1] { idbuf[c] = body[i2[0] + c]; c = c + 1 } 1027 idlen = i2[1] 1028 } else { idbuf[0] = 49 as u8; idlen = 1 } 1029 let m2: *i64 = sys_mmap(16) as *i64 1030 var meth: i64 = 0 1031 if ta_json_str(body, bn, "\"method\"" as *u8, m2) == 1 { 1032 let mp: *u8 = ((body as i64) + m2[0]) as *u8 1033 let ml: i64 = m2[1] 1034 if ta_streq_n(mp, ml, "tools/list" as *u8) == 1 { meth = 2 } else { if ta_streq_n(mp, ml, "initialize" as *u8) == 1 { meth = 1 } else { if ta_streq_n(mp, ml, "tools/call" as *u8) == 1 { meth = 3 } else { if ta_streq_n(mp, ml, "resources/list" as *u8) == 1 { meth = 4 } else { if ta_streq_n(mp, ml, "resources/read" as *u8) == 1 { meth = 5 } else { if ta_streq_n(mp, ml, "prompts/list" as *u8) == 1 { meth = 6 } else { if ta_streq_n(mp, ml, "prompts/get" as *u8) == 1 { meth = 7 } } } } } } } 1035 } 1036 let jb: *u8 = sys_mmap(TA_MAGIC_1048576) 1037 var b: i64 = ta_cat(jb, 0, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8) 1038 b = ta_catb(jb, b, idbuf, idlen) 1039 if meth == 1 { 1040 b = ta_cat(jb, b, ",\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{\"listChanged\":false},\"resources\":{\"listChanged\":false},\"prompts\":{\"listChanged\":false}},\"serverInfo\":{\"name\":\"nishi-tools\",\"version\":\"1\"}}}" as *u8) 1041 } else { if meth == 2 { 1042 // MCP cursor pagination: params.cursor is a numeric offset; emit a page + nextCursor iff more remain. 1043 var pgoff: i64 = 0 1044 let cb2: *i64 = sys_mmap(16) as *i64 1045 if ta_json_raw(body, bn, "\"cursor\"" as *u8, cb2) == 1 { pgoff = tr_atoi(body, cb2[0], cb2[1]) } 1046 let moreb: *i64 = sys_mmap(16) as *i64 1047 moreb[0] = 0 1048 b = ta_cat(jb, b, ",\"result\":{\"tools\":[" as *u8) 1049 b = ta_emit_tools(prefix, jb, b, 1, pgoff, TA_MCP_PAGE, moreb) 1050 b = ta_cat(jb, b, "]" as *u8) 1051 if moreb[0] == 1 { b = ta_cat(jb, b, ",\"nextCursor\":\"" as *u8); b = ta_catn(jb, b, pgoff + TA_MCP_PAGE); b = ta_cat(jb, b, "\"" as *u8) } 1052 b = ta_cat(jb, b, "}}" as *u8) 1053 } else { if meth == 3 { 1054 b = ta_mcp_call(prefix, req, req_n, body, bn, jb, b) 1055 } else { if meth == 4 { 1056 b = ta_cat(jb, b, ",\"result\":{\"resources\":[{\"uri\":\"nishi://sota/ecosystem-maturity\",\"name\":\"Ecosystem Maturity Grade\",\"description\":\"live liar-killed per-domain maturity toward S-class, measured from the sovereign seg_store\",\"mimeType\":\"text/plain\"},{\"uri\":\"nishi://doctrine/operating\",\"name\":\"Nishi Operating Doctrine\",\"description\":\"how to operate the ecosystem over sovereign MCP + APIs\",\"mimeType\":\"text/plain\"}]}}" as *u8) 1057 } else { if meth == 5 { 1058 b = ta_mcp_resread(body, bn, jb, b) 1059 } else { if meth == 6 { 1060 b = ta_cat(jb, b, ",\"result\":{\"prompts\":[{\"name\":\"operate-nishi\",\"description\":\"Prime an agent to operate the Nishi ecosystem over sovereign MCP\"},{\"name\":\"ship-target\",\"description\":\"Build and deploy a target on the NAS over sovereign APIs\",\"arguments\":[{\"name\":\"target\",\"description\":\"the runtime organ to build and deploy\",\"required\":true}]}]}}" as *u8) 1061 } else { if meth == 7 { 1062 b = ta_mcp_promget(body, bn, jb, b) 1063 } else { 1064 b = ta_cat(jb, b, ",\"error\":{\"code\":-32601,\"message\":\"method not found\"}}" as *u8) 1065 } } } } } } } 1066 return ta_emit(out, "HTTP/1.1 200 OK" as *u8, jb, b) 1067} 1068 1069// ---- THE router: pure request bytes -> response bytes (gate drives this directly, no socket) --------- 1070// POST /api/cap/issue -- SELF-SERVICE capability DELEGATION (pure ocap, no ambient identity). Present YOUR capability 1071// (X-Nishi-Cap header or params._cap) + a narrower "allow" (+ optional "exp"); get back a delegated capability that is a 1072// SUBSET of yours (capt_attenuate refuses any widening AND an invalid/forged input -> you can never delegate more than 1073// you hold). Root minting from the secret stays operator-side (nx_cap_mint). Ratchet: clamp exp' <= parent exp. 1074// R4 per-client consent ledger: cap_consent.log (append-only). One line per delegation: 1075// now <TAB> client <TAB> allow <TAB> exp <TAB> parent_nonce <TAB> child_nonce 1076// In the ocap model the operator's SCOPED, attenuated mint IS the per-client consent (a deliberate least-authority 1077// grant, superior to a yes/no consent screen); this makes that intent EXPLICIT (client-attributed) + AUDITABLE. 1078const TA_CONSENT_LOG: *u8 = "cap_consent.log" as *u8 1079func ta_append_consent(client: *u8, cl: i64, allowp: *u8, allowl: i64, exp: i64, pnonce: i64, cnonce: i64) -> i64 { 1080 let fd: i64 = ta_openat_append(TA_CONSENT_LOG) 1081 if fd < 0 { return 0 - 1 } 1082 let line: *u8 = sys_mmap(TA_MAGIC_8192) 1083 var o: i64 = ta_catn(line, 0, sys_now_realtime_sec()) 1084 line[o] = 9 as u8; o = o + 1 1085 o = ta_catb(line, o, client, cl) 1086 line[o] = 9 as u8; o = o + 1 1087 o = ta_catb(line, o, allowp, allowl) 1088 line[o] = 9 as u8; o = o + 1 1089 o = ta_catn(line, o, exp) 1090 line[o] = 9 as u8; o = o + 1 1091 o = ta_catn(line, o, pnonce) 1092 line[o] = 9 as u8; o = o + 1 1093 o = ta_catn(line, o, cnonce) 1094 line[o] = 10 as u8; o = o + 1 1095 sys_write(fd, line, o) 1096 sys_close(fd) 1097 return 0 1098} 1099// GET /api/cap/consent-log -- PUBLIC audit trail of who-delegated-what-to-which-client (no secrets: nonces + scope only). 1100func ta_cap_consent_log(out: *u8) -> i64 { 1101 let szp: *i64 = sys_mmap(16) as *i64 1102 let buf: *u8 = sys_read_file(TA_CONSENT_LOG, szp) 1103 let jb: *u8 = sys_mmap(TA_MAGIC_1048576) 1104 var b: i64 = ta_cat(jb, 0, "{\"consent_log\":\"" as *u8) 1105 if (buf as i64) != 0 { b = ta_json_esc(jb, b, buf, szp[0]) } 1106 b = ta_cat(jb, b, "\",\"schema\":\"now\\tclient\\tallow\\texp\\tparent_nonce\\tchild_nonce\",\"note\":\"append-only; ocap scoped+attenuated delegation IS the per-client consent, recorded here for audit + revocable via /api/cap/revoke\"}" as *u8) 1107 return ta_emit(out, "HTTP/1.1 200 OK" as *u8, jb, b) 1108} 1109func ta_cap_issue(req: *u8, req_n: i64, out: *u8) -> i64 { 1110 let bo: i64 = ta_body_off(req, req_n) 1111 let body: *u8 = ((req as i64) + bo) as *u8 1112 let bn: i64 = req_n - bo 1113 let slb: *i64 = sys_mmap(16) as *i64 1114 let secret: *u8 = ta_load_cap_secret(slb) 1115 let secretlen: i64 = slb[0] 1116 let cap2: *i64 = sys_mmap(16) as *i64 1117 var pp: *u8 = 0 as *u8 1118 var pl: i64 = 0 1119 if ta_header(req, req_n, "X-Nishi-Cap:" as *u8, cap2) == 1 { pp = ((req as i64) + cap2[0]) as *u8; pl = cap2[1] } 1120 else { let cb: *i64 = sys_mmap(16) as *i64; if ta_json_str(body, bn, "\"_cap\"" as *u8, cb) == 1 { pp = ((body as i64) + cb[0]) as *u8; pl = cb[1] } } 1121 if pl <= 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"present your capability (X-Nishi-Cap header or params._cap)\"}" as *u8) } 1122 let ab: *i64 = sys_mmap(16) as *i64 1123 var allowp: *u8 = 0 as *u8 1124 var allowl: i64 = 0 1125 if ta_json_str(body, bn, "\"allow\"" as *u8, ab) == 1 { allowp = ((body as i64) + ab[0]) as *u8; allowl = ab[1] } 1126 if allowl <= 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"specify a narrower \\\"allow\\\" tool-set (comma-separated)\"}" as *u8) } 1127 var exp: i64 = TA_MAGIC_9999999999 1128 let eb: *i64 = sys_mmap(16) as *i64 1129 if ta_json_raw(body, bn, "\"exp\"" as *u8, eb) == 1 { exp = tr_atoi(body, eb[0], eb[1]) } 1130 // caller-supplied nonce -> a UNIQUE identity per delegated cap so it can be revoked individually (default 7 if omitted) 1131 var dnonce: i64 = 7 1132 let nb: *i64 = sys_mmap(16) as *i64 1133 if ta_json_raw(body, bn, "\"nonce\"" as *u8, nb) == 1 { dnonce = tr_atoi(body, nb[0], nb[1]) } 1134 let ntok: *u8 = sys_mmap(TA_MAGIC_2048) 1135 let ntn: i64 = capt_attenuate(secret, secretlen, pp, pl, allowp, allowl, exp, dnonce, ntok, TA_MAGIC_2048) 1136 if ntn > 0 { 1137 // R4: record the per-client consent (who consented by holding the parent, to which client, what scope, when). 1138 let clb: *i64 = sys_mmap(16) as *i64 1139 var clientp: *u8 = "anonymous" as *u8 1140 var clientl: i64 = 9 1141 if ta_json_str(body, bn, "\"client\"" as *u8, clb) == 1 { clientp = ((body as i64) + clb[0]) as *u8; clientl = clb[1] } 1142 ta_append_consent(clientp, clientl, allowp, allowl, exp, capt_nonce_of(pp, pl), dnonce) 1143 let jb: *u8 = sys_mmap(TA_MAGIC_4096) 1144 var b: i64 = ta_cat(jb, 0, "{\"cap\":\"" as *u8) 1145 b = ta_catb(jb, b, ntok, ntn) 1146 b = ta_cat(jb, b, "\",\"consent_recorded\":true,\"note\":\"delegated capability -- a SUBSET of yours (attenuation is subset-only; widening is refused); per-client consent logged (see GET /api/cap/consent-log)\"}" as *u8) 1147 return ta_emit(out, "HTTP/1.1 200 OK" as *u8, jb, b) 1148 } 1149 return ta_emit_lit(out, "HTTP/1.1 403 Forbidden" as *u8, "{\"error\":\"cannot delegate: your capability is invalid/forged, or the requested tools exceed your grant (no widening)\"}" as *u8) 1150} 1151 1152// POST /api/cap/revoke -- REVOKE a capability you hold: present it (X-Nishi-Cap / params._cap); if its MAC is valid its 1153// nonce is added to the denylist and it (plus any cap sharing that nonce) is refused thereafter. Fail-closed: only a 1154// MAC-valid cap can be revoked (the denylist can't be spammed with garbage). Cascading parent->child is a ratchet. 1155func ta_cap_revoke(req: *u8, req_n: i64, out: *u8) -> i64 { 1156 let bo: i64 = ta_body_off(req, req_n) 1157 let body: *u8 = ((req as i64) + bo) as *u8 1158 let bn: i64 = req_n - bo 1159 let slb: *i64 = sys_mmap(16) as *i64 1160 let secret: *u8 = ta_load_cap_secret(slb) 1161 let secretlen: i64 = slb[0] 1162 let cap2: *i64 = sys_mmap(16) as *i64 1163 var pp: *u8 = 0 as *u8 1164 var pl: i64 = 0 1165 if ta_header(req, req_n, "X-Nishi-Cap:" as *u8, cap2) == 1 { pp = ((req as i64) + cap2[0]) as *u8; pl = cap2[1] } 1166 else { let cb: *i64 = sys_mmap(16) as *i64; if ta_json_str(body, bn, "\"_cap\"" as *u8, cb) == 1 { pp = ((body as i64) + cb[0]) as *u8; pl = cb[1] } } 1167 if pl <= 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"present the capability to revoke (X-Nishi-Cap or params._cap)\"}" as *u8) } 1168 let plb: *i64 = sys_mmap(16) as *i64 1169 if capt_mac_ok(secret, secretlen, pp, pl, plb) == 0 { return ta_emit_lit(out, "HTTP/1.1 403 Forbidden" as *u8, "{\"error\":\"invalid/forged capability -- nothing revoked\"}" as *u8) } 1170 let nn: i64 = capt_nonce_of(pp, pl) 1171 ta_append_revoked(nn) 1172 let jb: *u8 = sys_mmap(256) 1173 var b: i64 = ta_cat(jb, 0, "{\"revoked_nonce\":" as *u8) 1174 b = ta_catn(jb, b, nn) 1175 b = ta_cat(jb, b, ",\"note\":\"denylisted; this capability (and any sharing its nonce) is refused from now on\"}" as *u8) 1176 return ta_emit(out, "HTTP/1.1 200 OK" as *u8, jb, b) 1177} 1178 1179// GET /api/cap/status -- PUBLIC config-health readout for the capability-grant bootstrap: reports whether the server 1180// loaded a REAL signing keyfile or is still on the FORGEABLE baked placeholder. Reveals NO secret (only the boolean 1181// state) -- and if it says placeholder, that secret is already public in the source, so nothing leaks. This is what 1182// lets an operator confirm `nx_cap_keygen` + restart took effect BEFORE minting (otherwise a real token fails with an 1183// opaque -32001 and there is no way to tell why). Needs no capability -- it must be checkable before any cap exists. 1184func ta_cap_status(out: *u8) -> i64 { 1185 if ta_cap_provisioned() == 1 { 1186 return ta_emit_lit(out, "HTTP/1.1 200 OK" as *u8, "{\"cap_signing\":\"provisioned\",\"secure\":true,\"note\":\"a real keyfile is loaded; minted tokens will verify\"}" as *u8) 1187 } 1188 return ta_emit_lit(out, "HTTP/1.1 200 OK" as *u8, "{\"cap_signing\":\"insecure_placeholder\",\"secure\":false,\"note\":\"forgeable baked secret in use -- run nx_cap_keygen in the server CWD and restart nx_tools_api, then re-check\"}" as *u8) 1189} 1190 1191// RFC 9728 Protected Resource Metadata -- lets a stock MCP client DISCOVER how to authenticate to this resource 1192// (bearer token in the Authorization header) and where the authorization server is. GET /.well-known/oauth-protected- 1193// resource. Advertises the ocap-as-bearer interop (R2); native X-Nishi-Cap / _cap remain first-class. 1194func ta_oauth_prm(out: *u8) -> i64 { 1195 return ta_emit_lit(out, "HTTP/1.1 200 OK" as *u8, "{\"resource\":\"https://nishifamily.com/mcp\",\"authorization_servers\":[\"https://nishifamily.com\"],\"bearer_methods_supported\":[\"header\"],\"resource_name\":\"Nishi Sovereign MCP\",\"resource_documentation\":\"https://nishifamily.com/api\"}" as *u8) 1196} 1197// ============================================================================================================ 1198// OAuth 2.1 AUTHORIZATION SERVER (RFC 6749/8252 + PKCE RFC 7636 S256 + AS-metadata RFC 8414 + dynamic client 1199// registration RFC 7591). Interactive consent at /oauth/authorize. STATELESS signed tokens: client_id + auth-code 1200// are capt_issue-signed (no client/token DB); access_token is a real scoped cap. One-time-use codes tracked in a 1201// BOUNDED static-pointer table (like the rate limiter). The issued bearer IS an X-Nishi-Cap -> the resource server 1202// (tools/call) already accepts it (Authorization: Bearer). ocap remains superior; this is for stock OAuth clients. 1203// ============================================================================================================ 1204const OC_SLOTS: i64 = 256 1205static oc_nonce: *i64 // one-time auth-code nonce (0 = free slot) 1206static oc_rhash: *i64 // bound redirect_uri hash 1207static oc_exp: *i64 // code expiry (epoch) 1208static oc_bind: *u8 // OC_SLOTS*160: [0..48)=code_challenge(S256, 43ch) , [48..160)=granted scope (tool CSV) 1209func oauth_state_init() -> i64 { 1210 if (oc_nonce as i64) == 0 { 1211 oc_nonce = sys_mmap(OC_SLOTS * 8) as *i64 1212 oc_rhash = sys_mmap(OC_SLOTS * 8) as *i64 1213 oc_exp = sys_mmap(OC_SLOTS * 8) as *i64 1214 oc_bind = sys_mmap(OC_SLOTS * 160) 1215 } 1216 return 0 1217} 1218func oauth_slotv(h: i64, slots: i64) -> i64 { var x: i64 = h; if x < 0 { x = 0 - x } return x - (x / slots) * slots } 1219func oauth_strhash(s: *u8, n: i64) -> i64 { var h: i64 = TA_MAGIC_1469598; var i: i64 = 0; while i < n { h = h * 31 + (s[i] as i64); i = i + 1 } if h == 0 { h = 1 } return h } 1220// extract param "key=" from a query/form string -> value [off,len) into out2 (0 if absent). Boundary = start or '&'. 1221func oauth_param(src: *u8, slen: i64, key: *u8, klen: i64, out2: *i64) -> i64 { 1222 var i: i64 = 0 1223 while i + klen <= slen { 1224 var atb: i64 = 0 1225 if i == 0 { atb = 1 } else { if src[i - 1] == (38 as u8) { atb = 1 } } 1226 if atb == 1 { 1227 var k: i64 = 0 1228 while k < klen { if src[i + k] != key[k] { k = klen + 9 } else { k = k + 1 } } 1229 if k == klen { if i + klen < slen { if src[i + klen] == (61 as u8) { 1230 let v: i64 = i + klen + 1 1231 var e: i64 = v 1232 var go: i64 = 1 1233 while go == 1 { if e >= slen { go = 0 } else { if src[e] == (38 as u8) { go = 0 } else { e = e + 1 } } } 1234 out2[0] = v 1235 out2[1] = e - v 1236 return 1 1237 } } } 1238 } 1239 i = i + 1 1240 } 1241 return 0 1242} 1243// minimal percent-decode (+ -> space, %XX -> byte) for building the redirect Location. Returns decoded length. 1244func oauth_hexv(c: i64) -> i64 { if c >= 48 { if c <= 57 { return c - 48 } } if c >= 65 { if c <= 70 { return c - 55 } } if c >= 97 { if c <= 102 { return c - 87 } } return 0 } 1245func oauth_urldec(src: *u8, n: i64, dst: *u8) -> i64 { 1246 var i: i64 = 0 1247 var o: i64 = 0 1248 while i < n { 1249 let c: i64 = src[i] as i64 1250 if c == 37 { if i + 2 < n { dst[o] = ((oauth_hexv(src[i+1] as i64) * 16) + oauth_hexv(src[i+2] as i64)) as u8; o = o + 1; i = i + 3 } else { dst[o] = 37 as u8; o = o + 1; i = i + 1 } } 1251 else { if c == 43 { dst[o] = 32 as u8; o = o + 1; i = i + 1 } else { dst[o] = c as u8; o = o + 1; i = i + 1 } } 1252 } 1253 return o 1254} 1255// base64url (no padding) of raw bytes -> out. For 32-byte SHA-256 -> 43 chars (PKCE S256 challenge form). 1256func oauth_b64url(inb: *u8, n: i64, out: *u8) -> i64 { 1257 var o: i64 = 0 1258 var i: i64 = 0 1259 while i + 3 <= n { 1260 let x: i64 = ((inb[i] as i64) << 16) | ((inb[i+1] as i64) << 8) | (inb[i+2] as i64) 1261 out[o] = b64url_enc_char((x >> 18) & 63) as u8; o = o + 1 1262 out[o] = b64url_enc_char((x >> 12) & 63) as u8; o = o + 1 1263 out[o] = b64url_enc_char((x >> 6) & 63) as u8; o = o + 1 1264 out[o] = b64url_enc_char(x & 63) as u8; o = o + 1 1265 i = i + 3 1266 } 1267 let rem: i64 = n - i 1268 if rem == 1 { 1269 let x: i64 = (inb[i] as i64) << 16 1270 out[o] = b64url_enc_char((x >> 18) & 63) as u8; o = o + 1 1271 out[o] = b64url_enc_char((x >> 12) & 63) as u8; o = o + 1 1272 } else { if rem == 2 { 1273 let x: i64 = ((inb[i] as i64) << 16) | ((inb[i+1] as i64) << 8) 1274 out[o] = b64url_enc_char((x >> 18) & 63) as u8; o = o + 1 1275 out[o] = b64url_enc_char((x >> 12) & 63) as u8; o = o + 1 1276 out[o] = b64url_enc_char((x >> 6) & 63) as u8; o = o + 1 1277 } } 1278 return o 1279} 1280func oauth_resp(out: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 1281 var o: i64 = ta_cat(out, 0, "HTTP/1.1 " as *u8) 1282 o = ta_cat(out, o, status) 1283 o = ta_cat(out, o, "\r\nContent-Type: " as *u8) 1284 o = ta_cat(out, o, ctype) 1285 o = ta_cat(out, o, "\r\nCache-Control: no-store\r\nConnection: close\r\nContent-Length: " as *u8) 1286 o = capt_catn(out, o, blen) 1287 o = ta_cat(out, o, "\r\n\r\n" as *u8) 1288 o = ta_catb(out, o, body, blen) 1289 return o 1290} 1291// GET /.well-known/oauth-authorization-server (RFC 8414) 1292func ta_oauth_asmeta(out: *u8) -> i64 { 1293 let body: *u8 = "{\"issuer\":\"https://nishifamily.com\",\"authorization_endpoint\":\"https://nishifamily.com/oauth/authorize\",\"token_endpoint\":\"https://nishifamily.com/oauth/token\",\"registration_endpoint\":\"https://nishifamily.com/oauth/register\",\"response_types_supported\":[\"code\"],\"grant_types_supported\":[\"authorization_code\"],\"code_challenge_methods_supported\":[\"S256\"],\"token_endpoint_auth_methods_supported\":[\"none\"],\"scopes_supported\":[\"nishi_search\",\"nishi_doc\",\"nx_status\",\"nx_health\",\"nx_services\"]}" as *u8 1294 return oauth_resp(out, "200 OK" as *u8, "application/json" as *u8, body, capt_slen(body)) 1295} 1296// POST /oauth/register (RFC 7591 dynamic client registration). body JSON {redirect_uri, client_name}. client_id is a 1297// STATELESS capt-signed token binding the redirect_uri (its nonce = hash(redirect_uri)) -> no client DB. 1298func ta_oauth_register(req: *u8, req_n: i64, out: *u8) -> i64 { 1299 let bo: i64 = ta_body_off(req, req_n) 1300 let body: *u8 = ((req as i64) + bo) as *u8 1301 let bn: i64 = req_n - bo 1302 let slb: *i64 = sys_mmap(16) as *i64 1303 let secret: *u8 = ta_load_cap_secret(slb) 1304 let rb: *i64 = sys_mmap(16) as *i64 1305 if ta_json_str(body, bn, "\"redirect_uri\"" as *u8, rb) == 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"invalid_client_metadata\",\"error_description\":\"redirect_uri required\"}" as *u8) } 1306 let rp: *u8 = ((body as i64) + rb[0]) as *u8 1307 let rl: i64 = rb[1] 1308 let cidtok: *u8 = sys_mmap(TA_MAGIC_2048) 1309 let cidn: i64 = capt_issue(secret, slb[0], "oauth_client" as *u8, 12, TA_MAGIC_9999999999, oauth_strhash(rp, rl), cidtok, TA_MAGIC_2048) 1310 let jb: *u8 = sys_mmap(TA_MAGIC_4096) 1311 var b: i64 = ta_cat(jb, 0, "{\"client_id\":\"" as *u8) 1312 b = ta_catb(jb, b, cidtok, cidn) 1313 b = ta_cat(jb, b, "\",\"token_endpoint_auth_method\":\"none\",\"grant_types\":[\"authorization_code\"],\"response_types\":[\"code\"],\"redirect_uris\":[\"" as *u8) 1314 b = ta_catb(jb, b, rp, rl) 1315 b = ta_cat(jb, b, "\"]}" as *u8) 1316 return oauth_resp(out, "201 Created" as *u8, "application/json" as *u8, jb, b) 1317} 1318// GET /oauth/authorize?response_type=code&client_id&redirect_uri&scope&state&code_challenge&code_challenge_method=S256 1319// -> INTERACTIVE CONSENT page (the human approves the scope). Approve POSTs to /oauth/approve. 1320func ta_oauth_authorize(req: *u8, req_n: i64, out: *u8) -> i64 { 1321 let mo: *i64 = sys_mmap(16) as *i64 1322 let po: *i64 = sys_mmap(16) as *i64 1323 ta_reqline(req, req_n, mo, po) 1324 let full: *u8 = ((req as i64) + po[0]) as *u8 1325 let flen: i64 = po[1] 1326 var qpos: i64 = 0 - 1 1327 var qi: i64 = 0 1328 while qi < flen { if qpos < 0 { if full[qi] == (63 as u8) { qpos = qi } } qi = qi + 1 } 1329 if qpos < 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\",\"error_description\":\"missing query\"}" as *u8, 62) } 1330 let query: *u8 = ((full as i64) + qpos + 1) as *u8 1331 let qlen: i64 = flen - qpos - 1 1332 let aslb: *i64 = sys_mmap(16) as *i64 1333 let secret: *u8 = ta_load_cap_secret(aslb) 1334 let cib: *i64 = sys_mmap(16) as *i64 1335 let rib: *i64 = sys_mmap(16) as *i64 1336 let ccb: *i64 = sys_mmap(16) as *i64 1337 let scb: *i64 = sys_mmap(16) as *i64 1338 if oauth_param(query, qlen, "client_id" as *u8, 9, cib) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\"}" as *u8, 27) } 1339 if oauth_param(query, qlen, "redirect_uri" as *u8, 12, rib) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\"}" as *u8, 27) } 1340 if oauth_param(query, qlen, "code_challenge" as *u8, 14, ccb) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\",\"error_description\":\"PKCE code_challenge required (S256)\"}" as *u8, 82) } 1341 let cip: *u8 = ((query as i64) + cib[0]) as *u8 1342 let rip: *u8 = ((query as i64) + rib[0]) as *u8 1343 let plb: *i64 = sys_mmap(16) as *i64 1344 if capt_mac_ok(secret, aslb[0], cip, cib[1], plb) == 0 { return oauth_resp(out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"invalid_client\"}" as *u8, 26) } 1345 if capt_nonce_of(cip, cib[1]) != oauth_strhash(rip, rib[1]) { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\",\"error_description\":\"redirect_uri not registered for this client\"}" as *u8, 90) } 1346 var scp: *u8 = "nishi_search" as *u8 1347 var scl: i64 = 12 1348 if oauth_param(query, qlen, "scope" as *u8, 5, scb) == 1 { scp = ((query as i64) + scb[0]) as *u8; scl = scb[1] } 1349 // consent page: nishi-native, self-contained. Approve form re-submits the exact params to /oauth/approve. 1350 let jb: *u8 = sys_mmap(TA_MAGIC_16384) 1351 var b: i64 = ta_cat(jb, 0, "<html><head><meta charset=utf-8><title>Authorize</title><style>body{font-family:system-ui;max-width:34em;margin:3em auto;padding:0 1em;color:midnightblue}h1{color:seagreen}.s{background:honeydew;border-left:4px solid seagreen;padding:.5em 1em;border-radius:5px;margin:1em 0}button{font-size:1em;padding:.5em 1.4em;border:0;border-radius:6px;cursor:pointer}.a{background:seagreen;color:white}.d{background:gainsboro}</style></head><body><h1>Authorize access</h1><p>An application is requesting a Nishi capability. Review the scope you are granting.</p><div class=s><b>Scope:</b> <code>" as *u8) 1352 b = ta_catb(jb, b, scp, scl) 1353 b = ta_cat(jb, b, "</code></div><form method=post action=/oauth/approve>" as *u8) 1354 b = ta_cat(jb, b, "<input type=hidden name=client_id value=\"" as *u8); b = ta_catb(jb, b, cip, cib[1]); b = ta_cat(jb, b, "\">" as *u8) 1355 b = ta_cat(jb, b, "<input type=hidden name=redirect_uri value=\"" as *u8); b = ta_catb(jb, b, rip, rib[1]); b = ta_cat(jb, b, "\">" as *u8) 1356 b = ta_cat(jb, b, "<input type=hidden name=code_challenge value=\"" as *u8); b = ta_catb(jb, b, ((query as i64) + ccb[0]) as *u8, ccb[1]); b = ta_cat(jb, b, "\">" as *u8) 1357 b = ta_cat(jb, b, "<input type=hidden name=scope value=\"" as *u8); b = ta_catb(jb, b, scp, scl); b = ta_cat(jb, b, "\">" as *u8) 1358 let stb: *i64 = sys_mmap(16) as *i64 1359 if oauth_param(query, qlen, "state" as *u8, 5, stb) == 1 { b = ta_cat(jb, b, "<input type=hidden name=state value=\"" as *u8); b = ta_catb(jb, b, ((query as i64) + stb[0]) as *u8, stb[1]); b = ta_cat(jb, b, "\">" as *u8) } 1360 b = ta_cat(jb, b, "<button class=a type=submit>Approve</button></form><p style=color:slategray>Powered by the Nishi sovereign auth plane -- OPAQUE + object-capability. The issued bearer is an attenuated cap.</p></body></html>" as *u8) 1361 return oauth_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, jb, b) 1362} 1363// POST /oauth/approve (the consent GRANT). Issues a one-time auth code bound to {redirect_uri, code_challenge, scope} 1364// and 302-redirects to redirect_uri?code=..&state=.. 1365func ta_oauth_approve(req: *u8, req_n: i64, out: *u8) -> i64 { 1366 oauth_state_init() 1367 let bo: i64 = ta_body_off(req, req_n) 1368 let body: *u8 = ((req as i64) + bo) as *u8 1369 let bn: i64 = req_n - bo 1370 let pslb: *i64 = sys_mmap(16) as *i64 1371 let secret: *u8 = ta_load_cap_secret(pslb) 1372 let cib: *i64 = sys_mmap(16) as *i64 1373 let rib: *i64 = sys_mmap(16) as *i64 1374 let ccb: *i64 = sys_mmap(16) as *i64 1375 let scb: *i64 = sys_mmap(16) as *i64 1376 if oauth_param(body, bn, "client_id" as *u8, 9, cib) == 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"invalid_request\"}" as *u8) } 1377 if oauth_param(body, bn, "redirect_uri" as *u8, 12, rib) == 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"invalid_request\"}" as *u8) } 1378 if oauth_param(body, bn, "code_challenge" as *u8, 14, ccb) == 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"invalid_request\"}" as *u8) } 1379 let cip: *u8 = ((body as i64) + cib[0]) as *u8 1380 let rip: *u8 = ((body as i64) + rib[0]) as *u8 1381 let plb: *i64 = sys_mmap(16) as *i64 1382 if capt_mac_ok(secret, pslb[0], cip, cib[1], plb) == 0 { return ta_emit_lit(out, "HTTP/1.1 401 Unauthorized" as *u8, "{\"error\":\"invalid_client\"}" as *u8) } 1383 if capt_nonce_of(cip, cib[1]) != oauth_strhash(rip, rib[1]) { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"invalid_request\"}" as *u8) } 1384 var scp: *u8 = "nishi_search" as *u8 1385 var scl: i64 = 12 1386 if oauth_param(body, bn, "scope" as *u8, 5, scb) == 1 { scp = ((body as i64) + scb[0]) as *u8; scl = scb[1] } 1387 let ccp: *u8 = ((body as i64) + ccb[0]) as *u8 1388 let ccl: i64 = ccb[1] 1389 let now: i64 = sys_now_realtime_sec() 1390 var cnonce: i64 = (oauth_strhash(ccp, ccl) ^ (now * 131 + 7)) & TA_MAGIC_4294967295 // positive 32-bit -> capt_catn writes it 1391 if cnonce == 0 { cnonce = 1 } 1392 let s: i64 = oauth_slotv(cnonce, OC_SLOTS) 1393 oc_nonce[s] = cnonce 1394 oc_rhash[s] = oauth_strhash(rip, rib[1]) 1395 oc_exp[s] = now + 60 1396 var z: i64 = 0 1397 while z < ccl { if z < 47 { oc_bind[s * 160 + z] = ccp[z] } z = z + 1 } 1398 oc_bind[s * 160 + 47] = 0 as u8 1399 z = 0 1400 while z < scl { if z < 111 { oc_bind[s * 160 + 48 + z] = scp[z] } z = z + 1 } 1401 oc_bind[s * 160 + 48 + scl] = 0 as u8 1402 let code: *u8 = sys_mmap(TA_MAGIC_2048) 1403 let codn: i64 = capt_issue(secret, pslb[0], "oauth_code" as *u8, 10, now + 60, cnonce, code, TA_MAGIC_2048) 1404 // build Location = <decoded redirect_uri>?code=<code>&state=<state> 1405 let loc: *u8 = sys_mmap(TA_MAGIC_4096) 1406 var lo: i64 = oauth_urldec(rip, rib[1], loc) 1407 loc[lo] = 63 as u8; lo = lo + 1 1408 lo = ta_cat(loc, lo, "code=" as *u8) 1409 lo = ta_catb(loc, lo, code, codn) 1410 let stb: *i64 = sys_mmap(16) as *i64 1411 if oauth_param(body, bn, "state" as *u8, 5, stb) == 1 { 1412 lo = ta_cat(loc, lo, "&state=" as *u8) 1413 lo = ta_catb(loc, lo, ((body as i64) + stb[0]) as *u8, stb[1]) 1414 } 1415 var o: i64 = ta_cat(out, 0, "HTTP/1.1 302 Found\r\nLocation: " as *u8) 1416 o = ta_catb(out, o, loc, lo) 1417 o = ta_cat(out, o, "\r\nCache-Control: no-store\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" as *u8) 1418 return o 1419} 1420// POST /oauth/token grant_type=authorization_code&code&redirect_uri&code_verifier -> verify code sig + one-time-use + 1421// redirect binding + PKCE(S256) -> mint a scoped access cap. RFC 6749 s5.1 JSON. 1422func ta_oauth_token(req: *u8, req_n: i64, out: *u8) -> i64 { 1423 oauth_state_init() 1424 let bo: i64 = ta_body_off(req, req_n) 1425 let body: *u8 = ((req as i64) + bo) as *u8 1426 let bn: i64 = req_n - bo 1427 let slb: *i64 = sys_mmap(16) as *i64 1428 let secret: *u8 = ta_load_cap_secret(slb) 1429 let gtb: *i64 = sys_mmap(16) as *i64 1430 let cdb: *i64 = sys_mmap(16) as *i64 1431 let rib: *i64 = sys_mmap(16) as *i64 1432 let cvb: *i64 = sys_mmap(16) as *i64 1433 if oauth_param(body, bn, "grant_type" as *u8, 10, gtb) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\"}" as *u8, 27) } 1434 if oauth_param(body, bn, "code" as *u8, 4, cdb) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\",\"error_description\":\"code required\"}" as *u8, 60) } 1435 if oauth_param(body, bn, "redirect_uri" as *u8, 12, rib) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\"}" as *u8, 27) } 1436 if oauth_param(body, bn, "code_verifier" as *u8, 13, cvb) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_request\",\"error_description\":\"PKCE code_verifier required\"}" as *u8, 76) } 1437 let cdp: *u8 = ((body as i64) + cdb[0]) as *u8 1438 let plb: *i64 = sys_mmap(16) as *i64 1439 if capt_mac_ok(secret, slb[0], cdp, cdb[1], plb) == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_grant\",\"error_description\":\"bad code signature\"}" as *u8, 64) } 1440 let cnonce: i64 = capt_nonce_of(cdp, cdb[1]) 1441 let s: i64 = oauth_slotv(cnonce, OC_SLOTS) 1442 let now: i64 = sys_now_realtime_sec() 1443 if oc_nonce[s] != cnonce { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_grant\",\"error_description\":\"code unknown/already used\"}" as *u8, 72) } 1444 if now >= oc_exp[s] { oc_nonce[s] = 0; return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_grant\",\"error_description\":\"code expired\"}" as *u8, 58) } 1445 let rip: *u8 = ((body as i64) + rib[0]) as *u8 1446 if oc_rhash[s] != oauth_strhash(rip, rib[1]) { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_grant\",\"error_description\":\"redirect_uri mismatch\"}" as *u8, 68) } 1447 // PKCE S256: base64url(sha256(code_verifier)) == stored code_challenge 1448 let cvp: *u8 = ((body as i64) + cvb[0]) as *u8 1449 let dg: *u8 = sys_mmap(64) 1450 sha256_digest(cvp, cvb[1], dg) 1451 let calc: *u8 = sys_mmap(128) 1452 let cln: i64 = oauth_b64url(dg, 32, calc) 1453 let chal: *u8 = ((oc_bind as i64) + s * 160) as *u8 1454 var chn: i64 = 0 1455 var cdone: i64 = 0 1456 while cdone == 0 { if chn >= 47 { cdone = 1 } else { if chal[chn] == (0 as u8) { cdone = 1 } else { chn = chn + 1 } } } 1457 var pkok: i64 = 1 1458 if cln != chn { pkok = 0 } 1459 var mi: i64 = 0 1460 while mi < cln { if calc[mi] != chal[mi] { pkok = 0 } mi = mi + 1 } 1461 if pkok == 0 { return oauth_resp(out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"invalid_grant\",\"error_description\":\"PKCE verification failed\"}" as *u8, 70) } 1462 oc_nonce[s] = 0 // ONE-TIME USE: burn the code 1463 let scope: *u8 = ((oc_bind as i64) + s * 160 + 48) as *u8 1464 var scln: i64 = 0 1465 var sdone: i64 = 0 1466 while sdone == 0 { if scln >= 111 { sdone = 1 } else { if scope[scln] == (0 as u8) { sdone = 1 } else { scln = scln + 1 } } } 1467 let atok: *u8 = sys_mmap(TA_MAGIC_2048) 1468 let atn: i64 = capt_issue(secret, slb[0], scope, scln, now + TA_MAGIC_3600, (cnonce ^ TA_MAGIC_88161) & TA_MAGIC_4294967295, atok, TA_MAGIC_2048) 1469 let jb: *u8 = sys_mmap(TA_MAGIC_4096) 1470 var b: i64 = ta_cat(jb, 0, "{\"access_token\":\"" as *u8) 1471 b = ta_catb(jb, b, atok, atn) 1472 b = ta_cat(jb, b, "\",\"token_type\":\"Bearer\",\"expires_in\":3600,\"scope\":\"" as *u8) 1473 b = ta_catb(jb, b, scope, scln) 1474 b = ta_cat(jb, b, "\"}" as *u8) 1475 return oauth_resp(out, "200 OK" as *u8, "application/json" as *u8, jb, b) 1476} 1477 1478func ta_handle_pfx(prefix: *u8, req: *u8, req_n: i64, out: *u8) -> i64 { 1479 let mo: *i64 = sys_mmap(16) as *i64 1480 let po: *i64 = sys_mmap(16) as *i64 1481 if ta_reqline(req, req_n, mo, po) == 0 { return ta_emit_lit(out, "HTTP/1.1 400 Bad Request" as *u8, "{\"error\":\"bad_request_line\"}" as *u8) } 1482 let path: *u8 = ((req as i64) + po[0]) as *u8 1483 var plen: i64 = po[1] 1484 var qi: i64 = 0 1485 var qfound: i64 = 0 - 1 1486 while qi < po[1] { if qfound < 0 { if path[qi] == (63 as u8) { qfound = qi } } qi = qi + 1 } 1487 if qfound >= 0 { plen = qfound } 1488 let mp: *u8 = ((req as i64) + mo[0]) as *u8 1489 let ml: i64 = mo[1] 1490 let is_get: i64 = ta_streq_n(mp, ml, "GET" as *u8) 1491 let is_post: i64 = ta_streq_n(mp, ml, "POST" as *u8) 1492 if ta_streq_n(path, plen, "/api/tools" as *u8) == 1 { if is_get == 1 { return ta_tools(prefix, out) } } 1493 if ta_streq_n(path, plen, "/tools" as *u8) == 1 { if is_get == 1 { return ta_html_tools(out) } } 1494 if ta_streq_n(path, plen, "/mcp" as *u8) == 1 { if is_post == 1 { return ta_mcp(prefix, req, req_n, out) } if is_get == 1 { return ta_index(out) } } 1495 if ta_streq_n(path, plen, "/api/cap/issue" as *u8) == 1 { if is_post == 1 { return ta_cap_issue(req, req_n, out) } } 1496 if ta_streq_n(path, plen, "/api/cap/revoke" as *u8) == 1 { if is_post == 1 { return ta_cap_revoke(req, req_n, out) } } 1497 if ta_streq_n(path, plen, "/api/cap/status" as *u8) == 1 { if is_get == 1 { return ta_cap_status(out) } } 1498 if ta_streq_n(path, plen, "/api/cap/consent-log" as *u8) == 1 { if is_get == 1 { return ta_cap_consent_log(out) } } 1499 if ta_streq_n(path, plen, "/.well-known/oauth-protected-resource" as *u8) == 1 { if is_get == 1 { return ta_oauth_prm(out) } } 1500 if ta_streq_n(path, plen, "/.well-known/oauth-authorization-server" as *u8) == 1 { if is_get == 1 { return ta_oauth_asmeta(out) } } 1501 if ta_streq_n(path, plen, "/oauth/register" as *u8) == 1 { if is_post == 1 { return ta_oauth_register(req, req_n, out) } } 1502 if ta_streq_n(path, plen, "/oauth/authorize" as *u8) == 1 { if is_get == 1 { return ta_oauth_authorize(req, req_n, out) } } 1503 if ta_streq_n(path, plen, "/oauth/approve" as *u8) == 1 { if is_post == 1 { return ta_oauth_approve(req, req_n, out) } } 1504 if ta_streq_n(path, plen, "/oauth/token" as *u8) == 1 { if is_post == 1 { return ta_oauth_token(req, req_n, out) } } 1505 if ta_streq_n(path, plen, "/api" as *u8) == 1 { if is_get == 1 { return ta_index(out) } } 1506 if ta_streq_n(path, plen, "/" as *u8) == 1 { if is_get == 1 { return ta_index(out) } } 1507 return ta_emit_lit(out, "HTTP/1.1 404 Not Found" as *u8, "{\"error\":\"not_found\"}" as *u8) 1508} 1509// production-prefix convenience wrapper (the real registry). 1510func ta_handle(req: *u8, req_n: i64, out: *u8) -> i64 { return ta_handle_pfx(TA_TOOLPFX, req, req_n, out) }