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