palaryn 0.1.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/anomaly/detector.d.ts +7 -4
  5. package/dist/src/anomaly/detector.d.ts.map +1 -1
  6. package/dist/src/anomaly/detector.js +22 -12
  7. package/dist/src/anomaly/detector.js.map +1 -1
  8. package/dist/src/audit/logger.d.ts +10 -0
  9. package/dist/src/audit/logger.d.ts.map +1 -1
  10. package/dist/src/audit/logger.js +52 -38
  11. package/dist/src/audit/logger.js.map +1 -1
  12. package/dist/src/auth/routes.d.ts.map +1 -1
  13. package/dist/src/auth/routes.js +35 -0
  14. package/dist/src/auth/routes.js.map +1 -1
  15. package/dist/src/budget/manager.d.ts +5 -0
  16. package/dist/src/budget/manager.d.ts.map +1 -1
  17. package/dist/src/budget/manager.js +32 -0
  18. package/dist/src/budget/manager.js.map +1 -1
  19. package/dist/src/budget/model-pricing.d.ts +20 -0
  20. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  21. package/dist/src/budget/model-pricing.js +107 -0
  22. package/dist/src/budget/model-pricing.js.map +1 -0
  23. package/dist/src/budget/usage-extractor.d.ts +3 -1
  24. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  25. package/dist/src/budget/usage-extractor.js +47 -3
  26. package/dist/src/budget/usage-extractor.js.map +1 -1
  27. package/dist/src/config/defaults.d.ts.map +1 -1
  28. package/dist/src/config/defaults.js +65 -13
  29. package/dist/src/config/defaults.js.map +1 -1
  30. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  31. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  32. package/dist/src/dlp/tool-patterns.js +34 -0
  33. package/dist/src/dlp/tool-patterns.js.map +1 -0
  34. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  35. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  36. package/dist/src/executor/filesystem-executor.js +192 -0
  37. package/dist/src/executor/filesystem-executor.js.map +1 -0
  38. package/dist/src/executor/http-executor.d.ts.map +1 -1
  39. package/dist/src/executor/http-executor.js +22 -2
  40. package/dist/src/executor/http-executor.js.map +1 -1
  41. package/dist/src/executor/index.d.ts +4 -0
  42. package/dist/src/executor/index.d.ts.map +1 -1
  43. package/dist/src/executor/index.js +9 -1
  44. package/dist/src/executor/index.js.map +1 -1
  45. package/dist/src/executor/shell-executor.d.ts +22 -0
  46. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  47. package/dist/src/executor/shell-executor.js +119 -0
  48. package/dist/src/executor/shell-executor.js.map +1 -0
  49. package/dist/src/executor/sql-executor.d.ts +29 -0
  50. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  51. package/dist/src/executor/sql-executor.js +114 -0
  52. package/dist/src/executor/sql-executor.js.map +1 -0
  53. package/dist/src/executor/websocket-executor.d.ts +26 -0
  54. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  55. package/dist/src/executor/websocket-executor.js +205 -0
  56. package/dist/src/executor/websocket-executor.js.map +1 -0
  57. package/dist/src/interceptor/index.d.ts +2 -0
  58. package/dist/src/interceptor/index.d.ts.map +1 -0
  59. package/dist/src/interceptor/index.js +6 -0
  60. package/dist/src/interceptor/index.js.map +1 -0
  61. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  62. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  63. package/dist/src/interceptor/provider-interceptor.js +302 -0
  64. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  65. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  66. package/dist/src/mcp/auth-verifier.js +3 -2
  67. package/dist/src/mcp/auth-verifier.js.map +1 -1
  68. package/dist/src/mcp/bridge.d.ts +14 -10
  69. package/dist/src/mcp/bridge.d.ts.map +1 -1
  70. package/dist/src/mcp/bridge.js +51 -227
  71. package/dist/src/mcp/bridge.js.map +1 -1
  72. package/dist/src/mcp/http-transport.d.ts +2 -0
  73. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  74. package/dist/src/mcp/http-transport.js +117 -66
  75. package/dist/src/mcp/http-transport.js.map +1 -1
  76. package/dist/src/mcp/internal-auth.d.ts +13 -0
  77. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  78. package/dist/src/mcp/internal-auth.js +12 -0
  79. package/dist/src/mcp/internal-auth.js.map +1 -0
  80. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  81. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  82. package/dist/src/mcp/tool-definitions.js +491 -0
  83. package/dist/src/mcp/tool-definitions.js.map +1 -0
  84. package/dist/src/middleware/auth.js.map +1 -1
  85. package/dist/src/middleware/session.js.map +1 -1
  86. package/dist/src/middleware/validate.d.ts +8 -0
  87. package/dist/src/middleware/validate.d.ts.map +1 -1
  88. package/dist/src/middleware/validate.js +45 -0
  89. package/dist/src/middleware/validate.js.map +1 -1
  90. package/dist/src/policy/engine.d.ts +4 -0
  91. package/dist/src/policy/engine.d.ts.map +1 -1
  92. package/dist/src/policy/engine.js +117 -0
  93. package/dist/src/policy/engine.js.map +1 -1
  94. package/dist/src/saas/routes.d.ts.map +1 -1
  95. package/dist/src/saas/routes.js +355 -22
  96. package/dist/src/saas/routes.js.map +1 -1
  97. package/dist/src/server/app.d.ts.map +1 -1
  98. package/dist/src/server/app.js +24 -3
  99. package/dist/src/server/app.js.map +1 -1
  100. package/dist/src/server/gateway.d.ts.map +1 -1
  101. package/dist/src/server/gateway.js +17 -0
  102. package/dist/src/server/gateway.js.map +1 -1
  103. package/dist/src/server/index.d.ts.map +1 -1
  104. package/dist/src/server/index.js +18 -0
  105. package/dist/src/server/index.js.map +1 -1
  106. package/dist/src/storage/interfaces.d.ts +14 -3
  107. package/dist/src/storage/interfaces.d.ts.map +1 -1
  108. package/dist/src/storage/memory.d.ts +2 -0
  109. package/dist/src/storage/memory.d.ts.map +1 -1
  110. package/dist/src/storage/memory.js +6 -0
  111. package/dist/src/storage/memory.js.map +1 -1
  112. package/dist/src/storage/postgres.d.ts +5 -0
  113. package/dist/src/storage/postgres.d.ts.map +1 -1
  114. package/dist/src/storage/postgres.js +16 -0
  115. package/dist/src/storage/postgres.js.map +1 -1
  116. package/dist/src/storage/redis.d.ts +10 -0
  117. package/dist/src/storage/redis.d.ts.map +1 -1
  118. package/dist/src/storage/redis.js +65 -0
  119. package/dist/src/storage/redis.js.map +1 -1
  120. package/dist/src/types/budget.d.ts +4 -0
  121. package/dist/src/types/budget.d.ts.map +1 -1
  122. package/dist/src/types/config.d.ts +58 -0
  123. package/dist/src/types/config.d.ts.map +1 -1
  124. package/dist/src/types/events.d.ts +1 -0
  125. package/dist/src/types/events.d.ts.map +1 -1
  126. package/dist/src/types/policy.d.ts +11 -1
  127. package/dist/src/types/policy.d.ts.map +1 -1
  128. package/dist/src/types/tool-result.d.ts +11 -0
  129. package/dist/src/types/tool-result.d.ts.map +1 -1
  130. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  131. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  132. package/dist/tests/unit/app-routes.test.js +715 -0
  133. package/dist/tests/unit/app-routes.test.js.map +1 -0
  134. package/dist/tests/unit/audit-logger.test.js +105 -0
  135. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  136. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  137. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  138. package/dist/tests/unit/auth-providers.test.js +279 -0
  139. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  140. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  141. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  142. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  143. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  144. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  145. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  146. package/dist/tests/unit/auth-verifier.test.js +505 -0
  147. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  148. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  149. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  150. package/dist/tests/unit/billing-routes.test.js +432 -0
  151. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  152. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  153. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  154. package/dist/tests/unit/config-defaults.test.js +119 -0
  155. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  156. package/dist/tests/unit/defaults.test.js +0 -10
  157. package/dist/tests/unit/defaults.test.js.map +1 -1
  158. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  159. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  160. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  161. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  162. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  163. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  164. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  165. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  166. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  167. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  168. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  169. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  170. package/dist/tests/unit/logger.test.d.ts +2 -0
  171. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  172. package/dist/tests/unit/logger.test.js +97 -0
  173. package/dist/tests/unit/logger.test.js.map +1 -0
  174. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  175. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  176. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  177. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  178. package/dist/tests/unit/metrics.test.js +102 -0
  179. package/dist/tests/unit/metrics.test.js.map +1 -1
  180. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  181. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  182. package/dist/tests/unit/model-pricing.test.js +87 -0
  183. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  184. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  185. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  186. package/dist/tests/unit/oauth-stores.test.js +260 -0
  187. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  188. package/dist/tests/unit/policy-engine.test.js +466 -0
  189. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  190. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  191. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  192. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  193. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  194. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  195. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  196. package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
  197. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  198. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  199. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  200. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  201. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  202. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  203. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  204. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  205. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  206. package/dist/tests/unit/saas-routes.test.js +3 -3
  207. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  208. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  209. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  210. package/dist/tests/unit/shell-executor.test.js +145 -0
  211. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  212. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  213. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  214. package/dist/tests/unit/sql-executor.test.js +177 -0
  215. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  216. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  217. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  218. package/dist/tests/unit/stream-proxy.test.js +147 -0
  219. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  220. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  221. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  222. package/dist/tests/unit/tool-definitions.test.js +184 -0
  223. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  224. package/dist/tests/unit/usage-extractor.test.js +140 -0
  225. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  226. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  227. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  228. package/dist/tests/unit/webhook-handler.test.js +453 -0
  229. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  230. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  231. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  232. package/dist/tests/unit/webhook-routes.test.js +69 -0
  233. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  234. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  235. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  236. package/dist/tests/unit/websocket-executor.test.js +121 -0
  237. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  238. package/package.json +8 -2
  239. package/policy-packs/demo_fail.yaml +41 -0
  240. package/policy-packs/full_tools.yaml +136 -0
  241. package/src/admin/index.ts +1 -0
  242. package/src/admin/routes.ts +509 -0
  243. package/src/admin/templates.ts +572 -0
  244. package/src/anomaly/detector.ts +730 -0
  245. package/src/anomaly/index.ts +1 -0
  246. package/src/approval/manager.ts +569 -0
  247. package/src/approval/webhook.ts +133 -0
  248. package/src/audit/logger.ts +490 -0
  249. package/src/auth/index.ts +5 -0
  250. package/src/auth/password.ts +21 -0
  251. package/src/auth/pkce.ts +22 -0
  252. package/src/auth/providers.ts +208 -0
  253. package/src/auth/routes.ts +561 -0
  254. package/src/auth/session.ts +84 -0
  255. package/src/billing/index.ts +6 -0
  256. package/src/billing/plan-enforcer.ts +135 -0
  257. package/src/billing/routes.ts +229 -0
  258. package/src/billing/stripe-client.ts +58 -0
  259. package/src/billing/webhook-handler.ts +182 -0
  260. package/src/billing/webhook-routes.ts +28 -0
  261. package/src/budget/manager.ts +679 -0
  262. package/src/budget/model-pricing.ts +119 -0
  263. package/src/budget/usage-extractor.ts +214 -0
  264. package/src/cli.ts +91 -0
  265. package/src/config/defaults.ts +261 -0
  266. package/src/config/validate.ts +88 -0
  267. package/src/dlp/composite-scanner.ts +213 -0
  268. package/src/dlp/index.ts +9 -0
  269. package/src/dlp/interfaces.ts +34 -0
  270. package/src/dlp/patterns.ts +30 -0
  271. package/src/dlp/prompt-injection-backend.ts +181 -0
  272. package/src/dlp/prompt-injection-patterns.ts +302 -0
  273. package/src/dlp/regex-backend.ts +181 -0
  274. package/src/dlp/scanner.ts +502 -0
  275. package/src/dlp/text-normalizer.ts +225 -0
  276. package/src/dlp/tool-patterns.ts +35 -0
  277. package/src/dlp/trufflehog-backend.ts +190 -0
  278. package/src/executor/filesystem-executor.ts +196 -0
  279. package/src/executor/http-executor.ts +349 -0
  280. package/src/executor/index.ts +9 -0
  281. package/src/executor/interfaces.ts +11 -0
  282. package/src/executor/noop-executor.ts +23 -0
  283. package/src/executor/registry.ts +64 -0
  284. package/src/executor/shell-executor.ts +148 -0
  285. package/src/executor/slack-executor.ts +176 -0
  286. package/src/executor/sql-executor.ts +146 -0
  287. package/src/executor/websocket-executor.ts +211 -0
  288. package/src/index.ts +24 -0
  289. package/src/interceptor/index.ts +1 -0
  290. package/src/interceptor/provider-interceptor.ts +315 -0
  291. package/src/mcp/auth-verifier.ts +152 -0
  292. package/src/mcp/bridge.ts +703 -0
  293. package/src/mcp/http-transport.ts +698 -0
  294. package/src/mcp/index.ts +9 -0
  295. package/src/mcp/internal-auth.ts +14 -0
  296. package/src/mcp/oauth-pages.ts +139 -0
  297. package/src/mcp/oauth-postgres-stores.ts +278 -0
  298. package/src/mcp/oauth-provider.ts +536 -0
  299. package/src/mcp/oauth-stores.ts +202 -0
  300. package/src/mcp/server.ts +55 -0
  301. package/src/mcp/tool-definitions.ts +562 -0
  302. package/src/metrics/collector.ts +357 -0
  303. package/src/metrics/index.ts +1 -0
  304. package/src/middleware/auth.ts +814 -0
  305. package/src/middleware/session.ts +85 -0
  306. package/src/middleware/validate.ts +130 -0
  307. package/src/policy/engine.ts +815 -0
  308. package/src/policy/index.ts +2 -0
  309. package/src/policy/opa-engine.ts +829 -0
  310. package/src/proxy/forward-proxy.ts +649 -0
  311. package/src/proxy/index.ts +1 -0
  312. package/src/ratelimit/limiter.ts +196 -0
  313. package/src/replay/engine.ts +142 -0
  314. package/src/replay/index.ts +1 -0
  315. package/src/saas/index.ts +1 -0
  316. package/src/saas/routes.ts +2178 -0
  317. package/src/server/app.ts +985 -0
  318. package/src/server/errors.ts +49 -0
  319. package/src/server/gateway.ts +1130 -0
  320. package/src/server/index.ts +307 -0
  321. package/src/server/logger.ts +255 -0
  322. package/src/server/stream-proxy.ts +202 -0
  323. package/src/storage/file-persistence.ts +315 -0
  324. package/src/storage/index.ts +4 -0
  325. package/src/storage/interfaces.ts +287 -0
  326. package/src/storage/memory.ts +686 -0
  327. package/src/storage/postgres.ts +1831 -0
  328. package/src/storage/redis.ts +835 -0
  329. package/src/tracing/index.ts +1 -0
  330. package/src/tracing/provider.ts +100 -0
  331. package/src/trust/calculator.ts +141 -0
  332. package/src/trust/index.ts +7 -0
  333. package/src/types/budget.ts +36 -0
  334. package/src/types/config.ts +278 -0
  335. package/src/types/events.ts +41 -0
  336. package/src/types/express.d.ts +14 -0
  337. package/src/types/index.ts +7 -0
  338. package/src/types/policy.ts +83 -0
  339. package/src/types/stripe-config.ts +11 -0
  340. package/src/types/subscription.ts +59 -0
  341. package/src/types/tool-call.ts +47 -0
  342. package/src/types/tool-result.ts +82 -0
  343. package/src/types/user.ts +125 -0
  344. package/tsconfig.json +24 -0
