@solongate/proxy 0.29.0 → 0.30.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/README.md +7 -7
- package/dist/index.js +138 -28
- package/dist/init.js +138 -28
- package/hooks/audit.mjs +46 -13
- package/hooks/guard.mjs +116 -30
- package/hooks/stop.mjs +16 -8
- 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");
|
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/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,46 @@ 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
|
+
// Auto-detect agent from stdin data (overrides CLI args if detected)
|
|
42
|
+
if (data.cursor_version) {
|
|
43
|
+
AGENT_ID = 'cursor';
|
|
44
|
+
AGENT_NAME = 'Cursor';
|
|
45
|
+
} else if (data.gemini_version) {
|
|
46
|
+
AGENT_ID = 'gemini-cli';
|
|
47
|
+
AGENT_NAME = 'Gemini CLI';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Normalize field names across tools (Claude: tool_name, others may use toolName)
|
|
51
|
+
const toolName = data.tool_name || data.toolName || 'unknown';
|
|
52
|
+
const toolInput = data.tool_input || data.toolInput || data.params || {};
|
|
41
53
|
|
|
42
54
|
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
43
55
|
process.exit(0);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
// Check if guard.mjs already logged a DENY for this tool (avoid duplicate ALLOW after DENY)
|
|
59
|
+
let guardDenied = false;
|
|
60
|
+
try {
|
|
61
|
+
const denyFlagPath = resolve('.solongate', '.last-deny');
|
|
62
|
+
if (existsSync(denyFlagPath)) {
|
|
63
|
+
const flag = JSON.parse(readFileSync(denyFlagPath, 'utf-8'));
|
|
64
|
+
// If deny was recent (< 10s) and same tool, this postToolUse is a duplicate
|
|
65
|
+
if (flag.ts && Date.now() - flag.ts < 10000 && flag.tool === toolName) {
|
|
66
|
+
guardDenied = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
|
|
71
|
+
// Cursor uses result_json, Claude uses tool_response
|
|
72
|
+
const toolResponse = data.tool_response || data.toolResponse || {};
|
|
73
|
+
const resultJson = data.result_json ? (typeof data.result_json === 'string' ? data.result_json : JSON.stringify(data.result_json)) : '';
|
|
74
|
+
|
|
75
|
+
const hasError = guardDenied ||
|
|
76
|
+
toolResponse.error ||
|
|
77
|
+
toolResponse.exitCode > 0 ||
|
|
78
|
+
toolResponse.isError ||
|
|
79
|
+
(resultJson && resultJson.includes('"error"'));
|
|
49
80
|
|
|
50
81
|
const argsSummary = {};
|
|
51
82
|
for (const [k, v] of Object.entries(toolInput)) {
|
|
@@ -61,7 +92,8 @@ process.stdin.on('end', async () => {
|
|
|
61
92
|
writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
|
|
62
93
|
} catch {}
|
|
63
94
|
|
|
64
|
-
|
|
95
|
+
// Fire-and-forget: don't block tool execution waiting for API response
|
|
96
|
+
fetch(`${API_URL}/api/v1/audit-logs`, {
|
|
65
97
|
method: 'POST',
|
|
66
98
|
headers: {
|
|
67
99
|
'Authorization': `Bearer ${API_KEY}`,
|
|
@@ -71,16 +103,17 @@ process.stdin.on('end', async () => {
|
|
|
71
103
|
tool: toolName,
|
|
72
104
|
arguments: argsSummary,
|
|
73
105
|
decision: hasError ? 'DENY' : 'ALLOW',
|
|
74
|
-
reason: hasError ? 'tool returned error' : 'allowed',
|
|
75
|
-
source:
|
|
106
|
+
reason: guardDenied ? 'blocked by policy guard' : hasError ? 'tool returned error' : 'allowed',
|
|
107
|
+
source: `${AGENT_ID}-hook`,
|
|
76
108
|
evaluationTimeMs: 0,
|
|
77
109
|
agent_id: AGENT_ID,
|
|
78
110
|
agent_name: AGENT_NAME,
|
|
79
111
|
}),
|
|
80
112
|
signal: AbortSignal.timeout(5000),
|
|
81
|
-
});
|
|
113
|
+
}).catch(() => {}).finally(() => process.exit(0));
|
|
114
|
+
// Exit after short delay if fetch hangs on DNS/connect
|
|
115
|
+
setTimeout(() => process.exit(0), 3000);
|
|
82
116
|
} catch {
|
|
83
|
-
|
|
117
|
+
process.exit(0);
|
|
84
118
|
}
|
|
85
|
-
process.exit(0);
|
|
86
119
|
});
|
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 ──
|
|
@@ -909,19 +965,34 @@ process.stdin.on('end', async () => {
|
|
|
909
965
|
}
|
|
910
966
|
}
|
|
911
967
|
|
|
912
|
-
// ── Fetch PI config from Cloud ──
|
|
968
|
+
// ── Fetch PI config from Cloud (cached for 60s to avoid per-call latency) ──
|
|
913
969
|
let piCfg = { piEnabled: true, piThreshold: 0.5, piMode: 'block', piWhitelist: [], piToolConfig: {}, piCustomPatterns: [], piWebhookUrl: null };
|
|
914
970
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
971
|
+
const cacheFile = join(resolve('.solongate'), '.pi-config-cache.json');
|
|
972
|
+
let usedCache = false;
|
|
915
973
|
try {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
974
|
+
if (existsSync(cacheFile)) {
|
|
975
|
+
const cached = JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
976
|
+
if (cached._ts && Date.now() - cached._ts < 60000) {
|
|
977
|
+
const { _ts, ...rest } = cached;
|
|
978
|
+
piCfg = { ...piCfg, ...rest };
|
|
979
|
+
usedCache = true;
|
|
980
|
+
}
|
|
923
981
|
}
|
|
924
|
-
} catch {}
|
|
982
|
+
} catch {}
|
|
983
|
+
if (!usedCache) {
|
|
984
|
+
try {
|
|
985
|
+
const cfgRes = await fetch(API_URL + '/api/v1/project-config', {
|
|
986
|
+
headers: { 'Authorization': 'Bearer ' + API_KEY },
|
|
987
|
+
signal: AbortSignal.timeout(3000),
|
|
988
|
+
});
|
|
989
|
+
if (cfgRes.ok) {
|
|
990
|
+
const cfg = await cfgRes.json();
|
|
991
|
+
piCfg = { ...piCfg, ...cfg };
|
|
992
|
+
try { writeFileSync(cacheFile, JSON.stringify({ ...cfg, _ts: Date.now() })); } catch {}
|
|
993
|
+
}
|
|
994
|
+
} catch {} // Fallback: defaults (safe)
|
|
995
|
+
}
|
|
925
996
|
}
|
|
926
997
|
|
|
927
998
|
// ── Per-tool config: check if PI scanning is disabled for this tool ──
|
|
@@ -991,7 +1062,7 @@ process.stdin.on('end', async () => {
|
|
|
991
1062
|
arguments: args,
|
|
992
1063
|
decision: isLogOnly ? 'ALLOW' : 'DENY',
|
|
993
1064
|
reason: msg,
|
|
994
|
-
source:
|
|
1065
|
+
source: `${AGENT_ID}-guard`,
|
|
995
1066
|
agent_id: AGENT_ID, agent_name: AGENT_NAME,
|
|
996
1067
|
pi_detected: true,
|
|
997
1068
|
pi_trust_score: piResult.trustScore,
|
|
@@ -1029,9 +1100,8 @@ process.stdin.on('end', async () => {
|
|
|
1029
1100
|
process.stderr.write(msg);
|
|
1030
1101
|
// Fall through to policy evaluation (don't exit)
|
|
1031
1102
|
} else {
|
|
1032
|
-
writeDenyFlag();
|
|
1033
|
-
|
|
1034
|
-
process.exit(2);
|
|
1103
|
+
writeDenyFlag(toolName);
|
|
1104
|
+
blockTool(msg);
|
|
1035
1105
|
}
|
|
1036
1106
|
}
|
|
1037
1107
|
|
|
@@ -1053,7 +1123,7 @@ process.stdin.on('end', async () => {
|
|
|
1053
1123
|
arguments: args,
|
|
1054
1124
|
decision: 'ALLOW',
|
|
1055
1125
|
reason: 'Prompt injection detected but below threshold (trust: ' + (piResult.trustScore * 100).toFixed(0) + '%)',
|
|
1056
|
-
source:
|
|
1126
|
+
source: `${AGENT_ID}-guard`,
|
|
1057
1127
|
agent_id: AGENT_ID, agent_name: AGENT_NAME,
|
|
1058
1128
|
pi_detected: true,
|
|
1059
1129
|
pi_trust_score: piResult.trustScore,
|
|
@@ -1065,7 +1135,7 @@ process.stdin.on('end', async () => {
|
|
|
1065
1135
|
});
|
|
1066
1136
|
} catch {}
|
|
1067
1137
|
}
|
|
1068
|
-
|
|
1138
|
+
allowTool(); // No policy = allow all
|
|
1069
1139
|
}
|
|
1070
1140
|
|
|
1071
1141
|
let reason = evaluate(policy, args);
|
|
@@ -1078,8 +1148,23 @@ process.stdin.on('end', async () => {
|
|
|
1078
1148
|
let aiJudgeEndpoint = 'https://api.groq.com/openai';
|
|
1079
1149
|
let aiJudgeTimeout = 5000;
|
|
1080
1150
|
|
|
1081
|
-
// Check cloud config for AI Judge settings
|
|
1151
|
+
// Check cloud config for AI Judge settings (cached for 60s)
|
|
1082
1152
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
1153
|
+
const ajCacheFile = join(resolve('.solongate'), '.aj-config-cache.json');
|
|
1154
|
+
let ajCached = false;
|
|
1155
|
+
try {
|
|
1156
|
+
if (existsSync(ajCacheFile)) {
|
|
1157
|
+
const cached = JSON.parse(readFileSync(ajCacheFile, 'utf-8'));
|
|
1158
|
+
if (cached._ts && Date.now() - cached._ts < 60000) {
|
|
1159
|
+
aiJudgeEnabled = Boolean(cached.enabled);
|
|
1160
|
+
if (cached.model) aiJudgeModel = cached.model;
|
|
1161
|
+
if (cached.endpoint) aiJudgeEndpoint = cached.endpoint;
|
|
1162
|
+
if (cached.timeoutMs) aiJudgeTimeout = cached.timeoutMs;
|
|
1163
|
+
ajCached = true;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
} catch {}
|
|
1167
|
+
if (!ajCached) {
|
|
1083
1168
|
try {
|
|
1084
1169
|
const cfgRes = await fetch(API_URL + '/api/v1/project-config/ai-judge', {
|
|
1085
1170
|
headers: { 'Authorization': 'Bearer ' + API_KEY },
|
|
@@ -1091,8 +1176,10 @@ process.stdin.on('end', async () => {
|
|
|
1091
1176
|
if (cfg.model) aiJudgeModel = cfg.model;
|
|
1092
1177
|
if (cfg.endpoint) aiJudgeEndpoint = cfg.endpoint;
|
|
1093
1178
|
if (cfg.timeoutMs) aiJudgeTimeout = cfg.timeoutMs;
|
|
1179
|
+
try { writeFileSync(ajCacheFile, JSON.stringify({ ...cfg, _ts: Date.now() })); } catch {}
|
|
1094
1180
|
}
|
|
1095
1181
|
} catch {}
|
|
1182
|
+
}
|
|
1096
1183
|
}
|
|
1097
1184
|
|
|
1098
1185
|
if (aiJudgeEnabled && GROQ_KEY) {
|
|
@@ -1202,7 +1289,7 @@ Respond with ONLY valid JSON: {"decision": "ALLOW" or "DENY", "reason": "brief e
|
|
|
1202
1289
|
const logEntry = {
|
|
1203
1290
|
tool: toolName, arguments: args,
|
|
1204
1291
|
decision: 'DENY', reason,
|
|
1205
|
-
source:
|
|
1292
|
+
source: `${AGENT_ID}-guard`,
|
|
1206
1293
|
agent_id: AGENT_ID, agent_name: AGENT_NAME,
|
|
1207
1294
|
};
|
|
1208
1295
|
if (piResult) {
|
|
@@ -1220,10 +1307,9 @@ Respond with ONLY valid JSON: {"decision": "ALLOW" or "DENY", "reason": "brief e
|
|
|
1220
1307
|
});
|
|
1221
1308
|
} catch {}
|
|
1222
1309
|
}
|
|
1223
|
-
writeDenyFlag();
|
|
1224
|
-
|
|
1225
|
-
process.exit(2);
|
|
1310
|
+
writeDenyFlag(toolName);
|
|
1311
|
+
blockTool(reason);
|
|
1226
1312
|
}
|
|
1227
1313
|
} catch {}
|
|
1228
|
-
|
|
1314
|
+
allowTool();
|
|
1229
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.
|
|
@@ -49,8 +57,8 @@ process.stdin.on('end', async () => {
|
|
|
49
57
|
process.exit(0);
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
// No tool calls → log 1 ALLOW for the text-only response
|
|
53
|
-
|
|
60
|
+
// No tool calls → log 1 ALLOW for the text-only response (fire-and-forget)
|
|
61
|
+
fetch(`${API_URL}/api/v1/audit-logs`, {
|
|
54
62
|
method: 'POST',
|
|
55
63
|
headers: {
|
|
56
64
|
'Authorization': `Bearer ${API_KEY}`,
|
|
@@ -61,15 +69,15 @@ 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,
|
|
68
76
|
}),
|
|
69
77
|
signal: AbortSignal.timeout(5000),
|
|
70
|
-
});
|
|
78
|
+
}).catch(() => {}).finally(() => process.exit(0));
|
|
79
|
+
setTimeout(() => process.exit(0), 3000);
|
|
71
80
|
} catch {
|
|
72
|
-
|
|
81
|
+
process.exit(0);
|
|
73
82
|
}
|
|
74
|
-
process.exit(0);
|
|
75
83
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.30.0",
|
|
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",
|