code wiki / (root) / nx_tools_api_exec_gate.nx

nx_tools_api_exec_gate.nx source

↩ module page · 256 lines · 16447 B

1// nx_tools_api_exec_gate.nx -- GATE for R2: the /mcp tools/call STUB is now REAL execution. Drives the live 2// pure router ta_handle_pfx IN-PROCESS (no socket) and proves, end-to-end, that a capability-authorized 3// tools/call for a GREEN-allowlisted tool actually FORKS the organ and returns its real stdout -- with the 4// two never-brick negative controls that make this safe: (T2) no capability -> JSON-RPC -32001 (nothing 5// runs), (T3) a valid capability for a tool that is NOT on the GREEN execution allowlist -> tool-level error, 6// NOT executed. Composes R0 (nx_tool_run) + R1 (nx_tool_exec_allow) + the cap-token ocap layer. 7// 8// PREREQ: build nx_tool_ping FIRST (produces _offc/nx_tool_ping.elf, the allowlisted target). This gate 9// writes the real allowlist file (tool_allowlist.conf) in the server CWD, since ta_mcp_call resolves through 10// the production TEA_CONF path -- proving the ACTUAL wired path, not a test shim. 11// license_tier: ORIGINAL expect_exit: 0 12import "nx_tools_api.nx" // ta_handle_pfx + tea_* + capt_* (transitive) + sys_* 13import "nx_gate.nx" // gw / gn 14 15const EG_CONF: *u8 = "tool_allowlist.conf" as *u8 16const EG_PINGELF: *u8 = "_offc/nx_tool_ping.elf" as *u8 17 18func eg_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 } 19func eg_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 } 20func eg_has(out: *u8, n: i64, needle: *u8) -> i64 { if ta_indexof(out, n, needle) >= 0 { return 1 } return 0 } 21func eg_write_file(path: *u8, content: *u8) -> i64 { 22 let fd: i64 = __syscall(257, 0 - 100, path, 0x241, 0x1a4, 0, 0) // O_WRONLY|O_CREAT|O_TRUNC, 0644 23 if fd < 0 { return 0 - 1 } 24 var n: i64 = 0; while content[n] != (0 as u8) { n = n + 1 } 25 sys_write(fd, content, n); sys_close(fd) 26 return 0 27} 28 29// write "ping\t_offc/nx_tool_ping.elf\tGREEN\n" (+ a blocked row + a comment) to the real allowlist file. 30func eg_write_conf() -> i64 { 31 let fd: i64 = __syscall(257, 0 - 100, EG_CONF, 0x241, 0x1a4, 0, 0) // O_WRONLY|O_CREAT|O_TRUNC, 0644 32 if fd < 0 { return 0 - 1 } 33 let body: *u8 = "# nx execution allowlist (written by nx_tools_api_exec_gate)\nping\t_offc/nx_tool_ping.elf\tGREEN\nargecho\t_offc/nx_tool_argecho.elf\tGREEN\nnx_http_probe\t_offc/nx_http_probe.elf\tRED\n" as *u8 34 var n: i64 = 0; while body[n]!=(0 as u8){n=n+1} 35 sys_write(fd, body, n); sys_close(fd) 36 return 0 37} 38 39// build a POST /mcp tools/call request with params.name = `tool` and params._cap = `tok` (tlen bytes). Returns len. 40func eg_build_call(req: *u8, tool: *u8, tok: *u8, tlen: i64) -> i64 { 41 var o: i64 = eg_cat(req, 0, "POST /mcp HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n\r\n" as *u8) 42 o = eg_cat(req, o, "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"" as *u8) 43 o = eg_cat(req, o, tool) 44 o = eg_cat(req, o, "\",\"_cap\":\"" as *u8) 45 o = eg_catb(req, o, tok, tlen) 46 o = eg_cat(req, o, "\"}}" as *u8) 47 return o 48} 49 50// like eg_build_call but adds params.arguments.argv = `argvj` (a JSON string-array literal), to drive the 51// multi-arg tools/call path (params.arguments.argv -> native argv -> child). 52func eg_build_call_argv(req: *u8, tool: *u8, tok: *u8, tlen: i64, argvj: *u8) -> i64 { 53 var o: i64 = eg_cat(req, 0, "POST /mcp HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n\r\n" as *u8) 54 o = eg_cat(req, o, "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"" as *u8) 55 o = eg_cat(req, o, tool) 56 o = eg_cat(req, o, "\",\"_cap\":\"" as *u8) 57 o = eg_catb(req, o, tok, tlen) 58 o = eg_cat(req, o, "\",\"arguments\":{\"argv\":" as *u8) 59 o = eg_cat(req, o, argvj) 60 o = eg_cat(req, o, "}}}" as *u8) 61 return o 62} 63 64// like eg_build_call but presents the cap via the OAuth `Authorization: Bearer <cap>` HEADER (no _cap/X-Nishi-Cap) -- 65// exercises the stock-MCP-client interop path. 66func eg_build_call_bearer(req: *u8, tool: *u8, tok: *u8, tlen: i64) -> i64 { 67 var o: i64 = eg_cat(req, 0, "POST /mcp HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nAuthorization: Bearer " as *u8) 68 o = eg_catb(req, o, tok, tlen) 69 o = eg_cat(req, o, "\r\n\r\n" as *u8) 70 o = eg_cat(req, o, "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\",\"params\":{\"name\":\"" as *u8) 71 o = eg_cat(req, o, tool) 72 o = eg_cat(req, o, "\"}}" as *u8) 73 return o 74} 75 76func main() -> i64 { 77 gw("=== nx_tools_api_exec_gate (R2: /mcp tools/call is REAL execution, cap+allowlist gated) ===\n" as *u8) 78 let TP: *u8 = "knowledge/toolreg-test-exec-" as *u8 79 tool_register_pfx(TP, "ping" as *u8, "no-op smoke-test organ" as *u8, "ping" as *u8, "gate-proven" as *u8) 80 tool_register_pfx(TP, "nx_http_probe" as *u8, "sovereign HTTP GET probe" as *u8, "nx_http_probe <path>" as *u8, "gate-proven" as *u8) 81 tool_register_pfx(TP, "argecho" as *u8, "multi-arg witness organ (echoes argv[1..])" as *u8, "argecho <args...>" as *u8, "gate-proven" as *u8) 82 if eg_write_conf() != 0 { gw("FAIL: could not write tool_allowlist.conf\n" as *u8); sys_exit(1); return 1 } 83 84 let secret: *u8 = TA_CAP_SECRET 85 let slen: i64 = ta_slen(TA_CAP_SECRET) 86 let now: i64 = sys_now_realtime_sec() 87 let exp: i64 = now + 3600 88 let out: *u8 = sys_mmap(1048576) 89 let req: *u8 = sys_mmap(4096) 90 let tok: *u8 = sys_mmap(1024) 91 92 var pass: i64 = 0 93 var tot: i64 = 0 94 95 // T1: valid cap for "ping" + ping is GREEN-allowlisted -> EXECUTES, real stdout returned, isError:false 96 tot = tot + 1 97 let tl1: i64 = capt_issue(secret, slen, "ping" as *u8, 4, exp, 1001, tok, 1024) 98 let rn1: i64 = eg_build_call(req, "ping" as *u8, tok, tl1) 99 let on1: i64 = ta_handle_pfx(TP, req, rn1, out) 100 var t1: i64 = 0 101 if eg_has(out, on1, "NX_TOOL_PING_OK" as *u8) == 1 { if eg_has(out, on1, "\"isError\":false" as *u8) == 1 { if eg_has(out, on1, "\"exit_code\":0" as *u8) == 1 { t1 = 1 } } } 102 if t1 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 103 gw("T1 cap-authorized + GREEN-allowlisted 'ping' -> real fork+capture: 'NX_TOOL_PING_OK', exit_code 0\n" as *u8) 104 105 // T2 NEG (no ambient authority): tools/call WITHOUT any capability -> -32001, nothing executed 106 tot = tot + 1 107 var o2: i64 = eg_cat(req, 0, "POST /mcp HTTP/1.1\r\nHost: x\r\n\r\n" as *u8) 108 o2 = eg_cat(req, o2, "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\",\"params\":{\"name\":\"ping\"}}" as *u8) 109 let on2: i64 = ta_handle_pfx(TP, req, o2, out) 110 var t2: i64 = 0 111 if eg_has(out, on2, "-32001" as *u8) == 1 { if eg_has(out, on2, "NX_TOOL_PING_OK" as *u8) == 0 { t2 = 1 } } 112 if t2 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 113 gw("T2 no capability -> -32001, organ NOT executed (no ambient authority)\n" as *u8) 114 115 // T3 NEG (fail-closed allowlist): valid cap for 'nx_http_probe' but its row is RED -> tool error, not run 116 tot = tot + 1 117 let tl3: i64 = capt_issue(secret, slen, "nx_http_probe" as *u8, 13, exp, 1003, tok, 1024) 118 let rn3: i64 = eg_build_call(req, "nx_http_probe" as *u8, tok, tl3) 119 let on3: i64 = ta_handle_pfx(TP, req, rn3, out) 120 var t3: i64 = 0 121 if eg_has(out, on3, "not on the GREEN execution allowlist" as *u8) == 1 { if eg_has(out, on3, "\"isError\":true" as *u8) == 1 { t3 = 1 } } 122 if t3 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 123 gw("T3 cap-valid but non-GREEN allowlist row -> tool error, NOT executed (defense in depth)\n" as *u8) 124 125 // T4: MULTI-ARG tools/call -- params.arguments.argv is parsed into a native argv and delivered to the child 126 // as real argv[1..]. Proves the FULL arg-passing path end-to-end: parse -> cap-verify -> tea_run_argv -> 127 // resolve -> fork+execve(argecho, [path,alpha,bravo,charlie]) -> child echoes them -> captured. This is the 128 // exceed that makes multi-arg organs (e.g. nx_mgmt_client <url> call GET <path> <tok>) callable over MCP. 129 tot = tot + 1 130 let tl4: i64 = capt_issue(secret, slen, "argecho" as *u8, 7, exp, 1004, tok, 1024) 131 let rn4: i64 = eg_build_call_argv(req, "argecho" as *u8, tok, tl4, "[\"alpha\",\"bravo\",\"charlie\"]" as *u8) 132 let on4: i64 = ta_handle_pfx(TP, req, rn4, out) 133 var t4: i64 = 0 134 if eg_has(out, on4, "NX_TOOL_ARGECHO_OK" as *u8) == 1 { if eg_has(out, on4, "alpha" as *u8) == 1 { if eg_has(out, on4, "bravo" as *u8) == 1 { if eg_has(out, on4, "charlie" as *u8) == 1 { if eg_has(out, on4, "\"isError\":false" as *u8) == 1 { t4 = 1 } } } } } 135 if t4 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 136 gw("T4 multi-arg tools/call: params.arguments.argv -> child argv (alpha/bravo/charlie echoed) -- arg-passing exceed\n" as *u8) 137 138 // T5: GET /api/cap/status is PUBLIC (no cap) and honestly reports the INSECURE placeholder when no keyfile is 139 // provisioned (the gate CWD has none) -- the bootstrap self-check that prevents a blind mint against a stale key. 140 tot = tot + 1 141 let o5: i64 = eg_cat(req, 0, "GET /api/cap/status HTTP/1.1\r\nHost: x\r\n\r\n" as *u8) 142 let on5: i64 = ta_handle_pfx(TP, req, o5, out) 143 var t5: i64 = 0 144 if eg_has(out, on5, "200 OK" as *u8) == 1 { if eg_has(out, on5, "insecure_placeholder" as *u8) == 1 { if eg_has(out, on5, "\"secure\":false" as *u8) == 1 { t5 = 1 } } } 145 if t5 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 146 gw("T5 GET /api/cap/status (public) honestly reports insecure_placeholder when no keyfile is provisioned\n" as *u8) 147 148 // T6: ta_cap_provisioned detection -- real keyfile -> secure; absent OR a file literally holding the placeholder 149 // -> insecure. Proves the status readout flips correctly once nx_cap_keygen writes a real key on the server. 150 let KP: *u8 = "/tmp/eg_capkey.key" as *u8 151 __syscall(87, KP, 0, 0, 0, 0, 0) // ensure absent 152 tot = tot + 1 153 var t6: i64 = 0 154 if ta_cap_provisioned_from(KP) == 0 { 155 eg_write_file(KP, "a-real-256-bit-secret-not-the-placeholder-000000" as *u8) 156 if ta_cap_provisioned_from(KP) == 1 { 157 eg_write_file(KP, TA_CAP_SECRET) 158 if ta_cap_provisioned_from(KP) == 0 { t6 = 1 } 159 } 160 } 161 __syscall(87, KP, 0, 0, 0, 0, 0) // cleanup 162 if t6 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 163 gw("T6 ta_cap_provisioned: real keyfile -> secure; absent/placeholder-in-file -> insecure (status flips on provisioning)\n" as *u8) 164 165 // T7: OAuth 2.1 interop -- present the SAME ocap via `Authorization: Bearer <cap>` (not X-Nishi-Cap/_cap) -> EXECUTES. 166 tot = tot + 1 167 let tl7: i64 = capt_issue(secret, slen, "ping" as *u8, 4, exp, 1007, tok, 1024) 168 let rn7: i64 = eg_build_call_bearer(req, "ping" as *u8, tok, tl7) 169 let on7: i64 = ta_handle_pfx(TP, req, rn7, out) 170 var t7: i64 = 0 171 if eg_has(out, on7, "NX_TOOL_PING_OK" as *u8) == 1 { if eg_has(out, on7, "\"isError\":false" as *u8) == 1 { t7 = 1 } } 172 if t7 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 173 gw("T7 OAuth Bearer: 'Authorization: Bearer <cap>' authorizes tools/call (stock-MCP-client interop)\n" as *u8) 174 175 // T8: RFC 9728 discovery -- GET /.well-known/oauth-protected-resource advertises bearer-in-header auth + the AS. 176 tot = tot + 1 177 let o8: i64 = eg_cat(req, 0, "GET /.well-known/oauth-protected-resource HTTP/1.1\r\nHost: x\r\n\r\n" as *u8) 178 let on8: i64 = ta_handle_pfx(TP, req, o8, out) 179 var t8: i64 = 0 180 if eg_has(out, on8, "200 OK" as *u8) == 1 { if eg_has(out, on8, "bearer_methods_supported" as *u8) == 1 { if eg_has(out, on8, "authorization_servers" as *u8) == 1 { t8 = 1 } } } 181 if t8 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 182 gw("T8 RFC9728 GET /.well-known/oauth-protected-resource advertises bearer-in-header auth\n" as *u8) 183 184 // T9 (R4 per-client consent): delegate a NARROWER cap WITH a client id -> attenuated cap + consent_recorded. 185 tot = tot + 1 186 let ptl: i64 = capt_issue(secret, slen, "ping,argecho" as *u8, 12, exp, 1009, tok, 1024) 187 var o9: i64 = eg_cat(req, 0, "POST /api/cap/issue HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n\r\n" as *u8) 188 o9 = eg_cat(req, o9, "{\"_cap\":\"" as *u8) 189 o9 = eg_catb(req, o9, tok, ptl) 190 o9 = eg_cat(req, o9, "\",\"allow\":\"ping\",\"nonce\":42,\"client\":\"gate-test-client\"}" as *u8) 191 let on9: i64 = ta_handle_pfx(TP, req, o9, out) 192 var t9r: i64 = 0 193 if eg_has(out, on9, "\"consent_recorded\":true" as *u8) == 1 { if eg_has(out, on9, "\"cap\":\"" as *u8) == 1 { t9r = 1 } } 194 if t9r == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 195 gw("T9 R4 per-client consent: delegate w/ client id -> attenuated cap + consent_recorded\n" as *u8) 196 197 // T10 (R4): the append-only consent ledger records the client id (auditable). 198 tot = tot + 1 199 let o10: i64 = eg_cat(req, 0, "GET /api/cap/consent-log HTTP/1.1\r\nHost: x\r\n\r\n" as *u8) 200 let on10: i64 = ta_handle_pfx(TP, req, o10, out) 201 var t10: i64 = 0 202 if eg_has(out, on10, "gate-test-client" as *u8) == 1 { t10 = 1 } 203 if t10 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 204 gw("T10 R4 consent ledger: GET /api/cap/consent-log records the client id (auditable)\n" as *u8) 205 206 // T11 (rung 24): ASYNC JOB LANE -- "_async":"1" returns JOB-STARTED 207 // immediately; the detached worker lands .out then the ATOMIC .done 208 // marker (tmp+rename, written LAST); the echoed argv proves the worker 209 // ran the same cap+allowlist-gated pinned dispatch the sync lane runs. 210 tot = tot + 1 211 let tl11: i64 = capt_issue(secret, slen, "argecho" as *u8, 7, exp, 1011, tok, 1024) 212 var o11: i64 = eg_cat(req, 0, "POST /mcp HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n\r\n" as *u8) 213 o11 = eg_cat(req, o11, "{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"tools/call\",\"params\":{\"name\":\"argecho\",\"_cap\":\"" as *u8) 214 o11 = eg_catb(req, o11, tok, tl11) 215 o11 = eg_cat(req, o11, "\",\"arguments\":{\"argv\":[\"async_alpha\",\"async_bravo\"],\"_async\":\"1\"}}}" as *u8) 216 let on11: i64 = ta_handle_pfx(TP, req, o11, out) 217 var t11: i64 = 0 218 if eg_has(out, on11, "JOB-STARTED id=" as *u8) == 1 { if eg_has(out, on11, "\"state\":\"RUNNING\"" as *u8) == 1 { 219 let idp: i64 = ta_indexof(out, on11, "JOB-STARTED id=" as *u8) + 15 220 let dpath: *u8 = sys_mmap(256) 221 let opath: *u8 = sys_mmap(256) 222 var dq: i64 = eg_cat(dpath, 0, "_jobs/job_" as *u8) 223 var oq: i64 = eg_cat(opath, 0, "_jobs/job_" as *u8) 224 var di: i64 = idp 225 var digo: i64 = 1 226 while digo == 1 { 227 let dc: i64 = out[di] as i64 228 if dc >= 48 { if dc <= 57 { dpath[dq] = out[di]; opath[oq] = out[di]; dq = dq + 1; oq = oq + 1; di = di + 1 } else { digo = 0 } } else { digo = 0 } 229 } 230 dq = eg_cat(dpath, dq, ".done" as *u8) 231 oq = eg_cat(opath, oq, ".out" as *u8) 232 dpath[dq] = 0 as u8 233 opath[oq] = 0 as u8 234 var polls: i64 = 0 235 var seen: i64 = 0 236 while polls < 100 { 237 let dfd: i64 = sys_openat_rd(dpath) 238 if dfd >= 0 { sys_close(dfd); seen = 1; polls = 100 } else { sys_sleep_ms(50); polls = polls + 1 } 239 } 240 if seen == 1 { 241 let dl: *i64 = sys_mmap(16) as *i64 242 let db2: *u8 = sys_read_file(dpath, dl) 243 let ol: *i64 = sys_mmap(16) as *i64 244 let ob2: *u8 = sys_read_file(opath, ol) 245 if eg_has(db2, dl[0], "exit=0" as *u8) == 1 { if eg_has(ob2, ol[0], "async_alpha" as *u8) == 1 { if eg_has(ob2, ol[0], "async_bravo" as *u8) == 1 { t11 = 1 } } } 246 } 247 __syscall(87, dpath, 0, 0, 0, 0, 0) 248 __syscall(87, opath, 0, 0, 0, 0, 0) 249 } } 250 if t11 == 1 { pass = pass + 1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) } 251 gw("T11 async job: JOB-STARTED immediate, detached worker lands .out + atomic .done, argv echoed\n" as *u8) 252 253 gw("\n=== nx_tools_api_exec_gate " as *u8); gn(pass); gw("/" as *u8); gn(tot); gw(" ===\n" as *u8) 254 if pass == tot { gw("TOOLS-API-EXEC GREEN -- /mcp tools/call now RUNS vetted organs and returns real output; the stub is gone, ocap + allowlist both enforced\n" as *u8); sys_exit(0); return 0 } 255 gw("TOOLS-API-EXEC RED\n" as *u8); sys_exit(1); return 1 256}