@solongate/proxy 0.29.1 → 0.30.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.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @solongate/proxy
2
2
 
3
- **MCP Security Proxy** — Protect any MCP server with security policies, input validation, rate limiting, and audit logging. Zero code changes required.
3
+ **AI Tool Security Proxy** — Protect any AI tool server with security policies, input validation, rate limiting, and audit logging. Zero code changes required.
4
4
 
5
5
  ```
6
- MCP Client ──(stdio)──> SolonGate Proxy ──(stdio)──> MCP Server
6
+ AI Client ──(stdio)──> SolonGate Proxy ──(stdio)──> Tool Server
7
7
 
8
8
  [rate limit]
9
9
  [input guard]
@@ -11,19 +11,19 @@ MCP Client ──(stdio)──> SolonGate Proxy ──(stdio)──> MCP Server
11
11
  [audit log]
12
12
  ```
13
13
 
14
- **Works with every MCP client:** Claude Code, Claude Desktop, Cursor, Windsurf, Cline, Zed, and any application that supports the Model Context Protocol over stdio.
14
+ **Works with every AI platform:** Claude Code, Claude Desktop, Cursor, Windsurf, Cline, Zed, and any application that uses AI tool calls.
15
15
 
16
16
  ## Quick Start
17
17
 
18
18
  ### Automatic Setup
19
19
 
20
- Run this in your project directory (where your `.mcp.json` lives):
20
+ Run this in your project directory:
21
21
 
22
22
  ```bash
23
23
  npx @solongate/proxy init --all
24
24
  ```
25
25
 
26
- Restart your MCP client. Done.
26
+ Restart your AI client. Done.
27
27
 
28
28
  ### Manual Setup
29
29
 
@@ -129,9 +129,9 @@ npx @solongate/proxy init --restore
129
129
 
130
130
  ## Why SolonGate?
131
131
 
132
- MCP servers give AI agents direct access to your system — shell commands, file system, databases, network. A single prompt injection attack can turn your AI assistant into an attacker.
132
+ AI tool servers give AI agents direct access to your system — shell commands, file system, databases, network. A single prompt injection attack can turn your AI assistant into an attacker.
133
133
 
134
- SolonGate sits between the AI client and the MCP server, enforcing security policies on every tool call before it reaches the server.
134
+ SolonGate sits between the AI client and the tool server, enforcing security policies on every tool call before it reaches the server.
135
135
 
