@solongate/proxy 0.26.4 → 0.27.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/dist/index.js +115 -24
- package/dist/init.js +3 -2
- package/dist/lib.js +102 -19
- package/hooks/guard.mjs +45 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,7 +47,11 @@ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
|
|
|
47
47
|
}
|
|
48
48
|
async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
49
49
|
const url = `${apiUrl}/api/v1/audit-logs`;
|
|
50
|
-
const body = JSON.stringify(
|
|
50
|
+
const body = JSON.stringify({
|
|
51
|
+
...entry,
|
|
52
|
+
agent_id: entry.agent_id,
|
|
53
|
+
agent_name: entry.agent_name
|
|
54
|
+
});
|
|
51
55
|
for (let attempt = 0; attempt < AUDIT_MAX_RETRIES; attempt++) {
|
|
52
56
|
try {
|
|
53
57
|
const res = await fetch(url, {
|
|
@@ -165,6 +169,7 @@ function parseArgs(argv) {
|
|
|
165
169
|
let aiJudgeEndpoint = "https://api.groq.com/openai";
|
|
166
170
|
let aiJudgeApiKey;
|
|
167
171
|
let aiJudgeTimeout = 5e3;
|
|
172
|
+
let agentName;
|
|
168
173
|
let separatorIndex = args.indexOf("--");
|
|
169
174
|
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
170
175
|
const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
@@ -228,6 +233,9 @@ function parseArgs(argv) {
|
|
|
228
233
|
case "--ai-judge-timeout":
|
|
229
234
|
aiJudgeTimeout = parseInt(flags[++i], 10);
|
|
230
235
|
break;
|
|
236
|
+
case "--agent-name":
|
|
237
|
+
agentName = flags[++i];
|
|
238
|
+
break;
|
|
231
239
|
}
|
|
232
240
|
}
|
|
233
241
|
if (!aiJudgeApiKey) {
|
|
@@ -296,7 +304,8 @@ function parseArgs(argv) {
|
|
|
296
304
|
policyPath: resolvePolicyPath(cfgPolicySource) ?? void 0,
|
|
297
305
|
policyId: policyId ?? fileConfig.policyId,
|
|
298
306
|
advancedDetection: advancedDetection ? { enabled: true } : void 0,
|
|
299
|
-
aiJudge
|
|
307
|
+
aiJudge,
|
|
308
|
+
agentName
|
|
300
309
|
};
|
|
301
310
|
}
|
|
302
311
|
if (upstreamUrl) {
|
|
@@ -319,7 +328,8 @@ function parseArgs(argv) {
|
|
|
319
328
|
policyPath: resolvedPolicyPath ?? void 0,
|
|
320
329
|
policyId,
|
|
321
330
|
advancedDetection: advancedDetection ? { enabled: true } : void 0,
|
|
322
|
-
aiJudge
|
|
331
|
+
aiJudge,
|
|
332
|
+
agentName
|
|
323
333
|
};
|
|
324
334
|
}
|
|
325
335
|
if (upstreamArgs.length === 0) {
|
|
@@ -346,7 +356,8 @@ function parseArgs(argv) {
|
|
|
346
356
|
policyPath: resolvedPolicyPath ?? void 0,
|
|
347
357
|
policyId,
|
|
348
358
|
advancedDetection: advancedDetection ? { enabled: true } : void 0,
|
|
349
|
-
aiJudge
|
|
359
|
+
aiJudge,
|
|
360
|
+
agentName
|
|
350
361
|
};
|
|
351
362
|
}
|
|
352
363
|
function resolvePolicyPath(source) {
|
|
@@ -481,10 +492,11 @@ function isAlreadyProtected(server) {
|
|
|
481
492
|
}
|
|
482
493
|
return false;
|
|
483
494
|
}
|
|
484
|
-
function wrapServer(server, policy) {
|
|
495
|
+
function wrapServer(serverName, server, policy) {
|
|
485
496
|
const env = { ...server.env ?? {} };
|
|
486
497
|
env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
|
|
487
498
|
const proxyArgs = ["-y", "@solongate/proxy@latest"];
|
|
499
|
+
proxyArgs.push("--agent-name", serverName);
|
|
488
500
|
if (policy) {
|
|
489
501
|
proxyArgs.push("--policy", policy);
|
|
490
502
|
}
|
|
@@ -885,7 +897,7 @@ async function main() {
|
|
|
885
897
|
const newConfig = { mcpServers: {} };
|
|
886
898
|
for (const name of serverNames) {
|
|
887
899
|
if (toProtect.includes(name)) {
|
|
888
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], policyValue);
|
|
900
|
+
newConfig.mcpServers[name] = wrapServer(name, config.mcpServers[name], policyValue);
|
|
889
901
|
} else {
|
|
890
902
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
891
903
|
}
|
|
@@ -2889,25 +2901,25 @@ function checkEntropyLimits(value) {
|
|
|
2889
2901
|
}
|
|
2890
2902
|
function calculateShannonEntropy(str) {
|
|
2891
2903
|
const freq = new Uint32Array(128);
|
|
2892
|
-
|
|
2904
|
+
const nonAsciiFreq = /* @__PURE__ */ new Map();
|
|
2893
2905
|
for (let i = 0; i < str.length; i++) {
|
|
2894
2906
|
const code = str.charCodeAt(i);
|
|
2895
2907
|
if (code < 128) {
|
|
2896
|
-
freq[code] =
|
|
2908
|
+
freq[code] = freq[code] + 1;
|
|
2897
2909
|
} else {
|
|
2898
|
-
|
|
2910
|
+
nonAsciiFreq.set(code, (nonAsciiFreq.get(code) || 0) + 1);
|
|
2899
2911
|
}
|
|
2900
2912
|
}
|
|
2901
2913
|
let entropy = 0;
|
|
2902
2914
|
const len = str.length;
|
|
2903
2915
|
for (let i = 0; i < 128; i++) {
|
|
2904
|
-
if (
|
|
2905
|
-
const p =
|
|
2916
|
+
if (freq[i] > 0) {
|
|
2917
|
+
const p = freq[i] / len;
|
|
2906
2918
|
entropy -= p * Math.log2(p);
|
|
2907
2919
|
}
|
|
2908
2920
|
}
|
|
2909
|
-
|
|
2910
|
-
const p =
|
|
2921
|
+
for (const count of nonAsciiFreq.values()) {
|
|
2922
|
+
const p = count / len;
|
|
2911
2923
|
entropy -= p * Math.log2(p);
|
|
2912
2924
|
}
|
|
2913
2925
|
return entropy;
|
|
@@ -3978,7 +3990,7 @@ function urlConstraintsMatch(constraints, args) {
|
|
|
3978
3990
|
}
|
|
3979
3991
|
function evaluatePolicy(policySet, request) {
|
|
3980
3992
|
const startTime = performance.now();
|
|
3981
|
-
const sortedRules = policySet.rules;
|
|
3993
|
+
const sortedRules = [...policySet.rules].sort((a, b) => a.priority - b.priority);
|
|
3982
3994
|
for (const rule of sortedRules) {
|
|
3983
3995
|
if (ruleMatchesRequest(rule, request)) {
|
|
3984
3996
|
const endTime2 = performance.now();
|
|
@@ -5457,9 +5469,6 @@ var PolicySyncManager = class {
|
|
|
5457
5469
|
*/
|
|
5458
5470
|
policiesEqual(a, b) {
|
|
5459
5471
|
if (a.name !== b.name || a.rules.length !== b.rules.length) return false;
|
|
5460
|
-
if (a.version !== void 0 && b.version !== void 0 && a.version === b.version && a.id === b.id) {
|
|
5461
|
-
return true;
|
|
5462
|
-
}
|
|
5463
5472
|
return JSON.stringify(a.rules) === JSON.stringify(b.rules);
|
|
5464
5473
|
}
|
|
5465
5474
|
};
|
|
@@ -5499,12 +5508,17 @@ CRITICAL: Only DENY access to files EXPLICITLY in the protected_files list. "cat
|
|
|
5499
5508
|
|
|
5500
5509
|
Respond with ONLY valid JSON, no markdown, no explanation outside the JSON:
|
|
5501
5510
|
{"decision": "ALLOW" or "DENY", "reason": "brief one-line explanation", "confidence": 0.0 to 1.0}`;
|
|
5502
|
-
var AiJudge = class {
|
|
5511
|
+
var AiJudge = class _AiJudge {
|
|
5503
5512
|
config;
|
|
5504
5513
|
protectedFiles;
|
|
5505
5514
|
protectedPaths;
|
|
5506
5515
|
deniedActions;
|
|
5507
5516
|
isOllamaEndpoint;
|
|
5517
|
+
// Circuit breaker: after 3 consecutive failures in 60s, skip AI Judge
|
|
5518
|
+
consecutiveFailures = 0;
|
|
5519
|
+
lastFailureTime = 0;
|
|
5520
|
+
static CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
5521
|
+
static CIRCUIT_BREAKER_RESET_MS = 6e4;
|
|
5508
5522
|
constructor(config, protectedFiles, protectedPaths, deniedActions = [
|
|
5509
5523
|
"file deletion",
|
|
5510
5524
|
"data exfiltration",
|
|
@@ -5523,6 +5537,17 @@ var AiJudge = class {
|
|
|
5523
5537
|
* Fail-closed: any error (timeout, parse failure, connection refused) → DENY.
|
|
5524
5538
|
*/
|
|
5525
5539
|
async evaluate(toolName, args) {
|
|
5540
|
+
if (this.consecutiveFailures >= _AiJudge.CIRCUIT_BREAKER_THRESHOLD) {
|
|
5541
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
5542
|
+
if (elapsed < _AiJudge.CIRCUIT_BREAKER_RESET_MS) {
|
|
5543
|
+
return {
|
|
5544
|
+
decision: "ALLOW",
|
|
5545
|
+
reason: `AI Judge circuit breaker open (${this.consecutiveFailures} consecutive failures) \u2014 falling back to policy-only`,
|
|
5546
|
+
confidence: 0.5
|
|
5547
|
+
};
|
|
5548
|
+
}
|
|
5549
|
+
this.consecutiveFailures = 0;
|
|
5550
|
+
}
|
|
5526
5551
|
const sanitizedArgs = this.sanitizeArgs(args);
|
|
5527
5552
|
const userMessage = JSON.stringify({
|
|
5528
5553
|
tool: toolName,
|
|
@@ -5533,12 +5558,15 @@ var AiJudge = class {
|
|
|
5533
5558
|
});
|
|
5534
5559
|
try {
|
|
5535
5560
|
const response = await this.callLLM(userMessage);
|
|
5561
|
+
this.consecutiveFailures = 0;
|
|
5536
5562
|
return this.parseVerdict(response);
|
|
5537
5563
|
} catch (err) {
|
|
5564
|
+
this.consecutiveFailures++;
|
|
5565
|
+
this.lastFailureTime = Date.now();
|
|
5538
5566
|
const message = err instanceof Error ? err.message : String(err);
|
|
5539
5567
|
return {
|
|
5540
5568
|
decision: "DENY",
|
|
5541
|
-
reason: `AI Judge error (fail-closed): ${message}`,
|
|
5569
|
+
reason: `AI Judge error (fail-closed): ${message.slice(0, 100)}`,
|
|
5542
5570
|
confidence: 1
|
|
5543
5571
|
};
|
|
5544
5572
|
}
|
|
@@ -5551,8 +5579,15 @@ var AiJudge = class {
|
|
|
5551
5579
|
const sanitize = (val, depth = 0) => {
|
|
5552
5580
|
if (depth > 10) return "[nested]";
|
|
5553
5581
|
if (typeof val === "string") {
|
|
5554
|
-
|
|
5555
|
-
|
|
5582
|
+
let s = val.length > maxStringLen ? val.slice(0, maxStringLen) + "...[truncated]" : val;
|
|
5583
|
+
s = s.replace(/\{[^{}]*"decision"\s*:/gi, "{[redacted]:");
|
|
5584
|
+
s = s.replace(/\{[^{}]*"ALLOW"[^{}]*\}/gi, "[redacted-json]");
|
|
5585
|
+
s = s.replace(/\{[^{}]*"DENY"[^{}]*\}/gi, "[redacted-json]");
|
|
5586
|
+
s = s.replace(/respond\s+with\s*:/gi, "[redacted]");
|
|
5587
|
+
s = s.replace(/ignore\s+(previous|above|all)\s+(instructions?|prompts?)/gi, "[redacted]");
|
|
5588
|
+
s = s.replace(/you\s+are\s+now\s+/gi, "[redacted] ");
|
|
5589
|
+
s = s.replace(/system\s*:\s*/gi, "[redacted]: ");
|
|
5590
|
+
return s;
|
|
5556
5591
|
}
|
|
5557
5592
|
if (Array.isArray(val)) return val.slice(0, 50).map((v) => sanitize(v, depth + 1));
|
|
5558
5593
|
if (val && typeof val === "object") {
|
|
@@ -5595,7 +5630,8 @@ var AiJudge = class {
|
|
|
5595
5630
|
{ role: "user", content: userMessage }
|
|
5596
5631
|
],
|
|
5597
5632
|
temperature: 0,
|
|
5598
|
-
max_tokens: 200
|
|
5633
|
+
max_tokens: 200,
|
|
5634
|
+
response_format: { type: "json_object" }
|
|
5599
5635
|
});
|
|
5600
5636
|
if (this.config.apiKey) {
|
|
5601
5637
|
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
@@ -5721,9 +5757,20 @@ var SolonGateProxy = class {
|
|
|
5721
5757
|
aiJudge = null;
|
|
5722
5758
|
upstreamTools = [];
|
|
5723
5759
|
guardConfig;
|
|
5760
|
+
/** Agent identity for trust map — resolved from CLI flag, HTTP headers, or MCP clientInfo */
|
|
5761
|
+
agentId = null;
|
|
5762
|
+
agentName = null;
|
|
5763
|
+
/** Per-session agent info for HTTP mode (keyed by session ID) */
|
|
5764
|
+
httpAgentInfo = /* @__PURE__ */ new Map();
|
|
5765
|
+
/** Per-request sub-agent info from HTTP headers (transient, overwritten per request) */
|
|
5766
|
+
httpSubAgent = null;
|
|
5724
5767
|
constructor(config) {
|
|
5725
5768
|
this.config = config;
|
|
5726
5769
|
this.guardConfig = config.advancedDetection ? { ...DEFAULT_INPUT_GUARD_CONFIG, advancedDetection: config.advancedDetection } : DEFAULT_INPUT_GUARD_CONFIG;
|
|
5770
|
+
if (config.agentName) {
|
|
5771
|
+
this.agentName = config.agentName;
|
|
5772
|
+
this.agentId = config.agentName.toLowerCase().replace(/\s+/g, "-");
|
|
5773
|
+
}
|
|
5727
5774
|
this.gate = new SolonGate({
|
|
5728
5775
|
name: config.name ?? "solongate-proxy",
|
|
5729
5776
|
apiKey: "sg_test_proxy_internal_00000000",
|
|
@@ -5740,6 +5787,19 @@ var SolonGateProxy = class {
|
|
|
5740
5787
|
log2("WARNING:", w);
|
|
5741
5788
|
}
|
|
5742
5789
|
}
|
|
5790
|
+
/** Extract sub-agent identity from MCP _meta field */
|
|
5791
|
+
extractSubAgent(request) {
|
|
5792
|
+
const meta = request?.params?._meta;
|
|
5793
|
+
if (meta && typeof meta === "object") {
|
|
5794
|
+
const solonMeta = meta["io.solongate/agent"];
|
|
5795
|
+
if (solonMeta && typeof solonMeta === "object") {
|
|
5796
|
+
const agent = solonMeta;
|
|
5797
|
+
const id = agent.id ? String(agent.id) : null;
|
|
5798
|
+
if (id) return { subAgentId: id, subAgentName: agent.name ? String(agent.name) : id };
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
return null;
|
|
5802
|
+
}
|
|
5743
5803
|
/**
|
|
5744
5804
|
* Start the proxy: connect to upstream, then serve downstream.
|
|
5745
5805
|
*/
|
|
@@ -5920,6 +5980,16 @@ var SolonGateProxy = class {
|
|
|
5920
5980
|
}
|
|
5921
5981
|
}
|
|
5922
5982
|
);
|
|
5983
|
+
this.server.oninitialized = () => {
|
|
5984
|
+
if (!this.agentId && this.server) {
|
|
5985
|
+
const clientVersion = this.server.getClientVersion();
|
|
5986
|
+
if (clientVersion?.name) {
|
|
5987
|
+
this.agentId = clientVersion.version ? `${clientVersion.name}/${clientVersion.version}` : clientVersion.name;
|
|
5988
|
+
this.agentName = clientVersion.name;
|
|
5989
|
+
log2(`Agent identified from MCP clientInfo: ${this.agentName} (${this.agentId})`);
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
};
|
|
5923
5993
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
5924
5994
|
return { tools: this.upstreamTools };
|
|
5925
5995
|
});
|
|
@@ -5927,6 +5997,7 @@ var SolonGateProxy = class {
|
|
|
5927
5997
|
const MUTEX_TIMEOUT_MS = 3e4;
|
|
5928
5998
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5929
5999
|
const { name, arguments: args } = request.params;
|
|
6000
|
+
const subAgent = this.extractSubAgent(request) || this.httpSubAgent;
|
|
5930
6001
|
const argsSize = TEXT_ENCODER.encode(JSON.stringify(args ?? {})).length;
|
|
5931
6002
|
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
5932
6003
|
log2(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
@@ -5964,7 +6035,11 @@ var SolonGateProxy = class {
|
|
|
5964
6035
|
decision: "DENY",
|
|
5965
6036
|
reason: `Prompt injection detected: ${threats}`,
|
|
5966
6037
|
evaluationTimeMs: 0,
|
|
5967
|
-
promptInjection: piResult
|
|
6038
|
+
promptInjection: piResult,
|
|
6039
|
+
agent_id: this.agentId ?? void 0,
|
|
6040
|
+
agent_name: this.agentName ?? void 0,
|
|
6041
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
6042
|
+
sub_agent_name: subAgent?.subAgentName
|
|
5968
6043
|
});
|
|
5969
6044
|
}
|
|
5970
6045
|
return {
|
|
@@ -6056,7 +6131,11 @@ var SolonGateProxy = class {
|
|
|
6056
6131
|
reason,
|
|
6057
6132
|
matchedRule,
|
|
6058
6133
|
evaluationTimeMs,
|
|
6059
|
-
promptInjection: piResult
|
|
6134
|
+
promptInjection: piResult,
|
|
6135
|
+
agent_id: this.agentId ?? void 0,
|
|
6136
|
+
agent_name: this.agentName ?? void 0,
|
|
6137
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
6138
|
+
sub_agent_name: subAgent?.subAgentName
|
|
6060
6139
|
});
|
|
6061
6140
|
} else {
|
|
6062
6141
|
log2(`Skipping audit log (apiKey: ${this.config.apiKey ? "test key" : "not set"})`);
|
|
@@ -6341,6 +6420,18 @@ ${msg.content.text}`;
|
|
|
6341
6420
|
await this.server.connect(httpTransport);
|
|
6342
6421
|
const httpServer = createHttpServer(async (req, res) => {
|
|
6343
6422
|
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
6423
|
+
if (!this.agentId) {
|
|
6424
|
+
const headerAgentId = req.headers["x-agent-id"];
|
|
6425
|
+
const headerAgentName = req.headers["x-agent-name"];
|
|
6426
|
+
if (headerAgentId) {
|
|
6427
|
+
this.agentId = headerAgentId;
|
|
6428
|
+
this.agentName = headerAgentName || headerAgentId;
|
|
6429
|
+
log2(`Agent identified from HTTP headers: ${this.agentName} (${this.agentId})`);
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
const subAgentId = req.headers["x-sub-agent-id"];
|
|
6433
|
+
const subAgentName = req.headers["x-sub-agent-name"];
|
|
6434
|
+
this.httpSubAgent = subAgentId ? { subAgentId, subAgentName: subAgentName || subAgentId } : null;
|
|
6344
6435
|
await httpTransport.handleRequest(req, res);
|
|
6345
6436
|
} else if (req.url === "/health") {
|
|
6346
6437
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/dist/init.js
CHANGED
|
@@ -98,10 +98,11 @@ function isAlreadyProtected(server) {
|
|
|
98
98
|
}
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
|
-
function wrapServer(server, policy) {
|
|
101
|
+
function wrapServer(serverName, server, policy) {
|
|
102
102
|
const env = { ...server.env ?? {} };
|
|
103
103
|
env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
|
|
104
104
|
const proxyArgs = ["-y", "@solongate/proxy@latest"];
|
|
105
|
+
proxyArgs.push("--agent-name", serverName);
|
|
105
106
|
if (policy) {
|
|
106
107
|
proxyArgs.push("--policy", policy);
|
|
107
108
|
}
|
|
@@ -504,7 +505,7 @@ async function main() {
|
|
|
504
505
|
const newConfig = { mcpServers: {} };
|
|
505
506
|
for (const name of serverNames) {
|
|
506
507
|
if (toProtect.includes(name)) {
|
|
507
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], policyValue);
|
|
508
|
+
newConfig.mcpServers[name] = wrapServer(name, config.mcpServers[name], policyValue);
|
|
508
509
|
} else {
|
|
509
510
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
510
511
|
}
|
package/dist/lib.js
CHANGED
|
@@ -1064,25 +1064,25 @@ function checkEntropyLimits(value) {
|
|
|
1064
1064
|
}
|
|
1065
1065
|
function calculateShannonEntropy(str) {
|
|
1066
1066
|
const freq = new Uint32Array(128);
|
|
1067
|
-
|
|
1067
|
+
const nonAsciiFreq = /* @__PURE__ */ new Map();
|
|
1068
1068
|
for (let i = 0; i < str.length; i++) {
|
|
1069
1069
|
const code = str.charCodeAt(i);
|
|
1070
1070
|
if (code < 128) {
|
|
1071
|
-
freq[code] =
|
|
1071
|
+
freq[code] = freq[code] + 1;
|
|
1072
1072
|
} else {
|
|
1073
|
-
|
|
1073
|
+
nonAsciiFreq.set(code, (nonAsciiFreq.get(code) || 0) + 1);
|
|
1074
1074
|
}
|
|
1075
1075
|
}
|
|
1076
1076
|
let entropy = 0;
|
|
1077
1077
|
const len = str.length;
|
|
1078
1078
|
for (let i = 0; i < 128; i++) {
|
|
1079
|
-
if (
|
|
1080
|
-
const p =
|
|
1079
|
+
if (freq[i] > 0) {
|
|
1080
|
+
const p = freq[i] / len;
|
|
1081
1081
|
entropy -= p * Math.log2(p);
|
|
1082
1082
|
}
|
|
1083
1083
|
}
|
|
1084
|
-
|
|
1085
|
-
const p =
|
|
1084
|
+
for (const count of nonAsciiFreq.values()) {
|
|
1085
|
+
const p = count / len;
|
|
1086
1086
|
entropy -= p * Math.log2(p);
|
|
1087
1087
|
}
|
|
1088
1088
|
return entropy;
|
|
@@ -2178,7 +2178,7 @@ function urlConstraintsMatch(constraints, args) {
|
|
|
2178
2178
|
}
|
|
2179
2179
|
function evaluatePolicy(policySet, request) {
|
|
2180
2180
|
const startTime = performance.now();
|
|
2181
|
-
const sortedRules = policySet.rules;
|
|
2181
|
+
const sortedRules = [...policySet.rules].sort((a, b) => a.priority - b.priority);
|
|
2182
2182
|
for (const rule of sortedRules) {
|
|
2183
2183
|
if (ruleMatchesRequest(rule, request)) {
|
|
2184
2184
|
const endTime2 = performance.now();
|
|
@@ -3661,7 +3661,11 @@ var AUDIT_MAX_RETRIES = 3;
|
|
|
3661
3661
|
var AUDIT_LOG_BACKUP_PATH = resolve(".solongate-audit-backup.jsonl");
|
|
3662
3662
|
async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
3663
3663
|
const url = `${apiUrl}/api/v1/audit-logs`;
|
|
3664
|
-
const body = JSON.stringify(
|
|
3664
|
+
const body = JSON.stringify({
|
|
3665
|
+
...entry,
|
|
3666
|
+
agent_id: entry.agent_id,
|
|
3667
|
+
agent_name: entry.agent_name
|
|
3668
|
+
});
|
|
3665
3669
|
for (let attempt = 0; attempt < AUDIT_MAX_RETRIES; attempt++) {
|
|
3666
3670
|
try {
|
|
3667
3671
|
const res = await fetch(url, {
|
|
@@ -3930,9 +3934,6 @@ var PolicySyncManager = class {
|
|
|
3930
3934
|
*/
|
|
3931
3935
|
policiesEqual(a, b) {
|
|
3932
3936
|
if (a.name !== b.name || a.rules.length !== b.rules.length) return false;
|
|
3933
|
-
if (a.version !== void 0 && b.version !== void 0 && a.version === b.version && a.id === b.id) {
|
|
3934
|
-
return true;
|
|
3935
|
-
}
|
|
3936
3937
|
return JSON.stringify(a.rules) === JSON.stringify(b.rules);
|
|
3937
3938
|
}
|
|
3938
3939
|
};
|
|
@@ -3972,12 +3973,17 @@ CRITICAL: Only DENY access to files EXPLICITLY in the protected_files list. "cat
|
|
|
3972
3973
|
|
|
3973
3974
|
Respond with ONLY valid JSON, no markdown, no explanation outside the JSON:
|
|
3974
3975
|
{"decision": "ALLOW" or "DENY", "reason": "brief one-line explanation", "confidence": 0.0 to 1.0}`;
|
|
3975
|
-
var AiJudge = class {
|
|
3976
|
+
var AiJudge = class _AiJudge {
|
|
3976
3977
|
config;
|
|
3977
3978
|
protectedFiles;
|
|
3978
3979
|
protectedPaths;
|
|
3979
3980
|
deniedActions;
|
|
3980
3981
|
isOllamaEndpoint;
|
|
3982
|
+
// Circuit breaker: after 3 consecutive failures in 60s, skip AI Judge
|
|
3983
|
+
consecutiveFailures = 0;
|
|
3984
|
+
lastFailureTime = 0;
|
|
3985
|
+
static CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
3986
|
+
static CIRCUIT_BREAKER_RESET_MS = 6e4;
|
|
3981
3987
|
constructor(config, protectedFiles, protectedPaths, deniedActions = [
|
|
3982
3988
|
"file deletion",
|
|
3983
3989
|
"data exfiltration",
|
|
@@ -3996,6 +4002,17 @@ var AiJudge = class {
|
|
|
3996
4002
|
* Fail-closed: any error (timeout, parse failure, connection refused) → DENY.
|
|
3997
4003
|
*/
|
|
3998
4004
|
async evaluate(toolName, args) {
|
|
4005
|
+
if (this.consecutiveFailures >= _AiJudge.CIRCUIT_BREAKER_THRESHOLD) {
|
|
4006
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
4007
|
+
if (elapsed < _AiJudge.CIRCUIT_BREAKER_RESET_MS) {
|
|
4008
|
+
return {
|
|
4009
|
+
decision: "ALLOW",
|
|
4010
|
+
reason: `AI Judge circuit breaker open (${this.consecutiveFailures} consecutive failures) \u2014 falling back to policy-only`,
|
|
4011
|
+
confidence: 0.5
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
this.consecutiveFailures = 0;
|
|
4015
|
+
}
|
|
3999
4016
|
const sanitizedArgs = this.sanitizeArgs(args);
|
|
4000
4017
|
const userMessage = JSON.stringify({
|
|
4001
4018
|
tool: toolName,
|
|
@@ -4006,12 +4023,15 @@ var AiJudge = class {
|
|
|
4006
4023
|
});
|
|
4007
4024
|
try {
|
|
4008
4025
|
const response = await this.callLLM(userMessage);
|
|
4026
|
+
this.consecutiveFailures = 0;
|
|
4009
4027
|
return this.parseVerdict(response);
|
|
4010
4028
|
} catch (err) {
|
|
4029
|
+
this.consecutiveFailures++;
|
|
4030
|
+
this.lastFailureTime = Date.now();
|
|
4011
4031
|
const message = err instanceof Error ? err.message : String(err);
|
|
4012
4032
|
return {
|
|
4013
4033
|
decision: "DENY",
|
|
4014
|
-
reason: `AI Judge error (fail-closed): ${message}`,
|
|
4034
|
+
reason: `AI Judge error (fail-closed): ${message.slice(0, 100)}`,
|
|
4015
4035
|
confidence: 1
|
|
4016
4036
|
};
|
|
4017
4037
|
}
|
|
@@ -4024,8 +4044,15 @@ var AiJudge = class {
|
|
|
4024
4044
|
const sanitize = (val, depth = 0) => {
|
|
4025
4045
|
if (depth > 10) return "[nested]";
|
|
4026
4046
|
if (typeof val === "string") {
|
|
4027
|
-
|
|
4028
|
-
|
|
4047
|
+
let s = val.length > maxStringLen ? val.slice(0, maxStringLen) + "...[truncated]" : val;
|
|
4048
|
+
s = s.replace(/\{[^{}]*"decision"\s*:/gi, "{[redacted]:");
|
|
4049
|
+
s = s.replace(/\{[^{}]*"ALLOW"[^{}]*\}/gi, "[redacted-json]");
|
|
4050
|
+
s = s.replace(/\{[^{}]*"DENY"[^{}]*\}/gi, "[redacted-json]");
|
|
4051
|
+
s = s.replace(/respond\s+with\s*:/gi, "[redacted]");
|
|
4052
|
+
s = s.replace(/ignore\s+(previous|above|all)\s+(instructions?|prompts?)/gi, "[redacted]");
|
|
4053
|
+
s = s.replace(/you\s+are\s+now\s+/gi, "[redacted] ");
|
|
4054
|
+
s = s.replace(/system\s*:\s*/gi, "[redacted]: ");
|
|
4055
|
+
return s;
|
|
4029
4056
|
}
|
|
4030
4057
|
if (Array.isArray(val)) return val.slice(0, 50).map((v) => sanitize(v, depth + 1));
|
|
4031
4058
|
if (val && typeof val === "object") {
|
|
@@ -4068,7 +4095,8 @@ var AiJudge = class {
|
|
|
4068
4095
|
{ role: "user", content: userMessage }
|
|
4069
4096
|
],
|
|
4070
4097
|
temperature: 0,
|
|
4071
|
-
max_tokens: 200
|
|
4098
|
+
max_tokens: 200,
|
|
4099
|
+
response_format: { type: "json_object" }
|
|
4072
4100
|
});
|
|
4073
4101
|
if (this.config.apiKey) {
|
|
4074
4102
|
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
@@ -4194,9 +4222,20 @@ var SolonGateProxy = class {
|
|
|
4194
4222
|
aiJudge = null;
|
|
4195
4223
|
upstreamTools = [];
|
|
4196
4224
|
guardConfig;
|
|
4225
|
+
/** Agent identity for trust map — resolved from CLI flag, HTTP headers, or MCP clientInfo */
|
|
4226
|
+
agentId = null;
|
|
4227
|
+
agentName = null;
|
|
4228
|
+
/** Per-session agent info for HTTP mode (keyed by session ID) */
|
|
4229
|
+
httpAgentInfo = /* @__PURE__ */ new Map();
|
|
4230
|
+
/** Per-request sub-agent info from HTTP headers (transient, overwritten per request) */
|
|
4231
|
+
httpSubAgent = null;
|
|
4197
4232
|
constructor(config) {
|
|
4198
4233
|
this.config = config;
|
|
4199
4234
|
this.guardConfig = config.advancedDetection ? { ...DEFAULT_INPUT_GUARD_CONFIG, advancedDetection: config.advancedDetection } : DEFAULT_INPUT_GUARD_CONFIG;
|
|
4235
|
+
if (config.agentName) {
|
|
4236
|
+
this.agentName = config.agentName;
|
|
4237
|
+
this.agentId = config.agentName.toLowerCase().replace(/\s+/g, "-");
|
|
4238
|
+
}
|
|
4200
4239
|
this.gate = new SolonGate({
|
|
4201
4240
|
name: config.name ?? "solongate-proxy",
|
|
4202
4241
|
apiKey: "sg_test_proxy_internal_00000000",
|
|
@@ -4213,6 +4252,19 @@ var SolonGateProxy = class {
|
|
|
4213
4252
|
log2("WARNING:", w);
|
|
4214
4253
|
}
|
|
4215
4254
|
}
|
|
4255
|
+
/** Extract sub-agent identity from MCP _meta field */
|
|
4256
|
+
extractSubAgent(request) {
|
|
4257
|
+
const meta = request?.params?._meta;
|
|
4258
|
+
if (meta && typeof meta === "object") {
|
|
4259
|
+
const solonMeta = meta["io.solongate/agent"];
|
|
4260
|
+
if (solonMeta && typeof solonMeta === "object") {
|
|
4261
|
+
const agent = solonMeta;
|
|
4262
|
+
const id = agent.id ? String(agent.id) : null;
|
|
4263
|
+
if (id) return { subAgentId: id, subAgentName: agent.name ? String(agent.name) : id };
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
return null;
|
|
4267
|
+
}
|
|
4216
4268
|
/**
|
|
4217
4269
|
* Start the proxy: connect to upstream, then serve downstream.
|
|
4218
4270
|
*/
|
|
@@ -4393,6 +4445,16 @@ var SolonGateProxy = class {
|
|
|
4393
4445
|
}
|
|
4394
4446
|
}
|
|
4395
4447
|
);
|
|
4448
|
+
this.server.oninitialized = () => {
|
|
4449
|
+
if (!this.agentId && this.server) {
|
|
4450
|
+
const clientVersion = this.server.getClientVersion();
|
|
4451
|
+
if (clientVersion?.name) {
|
|
4452
|
+
this.agentId = clientVersion.version ? `${clientVersion.name}/${clientVersion.version}` : clientVersion.name;
|
|
4453
|
+
this.agentName = clientVersion.name;
|
|
4454
|
+
log2(`Agent identified from MCP clientInfo: ${this.agentName} (${this.agentId})`);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
};
|
|
4396
4458
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4397
4459
|
return { tools: this.upstreamTools };
|
|
4398
4460
|
});
|
|
@@ -4400,6 +4462,7 @@ var SolonGateProxy = class {
|
|
|
4400
4462
|
const MUTEX_TIMEOUT_MS = 3e4;
|
|
4401
4463
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4402
4464
|
const { name, arguments: args } = request.params;
|
|
4465
|
+
const subAgent = this.extractSubAgent(request) || this.httpSubAgent;
|
|
4403
4466
|
const argsSize = TEXT_ENCODER.encode(JSON.stringify(args ?? {})).length;
|
|
4404
4467
|
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
4405
4468
|
log2(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
@@ -4437,7 +4500,11 @@ var SolonGateProxy = class {
|
|
|
4437
4500
|
decision: "DENY",
|
|
4438
4501
|
reason: `Prompt injection detected: ${threats}`,
|
|
4439
4502
|
evaluationTimeMs: 0,
|
|
4440
|
-
promptInjection: piResult
|
|
4503
|
+
promptInjection: piResult,
|
|
4504
|
+
agent_id: this.agentId ?? void 0,
|
|
4505
|
+
agent_name: this.agentName ?? void 0,
|
|
4506
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
4507
|
+
sub_agent_name: subAgent?.subAgentName
|
|
4441
4508
|
});
|
|
4442
4509
|
}
|
|
4443
4510
|
return {
|
|
@@ -4529,7 +4596,11 @@ var SolonGateProxy = class {
|
|
|
4529
4596
|
reason,
|
|
4530
4597
|
matchedRule,
|
|
4531
4598
|
evaluationTimeMs,
|
|
4532
|
-
promptInjection: piResult
|
|
4599
|
+
promptInjection: piResult,
|
|
4600
|
+
agent_id: this.agentId ?? void 0,
|
|
4601
|
+
agent_name: this.agentName ?? void 0,
|
|
4602
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
4603
|
+
sub_agent_name: subAgent?.subAgentName
|
|
4533
4604
|
});
|
|
4534
4605
|
} else {
|
|
4535
4606
|
log2(`Skipping audit log (apiKey: ${this.config.apiKey ? "test key" : "not set"})`);
|
|
@@ -4814,6 +4885,18 @@ ${msg.content.text}`;
|
|
|
4814
4885
|
await this.server.connect(httpTransport);
|
|
4815
4886
|
const httpServer = createHttpServer(async (req, res) => {
|
|
4816
4887
|
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
4888
|
+
if (!this.agentId) {
|
|
4889
|
+
const headerAgentId = req.headers["x-agent-id"];
|
|
4890
|
+
const headerAgentName = req.headers["x-agent-name"];
|
|
4891
|
+
if (headerAgentId) {
|
|
4892
|
+
this.agentId = headerAgentId;
|
|
4893
|
+
this.agentName = headerAgentName || headerAgentId;
|
|
4894
|
+
log2(`Agent identified from HTTP headers: ${this.agentName} (${this.agentId})`);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
const subAgentId = req.headers["x-sub-agent-id"];
|
|
4898
|
+
const subAgentName = req.headers["x-sub-agent-name"];
|
|
4899
|
+
this.httpSubAgent = subAgentId ? { subAgentId, subAgentName: subAgentName || subAgentId } : null;
|
|
4817
4900
|
await httpTransport.handleRequest(req, res);
|
|
4818
4901
|
} else if (req.url === "/health") {
|
|
4819
4902
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/hooks/guard.mjs
CHANGED
|
@@ -7,9 +7,19 @@
|
|
|
7
7
|
* Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
|
|
8
8
|
* Auto-installed by: npx @solongate/proxy init
|
|
9
9
|
*/
|
|
10
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
|
|
13
|
+
// Safe file read with size limit (1MB max) to prevent DoS via large files
|
|
14
|
+
const MAX_FILE_READ = 1024 * 1024; // 1MB
|
|
15
|
+
function safeReadFileSync(filePath, encoding = 'utf-8') {
|
|
16
|
+
try {
|
|
17
|
+
const stat = statSync(filePath);
|
|
18
|
+
if (stat.size > MAX_FILE_READ) return '';
|
|
19
|
+
return readFileSync(filePath, encoding);
|
|
20
|
+
} catch { return ''; }
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
// ── Load API key from .env file (Claude Code doesn't load .env into process.env) ──
|
|
14
24
|
function loadEnvKey(dir) {
|
|
15
25
|
try {
|
|
@@ -157,6 +167,32 @@ function matchPathGlob(path, pattern) {
|
|
|
157
167
|
return matchGlob(p, g);
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
// ── Safe Webhook URL Validation (prevent SSRF) ──
|
|
171
|
+
function isSafeWebhookUrl(urlStr) {
|
|
172
|
+
try {
|
|
173
|
+
const u = new URL(urlStr);
|
|
174
|
+
if (u.protocol !== 'https:') return false;
|
|
175
|
+
const host = u.hostname.toLowerCase();
|
|
176
|
+
// Block private/reserved IPs and metadata endpoints
|
|
177
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1') return false;
|
|
178
|
+
if (host.startsWith('10.') || host.startsWith('192.168.') || host.startsWith('172.')) return false;
|
|
179
|
+
if (host === '169.254.169.254' || host === 'metadata.google.internal') return false;
|
|
180
|
+
if (host.endsWith('.internal') || host.endsWith('.local')) return false;
|
|
181
|
+
return true;
|
|
182
|
+
} catch { return false; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Safe Regex Validation (prevent ReDoS from cloud-supplied patterns) ──
|
|
186
|
+
function isSafeRegex(pattern) {
|
|
187
|
+
if (typeof pattern !== 'string' || pattern.length > 512) return false;
|
|
188
|
+
// Block nested quantifiers: (a+)+, (a*)+, (a{1,})+, etc.
|
|
189
|
+
if (/(\+|\*|\{[^}]+\})\s*(\+|\*|\{[^}]+\})/.test(pattern)) return false;
|
|
190
|
+
if (/\([^)]*(\+|\*|\{[^}]+\})[^)]*\)\s*(\+|\*|\{[^}]+\})/.test(pattern)) return false;
|
|
191
|
+
// Block excessive alternation groups (>10 alternatives)
|
|
192
|
+
if ((pattern.match(/\|/g) || []).length > 10) return false;
|
|
193
|
+
try { new RegExp(pattern); return true; } catch { return false; }
|
|
194
|
+
}
|
|
195
|
+
|
|
160
196
|
// ── Extract Functions (deep scan all string values) ──
|
|
161
197
|
function scanStrings(obj) {
|
|
162
198
|
const strings = [];
|
|
@@ -643,7 +679,7 @@ process.stdin.on('end', async () => {
|
|
|
643
679
|
try {
|
|
644
680
|
const targetPath = resolve(hookCwdForNpm, scriptFileMatch[1]);
|
|
645
681
|
if (existsSync(targetPath)) {
|
|
646
|
-
const targetContent =
|
|
682
|
+
const targetContent = safeReadFileSync(targetPath).toLowerCase();
|
|
647
683
|
// Check target file for protected paths
|
|
648
684
|
for (const pp of [...protectedPaths, ...writeProtectedPaths]) {
|
|
649
685
|
if (targetContent.includes(pp)) {
|
|
@@ -670,7 +706,7 @@ process.stdin.on('end', async () => {
|
|
|
670
706
|
const candidates = [impAbs, impAbs + '.mjs', impAbs + '.js', impAbs + '.cjs'];
|
|
671
707
|
for (const c of candidates) {
|
|
672
708
|
if (existsSync(c)) {
|
|
673
|
-
const impContent =
|
|
709
|
+
const impContent = safeReadFileSync(c).toLowerCase();
|
|
674
710
|
const iDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bglob\b/i.test(impContent);
|
|
675
711
|
const iDest = /\brmsync\b|\bunlinksync\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(impContent);
|
|
676
712
|
if ((iDisc && tDest) || (tDisc && iDest)) {
|
|
@@ -756,7 +792,7 @@ process.stdin.on('end', async () => {
|
|
|
756
792
|
? scriptPath
|
|
757
793
|
: resolve(hookCwdForScript, scriptPath);
|
|
758
794
|
if (existsSync(absPath)) {
|
|
759
|
-
const scriptContent =
|
|
795
|
+
const scriptContent = safeReadFileSync(absPath).toLowerCase();
|
|
760
796
|
// Check for discovery+destruction combo
|
|
761
797
|
const hasDiscovery = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b|\bls\s+-[adl]|\bls\s+\.\b|\bopendir\b|\bdir\.entries\b|\bwalkdir\b|\bls\b.*\.\[/.test(scriptContent);
|
|
762
798
|
const hasDestruction = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bremovesync\b|\bremove_tree\b|\bshutil\.rmtree\b|\bwritefilesync\b|\bexecsync\b.*\brm\b|\bchild_process\b|\bfs\.\s*(?:rm|unlink|rmdir|write)/.test(scriptContent);
|
|
@@ -778,7 +814,7 @@ process.stdin.on('end', async () => {
|
|
|
778
814
|
const candidates = [importAbs, importAbs + '.mjs', importAbs + '.js', importAbs + '.cjs'];
|
|
779
815
|
for (const candidate of candidates) {
|
|
780
816
|
if (existsSync(candidate)) {
|
|
781
|
-
const importContent =
|
|
817
|
+
const importContent = safeReadFileSync(candidate).toLowerCase();
|
|
782
818
|
// Cross-module: check if imported module has discovery/destruction/string construction
|
|
783
819
|
const importDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b/i.test(importContent);
|
|
784
820
|
const importDest = /\brmsync\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(importContent);
|
|
@@ -890,6 +926,7 @@ process.stdin.on('end', async () => {
|
|
|
890
926
|
if (piCfg.piEnabled !== false && Array.isArray(piCfg.piWhitelist) && piCfg.piWhitelist.length > 0) {
|
|
891
927
|
for (const wlPattern of piCfg.piWhitelist) {
|
|
892
928
|
try {
|
|
929
|
+
if (!isSafeRegex(wlPattern)) continue;
|
|
893
930
|
if (new RegExp(wlPattern, 'i').test(allText)) {
|
|
894
931
|
whitelisted = true;
|
|
895
932
|
break;
|
|
@@ -902,7 +939,7 @@ process.stdin.on('end', async () => {
|
|
|
902
939
|
const customCategories = [];
|
|
903
940
|
if (piCfg.piEnabled !== false && Array.isArray(piCfg.piCustomPatterns)) {
|
|
904
941
|
for (const cp of piCfg.piCustomPatterns) {
|
|
905
|
-
if (cp && cp.pattern) {
|
|
942
|
+
if (cp && cp.pattern && isSafeRegex(cp.pattern)) {
|
|
906
943
|
try {
|
|
907
944
|
customCategories.push({
|
|
908
945
|
name: cp.name || 'custom_pattern',
|
|
@@ -950,8 +987,8 @@ process.stdin.on('end', async () => {
|
|
|
950
987
|
});
|
|
951
988
|
} catch {}
|
|
952
989
|
|
|
953
|
-
// Webhook notification
|
|
954
|
-
if (piCfg.piWebhookUrl) {
|
|
990
|
+
// Webhook notification (SSRF-safe: HTTPS only, no private IPs)
|
|
991
|
+
if (piCfg.piWebhookUrl && isSafeWebhookUrl(piCfg.piWebhookUrl)) {
|
|
955
992
|
try {
|
|
956
993
|
await fetch(piCfg.piWebhookUrl, {
|
|
957
994
|
method: 'POST',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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": {
|