@solongate/proxy 0.24.0 → 0.25.1

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 (2) hide show
  1. package/dist/index.js +270 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -133,6 +133,11 @@ function parseArgs(argv) {
133
133
  let port;
134
134
  let policyId;
135
135
  let advancedDetection = true;
136
+ let aiJudgeEnabled = false;
137
+ let aiJudgeModel = "llama-3.1-8b-instant";
138
+ let aiJudgeEndpoint = "https://api.groq.com/openai";
139
+ let aiJudgeApiKey;
140
+ let aiJudgeTimeout = 5e3;
136
141
  let separatorIndex = args.indexOf("--");
137
142
  const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
138
143
  const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
@@ -178,8 +183,36 @@ function parseArgs(argv) {
178
183
  case "--no-advanced-detection":
179
184
  advancedDetection = false;
180
185
  break;
186
+ case "--ai-judge":
187
+ aiJudgeEnabled = true;
188
+ break;
189
+ case "--ai-judge-model":
190
+ aiJudgeModel = flags[++i];
191
+ aiJudgeEnabled = true;
192
+ break;
193
+ case "--ai-judge-endpoint":
194
+ aiJudgeEndpoint = flags[++i];
195
+ aiJudgeEnabled = true;
196
+ break;
197
+ case "--ai-judge-api-key":
198
+ aiJudgeApiKey = flags[++i];
199
+ aiJudgeEnabled = true;
200
+ break;
201
+ case "--ai-judge-timeout":
202
+ aiJudgeTimeout = parseInt(flags[++i], 10);
203
+ break;
181
204
  }
182
205
  }
206
+ if (!aiJudgeApiKey) {
207
+ aiJudgeApiKey = process.env.GROQ_API_KEY;
208
+ }
209
+ const aiJudge = aiJudgeEnabled ? {
210
+ enabled: true,
211
+ model: aiJudgeModel,
212
+ endpoint: aiJudgeEndpoint,
213
+ apiKey: aiJudgeApiKey,
214
+ timeoutMs: aiJudgeTimeout
215
+ } : void 0;
183
216
  if (apiKey && /^\$\{.+\}$/.test(apiKey)) {
184
217
  apiKey = void 0;
185
218
  }
@@ -227,7 +260,8 @@ function parseArgs(argv) {
227
260
  port: port ?? fileConfig.port,
228
261
  policyPath: resolvePolicyPath(cfgPolicySource) ?? void 0,
229
262
  policyId: policyId ?? fileConfig.policyId,
230
- advancedDetection: advancedDetection ? { enabled: true } : void 0
263
+ advancedDetection: advancedDetection ? { enabled: true } : void 0,
264
+ aiJudge
231
265
  };
232
266
  }
233
267
  if (upstreamUrl) {
@@ -249,7 +283,8 @@ function parseArgs(argv) {
249
283
  port,
250
284
  policyPath: resolvedPolicyPath ?? void 0,
251
285
  policyId,
252
- advancedDetection: advancedDetection ? { enabled: true } : void 0
286
+ advancedDetection: advancedDetection ? { enabled: true } : void 0,
287
+ aiJudge
253
288
  };
254
289
  }
255
290
  if (upstreamArgs.length === 0) {
@@ -275,7 +310,8 @@ function parseArgs(argv) {
275
310
  port,
276
311
  policyPath: resolvedPolicyPath ?? void 0,
277
312
  policyId,
278
- advancedDetection: advancedDetection ? { enabled: true } : void 0
313
+ advancedDetection: advancedDetection ? { enabled: true } : void 0,
314
+ aiJudge
279
315
  };
280
316
  }
281
317
  function resolvePolicyPath(source) {
@@ -6066,6 +6102,179 @@ var PolicySyncManager = class {
6066
6102
  }
6067
6103
  };
6068
6104
 
