@mcp-rating/gateway 0.2.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.
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/ads/ad-tracker.d.ts +63 -0
- package/dist/ads/ad-tracker.js +144 -0
- package/dist/ads/ad-tracker.js.map +1 -0
- package/dist/analytics/usage-tracker.d.ts +67 -0
- package/dist/analytics/usage-tracker.js +148 -0
- package/dist/analytics/usage-tracker.js.map +1 -0
- package/dist/audit/audit-log.d.ts +71 -0
- package/dist/audit/audit-log.js +72 -0
- package/dist/audit/audit-log.js.map +1 -0
- package/dist/config/gateway-config.d.ts +6 -0
- package/dist/config/gateway-config.js +135 -0
- package/dist/config/gateway-config.js.map +1 -0
- package/dist/config/groups.d.ts +29 -0
- package/dist/config/groups.js +102 -0
- package/dist/config/groups.js.map +1 -0
- package/dist/config/profiles.d.ts +42 -0
- package/dist/config/profiles.js +167 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/types.d.ts +76 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/connection/auto-installer.d.ts +13 -0
- package/dist/connection/auto-installer.js +61 -0
- package/dist/connection/auto-installer.js.map +1 -0
- package/dist/connection/connection-manager.d.ts +106 -0
- package/dist/connection/connection-manager.js +845 -0
- package/dist/connection/connection-manager.js.map +1 -0
- package/dist/connection/downstream-client.d.ts +50 -0
- package/dist/connection/downstream-client.js +170 -0
- package/dist/connection/downstream-client.js.map +1 -0
- package/dist/connection/http-client.d.ts +12 -0
- package/dist/connection/http-client.js +26 -0
- package/dist/connection/http-client.js.map +1 -0
- package/dist/connection/sse-client.d.ts +12 -0
- package/dist/connection/sse-client.js +26 -0
- package/dist/connection/sse-client.js.map +1 -0
- package/dist/connection/transport-factory.d.ts +31 -0
- package/dist/connection/transport-factory.js +67 -0
- package/dist/connection/transport-factory.js.map +1 -0
- package/dist/connection/types.d.ts +134 -0
- package/dist/connection/types.js +5 -0
- package/dist/connection/types.js.map +1 -0
- package/dist/connection/ws-client.d.ts +12 -0
- package/dist/connection/ws-client.js +26 -0
- package/dist/connection/ws-client.js.map +1 -0
- package/dist/discovery/auto-recommender.d.ts +51 -0
- package/dist/discovery/auto-recommender.js +176 -0
- package/dist/discovery/auto-recommender.js.map +1 -0
- package/dist/gateway-server.d.ts +42 -0
- package/dist/gateway-server.js +135 -0
- package/dist/gateway-server.js.map +1 -0
- package/dist/http-daemon.d.ts +26 -0
- package/dist/http-daemon.js +302 -0
- package/dist/http-daemon.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/meta-tools/ad-status.d.ts +8 -0
- package/dist/meta-tools/ad-status.js +41 -0
- package/dist/meta-tools/ad-status.js.map +1 -0
- package/dist/meta-tools/audit.d.ts +12 -0
- package/dist/meta-tools/audit.js +98 -0
- package/dist/meta-tools/audit.js.map +1 -0
- package/dist/meta-tools/call-tool.d.ts +11 -0
- package/dist/meta-tools/call-tool.js +65 -0
- package/dist/meta-tools/call-tool.js.map +1 -0
- package/dist/meta-tools/connect.d.ts +3 -0
- package/dist/meta-tools/connect.js +105 -0
- package/dist/meta-tools/connect.js.map +1 -0
- package/dist/meta-tools/disconnect.d.ts +3 -0
- package/dist/meta-tools/disconnect.js +30 -0
- package/dist/meta-tools/disconnect.js.map +1 -0
- package/dist/meta-tools/discover.d.ts +3 -0
- package/dist/meta-tools/discover.js +97 -0
- package/dist/meta-tools/discover.js.map +1 -0
- package/dist/meta-tools/groups.d.ts +7 -0
- package/dist/meta-tools/groups.js +237 -0
- package/dist/meta-tools/groups.js.map +1 -0
- package/dist/meta-tools/health.d.ts +4 -0
- package/dist/meta-tools/health.js +76 -0
- package/dist/meta-tools/health.js.map +1 -0
- package/dist/meta-tools/index.d.ts +28 -0
- package/dist/meta-tools/index.js +56 -0
- package/dist/meta-tools/index.js.map +1 -0
- package/dist/meta-tools/list-active.d.ts +3 -0
- package/dist/meta-tools/list-active.js +50 -0
- package/dist/meta-tools/list-active.js.map +1 -0
- package/dist/meta-tools/profiles.d.ts +6 -0
- package/dist/meta-tools/profiles.js +197 -0
- package/dist/meta-tools/profiles.js.map +1 -0
- package/dist/meta-tools/recommend.d.ts +14 -0
- package/dist/meta-tools/recommend.js +74 -0
- package/dist/meta-tools/recommend.js.map +1 -0
- package/dist/meta-tools/sandbox.d.ts +12 -0
- package/dist/meta-tools/sandbox.js +125 -0
- package/dist/meta-tools/sandbox.js.map +1 -0
- package/dist/meta-tools/server-info.d.ts +4 -0
- package/dist/meta-tools/server-info.js +156 -0
- package/dist/meta-tools/server-info.js.map +1 -0
- package/dist/meta-tools/usage.d.ts +6 -0
- package/dist/meta-tools/usage.js +149 -0
- package/dist/meta-tools/usage.js.map +1 -0
- package/dist/permissions/permission-manager.d.ts +26 -0
- package/dist/permissions/permission-manager.js +73 -0
- package/dist/permissions/permission-manager.js.map +1 -0
- package/dist/permissions/trust-tiers.d.ts +18 -0
- package/dist/permissions/trust-tiers.js +42 -0
- package/dist/permissions/trust-tiers.js.map +1 -0
- package/dist/plugins/builtin/logging-plugin.d.ts +6 -0
- package/dist/plugins/builtin/logging-plugin.js +41 -0
- package/dist/plugins/builtin/logging-plugin.js.map +1 -0
- package/dist/plugins/plugin-manager.d.ts +65 -0
- package/dist/plugins/plugin-manager.js +137 -0
- package/dist/plugins/plugin-manager.js.map +1 -0
- package/dist/proxy/prime-handlers.d.ts +27 -0
- package/dist/proxy/prime-handlers.js +47 -0
- package/dist/proxy/prime-handlers.js.map +1 -0
- package/dist/proxy/progress-forwarder.d.ts +44 -0
- package/dist/proxy/progress-forwarder.js +40 -0
- package/dist/proxy/progress-forwarder.js.map +1 -0
- package/dist/proxy/prompt-router.d.ts +25 -0
- package/dist/proxy/prompt-router.js +110 -0
- package/dist/proxy/prompt-router.js.map +1 -0
- package/dist/proxy/resource-router.d.ts +25 -0
- package/dist/proxy/resource-router.js +91 -0
- package/dist/proxy/resource-router.js.map +1 -0
- package/dist/proxy/schema-adapter.d.ts +11 -0
- package/dist/proxy/schema-adapter.js +36 -0
- package/dist/proxy/schema-adapter.js.map +1 -0
- package/dist/proxy/task-proxy.d.ts +28 -0
- package/dist/proxy/task-proxy.js +186 -0
- package/dist/proxy/task-proxy.js.map +1 -0
- package/dist/proxy/tool-router.d.ts +38 -0
- package/dist/proxy/tool-router.js +229 -0
- package/dist/proxy/tool-router.js.map +1 -0
- package/dist/proxy/ui-detect.d.ts +22 -0
- package/dist/proxy/ui-detect.js +30 -0
- package/dist/proxy/ui-detect.js.map +1 -0
- package/dist/registry/registry-client.d.ts +55 -0
- package/dist/registry/registry-client.js +211 -0
- package/dist/registry/registry-client.js.map +1 -0
- package/dist/registry/types.d.ts +69 -0
- package/dist/registry/types.js +2 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/sandbox/container-runtime.d.ts +91 -0
- package/dist/sandbox/container-runtime.js +178 -0
- package/dist/sandbox/container-runtime.js.map +1 -0
- package/dist/sandbox/egress-proxy.d.ts +51 -0
- package/dist/sandbox/egress-proxy.js +146 -0
- package/dist/sandbox/egress-proxy.js.map +1 -0
- package/dist/sandbox/env-scoper.d.ts +29 -0
- package/dist/sandbox/env-scoper.js +60 -0
- package/dist/sandbox/env-scoper.js.map +1 -0
- package/dist/sandbox/manifest-resolver.d.ts +40 -0
- package/dist/sandbox/manifest-resolver.js +185 -0
- package/dist/sandbox/manifest-resolver.js.map +1 -0
- package/dist/sandbox/types.d.ts +88 -0
- package/dist/sandbox/types.js +21 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/utils/errors.d.ts +39 -0
- package/dist/utils/errors.js +67 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +51 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { log } from "../utils/logger.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export const DEFAULT_IMAGES = {
|
|
6
|
+
node: "node:22-alpine",
|
|
7
|
+
python: "python:3.12-slim",
|
|
8
|
+
default: "node:22-alpine",
|
|
9
|
+
};
|
|
10
|
+
let cachedEngine;
|
|
11
|
+
/**
|
|
12
|
+
* Detect an available container engine (docker preferred, then podman).
|
|
13
|
+
* Result is cached for the process lifetime. Returns null if none is usable.
|
|
14
|
+
*/
|
|
15
|
+
export async function detectContainerEngine() {
|
|
16
|
+
if (cachedEngine !== undefined)
|
|
17
|
+
return cachedEngine;
|
|
18
|
+
for (const engine of ["docker", "podman"]) {
|
|
19
|
+
try {
|
|
20
|
+
await execFileAsync(engine, ["version", "--format", "{{.Client.Version}}"], {
|
|
21
|
+
timeout: 5_000,
|
|
22
|
+
});
|
|
23
|
+
log.info("Container engine detected", { engine });
|
|
24
|
+
cachedEngine = engine;
|
|
25
|
+
return engine;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// try next
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
log.warn("No container engine (docker/podman) available — L2 isolation unavailable");
|
|
32
|
+
cachedEngine = null;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** Reset the cached engine (for tests). */
|
|
36
|
+
export function _resetEngineCache() {
|
|
37
|
+
cachedEngine = undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Deterministic image name for a pre-baked server image. */
|
|
40
|
+
export function bakedImageName(slug) {
|
|
41
|
+
const safe = slug.replace(/[^a-z0-9_.-]/gi, "-").toLowerCase();
|
|
42
|
+
return `mcp-sandbox/${safe}:latest`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check whether a pre-baked image exists locally for this slug. A baked image
|
|
46
|
+
* has the server package installed at build time, so it runs offline under
|
|
47
|
+
* `network: none` without an npx/uvx fetch.
|
|
48
|
+
*
|
|
49
|
+
* Returns { image, bin } if present, else null.
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveBakedImage(engine, slug) {
|
|
52
|
+
const image = bakedImageName(slug);
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execFileAsync(engine, ["image", "inspect", "--format", '{{index .Config.Labels "mcp.sandbox.bin"}}', image], { timeout: 5_000 });
|
|
55
|
+
const bin = stdout.trim() || null;
|
|
56
|
+
log.debug("Using pre-baked server image", { slug, image, bin });
|
|
57
|
+
return { image, bin };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Choose a base image for the given command.
|
|
65
|
+
* - npx / npm / node / yarn / pnpm → node image
|
|
66
|
+
* - uvx / uv / python / python3 / pip → python image
|
|
67
|
+
* - anything else → default image
|
|
68
|
+
*/
|
|
69
|
+
export function selectImage(command, images = DEFAULT_IMAGES) {
|
|
70
|
+
const c = command.toLowerCase().replace(/\.(exe|cmd)$/, "");
|
|
71
|
+
if (["npx", "npm", "node", "yarn", "pnpm"].includes(c))
|
|
72
|
+
return images.node;
|
|
73
|
+
if (["uvx", "uv", "python", "python3", "pip", "pip3"].includes(c))
|
|
74
|
+
return images.python;
|
|
75
|
+
return images.default;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Compute the run command for a pre-baked image. Runs the baked bin directly
|
|
79
|
+
* with any args that followed the package spec in the original invocation.
|
|
80
|
+
* Falls back to the original command if no bin was detected.
|
|
81
|
+
*/
|
|
82
|
+
function bakedRunCommand(baked, command, args) {
|
|
83
|
+
if (!baked.bin) {
|
|
84
|
+
// No bin label — best effort: run original command (may fail offline).
|
|
85
|
+
return { cmd: command, cmdArgs: args };
|
|
86
|
+
}
|
|
87
|
+
// Pass through args that came AFTER the package token. The package token is
|
|
88
|
+
// the first non-flag arg (e.g. "@scope/pkg"); npx flags like -y precede it.
|
|
89
|
+
const pkgIdx = args.findIndex((a) => !a.startsWith("-"));
|
|
90
|
+
const passThrough = pkgIdx >= 0 ? args.slice(pkgIdx + 1) : [];
|
|
91
|
+
return { cmd: baked.bin, cmdArgs: passThrough };
|
|
92
|
+
}
|
|
93
|
+
export function buildContainerCommand(engine, manifest, command, args, opts = {}) {
|
|
94
|
+
const images = opts.images ?? DEFAULT_IMAGES;
|
|
95
|
+
const runArgs = ["run", "--rm", "-i"];
|
|
96
|
+
// ── Hardening (always applied) ──────────────────────────────────────────
|
|
97
|
+
runArgs.push("--cap-drop", "ALL");
|
|
98
|
+
runArgs.push("--security-opt", "no-new-privileges");
|
|
99
|
+
runArgs.push("--read-only");
|
|
100
|
+
// Writable scratch space the server can always use.
|
|
101
|
+
runArgs.push("--tmpfs", "/tmp:rw,exec,nosuid,size=128m");
|
|
102
|
+
// Run as a non-root uid:gid. 1000:1000 is the conventional first user.
|
|
103
|
+
runArgs.push("--user", "1000:1000");
|
|
104
|
+
// ── Network ─────────────────────────────────────────────────────────────
|
|
105
|
+
// NOTE: "allowlist" egress filtering requires a proxy/sidecar and is not yet
|
|
106
|
+
// hard-enforced — it currently maps to the default bridge with the intent
|
|
107
|
+
// declared in the manifest. "none" and "all" are enforced.
|
|
108
|
+
switch (manifest.network.mode) {
|
|
109
|
+
case "none":
|
|
110
|
+
runArgs.push("--network", "none");
|
|
111
|
+
break;
|
|
112
|
+
case "allowlist":
|
|
113
|
+
// Route egress through the gateway's allowlist proxy. The container uses
|
|
114
|
+
// the default bridge but only proxy-aware traffic to allowlisted hosts
|
|
115
|
+
// succeeds; the host.docker.internal mapping lets it reach the proxy.
|
|
116
|
+
if (opts.egressProxyUrl) {
|
|
117
|
+
runArgs.push("--add-host", "host.docker.internal:host-gateway");
|
|
118
|
+
runArgs.push("-e", `HTTPS_PROXY=${opts.egressProxyUrl}`);
|
|
119
|
+
runArgs.push("-e", `HTTP_PROXY=${opts.egressProxyUrl}`);
|
|
120
|
+
runArgs.push("-e", `https_proxy=${opts.egressProxyUrl}`);
|
|
121
|
+
runArgs.push("-e", `http_proxy=${opts.egressProxyUrl}`);
|
|
122
|
+
// Don't proxy localhost / the proxy host itself.
|
|
123
|
+
runArgs.push("-e", "NO_PROXY=localhost,127.0.0.1");
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case "all":
|
|
127
|
+
// default bridge network (egress allowed).
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
// ── Resource limits ─────────────────────────────────────────────────────
|
|
131
|
+
if (manifest.limits?.maxMemoryMb) {
|
|
132
|
+
runArgs.push("--memory", `${manifest.limits.maxMemoryMb}m`);
|
|
133
|
+
}
|
|
134
|
+
if (manifest.limits?.maxCpuPercent) {
|
|
135
|
+
const cpus = Math.max(0.1, manifest.limits.maxCpuPercent / 100);
|
|
136
|
+
runArgs.push("--cpus", cpus.toFixed(2));
|
|
137
|
+
}
|
|
138
|
+
// ── Filesystem mounts ───────────────────────────────────────────────────
|
|
139
|
+
for (const p of manifest.filesystem.read) {
|
|
140
|
+
runArgs.push("-v", `${p}:${p}:ro`);
|
|
141
|
+
}
|
|
142
|
+
for (const p of manifest.filesystem.write) {
|
|
143
|
+
runArgs.push("-v", `${p}:${p}:rw`);
|
|
144
|
+
}
|
|
145
|
+
// ── Writable cache/home redirection ─────────────────────────────────────
|
|
146
|
+
// Under --read-only root, the default HOME (/home/node, /root) and package
|
|
147
|
+
// caches (~/.npm, ~/.cache) are not writable. Redirect them to the tmpfs so
|
|
148
|
+
// npx / uvx can install the server on first run inside the sandbox.
|
|
149
|
+
runArgs.push("-e", "HOME=/tmp");
|
|
150
|
+
runArgs.push("-e", "XDG_CACHE_HOME=/tmp/.cache");
|
|
151
|
+
const image = selectImage(command, images);
|
|
152
|
+
if (image === images.python) {
|
|
153
|
+
runArgs.push("-e", "UV_CACHE_DIR=/tmp/.uv");
|
|
154
|
+
runArgs.push("-e", "PIP_CACHE_DIR=/tmp/.pip");
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
runArgs.push("-e", "npm_config_cache=/tmp/.npm");
|
|
158
|
+
}
|
|
159
|
+
// ── Environment forwarding (names only; values come from spawn env) ──────
|
|
160
|
+
const forwardedEnvNames = [];
|
|
161
|
+
for (const name of manifest.env.allow) {
|
|
162
|
+
runArgs.push("-e", name);
|
|
163
|
+
forwardedEnvNames.push(name);
|
|
164
|
+
}
|
|
165
|
+
// ── Image + the actual server command ───────────────────────────────────
|
|
166
|
+
// A pre-baked image already contains the package, so run its bin offline.
|
|
167
|
+
if (opts.bakedImage) {
|
|
168
|
+
const { cmd, cmdArgs } = bakedRunCommand(opts.bakedImage, command, args);
|
|
169
|
+
runArgs.push(opts.bakedImage.image);
|
|
170
|
+
runArgs.push(cmd, ...cmdArgs);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
runArgs.push(image);
|
|
174
|
+
runArgs.push(command, ...args);
|
|
175
|
+
}
|
|
176
|
+
return { command: engine, args: runArgs, forwardedEnvNames };
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=container-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-runtime.js","sourceRoot":"","sources":["../../src/sandbox/container-runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match a "host:port" target against an allowlist pattern.
|
|
3
|
+
*
|
|
4
|
+
* Supported patterns:
|
|
5
|
+
* - "api.github.com:443" exact host + port
|
|
6
|
+
* - "api.github.com" exact host, any port
|
|
7
|
+
* - "*.githubusercontent.com" wildcard subdomain, any port
|
|
8
|
+
* - "*.example.com:443" wildcard subdomain + port
|
|
9
|
+
*/
|
|
10
|
+
export declare function matchesAllowlist(host: string, port: number, allowlist: string[]): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* An in-process HTTP forward proxy that enforces a host allowlist via the
|
|
13
|
+
* CONNECT method (used by HTTPS clients). Connections to non-allowlisted
|
|
14
|
+
* hosts are refused with 403.
|
|
15
|
+
*
|
|
16
|
+
* Containerized servers are pointed at this proxy via HTTPS_PROXY/HTTP_PROXY
|
|
17
|
+
* and reach it through `host.docker.internal`. This gives `network: allowlist`
|
|
18
|
+
* real teeth without a container sidecar.
|
|
19
|
+
*
|
|
20
|
+
* Limitation: only proxy-aware clients (those honoring HTTP(S)_PROXY) are
|
|
21
|
+
* filtered. Clients that ignore proxy env vars bypass it — full enforcement
|
|
22
|
+
* still requires L2 network namespacing, but this covers the common case
|
|
23
|
+
* (node fetch/undici, axios, requests, etc. all honor the proxy env).
|
|
24
|
+
*/
|
|
25
|
+
export declare class EgressProxy {
|
|
26
|
+
private readonly allowlist;
|
|
27
|
+
private readonly slug;
|
|
28
|
+
/** Optional safety audit log — records blocked/allowed egress for forensics. */
|
|
29
|
+
private readonly auditLog?;
|
|
30
|
+
private server;
|
|
31
|
+
private port;
|
|
32
|
+
private blockedCount;
|
|
33
|
+
constructor(allowlist: string[], slug: string,
|
|
34
|
+
/** Optional safety audit log — records blocked/allowed egress for forensics. */
|
|
35
|
+
auditLog?: {
|
|
36
|
+
record: (e: {
|
|
37
|
+
type: "egress_blocked" | "egress_allowed";
|
|
38
|
+
slug: string;
|
|
39
|
+
target?: string;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}) => void;
|
|
42
|
+
} | undefined);
|
|
43
|
+
/** Start listening on a random loopback port. Resolves with the port. */
|
|
44
|
+
start(): Promise<number>;
|
|
45
|
+
private handleConnect;
|
|
46
|
+
/** The proxy URL the container should use (via host.docker.internal). */
|
|
47
|
+
get containerProxyUrl(): string;
|
|
48
|
+
get blocked(): number;
|
|
49
|
+
/** Stop the proxy. */
|
|
50
|
+
stop(): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { connect as netConnect } from "node:net";
|
|
3
|
+
import { log } from "../utils/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Match a "host:port" target against an allowlist pattern.
|
|
6
|
+
*
|
|
7
|
+
* Supported patterns:
|
|
8
|
+
* - "api.github.com:443" exact host + port
|
|
9
|
+
* - "api.github.com" exact host, any port
|
|
10
|
+
* - "*.githubusercontent.com" wildcard subdomain, any port
|
|
11
|
+
* - "*.example.com:443" wildcard subdomain + port
|
|
12
|
+
*/
|
|
13
|
+
export function matchesAllowlist(host, port, allowlist) {
|
|
14
|
+
const h = host.toLowerCase();
|
|
15
|
+
for (const raw of allowlist) {
|
|
16
|
+
const pattern = raw.toLowerCase().trim();
|
|
17
|
+
if (!pattern)
|
|
18
|
+
continue;
|
|
19
|
+
const [patHost, patPort] = pattern.includes(":")
|
|
20
|
+
? pattern.split(":")
|
|
21
|
+
: [pattern, undefined];
|
|
22
|
+
// Port check (if the pattern specified one)
|
|
23
|
+
if (patPort !== undefined && patPort !== String(port))
|
|
24
|
+
continue;
|
|
25
|
+
// Host check — exact or wildcard
|
|
26
|
+
if (patHost.startsWith("*.")) {
|
|
27
|
+
const suffix = patHost.slice(1); // ".githubusercontent.com"
|
|
28
|
+
if (h.endsWith(suffix) || h === patHost.slice(2))
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
else if (h === patHost) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* An in-process HTTP forward proxy that enforces a host allowlist via the
|
|
39
|
+
* CONNECT method (used by HTTPS clients). Connections to non-allowlisted
|
|
40
|
+
* hosts are refused with 403.
|
|
41
|
+
*
|
|
42
|
+
* Containerized servers are pointed at this proxy via HTTPS_PROXY/HTTP_PROXY
|
|
43
|
+
* and reach it through `host.docker.internal`. This gives `network: allowlist`
|
|
44
|
+
* real teeth without a container sidecar.
|
|
45
|
+
*
|
|
46
|
+
* Limitation: only proxy-aware clients (those honoring HTTP(S)_PROXY) are
|
|
47
|
+
* filtered. Clients that ignore proxy env vars bypass it — full enforcement
|
|
48
|
+
* still requires L2 network namespacing, but this covers the common case
|
|
49
|
+
* (node fetch/undici, axios, requests, etc. all honor the proxy env).
|
|
50
|
+
*/
|
|
51
|
+
export class EgressProxy {
|
|
52
|
+
allowlist;
|
|
53
|
+
slug;
|
|
54
|
+
auditLog;
|
|
55
|
+
server = null;
|
|
56
|
+
port = 0;
|
|
57
|
+
blockedCount = 0;
|
|
58
|
+
constructor(allowlist, slug,
|
|
59
|
+
/** Optional safety audit log — records blocked/allowed egress for forensics. */
|
|
60
|
+
auditLog) {
|
|
61
|
+
this.allowlist = allowlist;
|
|
62
|
+
this.slug = slug;
|
|
63
|
+
this.auditLog = auditLog;
|
|
64
|
+
}
|
|
65
|
+
/** Start listening on a random loopback port. Resolves with the port. */
|
|
66
|
+
async start() {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const server = createServer((req, res) => {
|
|
69
|
+
// Plain HTTP proxying is not supported — only CONNECT (HTTPS tunnel).
|
|
70
|
+
// Reject non-CONNECT requests; most MCP servers use HTTPS APIs.
|
|
71
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
72
|
+
res.end("Only CONNECT (HTTPS) is proxied by the gateway egress filter.\n");
|
|
73
|
+
});
|
|
74
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
75
|
+
this.handleConnect(req, clientSocket, head);
|
|
76
|
+
});
|
|
77
|
+
server.on("error", (err) => reject(err));
|
|
78
|
+
// Bind to loopback only — the container reaches it via host.docker.internal,
|
|
79
|
+
// which Docker maps to the host loopback.
|
|
80
|
+
server.listen(0, "0.0.0.0", () => {
|
|
81
|
+
const addr = server.address();
|
|
82
|
+
this.port = typeof addr === "object" && addr ? addr.port : 0;
|
|
83
|
+
this.server = server;
|
|
84
|
+
log.info("Egress proxy started", {
|
|
85
|
+
slug: this.slug,
|
|
86
|
+
port: this.port,
|
|
87
|
+
allowlist: this.allowlist,
|
|
88
|
+
});
|
|
89
|
+
resolve(this.port);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
handleConnect(req, clientSocket, head) {
|
|
94
|
+
// req.url for CONNECT is "host:port"
|
|
95
|
+
const target = req.url || "";
|
|
96
|
+
const [host, portStr] = target.split(":");
|
|
97
|
+
const port = parseInt(portStr || "443", 10);
|
|
98
|
+
if (!host || !matchesAllowlist(host, port, this.allowlist)) {
|
|
99
|
+
this.blockedCount++;
|
|
100
|
+
log.warn("Egress blocked by allowlist", {
|
|
101
|
+
slug: this.slug,
|
|
102
|
+
target,
|
|
103
|
+
blockedTotal: this.blockedCount,
|
|
104
|
+
});
|
|
105
|
+
this.auditLog?.record({
|
|
106
|
+
type: "egress_blocked",
|
|
107
|
+
slug: this.slug,
|
|
108
|
+
target,
|
|
109
|
+
reason: "not in network allowlist",
|
|
110
|
+
});
|
|
111
|
+
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\nBlocked by MCP gateway egress allowlist\r\n");
|
|
112
|
+
clientSocket.end();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.auditLog?.record({ type: "egress_allowed", slug: this.slug, target });
|
|
116
|
+
// Allowed — open the upstream tunnel and pipe bytes both ways.
|
|
117
|
+
const upstream = netConnect(port, host, () => {
|
|
118
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
119
|
+
if (head && head.length)
|
|
120
|
+
upstream.write(head);
|
|
121
|
+
upstream.pipe(clientSocket);
|
|
122
|
+
clientSocket.pipe(upstream);
|
|
123
|
+
});
|
|
124
|
+
upstream.on("error", (err) => {
|
|
125
|
+
log.debug("Egress upstream error", { slug: this.slug, target, error: String(err) });
|
|
126
|
+
clientSocket.end();
|
|
127
|
+
});
|
|
128
|
+
clientSocket.on("error", () => upstream.end());
|
|
129
|
+
}
|
|
130
|
+
/** The proxy URL the container should use (via host.docker.internal). */
|
|
131
|
+
get containerProxyUrl() {
|
|
132
|
+
return `http://host.docker.internal:${this.port}`;
|
|
133
|
+
}
|
|
134
|
+
get blocked() {
|
|
135
|
+
return this.blockedCount;
|
|
136
|
+
}
|
|
137
|
+
/** Stop the proxy. */
|
|
138
|
+
async stop() {
|
|
139
|
+
if (!this.server)
|
|
140
|
+
return;
|
|
141
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
142
|
+
this.server = null;
|
|
143
|
+
log.debug("Egress proxy stopped", { slug: this.slug });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=egress-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress-proxy.js","sourceRoot":"","sources":["../../src/sandbox/egress-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EnvPolicy } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build the scoped environment for a downstream stdio process.
|
|
4
|
+
*
|
|
5
|
+
* This is the first hard-enforced sandbox control (L1). Instead of passing the
|
|
6
|
+
* full inherited environment (which can leak API keys, cloud credentials, and
|
|
7
|
+
* tokens sitting in the parent process's env), we pass ONLY:
|
|
8
|
+
*
|
|
9
|
+
* 1. The SDK's safe default vars (PATH, HOME, etc.) — when `inheritDefaults`
|
|
10
|
+
* is true and the process needs them to start.
|
|
11
|
+
* 2. The explicitly allowlisted var NAMES from the manifest.
|
|
12
|
+
* 3. The user-supplied env values for this connection (always honored — the
|
|
13
|
+
* user clearly intends those to flow to this server).
|
|
14
|
+
*
|
|
15
|
+
* Everything else in `process.env` is withheld.
|
|
16
|
+
*
|
|
17
|
+
* @param policy Env policy from the resolved capability manifest
|
|
18
|
+
* @param userEnv Env values the user passed at connect time
|
|
19
|
+
* @param sourceEnv The environment to scope from (defaults to process.env)
|
|
20
|
+
* @returns The scoped environment object to hand to StdioClientTransport
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildScopedEnvironment(policy: EnvPolicy, userEnv: Record<string, string> | undefined, sourceEnv?: NodeJS.ProcessEnv): Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* Returns the names of environment variables that WOULD have been exposed
|
|
25
|
+
* under the old "inherit everything" behavior but are now withheld. Used for
|
|
26
|
+
* surfacing the security improvement to the user / logs without ever printing
|
|
27
|
+
* the values themselves.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withheldEnvNames(scoped: Record<string, string>, sourceEnv?: NodeJS.ProcessEnv): string[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2
|
+
import { log } from "../utils/logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the scoped environment for a downstream stdio process.
|
|
5
|
+
*
|
|
6
|
+
* This is the first hard-enforced sandbox control (L1). Instead of passing the
|
|
7
|
+
* full inherited environment (which can leak API keys, cloud credentials, and
|
|
8
|
+
* tokens sitting in the parent process's env), we pass ONLY:
|
|
9
|
+
*
|
|
10
|
+
* 1. The SDK's safe default vars (PATH, HOME, etc.) — when `inheritDefaults`
|
|
11
|
+
* is true and the process needs them to start.
|
|
12
|
+
* 2. The explicitly allowlisted var NAMES from the manifest.
|
|
13
|
+
* 3. The user-supplied env values for this connection (always honored — the
|
|
14
|
+
* user clearly intends those to flow to this server).
|
|
15
|
+
*
|
|
16
|
+
* Everything else in `process.env` is withheld.
|
|
17
|
+
*
|
|
18
|
+
* @param policy Env policy from the resolved capability manifest
|
|
19
|
+
* @param userEnv Env values the user passed at connect time
|
|
20
|
+
* @param sourceEnv The environment to scope from (defaults to process.env)
|
|
21
|
+
* @returns The scoped environment object to hand to StdioClientTransport
|
|
22
|
+
*/
|
|
23
|
+
export function buildScopedEnvironment(policy, userEnv, sourceEnv = process.env) {
|
|
24
|
+
const scoped = {};
|
|
25
|
+
// 1. Safe defaults required for the process to launch (PATH, HOME, etc.)
|
|
26
|
+
if (policy.inheritDefaults) {
|
|
27
|
+
Object.assign(scoped, getDefaultEnvironment());
|
|
28
|
+
}
|
|
29
|
+
// 2. Allowlisted variable names pulled from the source environment.
|
|
30
|
+
let leakedBlocked = 0;
|
|
31
|
+
for (const name of policy.allow) {
|
|
32
|
+
const value = sourceEnv[name];
|
|
33
|
+
if (value !== undefined && !value.startsWith("()")) {
|
|
34
|
+
scoped[name] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 3. User-supplied env always flows (explicit intent for this connection).
|
|
38
|
+
if (userEnv) {
|
|
39
|
+
Object.assign(scoped, userEnv);
|
|
40
|
+
}
|
|
41
|
+
// Diagnostics: how many parent-env vars were withheld (not the values).
|
|
42
|
+
const withheld = Object.keys(sourceEnv).filter((k) => !(k in scoped));
|
|
43
|
+
leakedBlocked = withheld.length;
|
|
44
|
+
log.debug("Scoped downstream environment", {
|
|
45
|
+
allowed: Object.keys(scoped).length,
|
|
46
|
+
withheld: leakedBlocked,
|
|
47
|
+
inheritDefaults: policy.inheritDefaults,
|
|
48
|
+
});
|
|
49
|
+
return scoped;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns the names of environment variables that WOULD have been exposed
|
|
53
|
+
* under the old "inherit everything" behavior but are now withheld. Used for
|
|
54
|
+
* surfacing the security improvement to the user / logs without ever printing
|
|
55
|
+
* the values themselves.
|
|
56
|
+
*/
|
|
57
|
+
export function withheldEnvNames(scoped, sourceEnv = process.env) {
|
|
58
|
+
return Object.keys(sourceEnv).filter((k) => !(k in scoped));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=env-scoper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-scoper.js","sourceRoot":"","sources":["../../src/sandbox/env-scoper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2CAA2C,CAAC;AAClF,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAiB,EACjB,OAA2C,EAC3C,YAA+B,OAAO,CAAC,GAAG;IAE1C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAE1C,yEAAyE;IACzE,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QAC3B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,oEAAoE;IACpE,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAED,wEAAwE;IACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAC5C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CACtB,CAAC;IACF,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC;IAChC,GAAG,CAAC,KAAK,CAAC,+BAA+B,EAAE;QACzC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM;QACnC,QAAQ,EAAE,aAAa;QACvB,eAAe,EAAE,MAAM,CAAC,eAAe;KACxC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAA8B,EAC9B,YAA+B,OAAO,CAAC,GAAG;IAE1C,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TrustTier, RegistryServer } from "../registry/types.js";
|
|
2
|
+
import type { CapabilityManifest, ManifestSummary } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves and persists per-server capability manifests.
|
|
5
|
+
*
|
|
6
|
+
* A manifest is derived from (in priority order):
|
|
7
|
+
* 1. A user-saved override (persisted to disk)
|
|
8
|
+
* 2. Trust-tier defaults + the server's declared auth env vars
|
|
9
|
+
*/
|
|
10
|
+
export declare class ManifestResolver {
|
|
11
|
+
private overrides;
|
|
12
|
+
private readonly filePath;
|
|
13
|
+
/**
|
|
14
|
+
* When true, untrusted tiers (community/unknown) default to l2-container
|
|
15
|
+
* enforcement instead of l1-process. Verified/trusted stay on L1.
|
|
16
|
+
*/
|
|
17
|
+
private readonly containerIsolation;
|
|
18
|
+
constructor(filePath: string, containerIsolation?: boolean);
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the effective manifest for a server. Uses a saved override if one
|
|
21
|
+
* exists; otherwise derives defaults from the trust tier and the server's
|
|
22
|
+
* declared auth env vars (so e.g. a GitHub server gets GITHUB_TOKEN allowed
|
|
23
|
+
* but nothing else).
|
|
24
|
+
*/
|
|
25
|
+
resolve(slug: string, trustTier: TrustTier, registryServer?: RegistryServer | null,
|
|
26
|
+
/** Env var names the user explicitly supplied at connect time. */
|
|
27
|
+
userProvidedEnvKeys?: string[]): CapabilityManifest;
|
|
28
|
+
/** Get a saved override for a slug, if one exists. */
|
|
29
|
+
getOverride(slug: string): CapabilityManifest | undefined;
|
|
30
|
+
/** Persist a user-customized manifest. */
|
|
31
|
+
saveOverride(manifest: CapabilityManifest): void;
|
|
32
|
+
/** Remove a saved override, reverting to trust-tier defaults. */
|
|
33
|
+
clearOverride(slug: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Build a human-readable summary of a manifest for the HITL prompt.
|
|
36
|
+
*/
|
|
37
|
+
static summarize(manifest: CapabilityManifest): ManifestSummary;
|
|
38
|
+
private loadFromDisk;
|
|
39
|
+
private saveToDisk;
|
|
40
|
+
}
|