136
136
  Learn more at [solongate.com](https://solongate.com)
137
137
 
package/dist/index.js CHANGED
@@ -461,6 +461,7 @@ var init_cli_utils = __esm({
461
461
  var init_exports = {};
462
462
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
463
463
  import { resolve as resolve3, join, dirname as dirname2 } from "path";
464
+ import { homedir } from "os";
464
465
  import { fileURLToPath } from "url";
465
466
  import { execFileSync } from "child_process";
466
467
  import { createInterface } from "readline";
@@ -684,7 +685,7 @@ function unlockProtectedDirs() {
684
685
  }
685
686
  }
686
687
  }
687
- function installHooks(selectedTools = []) {
688
+ function installHooks(selectedTools = [], wrappedMcpConfig) {
688
689
  unlockProtectedDirs();
689
690
  const hooksDir = resolve3(".solongate", "hooks");
690
691
  mkdirSync2(hooksDir, { recursive: true });
@@ -725,35 +726,25 @@ function installHooks(selectedTools = []) {
725
726
  for (const client of clients) {
726
727
  const clientDir = resolve3(client.dir);
727
728
  mkdirSync2(clientDir, { recursive: true });
728
- const settingsPath = join(clientDir, "settings.json");
729
729
  const guardCmd = `node .solongate/hooks/guard.mjs ${client.agentId} "${client.agentName}"`;
730
730
  const auditCmd = `node .solongate/hooks/audit.mjs ${client.agentId} "${client.agentName}"`;
731
731
  const stopCmd = `node .solongate/hooks/stop.mjs ${client.agentId} "${client.agentName}"`;
732
- const hookSettings = {
733
- PreToolUse: [
734
- { matcher: "", hooks: [{ type: "command", command: guardCmd }] }
735
- ],
736
- PostToolUse: [
737
- { matcher: "", hooks: [{ type: "command", command: auditCmd }] }
738
- ],
739
- Stop: [
740
- { matcher: "", hooks: [{ type: "command", command: stopCmd }] }
741
- ]
742
- };
743
- let existing = {};
744
- try {
745
- existing = JSON.parse(readFileSync4(settingsPath, "utf-8"));
746
- } catch {
747
- }
748
- const merged = { ...existing, hooks: hookSettings };
749
- const mergedStr = JSON.stringify(merged, null, 2) + "\n";
750
- const existingStr = existsSync4(settingsPath) ? readFileSync4(settingsPath, "utf-8") : "";
751
- if (mergedStr === existingStr) {
752
- skippedNames.push(client.name);
732
+ if (client.key === "cursor") {
733
+ const result = installCursorConfig(clientDir, guardCmd, auditCmd, stopCmd, wrappedMcpConfig);
734
+ if (result === "skipped") skippedNames.push(client.name);
735
+ else activatedNames.push(client.name);
736
+ } else if (client.key === "gemini") {
737
+ const result = installGeminiConfig(clientDir, guardCmd, auditCmd, stopCmd, wrappedMcpConfig);
738
+ if (result === "skipped") skippedNames.push(client.name);
739
+ else activatedNames.push(client.name);
740
+ } else if (client.key === "openclaw") {
741
+ const result = installOpenClawConfig(clientDir);
742
+ if (result === "skipped") skippedNames.push(client.name);
743
+ else activatedNames.push(client.name);
753
744
  } else {
754
- writeFileSync2(settingsPath, mergedStr);
755
- console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
756
- activatedNames.push(client.name);
745
+ const result = installStandardHookConfig(clientDir, client.name, guardCmd, auditCmd, stopCmd);
746
+ if (result === "skipped") skippedNames.push(client.name);
747
+ else activatedNames.push(client.name);
757
748
  }
758
749
  }
759
750
  console.log("");
@@ -768,6 +759,125 @@ function installHooks(selectedTools = []) {
768
759
  console.log(` Already configured: ${skippedNames.join(", ")}`);
769
760
  }
770
761
  }
762
+ function installStandardHookConfig(clientDir, clientName, guardCmd, auditCmd, stopCmd) {
763
+ const settingsPath = join(clientDir, "settings.json");
764
+ const hookSettings = {
765
+ PreToolUse: [
766
+ { matcher: "", hooks: [{ type: "command", command: guardCmd }] }
767
+ ],
768
+ PostToolUse: [
769
+ { matcher: "", hooks: [{ type: "command", command: auditCmd }] }
770
+ ],
771
+ Stop: [
772
+ { matcher: "", hooks: [{ type: "command", command: stopCmd }] }
773
+ ]
774
+ };
775
+ let existing = {};
776
+ try {
777
+ existing = JSON.parse(readFileSync4(settingsPath, "utf-8"));
778
+ } catch {
779
+ }
780
+ const merged = { ...existing, hooks: hookSettings };
781
+ const mergedStr = JSON.stringify(merged, null, 2) + "\n";
782
+ const existingStr = existsSync4(settingsPath) ? readFileSync4(settingsPath, "utf-8") : "";
783
+ if (mergedStr === existingStr) return "skipped";
784
+ writeFileSync2(settingsPath, mergedStr);
785
+ console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
786
+ return "installed";
787
+ }
788
+ function installCursorConfig(clientDir, guardCmd, auditCmd, _stopCmd, wrappedMcpConfig) {
789
+ let changed = false;
790
+ const hookConfig = {
791
+ preToolUse: [
792
+ { matcher: "", command: guardCmd, failClosed: true }
793
+ ],
794
+ postToolUse: [
795
+ { matcher: "", command: auditCmd }
796
+ ]
797
+ };
798
+ const hooksContent = JSON.stringify({ version: 1, hooks: hookConfig }, null, 2) + "\n";
799
+ const userCursorDir = join(homedir(), ".cursor");
800
+ if (existsSync4(userCursorDir)) {
801
+ const userHooksPath = join(userCursorDir, "hooks.json");
802
+ const existingUserHooks = existsSync4(userHooksPath) ? readFileSync4(userHooksPath, "utf-8") : "";
803
+ if (hooksContent !== existingUserHooks) {
804
+ writeFileSync2(userHooksPath, hooksContent);
805
+ console.log(` ${existingUserHooks ? "Updated" : "Created"} ${userHooksPath} (user-level)`);
806
+ changed = true;
807
+ }
808
+ }
809
+ const hooksPath = join(clientDir, "hooks.json");
810
+ const existingHooks = existsSync4(hooksPath) ? readFileSync4(hooksPath, "utf-8") : "";
811
+ if (hooksContent !== existingHooks) {
812
+ writeFileSync2(hooksPath, hooksContent);
813
+ console.log(` ${existingHooks ? "Updated" : "Created"} ${hooksPath}`);
814
+ changed = true;
815
+ }
816
+ if (wrappedMcpConfig) {
817
+ const mcpPath = join(clientDir, "mcp.json");
818
+ const mcpStr = JSON.stringify(wrappedMcpConfig, null, 2) + "\n";
819
+ const existingMcp = existsSync4(mcpPath) ? readFileSync4(mcpPath, "utf-8") : "";
820
+ if (mcpStr !== existingMcp) {
821
+ writeFileSync2(mcpPath, mcpStr);
822
+ console.log(` ${existingMcp ? "Updated" : "Created"} ${mcpPath}`);
823
+ changed = true;
824
+ }
825
+ }
826
+ const staleSettings = join(clientDir, "settings.json");
827
+ if (existsSync4(staleSettings)) {
828
+ try {
829
+ const content = JSON.parse(readFileSync4(staleSettings, "utf-8"));
830
+ if (content.hooks?.PreToolUse || content.hooks?.PostToolUse) {
831
+ writeFileSync2(staleSettings, JSON.stringify({}, null, 2) + "\n");
832
+ console.log(` Cleaned stale ${staleSettings} (had wrong hook format)`);
833
+ changed = true;
834
+ }
835
+ } catch {
836
+ }
837
+ }
838
+ return changed ? "installed" : "skipped";
839
+ }
840
+ function installGeminiConfig(clientDir, guardCmd, auditCmd, _stopCmd, wrappedMcpConfig) {
841
+ const settingsPath = join(clientDir, "settings.json");
842
+ let existing = {};
843
+ try {
844
+ existing = JSON.parse(readFileSync4(settingsPath, "utf-8"));
845
+ } catch {
846
+ }
847
+ const merged = { ...existing };
848
+ if (wrappedMcpConfig) {
849
+ merged.mcpServers = wrappedMcpConfig.mcpServers;
850
+ }
851
+ merged.hooks = {
852
+ BeforeTool: [
853
+ { matcher: ".*", command: guardCmd }
854
+ ],
855
+ AfterTool: [
856
+ { matcher: ".*", command: auditCmd }
857
+ ]
858
+ };
859
+ const mergedStr = JSON.stringify(merged, null, 2) + "\n";
860
+ const existingStr = existsSync4(settingsPath) ? readFileSync4(settingsPath, "utf-8") : "";
861
+ if (mergedStr === existingStr) return "skipped";
862
+ writeFileSync2(settingsPath, mergedStr);
863
+ console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
864
+ return "installed";
865
+ }
866
+ function installOpenClawConfig(clientDir) {
867
+ const yamlPath = resolve3("openclaw.yaml");
868
+ const pluginLine = "@solongate/openclaw-plugin";
869
+ if (existsSync4(yamlPath)) {
870
+ const content = readFileSync4(yamlPath, "utf-8");
871
+ if (content.includes(pluginLine)) {
872
+ return "skipped";
873
+ }
874
+ }
875
+ console.log(` OpenClaw uses plugins, not hooks. Add to your openclaw.yaml:`);
876
+ console.log(` plugins:`);
877
+ console.log(` - "${pluginLine}"`);
878
+ console.log(` Then: npm install ${pluginLine}`);
879
+ return "installed";
880
+ }
771
881
  function ensureEnvFile() {
772
882
  let envChanged = false;
773
883
  let gitignoreChanged = false;
@@ -918,7 +1028,7 @@ async function main() {
918
1028
  console.log(" All servers are already protected by SolonGate!");
919
1029
  ensureEnvFile();
920
1030
  console.log("");
921
- installHooks(options.tools);
1031
+ installHooks(options.tools, config);
922
1032
  process.exit(0);
923
1033
  }
924
1034
  if (!options.all) {
@@ -1021,7 +1131,7 @@ async function main() {
1021
1131
  startSpinner("Installing hooks...");
1022
1132
  await sleep(300);
1023
1133
  stopSpinner("\u2713 Hooks");
1024
- installHooks(options.tools);
1134
+ installHooks(options.tools, newConfig);
1025
1135
  console.log("");
1026
1136
  const allUpToDate = toProtect.length === 0 && alreadyProtected.length === serverNames.length;
1027
1137
  console.log(" \u2500\u2500 Summary \u2500\u2500");
@@ -5902,6 +6012,17 @@ var SolonGateProxy = class {
5902
6012
  log2("WARNING:", w);
5903
6013
  }
5904
6014
  }
6015
+ /** Normalize well-known MCP client names to display-friendly agent identities */
6016
+ normalizeAgentName(raw) {
6017
+ const lower = raw.toLowerCase();
6018
+ if (lower.includes("cursor")) return { id: "cursor", name: "Cursor" };
6019
+ if (lower.includes("claude-code") || lower === "claude code") return { id: "claude-code", name: "Claude Code" };
6020
+ if (lower.includes("claude")) return { id: "claude-desktop", name: "Claude Desktop" };
6021
+ if (lower.includes("gemini")) return { id: "gemini-cli", name: "Gemini CLI" };
6022
+ if (lower.includes("antigravity")) return { id: "antigravity", name: "Antigravity" };
6023
+ if (lower.includes("perplexity")) return { id: "perplexity", name: "Perplexity" };
6024
+ return { id: raw.toLowerCase().replace(/\s+/g, "-"), name: raw };
6025
+ }
5905
6026
  /** Extract sub-agent identity from MCP _meta field */
5906
6027
  extractSubAgent(request) {
5907
6028
  const meta = request?.params?._meta;
@@ -6098,10 +6219,12 @@ var SolonGateProxy = class {
6098
6219
  this.server.oninitialized = () => {
6099
6220
  if (this.server) {
6100
6221
  const clientVersion = this.server.getClientVersion();
6222
+ log2(`MCP clientInfo raw: ${JSON.stringify(clientVersion)}`);
6101
6223
  if (clientVersion?.name) {
6102
- this.agentId = clientVersion.name;
6103
- this.agentName = clientVersion.name;
6104
- log2(`Agent identified from MCP clientInfo: ${this.agentName}`);
6224
+ const normalized = this.normalizeAgentName(clientVersion.name);
6225
+ this.agentId = normalized.id;
6226
+ this.agentName = normalized.name;
6227
+ log2(`Agent identified from MCP clientInfo: ${this.agentName} (raw: ${clientVersion.name})`);
6105
6228
  }
6106
6229
  }
6107
6230
  };
package/dist/init.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // src/init.ts
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
5
  import { resolve, join, dirname } from "path";
6
+ import { homedir } from "os";
6
7
  import { fileURLToPath } from "url";
7
8
  import { execFileSync } from "child_process";
8
9
  import { createInterface } from "readline";
@@ -270,7 +271,7 @@ function unlockProtectedDirs() {
270
271
  }
271
272
  }
272
273
  }
273
- function installHooks(selectedTools = []) {
274
+ function installHooks(selectedTools = [], wrappedMcpConfig) {
274
275
  unlockProtectedDirs();
275
276
  const hooksDir = resolve(".solongate", "hooks");
276
277
  mkdirSync(hooksDir, { recursive: true });
@@ -311,35 +312,25 @@ function installHooks(selectedTools = []) {
311
312
  for (const client of clients) {
312
313
  const clientDir = resolve(client.dir);
313
314
  mkdirSync(clientDir, { recursive: true });
314
- const settingsPath = join(clientDir, "settings.json");
315
315
  const guardCmd = `node .solongate/hooks/guard.mjs ${client.agentId} "${client.agentName}"`;
316
316
  const auditCmd = `node .solongate/hooks/audit.mjs ${client.agentId} "${client.agentName}"`;
317
317
  const stopCmd = `node .solongate/hooks/stop.mjs ${client.agentId} "${client.agentName}"`;
318
- const hookSettings = {
319
- PreToolUse: [
320
- { matcher: "", hooks: [{ type: "command", command: guardCmd }] }
321
- ],
322
- PostToolUse: [
323
- { matcher: "", hooks: [{ type: "command", command: auditCmd }] }
324
- ],
325
- Stop: [
326
- { matcher: "", hooks: [{ type: "command", command: stopCmd }] }
327
- ]
328
- };
329
- let existing = {};
330
- try {
331
- existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
332
- } catch {
333
- }
334
- const merged = { ...existing, hooks: hookSettings };
335
- const mergedStr = JSON.stringify(merged, null, 2) + "\n";
336
- const existingStr = existsSync(settingsPath) ? readFileSync(settingsPath, "utf-8") : "";
337
- if (mergedStr === existingStr) {
338
- skippedNames.push(client.name);
318
+ if (client.key === "cursor") {
319
+ const result = installCursorConfig(clientDir, guardCmd, auditCmd, stopCmd, wrappedMcpConfig);
320
+ if (result === "skipped") skippedNames.push(client.name);
321
+ else activatedNames.push(client.name);
322
+ } else if (client.key === "gemini") {
323
+ const result = installGeminiConfig(clientDir, guardCmd, auditCmd, stopCmd, wrappedMcpConfig);
324
+ if (result === "skipped") skippedNames.push(client.name);
325
+ else activatedNames.push(client.name);
326
+ } else if (client.key === "openclaw") {
327
+ const result = installOpenClawConfig(clientDir);
328
+ if (result === "skipped") skippedNames.push(client.name);
329
+ else activatedNames.push(client.name);
339
330
  } else {
340
- writeFileSync(settingsPath, mergedStr);
341
- console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
342
- activatedNames.push(client.name);
331
+ const result = installStandardHookConfig(clientDir, client.name, guardCmd, auditCmd, stopCmd);
332
+ if (result === "skipped") skippedNames.push(client.name);
333
+ else activatedNames.push(client.name);
343
334
  }
344
335
  }
345
336
  console.log("");
@@ -354,6 +345,125 @@ function installHooks(selectedTools = []) {
354
345
  console.log(` Already configured: ${skippedNames.join(", ")}`);
355
346
  }
356
347
  }
348
+ function installStandardHookConfig(clientDir, clientName, guardCmd, auditCmd, stopCmd) {
349
+ const settingsPath = join(clientDir, "settings.json");
350
+ const hookSettings = {
351
+ PreToolUse: [
352
+ { matcher: "", hooks: [{ type: "command", command: guardCmd }] }
353
+ ],
354
+ PostToolUse: [
355
+ { matcher: "", hooks: [{ type: "command", command: auditCmd }] }
356
+ ],
357
+ Stop: [
358
+ { matcher: "", hooks: [{ type: "command", command: stopCmd }] }
359
+ ]
360
+ };
361
+ let existing = {};
362
+ try {
363
+ existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
364
+ } catch {
365
+ }
366
+ const merged = { ...existing, hooks: hookSettings };
367
+ const mergedStr = JSON.stringify(merged, null, 2) + "\n";
368
+ const existingStr = existsSync(settingsPath) ? readFileSync(settingsPath, "utf-8") : "";
369
+ if (mergedStr === existingStr) return "skipped";
370
+ writeFileSync(settingsPath, mergedStr);
371
+ console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
372
+ return "installed";
373
+ }
374
+ function installCursorConfig(clientDir, guardCmd, auditCmd, _stopCmd, wrappedMcpConfig) {
375
+ let changed = false;
376
+ const hookConfig = {
377
+ preToolUse: [
378
+ { matcher: "", command: guardCmd, failClosed: true }
379
+ ],
380
+ postToolUse: [
381
+ { matcher: "", command: auditCmd }
382
+ ]
383
+ };
384
+ const hooksContent = JSON.stringify({ version: 1, hooks: hookConfig }, null, 2) + "\n";
385
+ const userCursorDir = join(homedir(), ".cursor");
386
+ if (existsSync(userCursorDir)) {
387
+ const userHooksPath = join(userCursorDir, "hooks.json");
388
+ const existingUserHooks = existsSync(userHooksPath) ? readFileSync(userHooksPath, "utf-8") : "";
389
+ if (hooksContent !== existingUserHooks) {
390
+ writeFileSync(userHooksPath, hooksContent);
391
+ console.log(` ${existingUserHooks ? "Updated" : "Created"} ${userHooksPath} (user-level)`);
392
+ changed = true;
393
+ }
394
+ }
395
+ const hooksPath = join(clientDir, "hooks.json");
396
+ const existingHooks = existsSync(hooksPath) ? readFileSync(hooksPath, "utf-8") : "";
397
+ if (hooksContent !== existingHooks) {
398
+ writeFileSync(hooksPath, hooksContent);
399
+ console.log(` ${existingHooks ? "Updated" : "Created"} ${hooksPath}`);
400
+ changed = true;
401
+ }
402
+ if (wrappedMcpConfig) {
403
+ const mcpPath = join(clientDir, "mcp.json");
404
+ const mcpStr = JSON.stringify(wrappedMcpConfig, null, 2) + "\n";
405
+ const existingMcp = existsSync(mcpPath) ? readFileSync(mcpPath, "utf-8") : "";
406
+ if (mcpStr !== existingMcp) {
407
+ writeFileSync(mcpPath, mcpStr);
408
+ console.log(` ${existingMcp ? "Updated" : "Created"} ${mcpPath}`);
409
+ changed = true;
410
+ }
411
+ }
412
+ const staleSettings = join(clientDir, "settings.json");
413
+ if (existsSync(staleSettings)) {
414
+ try {
415
+ const content = JSON.parse(readFileSync(staleSettings, "utf-8"));
416
+ if (content.hooks?.PreToolUse || content.hooks?.PostToolUse) {
417
+ writeFileSync(staleSettings, JSON.stringify({}, null, 2) + "\n");
418
+ console.log(` Cleaned stale ${staleSettings} (had wrong hook format)`);
419
+ changed = true;
420
+ }
421
+ } catch {
422
+ }
423
+ }
424
+ return changed ? "installed" : "skipped";
425
+ }
426
+ function installGeminiConfig(clientDir, guardCmd, auditCmd, _stopCmd, wrappedMcpConfig) {
427
+ const settingsPath = join(clientDir, "settings.json");
428
+ let existing = {};
429
+ try {
430
+ existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
431
+ } catch {
432
+ }
433
+ const merged = { ...existing };
434
+ if (wrappedMcpConfig) {
435
+ merged.mcpServers = wrappedMcpConfig.mcpServers;
436
+ }
437
+ merged.hooks = {
438
+ BeforeTool: [
439
+ { matcher: ".*", command: guardCmd }
440
+ ],
441
+ AfterTool: [
442
+ { matcher: ".*", command: auditCmd }
443
+ ]
444
+ };
445
+ const mergedStr = JSON.stringify(merged, null, 2) + "\n";
446
+ const existingStr = existsSync(settingsPath) ? readFileSync(settingsPath, "utf-8") : "";
447
+ if (mergedStr === existingStr) return "skipped";
448
+ writeFileSync(settingsPath, mergedStr);
449
+ console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
450
+ return "installed";
451
+ }
452
+ function installOpenClawConfig(clientDir) {
453
+ const yamlPath = resolve("openclaw.yaml");
454
+ const pluginLine = "@solongate/openclaw-plugin";
455
+ if (existsSync(yamlPath)) {
456
+ const content = readFileSync(yamlPath, "utf-8");
457
+ if (content.includes(pluginLine)) {
458
+ return "skipped";
459
+ }
460
+ }
461
+ console.log(` OpenClaw uses plugins, not hooks. Add to your openclaw.yaml:`);
462
+ console.log(` plugins:`);
463
+ console.log(` - "${pluginLine}"`);
464
+ console.log(` Then: npm install ${pluginLine}`);
465
+ return "installed";
466
+ }
357
467
  function ensureEnvFile() {
358
468
  let envChanged = false;
359
469
  let gitignoreChanged = false;
@@ -504,7 +614,7 @@ async function main() {
504
614
  console.log(" All servers are already protected by SolonGate!");
505
615
  ensureEnvFile();
506
616
  console.log("");
507
- installHooks(options.tools);
617
+ installHooks(options.tools, config);
508
618
  process.exit(0);
509
619
  }
510
620
  if (!options.all) {
@@ -607,7 +717,7 @@ async function main() {
607
717
  startSpinner("Installing hooks...");
608
718
  await sleep(300);
609
719
  stopSpinner("\u2713 Hooks");
610
- installHooks(options.tools);
720
+ installHooks(options.tools, newConfig);
611
721
  console.log("");
612
722
  const allUpToDate = toProtect.length === 0 && alreadyProtected.length === serverNames.length;
613
723
  console.log(" \u2500\u2500 Summary \u2500\u2500");
package/dist/lib.js CHANGED
@@ -4252,6 +4252,17 @@ var SolonGateProxy = class {
4252
4252
  log2("WARNING:", w);
4253
4253
  }
4254
4254
  }
4255
+ /** Normalize well-known MCP client names to display-friendly agent identities */
4256
+ normalizeAgentName(raw) {
4257
+ const lower = raw.toLowerCase();
4258
+ if (lower.includes("cursor")) return { id: "cursor", name: "Cursor" };
4259
+ if (lower.includes("claude-code") || lower === "claude code") return { id: "claude-code", name: "Claude Code" };
4260
+ if (lower.includes("claude")) return { id: "claude-desktop", name: "Claude Desktop" };
4261
+ if (lower.includes("gemini")) return { id: "gemini-cli", name: "Gemini CLI" };
4262
+ if (lower.includes("antigravity")) return { id: "antigravity", name: "Antigravity" };
4263
+ if (lower.includes("perplexity")) return { id: "perplexity", name: "Perplexity" };
4264
+ return { id: raw.toLowerCase().replace(/\s+/g, "-"), name: raw };
4265
+ }
4255
4266
  /** Extract sub-agent identity from MCP _meta field */
4256
4267
  extractSubAgent(request) {
4257
4268
  const meta = request?.params?._meta;
@@ -4448,10 +4459,12 @@ var SolonGateProxy = class {
4448
4459
  this.server.oninitialized = () => {
4449
4460
  if (this.server) {
4450
4461
  const clientVersion = this.server.getClientVersion();
4462
+ log2(`MCP clientInfo raw: ${JSON.stringify(clientVersion)}`);
4451
4463
  if (clientVersion?.name) {
4452
- this.agentId = clientVersion.name;
4453
- this.agentName = clientVersion.name;
4454
- log2(`Agent identified from MCP clientInfo: ${this.agentName}`);
4464
+ const normalized = this.normalizeAgentName(clientVersion.name);
4465
+ this.agentId = normalized.id;
4466
+ this.agentName = normalized.name;
4467
+ log2(`Agent identified from MCP clientInfo: ${this.agentName} (raw: ${clientVersion.name})`);
4455
4468
  }
4456
4469
  }
4457
4470
  };
package/hooks/audit.mjs CHANGED
@@ -26,8 +26,9 @@ const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
26
26
  const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
27
27
 
28
28
  // Agent identity from CLI args: node audit.mjs <agent_id> <agent_name>
29
- const AGENT_ID = process.argv[2] || 'claude-code';
30
- const AGENT_NAME = process.argv[3] || 'Claude Code';
29
+ // Can be overridden at runtime when stdin contains cursor_version or gemini_version
30
+ let AGENT_ID = process.argv[2] || 'claude-code';
31
+ let AGENT_NAME = process.argv[3] || 'Claude Code';
31
32
 
32
33
  if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
33
34
 
@@ -36,16 +37,49 @@ process.stdin.on('data', c => input += c);
36
37
  process.stdin.on('end', async () => {
37
38
  try {
38
39
  const data = JSON.parse(input);
39
- const toolName = data.tool_name || 'unknown';
40
- const toolInput = data.tool_input || {};
40
+
41
+ // Debug: dump raw stdin to file for agent detection troubleshooting
42
+ try { writeFileSync(resolve('.solongate', '.debug-stdin'), JSON.stringify(data, null, 2)); } catch {}
43
+
44
+ // Auto-detect agent from stdin data (overrides CLI args if detected)
45
+ if (data.cursor_version) {
46
+ AGENT_ID = 'cursor';
47
+ AGENT_NAME = 'Cursor';
48
+ } else if (data.gemini_version) {
49
+ AGENT_ID = 'gemini-cli';
50
+ AGENT_NAME = 'Gemini CLI';
51
+ }
52
+
53
+ // Normalize field names across tools (Claude: tool_name, others may use toolName)
54
+ const toolName = data.tool_name || data.toolName || 'unknown';
55
+ const toolInput = data.tool_input || data.toolInput || data.params || {};
41
56
 
42
57
  if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
43
58
  process.exit(0);
44
59
  }
45
60
 
46
- const hasError = data.tool_response?.error ||
47
- data.tool_response?.exitCode > 0 ||
48
- data.tool_response?.isError;
61
+ // Check if guard.mjs already logged a DENY for this tool (avoid duplicate ALLOW after DENY)
62
+ let guardDenied = false;
63
+ try {
64
+ const denyFlagPath = resolve('.solongate', '.last-deny');
65
+ if (existsSync(denyFlagPath)) {
66
+ const flag = JSON.parse(readFileSync(denyFlagPath, 'utf-8'));
67
+ // If deny was recent (< 10s) and same tool, this postToolUse is a duplicate
68
+ if (flag.ts && Date.now() - flag.ts < 10000 && flag.tool === toolName) {
69
+ guardDenied = true;
70
+ }
71
+ }
72
+ } catch {}
73
+
74
+ // Cursor uses result_json, Claude uses tool_response
75
+ const toolResponse = data.tool_response || data.toolResponse || {};
76
+ const resultJson = data.result_json ? (typeof data.result_json === 'string' ? data.result_json : JSON.stringify(data.result_json)) : '';
77
+
78
+ const hasError = guardDenied ||
79
+ toolResponse.error ||
80
+ toolResponse.exitCode > 0 ||
81
+ toolResponse.isError ||
82
+ (resultJson && resultJson.includes('"error"'));
49
83
 
50
84
  const argsSummary = {};
51
85
  for (const [k, v] of Object.entries(toolInput)) {
@@ -72,8 +106,8 @@ process.stdin.on('end', async () => {
72
106
  tool: toolName,
73
107
  arguments: argsSummary,
74
108
  decision: hasError ? 'DENY' : 'ALLOW',
75
- reason: hasError ? 'tool returned error' : 'allowed',
76
- source: 'claude-code-hook',
109
+ reason: guardDenied ? 'blocked by policy guard' : hasError ? 'tool returned error' : 'allowed',
110
+ source: `${AGENT_ID}-hook`,
77
111
  evaluationTimeMs: 0,
78
112
  agent_id: AGENT_ID,
79
113
  agent_name: AGENT_NAME,
package/hooks/guard.mjs CHANGED
@@ -41,15 +41,53 @@ const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
41
41
  const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
42
42
 
43
43
  // Agent identity from CLI args: node guard.mjs <agent_id> <agent_name>
44
- const AGENT_ID = process.argv[2] || 'claude-code';
45
- const AGENT_NAME = process.argv[3] || 'Claude Code';
44
+ // Can be overridden at runtime when stdin contains cursor_version or gemini_version
45
+ let AGENT_ID = process.argv[2] || 'claude-code';
46
+ let AGENT_NAME = process.argv[3] || 'Claude Code';
47
+
48
+ // ── Per-tool block/allow output ──
49
+ // Claude Code: exit 2 + stderr = BLOCK, exit 0 = ALLOW
50
+ // Cursor: JSON stdout {"permission": "deny", "user_message": "..."} = BLOCK
51
+ // Gemini CLI: JSON stdout {"decision": "deny", "reason": "..."} = BLOCK
52
+ function blockTool(reason) {
53
+ if (AGENT_ID === 'cursor') {
54
+ // Cursor: JSON stdout with permission: "deny", exit 0
55
+ process.stdout.write(JSON.stringify({
56
+ permission: 'deny',
57
+ user_message: `[SolonGate] ${reason}`,
58
+ agent_message: `BLOCKED by SolonGate security policy: ${reason}`,
59
+ }));
60
+ process.exit(0);
61
+ } else if (AGENT_ID === 'gemini-cli') {
62
+ process.stdout.write(JSON.stringify({
63
+ decision: 'deny',
64
+ reason: `[SolonGate] ${reason}`,
65
+ }));
66
+ process.exit(0);
67
+ } else {
68
+ // Claude Code, Antigravity, Perplexity — exit code 2
69
+ process.stderr.write(reason);
70
+ process.exit(2);
71
+ }
72
+ }
73
+
74
+ function allowTool() {
75
+ if (AGENT_ID === 'cursor') {
76
+ process.stdout.write(JSON.stringify({ permission: 'allow' }));
77
+ } else if (AGENT_ID === 'gemini-cli') {
78
+ process.stdout.write(JSON.stringify({ decision: 'allow' }));
79
+ }
80
+ process.exit(0);
81
+ }
46
82
 
47
83
  // Write flag file so stop.mjs knows a tool call (DENY) happened and doesn't log extra ALLOW
48
- function writeDenyFlag() {
84
+ function writeDenyFlag(toolName) {
49
85
  try {
50
86
  const flagDir = resolve('.solongate');
51
87
  mkdirSync(flagDir, { recursive: true });
52
88
  writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
89
+ // Write deny-specific flag so audit.mjs can detect and skip duplicate ALLOW logging
90
+ writeFileSync(join(flagDir, '.last-deny'), JSON.stringify({ tool: toolName, ts: Date.now() }));
53
91
  } catch {}
54
92
  }
55
93
 
@@ -335,8 +373,27 @@ let input = '';
335
373
  process.stdin.on('data', c => input += c);
336
374
  process.stdin.on('end', async () => {
337
375
  try {
338
- const data = JSON.parse(input);
339
- const args = data.tool_input || {};
376
+ const raw = JSON.parse(input);
377
+
378
+ // Auto-detect agent from stdin data (overrides CLI args if detected)
379
+ if (raw.cursor_version) {
380
+ AGENT_ID = 'cursor';
381
+ AGENT_NAME = 'Cursor';
382
+ } else if (raw.gemini_version) {
383
+ AGENT_ID = 'gemini-cli';
384
+ AGENT_NAME = 'Gemini CLI';
385
+ }
386
+
387
+ // Normalize field names across tools (Claude: tool_name/tool_input, others may use toolName/toolInput/params)
388
+ const data = {
389
+ ...raw,
390
+ tool_name: raw.tool_name || raw.toolName || '',
391
+ tool_input: raw.tool_input || raw.toolInput || raw.params || {},
392
+ tool_response: raw.tool_response || raw.toolResponse || {},
393
+ cwd: raw.cwd || process.cwd(),
394
+ session_id: raw.session_id || raw.sessionId || '',
395
+ };
396
+ const args = data.tool_input;
340
397
 
341
398
  // ── Self-protection: block access to hook files and settings ──
342
399
  // Hardcoded, no bypass possible — runs before policy/PI config
@@ -358,16 +415,15 @@ process.stdin.on('end', async () => {
358
415
  body: JSON.stringify({
359
416
  tool: data.tool_name || '', arguments: args,
360
417
  decision: 'DENY', reason,
361
- source: 'claude-code-guard',
418
+ source: `${AGENT_ID}-guard`,
362
419
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
363
420
  }),
364
421
  signal: AbortSignal.timeout(3000),
365
422
  });
366
423
  } catch {}
367
424
  }
368
- writeDenyFlag();
369
- process.stderr.write(reason);
370
- process.exit(2);
425
+ writeDenyFlag(toolName);
426
+ blockTool(reason);
371
427
  }
372
428
 
373
429
  // ── Normalization layers ──
@@ -1006,7 +1062,7 @@ process.stdin.on('end', async () => {
1006
1062
  arguments: args,
1007
1063
  decision: isLogOnly ? 'ALLOW' : 'DENY',
1008
1064
  reason: msg,
1009
- source: 'claude-code-guard',
1065
+ source: `${AGENT_ID}-guard`,
1010
1066
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
1011
1067
  pi_detected: true,
1012
1068
  pi_trust_score: piResult.trustScore,
@@ -1044,9 +1100,8 @@ process.stdin.on('end', async () => {
1044
1100
  process.stderr.write(msg);
1045
1101
  // Fall through to policy evaluation (don't exit)
1046
1102
  } else {
1047
- writeDenyFlag();
1048
- process.stderr.write(msg);
1049
- process.exit(2);
1103
+ writeDenyFlag(toolName);
1104
+ blockTool(msg);
1050
1105
  }
1051
1106
  }
1052
1107
 
@@ -1068,7 +1123,7 @@ process.stdin.on('end', async () => {
1068
1123
  arguments: args,
1069
1124
  decision: 'ALLOW',
1070
1125
  reason: 'Prompt injection detected but below threshold (trust: ' + (piResult.trustScore * 100).toFixed(0) + '%)',
1071
- source: 'claude-code-guard',
1126
+ source: `${AGENT_ID}-guard`,
1072
1127
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
1073
1128
  pi_detected: true,
1074
1129
  pi_trust_score: piResult.trustScore,
@@ -1080,7 +1135,7 @@ process.stdin.on('end', async () => {
1080
1135
  });
1081
1136
  } catch {}
1082
1137
  }
1083
- process.exit(0); // No policy = allow all
1138
+ allowTool(); // No policy = allow all
1084
1139
  }
1085
1140
 
1086
1141
  let reason = evaluate(policy, args);
@@ -1234,7 +1289,7 @@ Respond with ONLY valid JSON: {"decision": "ALLOW" or "DENY", "reason": "brief e
1234
1289
  const logEntry = {
1235
1290
  tool: toolName, arguments: args,
1236
1291
  decision: 'DENY', reason,
1237
- source: 'claude-code-guard',
1292
+ source: `${AGENT_ID}-guard`,
1238
1293
  agent_id: AGENT_ID, agent_name: AGENT_NAME,
1239
1294
  };
1240
1295
  if (piResult) {
@@ -1252,10 +1307,9 @@ Respond with ONLY valid JSON: {"decision": "ALLOW" or "DENY", "reason": "brief e
1252
1307
  });
1253
1308
  } catch {}
1254
1309
  }
1255
- writeDenyFlag();
1256
- process.stderr.write(reason);
1257
- process.exit(2);
1310
+ writeDenyFlag(toolName);
1311
+ blockTool(reason);
1258
1312
  }
1259
1313
  } catch {}
1260
- process.exit(0);
1314
+ allowTool();
1261
1315
  });
package/hooks/stop.mjs CHANGED
@@ -27,8 +27,9 @@ const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
27
27
  const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
28
28
 
29
29
  // Agent identity from CLI args: node stop.mjs <agent_id> <agent_name>
30
- const AGENT_ID = process.argv[2] || 'claude-code';
31
- const AGENT_NAME = process.argv[3] || 'Claude Code';
30
+ // Can be overridden at runtime when stdin contains cursor_version or gemini_version
31
+ let AGENT_ID = process.argv[2] || 'claude-code';
32
+ let AGENT_NAME = process.argv[3] || 'Claude Code';
32
33
 
33
34
  if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
34
35
 
@@ -42,6 +43,13 @@ let input = '';
42
43
  process.stdin.on('data', c => input += c);
43
44
  process.stdin.on('end', async () => {
44
45
  try {
46
+ // Auto-detect agent from stdin data
47
+ try {
48
+ const raw = JSON.parse(input);
49
+ if (raw.cursor_version) { AGENT_ID = 'cursor'; AGENT_NAME = 'Cursor'; }
50
+ else if (raw.gemini_version) { AGENT_ID = 'gemini-cli'; AGENT_NAME = 'Gemini CLI'; }
51
+ } catch {}
52
+
45
53
  // Check if tool calls were made in this turn
46
54
  if (existsSync(flagFile)) {
47
55
  // Tool calls happened → audit.mjs already logged them. Clean up flag.
@@ -61,7 +69,7 @@ process.stdin.on('end', async () => {
61
69
  arguments: {},
62
70
  decision: 'ALLOW',
63
71
  reason: 'text response (no tool calls)',
64
- source: 'claude-code-hook',
72
+ source: `${AGENT_ID}-hook`,
65
73
  evaluationTimeMs: 0,
66
74
  agent_id: AGENT_ID,
67
75
  agent_name: AGENT_NAME,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.29.1",
4
- "description": "MCP security proxy — protect any MCP server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
3
+ "version": "0.30.1",
4
+ "description": "AI tool security proxy — protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "solongate-proxy": "./dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "clean": "rm -rf dist .turbo"
30
30
  },
31
31
  "keywords": [
32
- "mcp",
33
- "model-context-protocol",
32
+ "ai-tool-security",
33
+ "ai-tool-proxy",
34
34
  "security",
35
35
  "proxy",
36
36
  "gateway",