6105
+ // src/ai-judge.ts
6106
+ var SYSTEM_PROMPT = `You are a security judge for an MCP (Model Context Protocol) proxy. You evaluate tool calls and decide if they should be ALLOWED or DENIED.
6107
+
6108
+ You will receive a JSON object with:
6109
+ - "tool": the MCP tool name being called
6110
+ - "arguments": the tool's arguments
6111
+ - "protected_files": list of files that must NEVER be read, written, copied, moved, or accessed
6112
+ - "protected_paths": list of directories/paths that must NEVER be accessed
6113
+ - "denied_actions": list of actions that are never allowed
6114
+
6115
+ DENY if the tool call could, directly or indirectly:
6116
+ - Read, display, copy, move, or exfiltrate any protected file \u2014 even through:
6117
+ - Shell glob patterns (e.g., "cred*" could match "credentials.json")
6118
+ - Command substitution ($(...), backticks)
6119
+ - Variable interpolation or string concatenation
6120
+ - Input redirection (< file)
6121
+ - Script files that might read protected files
6122
+ - Find/exec patterns
6123
+ - Any Unix/Windows utility that reads file content (cat, head, tail, less, more, type, perl, awk, sed, sort, diff, comm, paste, tee, xxd, od, strings, dd, tr, etc.)
6124
+ - Delete, modify, or overwrite any protected file or path
6125
+ - Send data to external URLs, webhooks, or attacker-controlled endpoints
6126
+ - Execute remotely downloaded code (curl|bash, wget|sh, etc.)
6127
+ - Leak environment variables (printenv, env, /proc/self/environ, npm run env, process.env)
6128
+ - Create scripts that bypass security controls
6129
+
6130
+ ALLOW if:
6131
+ - The action is a normal development operation (ls, git status, npm build, etc.)
6132
+ - The action does not touch any protected file or path
6133
+ - The action is clearly benign (creating hello.js, reading public docs, etc.)
6134
+
6135
+ When in doubt, DENY. False positives are acceptable; false negatives are not.
6136
+
6137
+ Respond with ONLY valid JSON, no markdown, no explanation outside the JSON:
6138
+ {"decision": "ALLOW" or "DENY", "reason": "brief one-line explanation", "confidence": 0.0 to 1.0}`;
6139
+ var AiJudge = class {
6140
+ config;
6141
+ protectedFiles;
6142
+ protectedPaths;
6143
+ deniedActions;
6144
+ isOllamaEndpoint;
6145
+ constructor(config, protectedFiles, protectedPaths, deniedActions = [
6146
+ "file deletion",
6147
+ "data exfiltration",
6148
+ "remote code execution",
6149
+ "environment variable leak",
6150
+ "security control bypass"
6151
+ ]) {
6152
+ this.config = config;
6153
+ this.protectedFiles = protectedFiles;
6154
+ this.protectedPaths = protectedPaths;
6155
+ this.deniedActions = deniedActions;
6156
+ this.isOllamaEndpoint = config.endpoint.includes("11434") || config.endpoint.includes("ollama");
6157
+ }
6158
+ /**
6159
+ * Evaluate a tool call. Returns ALLOW or DENY verdict.
6160
+ * Fail-closed: any error (timeout, parse failure, connection refused) → DENY.
6161
+ */
6162
+ async evaluate(toolName, args) {
6163
+ const userMessage = JSON.stringify({
6164
+ tool: toolName,
6165
+ arguments: args,
6166
+ protected_files: this.protectedFiles,
6167
+ protected_paths: this.protectedPaths,
6168
+ denied_actions: this.deniedActions
6169
+ });
6170
+ try {
6171
+ const response = await this.callLLM(userMessage);
6172
+ return this.parseVerdict(response);
6173
+ } catch (err) {
6174
+ const message = err instanceof Error ? err.message : String(err);
6175
+ return {
6176
+ decision: "DENY",
6177
+ reason: `AI Judge error (fail-closed): ${message}`,
6178
+ confidence: 1
6179
+ };
6180
+ }
6181
+ }
6182
+ /**
6183
+ * Call the LLM endpoint. Supports Groq, OpenAI, and Ollama.
6184
+ */
6185
+ async callLLM(userMessage) {
6186
+ const controller = new AbortController();
6187
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
6188
+ try {
6189
+ let url;
6190
+ let body;
6191
+ const headers = { "Content-Type": "application/json" };
6192
+ if (this.isOllamaEndpoint) {
6193
+ url = `${this.config.endpoint}/api/chat`;
6194
+ body = JSON.stringify({
6195
+ model: this.config.model,
6196
+ messages: [
6197
+ { role: "system", content: SYSTEM_PROMPT },
6198
+ { role: "user", content: userMessage }
6199
+ ],
6200
+ stream: false,
6201
+ options: { temperature: 0, num_predict: 200 }
6202
+ });
6203
+ } else {
6204
+ url = `${this.config.endpoint}/v1/chat/completions`;
6205
+ body = JSON.stringify({
6206
+ model: this.config.model,
6207
+ messages: [
6208
+ { role: "system", content: SYSTEM_PROMPT },
6209
+ { role: "user", content: userMessage }
6210
+ ],
6211
+ temperature: 0,
6212
+ max_tokens: 200
6213
+ });
6214
+ if (this.config.apiKey) {
6215
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
6216
+ }
6217
+ }
6218
+ const res = await fetch(url, {
6219
+ method: "POST",
6220
+ headers,
6221
+ body,
6222
+ signal: controller.signal
6223
+ });
6224
+ if (!res.ok) {
6225
+ const errBody = await res.text().catch(() => "");
6226
+ throw new Error(`LLM endpoint returned ${res.status}: ${errBody.slice(0, 200)}`);
6227
+ }
6228
+ const data = await res.json();
6229
+ if (this.isOllamaEndpoint) {
6230
+ const message = data.message;
6231
+ return message?.content ?? "";
6232
+ } else {
6233
+ const choices = data.choices;
6234
+ const first = choices?.[0];
6235
+ const message = first?.message;
6236
+ return message?.content ?? "";
6237
+ }
6238
+ } finally {
6239
+ clearTimeout(timeout);
6240
+ }
6241
+ }
6242
+ /**
6243
+ * Parse the LLM response into a structured verdict.
6244
+ * If parsing fails → DENY (fail-closed).
6245
+ */
6246
+ parseVerdict(response) {
6247
+ try {
6248
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
6249
+ if (!jsonMatch) {
6250
+ return {
6251
+ decision: "DENY",
6252
+ reason: `AI Judge could not parse response (fail-closed): ${response.slice(0, 100)}`,
6253
+ confidence: 1
6254
+ };
6255
+ }
6256
+ const parsed = JSON.parse(jsonMatch[0]);
6257
+ const decision = String(parsed.decision ?? "").toUpperCase();
6258
+ const reason = String(parsed.reason ?? "no reason provided");
6259
+ const confidence = typeof parsed.confidence === "number" ? Math.min(1, Math.max(0, parsed.confidence)) : 0.5;
6260
+ if (decision !== "ALLOW" && decision !== "DENY") {
6261
+ return {
6262
+ decision: "DENY",
6263
+ reason: `AI Judge returned invalid decision "${decision}" (fail-closed)`,
6264
+ confidence: 1
6265
+ };
6266
+ }
6267
+ return { decision, reason, confidence };
6268
+ } catch {
6269
+ return {
6270
+ decision: "DENY",
6271
+ reason: `AI Judge JSON parse error (fail-closed): ${response.slice(0, 100)}`,
6272
+ confidence: 1
6273
+ };
6274
+ }
6275
+ }
6276
+ };
6277
+
6069
6278
  // src/proxy.ts