@@ -0,0 +1,349 @@
1
+ import * as http from 'http';
2
+ import * as https from 'https';
3
+ import * as dns from 'dns';
4
+ import * as net from 'net';
5
+ import { URL } from 'url';
6
+ import { ToolCall } from '../types/tool-call';
7
+ import { ToolOutput } from '../types/tool-result';
8
+ import { ExecutorConfig } from '../types/config';
9
+ import { ToolExecutor } from './interfaces';
10
+ import { internalAuthTokens } from '../mcp/internal-auth';
11
+
12
+ /** Maximum response body size in bytes (10 MB) */
13
+ const MAX_RESPONSE_BODY_BYTES = 10 * 1024 * 1024;
14
+
15
+ /**
16
+ * Check whether an IP address belongs to a private/reserved range.
17
+ * Blocks: loopback, link-local, private (RFC 1918), metadata endpoints,
18
+ * broadcast, multicast, and IPv6 equivalents.
19
+ */
20
+ export function isPrivateIP(ip: string): boolean {
21
+ // IPv4 checks
22
+ if (net.isIPv4(ip)) {
23
+ const parts = ip.split('.').map(Number);
24
+ const [a, b] = parts;
25
+ // Loopback: 127.0.0.0/8
26
+ if (a === 127) return true;
27
+ // Link-local: 169.254.0.0/16 (includes cloud metadata 169.254.169.254)
28
+ if (a === 169 && b === 254) return true;
29
+ // Private: 10.0.0.0/8
30
+ if (a === 10) return true;
31
+ // Private: 172.16.0.0/12
32
+ if (a === 172 && b >= 16 && b <= 31) return true;
33
+ // Private: 192.168.0.0/16
34
+ if (a === 192 && b === 168) return true;
35
+ // Current network: 0.0.0.0/8
36
+ if (a === 0) return true;
37
+ // Broadcast
38
+ if (ip === '255.255.255.255') return true;
39
+ // Multicast: 224.0.0.0/4
40
+ if (a >= 224 && a <= 239) return true;
41
+ return false;
42
+ }
43
+
44
+ // IPv6 checks
45
+ if (net.isIPv6(ip)) {
46
+ const normalized = ip.toLowerCase();
47
+ // Loopback: ::1
48
+ if (normalized === '::1') return true;
49
+ // Unspecified: ::
50
+ if (normalized === '::') return true;
51
+ // Link-local: fe80::/10
52
+ if (normalized.startsWith('fe80:')) return true;
53
+ // Unique local: fc00::/7
54
+ if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
55
+ // IPv4-mapped IPv6: ::ffff:x.x.x.x
56
+ const v4Mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(normalized);
57
+ if (v4Mapped) return isPrivateIP(v4Mapped[1]);
58
+ return false;
59
+ }
60
+
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * Resolve a hostname and verify the resolved IP is not private/internal.
66
+ * Returns the pinned IP address to prevent DNS TOCTOU rebinding attacks.
67
+ * Prevents SSRF attacks targeting internal services or cloud metadata.
68
+ */
69
+ function validateResolvedIP(hostname: string): Promise<string> {
70
+ // Direct IP addresses — check immediately
71
+ if (net.isIP(hostname)) {
72
+ if (isPrivateIP(hostname)) {
73
+ return Promise.reject(new Error(`SSRF blocked: resolved IP ${hostname} is a private/reserved address`));
74
+ }
75
+ return Promise.resolve(hostname);
76
+ }
77
+
78
+ return new Promise((resolve, reject) => {
79
+ dns.lookup(hostname, { all: true }, (err, addresses) => {
80
+ if (err) return reject(err);
81
+ if (!addresses || addresses.length === 0) {
82
+ return reject(new Error(`DNS lookup failed: no addresses found for "${hostname}"`));
83
+ }
84
+ for (const addr of addresses) {
85
+ if (isPrivateIP(addr.address)) {
86
+ return reject(
87
+ new Error(`SSRF blocked: hostname "${hostname}" resolved to private IP ${addr.address}`)
88
+ );
89
+ }
90
+ }
91
+ // Return the first resolved IP for DNS pinning
92
+ resolve(addresses[0].address);
93
+ });
94
+ });
95
+ }
96
+
97
+ interface CacheEntry {
98
+ output: ToolOutput;
99
+ expiresAt: number;
100
+ }
101
+
102
+ export class HttpExecutor implements ToolExecutor {
103
+ private config: ExecutorConfig;
104
+ private cache: Map<string, CacheEntry>;
105
+ /** Set to false to disable SSRF protection (for testing only). */
106
+ public ssrfProtectionEnabled: boolean = true;
107
+
108
+ constructor(config: ExecutorConfig) {
109
+ this.config = config;
110
+ this.cache = new Map();
111
+ }
112
+
113
+ // Execute an HTTP request based on ToolCall args
114
+ async execute(toolCall: ToolCall): Promise<ToolOutput> {
115
+ const { method = 'GET', url, body } = toolCall.args;
116
+ const headers: Record<string, string> = (toolCall.args.headers as Record<string, string>) || {};
117
+
118
+ if (!url || typeof url !== 'string') {
119
+ throw new Error('Missing or invalid URL in tool call args');
120
+ }
121
+
122
+ // Check for internal auth (self-referencing gateway request via MCP)
123
+ const internalAuth = internalAuthTokens.get(toolCall.tool_call_id);
124
+ let skipSsrf = false;
125
+ if (internalAuth) {
126
+ internalAuthTokens.delete(toolCall.tool_call_id);
127
+ if (url.startsWith(internalAuth.gateway_base_url)) {
128
+ // Inject Bearer token unless the user already set an Authorization header
129
+ const hasAuth = headers && Object.keys(headers).some(
130
+ k => k.toLowerCase() === 'authorization',
131
+ );
132
+ if (!hasAuth) {
133
+ headers['Authorization'] = `Bearer ${internalAuth.token}`;
134
+ }
135
+ skipSsrf = true;
136
+ }
137
+ }
138
+
139
+ // SSRF protection: block requests to private/internal IPs and pin DNS
140
+ let pinnedIP: string | undefined;
141
+ if (this.ssrfProtectionEnabled && !skipSsrf) {
142
+ const parsedUrl = new URL(url);
143
+ pinnedIP = await validateResolvedIP(parsedUrl.hostname);
144
+ }
145
+
146
+ // Check cache for GET requests
147
+ if (method === 'GET' && this.config.cache.enabled) {
148
+ const cacheKey = this.getCacheKey(url, headers);
149
+ const cached = this.getFromCache(cacheKey);
150
+ if (cached) return cached;
151
+ }
152
+
153
+ // Determine timeout from constraints or config
154
+ const timeoutMs = toolCall.constraints?.timeout_ms || this.config.http.timeout_ms;
155
+
156
+ // Execute with retries
157
+ let lastError: Error | null = null;
158
+ const maxRetries = this.config.http.max_retries;
159
+
160
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
161
+ try {
162
+ const output = await this.makeRequest(method, url, headers, body, timeoutMs, pinnedIP);
163
+
164
+ // Cache successful GET responses
165
+ if (method === 'GET' && this.config.cache.enabled && output.http_status && output.http_status < 400) {
166
+ const cacheKey = this.getCacheKey(url, headers);
167
+ this.setCache(cacheKey, output);
168
+ }
169
+
170
+ return output;
171
+ } catch (err) {
172
+ lastError = err instanceof Error ? err : new Error(String(err));
173
+
174
+ // Don't retry on client errors (4xx) except 429
175
+ if (lastError.message.includes('HTTP 4') && !lastError.message.includes('HTTP 429')) {
176
+ throw lastError;
177
+ }
178
+
179
+ // Don't retry on SSRF blocks
180
+ if (lastError.message.includes('SSRF blocked')) {
181
+ throw lastError;
182
+ }
183
+
184
+ // Wait before retry with exponential backoff
185
+ if (attempt < maxRetries) {
186
+ const backoffMs = this.config.http.backoff_base_ms * Math.pow(2, attempt);
187
+ await this.sleep(backoffMs);
188
+ }
189
+ }
190
+ }
191
+
192
+ throw lastError || new Error('Request failed after retries');
193
+ }
194
+
195
+ // Make a single HTTP request using Node.js built-in http/https
196
+ // When pinnedIP is provided (from SSRF DNS validation), the TCP connection
197
+ // goes to that IP while the Host header and TLS SNI use the original hostname,
198
+ // preventing DNS rebinding attacks.
199
+ private makeRequest(
200
+ method: string,
201
+ url: string,
202
+ headers: Record<string, string>,
203
+ body: unknown,
204
+ timeoutMs: number,
205
+ pinnedIP?: string,
206
+ ): Promise<ToolOutput> {
207
+ return new Promise((resolve, reject) => {
208
+ const parsedUrl = new URL(url);
209
+ const isHttps = parsedUrl.protocol === 'https:';
210
+ const transport = isHttps ? https : http;
211
+
212
+ const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined;
213
+
214
+ const requestHeaders: Record<string, string> = { ...headers };
215
+ // Default User-Agent so APIs like GitHub don't reject requests
216
+ if (!requestHeaders['user-agent'] && !requestHeaders['User-Agent']) {
217
+ requestHeaders['User-Agent'] = 'Palaryn/1.0';
218
+ }
219
+ if (bodyStr && !requestHeaders['content-type'] && !requestHeaders['Content-Type']) {
220
+ requestHeaders['Content-Type'] = 'application/json';
221
+ }
222
+ if (bodyStr) {
223
+ requestHeaders['Content-Length'] = Buffer.byteLength(bodyStr).toString();
224
+ }
225
+ // Preserve original Host header when connecting to pinned IP
226
+ if (pinnedIP && parsedUrl.hostname !== pinnedIP) {
227
+ requestHeaders['Host'] = parsedUrl.host;
228
+ }
229
+
230
+ const options: http.RequestOptions & { servername?: string } = {
231
+ hostname: pinnedIP || parsedUrl.hostname,
232
+ port: parsedUrl.port || (isHttps ? 443 : 80),
233
+ path: parsedUrl.pathname + parsedUrl.search,
234
+ method: method.toUpperCase(),
235
+ headers: requestHeaders,
236
+ timeout: timeoutMs,
237
+ };
238
+
239
+ // When using pinned IP with HTTPS, set servername for correct TLS SNI
240
+ if (pinnedIP && isHttps) {
241
+ options.servername = parsedUrl.hostname;
242
+ }
243
+
244
+ // Pin DNS resolution to the pre-validated IP to prevent TOCTOU rebinding
245
+ if (pinnedIP) {
246
+ const family = pinnedIP.includes(':') ? 6 : 4;
247
+ const agent = new (isHttps ? https : http).Agent({
248
+ lookup: (_hostname: string, _options: any, callback: Function) => {
249
+ callback(null, pinnedIP, family);
250
+ },
251
+ } as any);
252
+ options.agent = agent;
253
+ }
254
+
255
+ const req = transport.request(options, (res) => {
256
+ let data = '';
257
+ let receivedBytes = 0;
258
+ let aborted = false;
259
+
260
+ res.on('data', (chunk) => {
261
+ if (aborted) return;
262
+ receivedBytes += Buffer.byteLength(chunk);
263
+ if (receivedBytes > MAX_RESPONSE_BODY_BYTES) {
264
+ aborted = true;
265
+ req.destroy();
266
+ reject(new Error(`Response body exceeds maximum size of ${MAX_RESPONSE_BODY_BYTES} bytes`));
267
+ return;
268
+ }
269
+ data += chunk;
270
+ });
271
+ res.on('end', () => {
272
+ if (aborted) return;
273
+ let parsedBody: unknown = data;
274
+ const contentType = res.headers['content-type'] || '';
275
+ if (contentType.includes('application/json')) {
276
+ try { parsedBody = JSON.parse(data); } catch { parsedBody = data; }
277
+ }
278
+
279
+ // Convert response headers to Record<string, string>
280
+ const responseHeaders: Record<string, string> = {};
281
+ for (const [key, value] of Object.entries(res.headers)) {
282
+ if (value) responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
283
+ }
284
+
285
+ resolve({
286
+ http_status: res.statusCode || 0,
287
+ body: parsedBody,
288
+ headers: responseHeaders,
289
+ });
290
+ });
291
+ });
292
+
293
+ req.on('error', (err) => reject(err));
294
+ req.on('timeout', () => {
295
+ req.destroy();
296
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
297
+ });
298
+
299
+ if (bodyStr) {
300
+ req.write(bodyStr);
301
+ }
302
+ req.end();
303
+ });
304
+ }
305
+
306
+ // Cache key generation
307
+ private getCacheKey(url: string, headers: Record<string, string>): string {
308
+ // Use URL + sorted header keys that affect response (Accept, Authorization)
309
+ const relevantHeaders = ['accept', 'authorization'];
310
+ const headerStr = relevantHeaders
311
+ .map(h => `${h}:${headers[h] || headers[h.charAt(0).toUpperCase() + h.slice(1)] || ''}`)
312
+ .join('|');
313
+ return `${url}|${headerStr}`;
314
+ }
315
+
316
+ // Get from cache
317
+ private getFromCache(key: string): ToolOutput | null {
318
+ const entry = this.cache.get(key);
319
+ if (!entry) return null;
320
+ if (Date.now() > entry.expiresAt) {
321
+ this.cache.delete(key);
322
+ return null;
323
+ }
324
+ return entry.output;
325
+ }
326
+
327
+ // Set cache entry
328
+ private setCache(key: string, output: ToolOutput): void {
329
+ this.cache.set(key, {
330
+ output,
331
+ expiresAt: Date.now() + this.config.cache.ttl_ms,
332
+ });
333
+ // Evict old entries if cache gets too large
334
+ if (this.cache.size > 1000) {
335
+ const oldest = this.cache.keys().next().value;
336
+ if (oldest) this.cache.delete(oldest);
337
+ }
338
+ }
339
+
340
+ // Clear cache
341
+ clearCache(): void {
342
+ this.cache.clear();
343
+ }
344
+
345
+ // Sleep utility
346
+ private sleep(ms: number): Promise<void> {
347
+ return new Promise(resolve => setTimeout(resolve, ms));
348
+ }
349
+ }
@@ -0,0 +1,9 @@
1
+ export { ToolExecutor } from './interfaces';
2
+ export { ExecutorRegistry } from './registry';
3
+ export { HttpExecutor } from './http-executor';
4
+ export { NoopExecutor } from './noop-executor';
5
+ export { SlackExecutor } from './slack-executor';
6
+ export { FilesystemExecutor } from './filesystem-executor';
7
+ export { SQLExecutor } from './sql-executor';
8
+ export { ShellExecutor } from './shell-executor';
9
+ export { WebSocketExecutor } from './websocket-executor';
@@ -0,0 +1,11 @@
1
+ import { ToolCall } from '../types/tool-call';
2
+ import { ToolOutput } from '../types/tool-result';
3
+
4
+ /**
5
+ * Base interface for all tool executors.
6
+ * Implement this to add support for new tool types (Slack, Git, DB, etc.).
7
+ */
8
+ export interface ToolExecutor {
9
+ /** Execute a tool call and return the output */
10
+ execute(toolCall: ToolCall): Promise<ToolOutput>;
11
+ }
@@ -0,0 +1,23 @@
1
+ import { ToolCall } from '../types/tool-call';
2
+ import { ToolOutput } from '../types/tool-result';
3
+ import { ToolExecutor } from './interfaces';
4
+
5
+ /**
6
+ * No-op executor that returns a canned response without making any external calls.
7
+ * Useful for dry-run policy testing, development, and unit tests.
8
+ */
9
+ export class NoopExecutor implements ToolExecutor {
10
+ private response: ToolOutput;
11
+
12
+ constructor(response?: Partial<ToolOutput>) {
13
+ this.response = {
14
+ http_status: response?.http_status ?? 200,
15
+ body: response?.body ?? { dry_run: true, tool: 'noop' },
16
+ headers: response?.headers ?? {},
17
+ };
18
+ }
19
+
20
+ async execute(toolCall: ToolCall): Promise<ToolOutput> {
21
+ return { ...this.response };
22
+ }
23
+ }
@@ -0,0 +1,64 @@
1
+ import { ToolCall } from '../types/tool-call';
2
+ import { ToolOutput } from '../types/tool-result';
3
+ import { ToolExecutor } from './interfaces';
4
+
5
+ /**
6
+ * Registry that maps tool name patterns to executor instances.
7
+ * Patterns are matched in registration order; first match wins.
8
+ *
9
+ * Usage:
10
+ * registry.register('http.*', httpExecutor); // glob-like
11
+ * registry.register('http.request', httpExecutor); // exact match
12
+ * registry.register('slack.*', slackExecutor);
13
+ * registry.register('*', fallbackExecutor); // catch-all
14
+ */
15
+ export class ExecutorRegistry {
16
+ private entries: Array<{ pattern: string; regex: RegExp; executor: ToolExecutor }> = [];
17
+
18
+ /** Register an executor for a tool name pattern. Patterns support * as wildcard.
19
+ * If prepend is true, the entry is added at the front (higher priority than existing). */
20
+ register(pattern: string, executor: ToolExecutor, prepend = false): void {
21
+ // Convert glob pattern to regex: "http.*" -> /^http\..*$/
22
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
23
+ const regex = new RegExp(`^${escaped}$`);
24
+ if (prepend) {
25
+ this.entries.unshift({ pattern, regex, executor });
26
+ } else {
27
+ this.entries.push({ pattern, regex, executor });
28
+ }
29
+ }
30
+
31
+ /** Find the executor for a tool name. Returns undefined if no match. */
32
+ resolve(toolName: string): ToolExecutor | undefined {
33
+ for (const entry of this.entries) {
34
+ if (entry.regex.test(toolName)) {
35
+ return entry.executor;
36
+ }
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ /** Execute a tool call by resolving the executor from the tool name. */
42
+ async execute(toolCall: ToolCall): Promise<ToolOutput> {
43
+ const executor = this.resolve(toolCall.tool.name);
44
+ if (!executor) {
45
+ throw new Error(`No executor registered for tool "${toolCall.tool.name}"`);
46
+ }
47
+ return executor.execute(toolCall);
48
+ }
49
+
50
+ /** List all registered patterns (for debugging/introspection) */
51
+ listPatterns(): string[] {
52
+ return this.entries.map(e => e.pattern);
53
+ }
54
+
55
+ /** Check if any executor is registered for a tool name */
56
+ has(toolName: string): boolean {
57
+ return this.resolve(toolName) !== undefined;
58
+ }
59
+
60
+ /** Remove all registered executors */
61
+ clear(): void {
62
+ this.entries = [];
63
+ }
64
+ }
@@ -0,0 +1,148 @@
1
+ import { execFile } from 'child_process';
2
+ import { ToolCall } from '../types/tool-call';
3
+ import { ToolOutput } from '../types/tool-result';
4
+ import { ToolExecutor } from './interfaces';
5
+ import { ShellExecutorConfig } from '../types/config';
6
+
7
+ /**
8
+ * Shell executor for sandboxed command execution.
9
+ * Handles tool calls with tool name `shell.*` (e.g., shell.exec).
10
+ * Uses execFile (not exec) to prevent shell injection.
11
+ * Empty allowlist by default - nothing runs until configured.
12
+ */
13
+ export class ShellExecutor implements ToolExecutor {
14
+ private config: ShellExecutorConfig;
15
+
16
+ /** Default blocklist of dangerous commands/patterns */
17
+ private static readonly DEFAULT_BLOCKED = [
18
+ 'rm -rf /',
19
+ 'rm -rf /*',
20
+ 'dd',
21
+ 'mkfs',
22
+ 'fdisk',
23
+ 'format',
24
+ ':(){:|:&};:',
25
+ 'chmod -R 777 /',
26
+ 'chown -R',
27
+ ];
28
+
29
+ constructor(config: ShellExecutorConfig) {
30
+ this.config = config;
31
+ }
32
+
33
+ async execute(toolCall: ToolCall): Promise<ToolOutput> {
34
+ const action = this.resolveAction(toolCall);
35
+
36
+ switch (action) {
37
+ case 'exec':
38
+ return this.exec(toolCall);
39
+ default:
40
+ throw new Error(`Unsupported shell action: ${action}`);
41
+ }
42
+ }
43
+
44
+ private resolveAction(toolCall: ToolCall): string {
45
+ if (toolCall.args.action && typeof toolCall.args.action === 'string') {
46
+ return toolCall.args.action;
47
+ }
48
+
49
+ const toolName = toolCall.tool.name;
50
+ const dotIndex = toolName.indexOf('.');
51
+ if (dotIndex !== -1) {
52
+ return toolName.substring(dotIndex + 1);
53
+ }
54
+
55
+ throw new Error(`Unsupported shell action: ${toolName}`);
56
+ }
57
+
58
+ private isCommandAllowed(command: string): boolean {
59
+ return this.config.allowed_commands.includes(command);
60
+ }
61
+
62
+ private isCommandBlocked(command: string, args: string[]): boolean {
63
+ const fullCommand = [command, ...args].join(' ');
64
+
65
+ // Check config blocked_commands
66
+ if (this.config.blocked_commands) {
67
+ for (const blocked of this.config.blocked_commands) {
68
+ if (command === blocked || fullCommand.includes(blocked)) {
69
+ return true;
70
+ }
71
+ }
72
+ }
73
+
74
+ // Check default blocklist
75
+ for (const blocked of ShellExecutor.DEFAULT_BLOCKED) {
76
+ if (fullCommand.includes(blocked)) {
77
+ return true;
78
+ }
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ private async exec(toolCall: ToolCall): Promise<ToolOutput> {
85
+ const { command, args: cmdArgs, cwd, env, timeout_ms } = toolCall.args;
86
+
87
+ if (!command || typeof command !== 'string') {
88
+ throw new Error('Missing or invalid "command" argument for shell.exec');
89
+ }
90
+
91
+ if (!this.isCommandAllowed(command)) {
92
+ throw new Error(`Command "${command}" is not in the allowed commands list`);
93
+ }
94
+
95
+ const argsList: string[] = Array.isArray(cmdArgs) ? cmdArgs.map(String) : [];
96
+
97
+ if (this.isCommandBlocked(command, argsList)) {
98
+ throw new Error(`Command "${command}" with the given arguments is blocked`);
99
+ }
100
+
101
+ const timeoutMs = (typeof timeout_ms === 'number' ? timeout_ms : null) || this.config.timeout_ms;
102
+ const workingDir = (typeof cwd === 'string' ? cwd : null) || this.config.cwd;
103
+
104
+ return new Promise<ToolOutput>((resolve, reject) => {
105
+ const child = execFile(
106
+ command,
107
+ argsList,
108
+ {
109
+ timeout: timeoutMs,
110
+ maxBuffer: this.config.max_output_bytes,
111
+ cwd: workingDir || undefined,
112
+ env: env && typeof env === 'object' ? { ...process.env, ...(env as Record<string, string>) } : undefined,
113
+ },
114
+ (error, stdout, stderr) => {
115
+ if (error && !('code' in error)) {
116
+ reject(error);
117
+ return;
118
+ }
119
+
120
+ const exitCode = error && 'code' in error ? (error as { code: number }).code : 0;
121
+ const stdoutStr = typeof stdout === 'string' ? stdout : '';
122
+ const stderrStr = typeof stderr === 'string' ? stderr : '';
123
+
124
+ // Enforce max_output_bytes on combined output
125
+ const totalBytes = Buffer.byteLength(stdoutStr) + Buffer.byteLength(stderrStr);
126
+ if (totalBytes > this.config.max_output_bytes) {
127
+ resolve({
128
+ body: stdoutStr.substring(0, this.config.max_output_bytes),
129
+ exit_code: exitCode,
130
+ stderr: stderrStr.substring(0, 1024),
131
+ metadata: { truncated: true, total_bytes: totalBytes },
132
+ });
133
+ return;
134
+ }
135
+
136
+ resolve({
137
+ body: stdoutStr,
138
+ exit_code: exitCode,
139
+ stderr: stderrStr || undefined,
140
+ });
141
+ },
142
+ );
143
+
144
+ // Safety: ensure child process is cleaned up
145
+ child.on('error', reject);
146
+ });
147
+ }
148
+ }