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.
- package/README.md +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- 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
|
+
}
|