6070
6279
  var log2 = (...args) => process.stderr.write(`[SolonGate] ${args.map(String).join(" ")}
6071
6280
  `);
@@ -6117,6 +6326,7 @@ var SolonGateProxy = class {
6117
6326
  server = null;
6118
6327
  toolMutexes = new ToolMutexMap();
6119
6328
  syncManager = null;
6329
+ aiJudge = null;
6120
6330
  upstreamTools = [];
6121
6331
  constructor(config) {
6122
6332
  this.config = config;
@@ -6198,6 +6408,16 @@ var SolonGateProxy = class {
6198
6408
  this.registerToolsToCloud();
6199
6409
  this.registerServerToCloud();
6200
6410
  this.startPolicySync();
6411
+ if (this.config.aiJudge?.enabled) {
6412
+ const protectedFiles = this.extractProtectedFiles();
6413
+ const protectedPaths = this.extractProtectedPaths();
6414
+ this.aiJudge = new AiJudge(
6415
+ this.config.aiJudge,
6416
+ protectedFiles,
6417
+ protectedPaths
6418
+ );
6419
+ log2(`AI Judge enabled \u2014 model: ${this.config.aiJudge.model}, endpoint: ${this.config.aiJudge.endpoint}`);
6420
+ }
6201
6421
  this.createServer();
6202
6422
  await this.serve();
6203
6423
  }
@@ -6360,6 +6580,23 @@ var SolonGateProxy = class {
6360
6580
  { name, arguments: args ?? {} },
6361
6581
  async (params) => {
6362
6582
  if (!this.client) throw new Error("Upstream client disconnected");
6583
+ if (this.aiJudge) {
6584
+ const verdict = await this.aiJudge.evaluate(
6585
+ params.name,
6586
+ params.arguments ?? {}
6587
+ );
6588
+ if (verdict.decision === "DENY") {
6589
+ log2(`AI Judge DENY: ${params.name} \u2014 ${verdict.reason} (confidence: ${verdict.confidence})`);
6590
+ return {
6591
+ content: [{
6592
+ type: "text",
6593
+ text: `[SolonGate AI Judge] Blocked: ${verdict.reason}`
6594
+ }],
6595
+ isError: true
6596
+ };
6597
+ }
6598
+ log2(`AI Judge ALLOW: ${params.name} \u2014 ${verdict.reason}`);
6599
+ }
6363
6600
  const upstreamResult = await this.client.callTool({
6364
6601
  name: params.name,
6365
6602
  arguments: params.arguments
@@ -6609,6 +6846,36 @@ ${msg.content.text}`;
6609
6846
  * - Polls cloud API for dashboard changes → writes to local policy.json
6610
6847
  * - Version number determines which is newer (higher wins, cloud wins on tie)
6611
6848
  */
6849
+ /**
6850
+ * Extract protected filenames from policy DENY rules (filenameConstraints.denied).
6851
+ */
6852
+ extractProtectedFiles() {
6853
+ const files = /* @__PURE__ */ new Set();
6854
+ for (const rule of this.config.policy.rules) {
6855
+ if (rule.effect === "DENY" && rule.enabled !== false) {
6856
+ const denied = rule.filenameConstraints?.denied;
6857
+ if (denied) {
6858
+ for (const f of denied) files.add(f);
6859
+ }
6860
+ }
6861
+ }
6862
+ return [...files];
6863
+ }
6864
+ /**
6865
+ * Extract protected paths from policy DENY rules (pathConstraints.denied).
6866
+ */
6867
+ extractProtectedPaths() {
6868
+ const paths = /* @__PURE__ */ new Set();
6869
+ for (const rule of this.config.policy.rules) {
6870
+ if (rule.effect === "DENY" && rule.enabled !== false) {
6871
+ const denied = rule.pathConstraints?.denied;
6872
+ if (denied) {
6873
+ for (const p of denied) paths.add(p);
6874
+ }
6875
+ }
6876
+ }
6877
+ return [...paths];
6878
+ }
6612
6879
  startPolicySync() {
6613
6880
  const apiKey = this.config.apiKey;
6614
6881
  if (!apiKey) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "MCP security proxy — protect any MCP server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
5
5
  "type": "module",
6
6
  "bin": {