palaryn 0.1.0 → 0.3.0

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 (328) 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/audit/logger.d.ts +10 -0
  5. package/dist/src/audit/logger.d.ts.map +1 -1
  6. package/dist/src/audit/logger.js +52 -38
  7. package/dist/src/audit/logger.js.map +1 -1
  8. package/dist/src/auth/routes.js.map +1 -1
  9. package/dist/src/budget/manager.d.ts +5 -0
  10. package/dist/src/budget/manager.d.ts.map +1 -1
  11. package/dist/src/budget/manager.js +32 -0
  12. package/dist/src/budget/manager.js.map +1 -1
  13. package/dist/src/budget/model-pricing.d.ts +20 -0
  14. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  15. package/dist/src/budget/model-pricing.js +107 -0
  16. package/dist/src/budget/model-pricing.js.map +1 -0
  17. package/dist/src/budget/usage-extractor.d.ts +3 -1
  18. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  19. package/dist/src/budget/usage-extractor.js +47 -3
  20. package/dist/src/budget/usage-extractor.js.map +1 -1
  21. package/dist/src/config/defaults.d.ts.map +1 -1
  22. package/dist/src/config/defaults.js +65 -13
  23. package/dist/src/config/defaults.js.map +1 -1
  24. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  25. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  26. package/dist/src/dlp/tool-patterns.js +34 -0
  27. package/dist/src/dlp/tool-patterns.js.map +1 -0
  28. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  29. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  30. package/dist/src/executor/filesystem-executor.js +192 -0
  31. package/dist/src/executor/filesystem-executor.js.map +1 -0
  32. package/dist/src/executor/http-executor.d.ts.map +1 -1
  33. package/dist/src/executor/http-executor.js +4 -0
  34. package/dist/src/executor/http-executor.js.map +1 -1
  35. package/dist/src/executor/index.d.ts +4 -0
  36. package/dist/src/executor/index.d.ts.map +1 -1
  37. package/dist/src/executor/index.js +9 -1
  38. package/dist/src/executor/index.js.map +1 -1
  39. package/dist/src/executor/shell-executor.d.ts +22 -0
  40. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  41. package/dist/src/executor/shell-executor.js +119 -0
  42. package/dist/src/executor/shell-executor.js.map +1 -0
  43. package/dist/src/executor/sql-executor.d.ts +29 -0
  44. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  45. package/dist/src/executor/sql-executor.js +114 -0
  46. package/dist/src/executor/sql-executor.js.map +1 -0
  47. package/dist/src/executor/websocket-executor.d.ts +26 -0
  48. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  49. package/dist/src/executor/websocket-executor.js +205 -0
  50. package/dist/src/executor/websocket-executor.js.map +1 -0
  51. package/dist/src/interceptor/index.d.ts +2 -0
  52. package/dist/src/interceptor/index.d.ts.map +1 -0
  53. package/dist/src/interceptor/index.js +6 -0
  54. package/dist/src/interceptor/index.js.map +1 -0
  55. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  56. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  57. package/dist/src/interceptor/provider-interceptor.js +302 -0
  58. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  59. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  60. package/dist/src/mcp/auth-verifier.js +3 -2
  61. package/dist/src/mcp/auth-verifier.js.map +1 -1
  62. package/dist/src/mcp/bridge.d.ts +14 -10
  63. package/dist/src/mcp/bridge.d.ts.map +1 -1
  64. package/dist/src/mcp/bridge.js +51 -227
  65. package/dist/src/mcp/bridge.js.map +1 -1
  66. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  67. package/dist/src/mcp/http-transport.js +101 -65
  68. package/dist/src/mcp/http-transport.js.map +1 -1
  69. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  70. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  71. package/dist/src/mcp/tool-definitions.js +491 -0
  72. package/dist/src/mcp/tool-definitions.js.map +1 -0
  73. package/dist/src/middleware/auth.js.map +1 -1
  74. package/dist/src/middleware/session.js.map +1 -1
  75. package/dist/src/middleware/validate.d.ts +8 -0
  76. package/dist/src/middleware/validate.d.ts.map +1 -1
  77. package/dist/src/middleware/validate.js +45 -0
  78. package/dist/src/middleware/validate.js.map +1 -1
  79. package/dist/src/policy/engine.d.ts +4 -0
  80. package/dist/src/policy/engine.d.ts.map +1 -1
  81. package/dist/src/policy/engine.js +117 -0
  82. package/dist/src/policy/engine.js.map +1 -1
  83. package/dist/src/saas/routes.d.ts.map +1 -1
  84. package/dist/src/saas/routes.js +327 -10
  85. package/dist/src/saas/routes.js.map +1 -1
  86. package/dist/src/server/app.d.ts.map +1 -1
  87. package/dist/src/server/app.js +19 -2
  88. package/dist/src/server/app.js.map +1 -1
  89. package/dist/src/server/gateway.d.ts.map +1 -1
  90. package/dist/src/server/gateway.js +17 -0
  91. package/dist/src/server/gateway.js.map +1 -1
  92. package/dist/src/server/index.d.ts.map +1 -1
  93. package/dist/src/server/index.js +18 -0
  94. package/dist/src/server/index.js.map +1 -1
  95. package/dist/src/storage/interfaces.d.ts +14 -3
  96. package/dist/src/storage/interfaces.d.ts.map +1 -1
  97. package/dist/src/storage/memory.d.ts +2 -0
  98. package/dist/src/storage/memory.d.ts.map +1 -1
  99. package/dist/src/storage/memory.js +6 -0
  100. package/dist/src/storage/memory.js.map +1 -1
  101. package/dist/src/storage/postgres.d.ts +5 -0
  102. package/dist/src/storage/postgres.d.ts.map +1 -1
  103. package/dist/src/storage/postgres.js +16 -0
  104. package/dist/src/storage/postgres.js.map +1 -1
  105. package/dist/src/storage/redis.d.ts +10 -0
  106. package/dist/src/storage/redis.d.ts.map +1 -1
  107. package/dist/src/storage/redis.js +65 -0
  108. package/dist/src/storage/redis.js.map +1 -1
  109. package/dist/src/types/budget.d.ts +4 -0
  110. package/dist/src/types/budget.d.ts.map +1 -1
  111. package/dist/src/types/config.d.ts +58 -0
  112. package/dist/src/types/config.d.ts.map +1 -1
  113. package/dist/src/types/events.d.ts +1 -0
  114. package/dist/src/types/events.d.ts.map +1 -1
  115. package/dist/src/types/policy.d.ts +11 -1
  116. package/dist/src/types/policy.d.ts.map +1 -1
  117. package/dist/src/types/tool-result.d.ts +11 -0
  118. package/dist/src/types/tool-result.d.ts.map +1 -1
  119. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  120. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  121. package/dist/tests/unit/app-routes.test.js +715 -0
  122. package/dist/tests/unit/app-routes.test.js.map +1 -0
  123. package/dist/tests/unit/audit-logger.test.js +105 -0
  124. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  125. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  126. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  127. package/dist/tests/unit/auth-providers.test.js +279 -0
  128. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  129. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  130. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  131. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  132. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  133. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  134. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  135. package/dist/tests/unit/auth-verifier.test.js +505 -0
  136. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  137. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  138. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  139. package/dist/tests/unit/billing-routes.test.js +432 -0
  140. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  141. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  142. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  143. package/dist/tests/unit/config-defaults.test.js +119 -0
  144. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  145. package/dist/tests/unit/defaults.test.js +0 -10
  146. package/dist/tests/unit/defaults.test.js.map +1 -1
  147. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  148. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  149. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  150. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  151. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  152. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  153. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  154. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  155. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  156. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  157. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  158. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  159. package/dist/tests/unit/logger.test.d.ts +2 -0
  160. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  161. package/dist/tests/unit/logger.test.js +97 -0
  162. package/dist/tests/unit/logger.test.js.map +1 -0
  163. package/dist/tests/unit/metrics.test.js +102 -0
  164. package/dist/tests/unit/metrics.test.js.map +1 -1
  165. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  166. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  167. package/dist/tests/unit/model-pricing.test.js +87 -0
  168. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  169. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  170. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  171. package/dist/tests/unit/oauth-stores.test.js +260 -0
  172. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  173. package/dist/tests/unit/policy-engine.test.js +466 -0
  174. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  175. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  176. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  177. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  178. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  179. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  180. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  181. package/dist/tests/unit/saas-routes-branches.test.js +2040 -0
  182. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  183. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  184. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  185. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  186. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  187. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  188. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  189. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  190. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  191. package/dist/tests/unit/saas-routes.test.js +3 -3
  192. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  193. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  194. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  195. package/dist/tests/unit/shell-executor.test.js +145 -0
  196. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  197. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  198. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  199. package/dist/tests/unit/sql-executor.test.js +177 -0
  200. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  201. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  202. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  203. package/dist/tests/unit/stream-proxy.test.js +147 -0
  204. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  205. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  206. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  207. package/dist/tests/unit/tool-definitions.test.js +184 -0
  208. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  209. package/dist/tests/unit/usage-extractor.test.js +140 -0
  210. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  211. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  212. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  213. package/dist/tests/unit/webhook-handler.test.js +453 -0
  214. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  215. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  216. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  217. package/dist/tests/unit/webhook-routes.test.js +69 -0
  218. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  219. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  220. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  221. package/dist/tests/unit/websocket-executor.test.js +121 -0
  222. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  223. package/package.json +8 -2
  224. package/policy-packs/demo_fail.yaml +41 -0
  225. package/policy-packs/full_tools.yaml +136 -0
  226. package/src/admin/index.ts +1 -0
  227. package/src/admin/routes.ts +509 -0
  228. package/src/admin/templates.ts +572 -0
  229. package/src/anomaly/detector.ts +717 -0
  230. package/src/anomaly/index.ts +1 -0
  231. package/src/approval/manager.ts +569 -0
  232. package/src/approval/webhook.ts +133 -0
  233. package/src/audit/logger.ts +490 -0
  234. package/src/auth/index.ts +5 -0
  235. package/src/auth/password.ts +21 -0
  236. package/src/auth/pkce.ts +22 -0
  237. package/src/auth/providers.ts +208 -0
  238. package/src/auth/routes.ts +521 -0
  239. package/src/auth/session.ts +84 -0
  240. package/src/billing/index.ts +6 -0
  241. package/src/billing/plan-enforcer.ts +135 -0
  242. package/src/billing/routes.ts +229 -0
  243. package/src/billing/stripe-client.ts +58 -0
  244. package/src/billing/webhook-handler.ts +182 -0
  245. package/src/billing/webhook-routes.ts +28 -0
  246. package/src/budget/manager.ts +679 -0
  247. package/src/budget/model-pricing.ts +119 -0
  248. package/src/budget/usage-extractor.ts +214 -0
  249. package/src/cli.ts +91 -0
  250. package/src/config/defaults.ts +261 -0
  251. package/src/config/validate.ts +88 -0
  252. package/src/dlp/composite-scanner.ts +213 -0
  253. package/src/dlp/index.ts +9 -0
  254. package/src/dlp/interfaces.ts +34 -0
  255. package/src/dlp/patterns.ts +30 -0
  256. package/src/dlp/prompt-injection-backend.ts +181 -0
  257. package/src/dlp/prompt-injection-patterns.ts +302 -0
  258. package/src/dlp/regex-backend.ts +181 -0
  259. package/src/dlp/scanner.ts +502 -0
  260. package/src/dlp/text-normalizer.ts +225 -0
  261. package/src/dlp/tool-patterns.ts +35 -0
  262. package/src/dlp/trufflehog-backend.ts +190 -0
  263. package/src/executor/filesystem-executor.ts +196 -0
  264. package/src/executor/http-executor.ts +330 -0
  265. package/src/executor/index.ts +9 -0
  266. package/src/executor/interfaces.ts +11 -0
  267. package/src/executor/noop-executor.ts +23 -0
  268. package/src/executor/registry.ts +64 -0
  269. package/src/executor/shell-executor.ts +148 -0
  270. package/src/executor/slack-executor.ts +176 -0
  271. package/src/executor/sql-executor.ts +146 -0
  272. package/src/executor/websocket-executor.ts +211 -0
  273. package/src/index.ts +24 -0
  274. package/src/interceptor/index.ts +1 -0
  275. package/src/interceptor/provider-interceptor.ts +315 -0
  276. package/src/mcp/auth-verifier.ts +152 -0
  277. package/src/mcp/bridge.ts +703 -0
  278. package/src/mcp/http-transport.ts +672 -0
  279. package/src/mcp/index.ts +9 -0
  280. package/src/mcp/oauth-pages.ts +139 -0
  281. package/src/mcp/oauth-postgres-stores.ts +278 -0
  282. package/src/mcp/oauth-provider.ts +536 -0
  283. package/src/mcp/oauth-stores.ts +202 -0
  284. package/src/mcp/server.ts +55 -0
  285. package/src/mcp/tool-definitions.ts +562 -0
  286. package/src/metrics/collector.ts +357 -0
  287. package/src/metrics/index.ts +1 -0
  288. package/src/middleware/auth.ts +814 -0
  289. package/src/middleware/session.ts +85 -0
  290. package/src/middleware/validate.ts +130 -0
  291. package/src/policy/engine.ts +815 -0
  292. package/src/policy/index.ts +2 -0
  293. package/src/policy/opa-engine.ts +829 -0
  294. package/src/proxy/forward-proxy.ts +649 -0
  295. package/src/proxy/index.ts +1 -0
  296. package/src/ratelimit/limiter.ts +196 -0
  297. package/src/replay/engine.ts +142 -0
  298. package/src/replay/index.ts +1 -0
  299. package/src/saas/index.ts +1 -0
  300. package/src/saas/routes.ts +2161 -0
  301. package/src/server/app.ts +981 -0
  302. package/src/server/errors.ts +49 -0
  303. package/src/server/gateway.ts +1130 -0
  304. package/src/server/index.ts +307 -0
  305. package/src/server/logger.ts +255 -0
  306. package/src/server/stream-proxy.ts +202 -0
  307. package/src/storage/file-persistence.ts +315 -0
  308. package/src/storage/index.ts +4 -0
  309. package/src/storage/interfaces.ts +287 -0
  310. package/src/storage/memory.ts +686 -0
  311. package/src/storage/postgres.ts +1831 -0
  312. package/src/storage/redis.ts +835 -0
  313. package/src/tracing/index.ts +1 -0
  314. package/src/tracing/provider.ts +100 -0
  315. package/src/trust/calculator.ts +141 -0
  316. package/src/trust/index.ts +7 -0
  317. package/src/types/budget.ts +36 -0
  318. package/src/types/config.ts +278 -0
  319. package/src/types/events.ts +41 -0
  320. package/src/types/express.d.ts +14 -0
  321. package/src/types/index.ts +7 -0
  322. package/src/types/policy.ts +83 -0
  323. package/src/types/stripe-config.ts +11 -0
  324. package/src/types/subscription.ts +59 -0
  325. package/src/types/tool-call.ts +47 -0
  326. package/src/types/tool-result.ts +82 -0
  327. package/src/types/user.ts +125 -0
  328. package/tsconfig.json +24 -0
