@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 +7 -7
- package/dist/index.js +154 -31
- package/dist/init.js +138 -28
- package/dist/lib.js +16 -3
- package/hooks/audit.mjs +43 -9
- package/hooks/guard.mjs +74 -20
- package/hooks/stop.mjs +11 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @solongate/proxy
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
755
|
-
|
|
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
|
-
|
|
6103
|
-
this.
|
|
6104
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
4453
|
-
this.
|
|
4454
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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:
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
339
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1257
|
-
process.exit(2);
|
|
1310
|
+
writeDenyFlag(toolName);
|
|
1311
|
+
blockTool(reason);
|
|
1258
1312
|
}
|
|
1259
1313
|
} catch {}
|
|
1260
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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:
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"ai-tool-security",
|
|
33
|
+
"ai-tool-proxy",
|
|
34
34
|
"security",
|
|
35
35
|
"proxy",
|
|
36
36
|
"gateway",
|