@@ -0,0 +1,330 @@
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
+
11
+ /** Maximum response body size in bytes (10 MB) */
12
+ const MAX_RESPONSE_BODY_BYTES = 10 * 1024 * 1024;
13
+
14
+ /**
15
+ * Check whether an IP address belongs to a private/reserved range.
16
+ * Blocks: loopback, link-local, private (RFC 1918), metadata endpoints,
17
+ * broadcast, multicast, and IPv6 equivalents.
18
+ */
19
+ export function isPrivateIP(ip: string): boolean {
20
+ // IPv4 checks
21
+ if (net.isIPv4(ip)) {
22
+ const parts = ip.split('.').map(Number);
23
+ const [a, b] = parts;
24
+ // Loopback: 127.0.0.0/8
25
+ if (a === 127) return true;
26
+ // Link-local: 169.254.0.0/16 (includes cloud metadata 169.254.169.254)
27
+ if (a === 169 && b === 254) return true;
28
+ // Private: 10.0.0.0/8
29
+ if (a === 10) return true;
30
+ // Private: 172.16.0.0/12
31
+ if (a === 172 && b >= 16 && b <= 31) return true;
32
+ // Private: 192.168.0.0/16
33
+ if (a === 192 && b === 168) return true;
34
+ // Current network: 0.0.0.0/8
35
+ if (a === 0) return true;
36
+ // Broadcast
37
+ if (ip === '255.255.255.255') return true;
38
+ // Multicast: 224.0.0.0/4
39
+ if (a >= 224 && a <= 239) return true;
40
+ return false;
41
+ }
42
+
43
+ // IPv6 checks
44
+ if (net.isIPv6(ip)) {
45
+ const normalized = ip.toLowerCase();
46
+ // Loopback: ::1
47
+ if (normalized === '::1') return true;
48
+ // Unspecified: ::
49
+ if (normalized === '::') return true;
50
+ // Link-local: fe80::/10
51
+ if (normalized.startsWith('fe80:')) return true;
52
+ // Unique local: fc00::/7
53
+ if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
54
+ // IPv4-mapped IPv6: ::ffff:x.x.x.x
55
+ const v4Mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(normalized);
56
+ if (v4Mapped) return isPrivateIP(v4Mapped[1]);
57
+ return false;
58
+ }
59
+
60
+ return false;
61
+ }
62
+
63
+ /**
64
+ * Resolve a hostname and verify the resolved IP is not private/internal.
65
+ * Returns the pinned IP address to prevent DNS TOCTOU rebinding attacks.
66
+ * Prevents SSRF attacks targeting internal services or cloud metadata.
67
+ */
68
+ function validateResolvedIP(hostname: string): Promise<string> {
69
+ // Direct IP addresses — check immediately
70
+ if (net.isIP(hostname)) {
71
+ if (isPrivateIP(hostname)) {
72
+ return Promise.reject(new Error(`SSRF blocked: resolved IP ${hostname} is a private/reserved address`));
73
+ }
74
+ return Promise.resolve(hostname);
75
+ }
76
+
77
+ return new Promise((resolve, reject) => {
78
+ dns.lookup(hostname, { all: true }, (err, addresses) => {
79
+ if (err) return reject(err);
80
+ if (!addresses || addresses.length === 0) {
81
+ return reject(new Error(`DNS lookup failed: no addresses found for "${hostname}"`));
82
+ }
83
+ for (const addr of addresses) {
84
+ if (isPrivateIP(addr.address)) {
85
+ return reject(
86
+ new Error(`SSRF blocked: hostname "${hostname}" resolved to private IP ${addr.address}`)
87
+ );
88
+ }
89
+ }
90
+ // Return the first resolved IP for DNS pinning
91
+ resolve(addresses[0].address);
92
+ });
93
+ });
94
+ }
95
+
96
+ interface CacheEntry {
97
+ output: ToolOutput;
98
+ expiresAt: number;
99
+ }
100
+
101
+ export class HttpExecutor implements ToolExecutor {
102
+ private config: ExecutorConfig;
103
+ private cache: Map<string, CacheEntry>;
104
+ /** Set to false to disable SSRF protection (for testing only). */
105
+ public ssrfProtectionEnabled: boolean = true;
106
+
107
+ constructor(config: ExecutorConfig) {
108
+ this.config = config;
109
+ this.cache = new Map();
110
+ }
111
+
112
+ // Execute an HTTP request based on ToolCall args
113
+ async execute(toolCall: ToolCall): Promise<ToolOutput> {
114
+ const { method = 'GET', url, headers = {}, body } = toolCall.args;
115
+
116
+ if (!url || typeof url !== 'string') {
117
+ throw new Error('Missing or invalid URL in tool call args');
118
+ }
119
+
120
+ // SSRF protection: block requests to private/internal IPs and pin DNS
121
+ let pinnedIP: string | undefined;
122
+ if (this.ssrfProtectionEnabled) {
123
+ const parsedUrl = new URL(url);
124
+ pinnedIP = await validateResolvedIP(parsedUrl.hostname);
125
+ }
126
+
127
+ // Check cache for GET requests
128
+ if (method === 'GET' && this.config.cache.enabled) {
129
+ const cacheKey = this.getCacheKey(url, headers);
130
+ const cached = this.getFromCache(cacheKey);
131
+ if (cached) return cached;
132
+ }
133
+
134
+ // Determine timeout from constraints or config
135
+ const timeoutMs = toolCall.constraints?.timeout_ms || this.config.http.timeout_ms;
136
+
137
+ // Execute with retries
138
+ let lastError: Error | null = null;
139
+ const maxRetries = this.config.http.max_retries;
140
+
141
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
142
+ try {
143
+ const output = await this.makeRequest(method, url, headers, body, timeoutMs, pinnedIP);
144
+
145
+ // Cache successful GET responses
146
+ if (method === 'GET' && this.config.cache.enabled && output.http_status && output.http_status < 400) {
147
+ const cacheKey = this.getCacheKey(url, headers);
148
+ this.setCache(cacheKey, output);
149
+ }
150
+
151
+ return output;
152
+ } catch (err) {
153
+ lastError = err instanceof Error ? err : new Error(String(err));
154
+
155
+ // Don't retry on client errors (4xx) except 429
156
+ if (lastError.message.includes('HTTP 4') && !lastError.message.includes('HTTP 429')) {
157
+ throw lastError;
158
+ }
159
+
160
+ // Don't retry on SSRF blocks
161
+ if (lastError.message.includes('SSRF blocked')) {
162
+ throw lastError;
163
+ }
164
+
165
+ // Wait before retry with exponential backoff
166
+ if (attempt < maxRetries) {
167
+ const backoffMs = this.config.http.backoff_base_ms * Math.pow(2, attempt);
168
+ await this.sleep(backoffMs);
169
+ }
170
+ }
171
+ }
172
+
173
+ throw lastError || new Error('Request failed after retries');
174
+ }
175
+
176
+ // Make a single HTTP request using Node.js built-in http/https
177
+ // When pinnedIP is provided (from SSRF DNS validation), the TCP connection
178
+ // goes to that IP while the Host header and TLS SNI use the original hostname,
179
+ // preventing DNS rebinding attacks.
180
+ private makeRequest(
181
+ method: string,
182
+ url: string,
183
+ headers: Record<string, string>,
184
+ body: unknown,
185
+ timeoutMs: number,
186
+ pinnedIP?: string,
187
+ ): Promise<ToolOutput> {
188
+ return new Promise((resolve, reject) => {
189
+ const parsedUrl = new URL(url);
190
+ const isHttps = parsedUrl.protocol === 'https:';
191
+ const transport = isHttps ? https : http;
192
+
193
+ const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined;
194
+
195
+ const requestHeaders: Record<string, string> = { ...headers };
196
+ // Default User-Agent so APIs like GitHub don't reject requests
197
+ if (!requestHeaders['user-agent'] && !requestHeaders['User-Agent']) {
198
+ requestHeaders['User-Agent'] = 'Palaryn/1.0';
199
+ }
200
+ if (bodyStr && !requestHeaders['content-type'] && !requestHeaders['Content-Type']) {
201
+ requestHeaders['Content-Type'] = 'application/json';
202
+ }
203
+ if (bodyStr) {
204
+ requestHeaders['Content-Length'] = Buffer.byteLength(bodyStr).toString();
205
+ }
206
+ // Preserve original Host header when connecting to pinned IP
207
+ if (pinnedIP && parsedUrl.hostname !== pinnedIP) {
208
+ requestHeaders['Host'] = parsedUrl.host;
209
+ }
210
+
211
+ const options: http.RequestOptions & { servername?: string } = {
212
+ hostname: pinnedIP || parsedUrl.hostname,
213
+ port: parsedUrl.port || (isHttps ? 443 : 80),
214
+ path: parsedUrl.pathname + parsedUrl.search,
215
+ method: method.toUpperCase(),
216
+ headers: requestHeaders,
217
+ timeout: timeoutMs,
218
+ };
219
+
220
+ // When using pinned IP with HTTPS, set servername for correct TLS SNI
221
+ if (pinnedIP && isHttps) {
222
+ options.servername = parsedUrl.hostname;
223
+ }
224
+
225
+ // Pin DNS resolution to the pre-validated IP to prevent TOCTOU rebinding
226
+ if (pinnedIP) {
227
+ const family = pinnedIP.includes(':') ? 6 : 4;
228
+ const agent = new (isHttps ? https : http).Agent({
229
+ lookup: (_hostname: string, _options: any, callback: Function) => {
230
+ callback(null, pinnedIP, family);
231
+ },
232
+ } as any);
233
+ options.agent = agent;
234
+ }
235
+
236
+ const req = transport.request(options, (res) => {
237
+ let data = '';
238
+ let receivedBytes = 0;
239
+ let aborted = false;
240
+
241
+ res.on('data', (chunk) => {
242
+ if (aborted) return;
243
+ receivedBytes += Buffer.byteLength(chunk);
244
+ if (receivedBytes > MAX_RESPONSE_BODY_BYTES) {
245
+ aborted = true;
246
+ req.destroy();
247
+ reject(new Error(`Response body exceeds maximum size of ${MAX_RESPONSE_BODY_BYTES} bytes`));
248
+ return;
249
+ }
250
+ data += chunk;
251
+ });
252
+ res.on('end', () => {
253
+ if (aborted) return;
254
+ let parsedBody: unknown = data;
255
+ const contentType = res.headers['content-type'] || '';
256
+ if (contentType.includes('application/json')) {
257
+ try { parsedBody = JSON.parse(data); } catch { parsedBody = data; }
258
+ }
259
+
260
+ // Convert response headers to Record<string, string>
261
+ const responseHeaders: Record<string, string> = {};
262
+ for (const [key, value] of Object.entries(res.headers)) {
263
+ if (value) responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
264
+ }
265
+
266
+ resolve({
267
+ http_status: res.statusCode || 0,
268
+ body: parsedBody,
269
+ headers: responseHeaders,
270
+ });
271
+ });
272
+ });
273
+
274
+ req.on('error', (err) => reject(err));
275
+ req.on('timeout', () => {
276
+ req.destroy();
277
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
278
+ });
279
+
280
+ if (bodyStr) {
281
+ req.write(bodyStr);
282
+ }
283
+ req.end();
284
+ });
285
+ }
286
+
287
+ // Cache key generation
288
+ private getCacheKey(url: string, headers: Record<string, string>): string {
289
+ // Use URL + sorted header keys that affect response (Accept, Authorization)
290
+ const relevantHeaders = ['accept', 'authorization'];
291
+ const headerStr = relevantHeaders
292
+ .map(h => `${h}:${headers[h] || headers[h.charAt(0).toUpperCase() + h.slice(1)] || ''}`)
293
+ .join('|');
294
+ return `${url}|${headerStr}`;
295
+ }
296
+
297
+ // Get from cache
298
+ private getFromCache(key: string): ToolOutput | null {
299
+ const entry = this.cache.get(key);
300
+ if (!entry) return null;
301
+ if (Date.now() > entry.expiresAt) {
302
+ this.cache.delete(key);
303
+ return null;
304
+ }
305
+ return entry.output;
306
+ }
307
+
308
+ // Set cache entry
309
+ private setCache(key: string, output: ToolOutput): void {
310
+ this.cache.set(key, {
311
+ output,
312
+ expiresAt: Date.now() + this.config.cache.ttl_ms,
313
+ });
314
+ // Evict old entries if cache gets too large
315
+ if (this.cache.size > 1000) {
316
+ const oldest = this.cache.keys().next().value;
317
+ if (oldest) this.cache.delete(oldest);
318
+ }
319
+ }
320
+
321
+ // Clear cache
322
+ clearCache(): void {
323
+ this.cache.clear();
324
+ }
325
+
326
+ // Sleep utility
327
+ private sleep(ms: number): Promise<void> {
328
+ return new Promise(resolve => setTimeout(resolve, ms));
329
+ }
330
+ }
@@ -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
+ }