@solongate/proxy 0.81.1 → 0.81.3
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/hooks/audit.mjs +19 -7
- package/hooks/guard.bundled.mjs +259 -211
- package/hooks/guard.mjs +52 -10
- package/package.json +1 -1
package/hooks/audit.mjs
CHANGED
|
@@ -392,8 +392,13 @@ const AGENT_NAME = process.argv[3] || 'Claude Code';
|
|
|
392
392
|
if (!API_KEY || !(API_KEY.startsWith('sg_live_') || API_KEY.startsWith('sg_test_'))) process.exit(0);
|
|
393
393
|
|
|
394
394
|
let input = '';
|
|
395
|
-
|
|
396
|
-
process.stdin
|
|
395
|
+
// Read stdin SYNCHRONOUSLY (fd 0). Calling process.exit() from inside the
|
|
396
|
+
// process.stdin stream 'end' callback aborts on Windows + Node 24 with
|
|
397
|
+
// `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c`
|
|
398
|
+
// (libuv double-closes the stdin pipe handle during teardown). Reading fd 0 to
|
|
399
|
+
// EOF avoids creating that handle, so the exits below tear down cleanly.
|
|
400
|
+
try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
401
|
+
;(async () => {
|
|
397
402
|
try {
|
|
398
403
|
const data = JSON.parse(input);
|
|
399
404
|
let EMITTED_PAYLOAD = null;
|
|
@@ -526,7 +531,12 @@ process.stdin.on('end', async () => {
|
|
|
526
531
|
// on the write's flush callback (and on the fire-and-forget audit POST).
|
|
527
532
|
let flushed = (typeof EMITTED_PAYLOAD !== 'string');
|
|
528
533
|
let fetchDone = false;
|
|
529
|
-
|
|
534
|
+
// Defer the exit by one loop tick (setImmediate). Calling process.exit() in the
|
|
535
|
+
// same tick the audit-log fetch settled aborts on Windows + Node 24 with
|
|
536
|
+
// `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c`
|
|
537
|
+
// — a threadpool worker is still mid-uv_async_send when exit() closes the loop's
|
|
538
|
+
// async handle. One tick lets libuv drain that completion; we then still hard-exit.
|
|
539
|
+
const maybeExit = () => { if (flushed && fetchDone) { process.exitCode = 0; setImmediate(() => { try { process.exit(0); } catch {} }); } };
|
|
530
540
|
if (typeof EMITTED_PAYLOAD === 'string') {
|
|
531
541
|
try { process.stdout.write(EMITTED_PAYLOAD, () => { flushed = true; maybeExit(); }); }
|
|
532
542
|
catch { flushed = true; }
|
|
@@ -576,9 +586,11 @@ process.stdin.on('end', async () => {
|
|
|
576
586
|
signal: AbortSignal.timeout(5000),
|
|
577
587
|
}).catch(() => {}).finally(() => { fetchDone = true; maybeExit(); });
|
|
578
588
|
}
|
|
579
|
-
// Hard backstop: exit even if the write callback or fetch never settles.
|
|
580
|
-
|
|
589
|
+
// Hard backstop: exit even if the write callback or fetch never settles. By 3s
|
|
590
|
+
// the threadpool has long drained, so a direct exit here is safe.
|
|
591
|
+
setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 3000);
|
|
581
592
|
} catch {
|
|
582
|
-
|
|
593
|
+
// Defer one tick in case an error was thrown while a fetch was still settling.
|
|
594
|
+
try { setImmediate(() => { try { process.exit(0); } catch {} }); } catch { try { process.exit(0); } catch {} }
|
|
583
595
|
}
|
|
584
|
-
});
|
|
596
|
+
})();
|
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -6757,7 +6757,44 @@ async function refreshPolicyCache() {
|
|
|
6757
6757
|
}
|
|
6758
6758
|
}
|
|
6759
6759
|
if (REFRESH_MODE) {
|
|
6760
|
-
refreshPolicyCache().finally(() =>
|
|
6760
|
+
refreshPolicyCache().finally(() => {
|
|
6761
|
+
process.exitCode = 0;
|
|
6762
|
+
try {
|
|
6763
|
+
setImmediate(() => {
|
|
6764
|
+
try {
|
|
6765
|
+
process.exit(0);
|
|
6766
|
+
} catch {
|
|
6767
|
+
}
|
|
6768
|
+
});
|
|
6769
|
+
} catch {
|
|
6770
|
+
try {
|
|
6771
|
+
process.exit(0);
|
|
6772
|
+
} catch {
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
});
|
|
6776
|
+
}
|
|
6777
|
+
var SG_DONE = Symbol("sg-done");
|
|
6778
|
+
var _sgDone = false;
|
|
6779
|
+
function sgFinish(code) {
|
|
6780
|
+
if (!_sgDone) {
|
|
6781
|
+
_sgDone = true;
|
|
6782
|
+
process.exitCode = code;
|
|
6783
|
+
try {
|
|
6784
|
+
setImmediate(() => {
|
|
6785
|
+
try {
|
|
6786
|
+
process.exit(process.exitCode || 0);
|
|
6787
|
+
} catch {
|
|
6788
|
+
}
|
|
6789
|
+
});
|
|
6790
|
+
} catch {
|
|
6791
|
+
try {
|
|
6792
|
+
process.exit(code);
|
|
6793
|
+
} catch {
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
throw SG_DONE;
|
|
6761
6798
|
}
|
|
6762
6799
|
function blockTool(reason) {
|
|
6763
6800
|
if (AGENT_TYPE === "gemini-cli") {
|
|
@@ -6765,23 +6802,23 @@ function blockTool(reason) {
|
|
|
6765
6802
|
decision: "deny",
|
|
6766
6803
|
reason: `[SolonGate] ${reason}`
|
|
6767
6804
|
}));
|
|
6768
|
-
|
|
6805
|
+
sgFinish(0);
|
|
6769
6806
|
} else {
|
|
6770
6807
|
process.stderr.write(reason);
|
|
6771
|
-
|
|
6808
|
+
sgFinish(2);
|
|
6772
6809
|
}
|
|
6773
6810
|
}
|
|
6774
6811
|
function allowTool() {
|
|
6775
6812
|
if (AGENT_TYPE === "gemini-cli") {
|
|
6776
6813
|
process.stdout.write(JSON.stringify({ decision: "allow" }));
|
|
6777
6814
|
}
|
|
6778
|
-
|
|
6815
|
+
sgFinish(0);
|
|
6779
6816
|
}
|
|
6780
6817
|
function rewriteTool(updatedInput) {
|
|
6781
6818
|
process.stdout.write(JSON.stringify({
|
|
6782
6819
|
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput }
|
|
6783
6820
|
}));
|
|
6784
|
-
|
|
6821
|
+
sgFinish(0);
|
|
6785
6822
|
}
|
|
6786
6823
|
function writeDenyFlag(toolName) {
|
|
6787
6824
|
try {
|
|
@@ -7654,248 +7691,259 @@ function readReferencedFiles(args, cwd) {
|
|
|
7654
7691
|
}
|
|
7655
7692
|
return out;
|
|
7656
7693
|
}
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
if (process.env.SOLONGATE_DEBUG) {
|
|
7662
|
-
}
|
|
7663
|
-
if (!API_KEY) {
|
|
7664
|
-
allowTool();
|
|
7665
|
-
return;
|
|
7694
|
+
if (!REFRESH_MODE) {
|
|
7695
|
+
try {
|
|
7696
|
+
input += readFileSync(0, "utf-8");
|
|
7697
|
+
} catch {
|
|
7666
7698
|
}
|
|
7667
|
-
|
|
7699
|
+
}
|
|
7700
|
+
(async () => {
|
|
7668
7701
|
try {
|
|
7669
|
-
|
|
7702
|
+
if (REFRESH_MODE)
|
|
7703
|
+
return;
|
|
7670
7704
|
if (process.env.SOLONGATE_DEBUG) {
|
|
7671
|
-
try {
|
|
7672
|
-
const { appendFileSync: afs, mkdirSync: mds } = await import("node:fs");
|
|
7673
|
-
mds(resolve(".solongate"), { recursive: true });
|
|
7674
|
-
const debugLine = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook: "guard", argv: process.argv.slice(2), tool_name: raw.tool_name || raw.toolName || raw.command, agent_id: AGENT_ID }) + "\n";
|
|
7675
|
-
afs(resolve(".solongate", ".debug-guard-log"), debugLine);
|
|
7676
|
-
} catch {
|
|
7677
|
-
}
|
|
7678
7705
|
}
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
tool_input: mappedToolInput,
|
|
7685
|
-
tool_response: raw.tool_response || raw.toolResponse || {},
|
|
7686
|
-
cwd: raw.cwd || process.cwd(),
|
|
7687
|
-
session_id: raw.session_id || raw.sessionId || raw.conversation_id || ""
|
|
7688
|
-
};
|
|
7689
|
-
const args = data.tool_input;
|
|
7690
|
-
const toolName = data.tool_name || "";
|
|
7691
|
-
const hookCwd = data.cwd || process.cwd();
|
|
7692
|
-
let policy;
|
|
7693
|
-
let selfProtectEnabled = true;
|
|
7694
|
-
let securityCfg = null;
|
|
7695
|
-
const agentKey = (AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7696
|
-
const policyCacheFile = join(resolve(homedir(), ".solongate"), ".policy-cache-" + agentKey + ".json");
|
|
7697
|
-
const POLICY_TTL_MS = 1e4;
|
|
7706
|
+
if (!API_KEY) {
|
|
7707
|
+
allowTool();
|
|
7708
|
+
return;
|
|
7709
|
+
}
|
|
7710
|
+
const _evalStart = Date.now();
|
|
7698
7711
|
try {
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7712
|
+
const raw = JSON.parse(input);
|
|
7713
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7714
|
+
try {
|
|
7715
|
+
const { appendFileSync: afs, mkdirSync: mds } = await import("node:fs");
|
|
7716
|
+
mds(resolve(".solongate"), { recursive: true });
|
|
7717
|
+
const debugLine = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook: "guard", argv: process.argv.slice(2), tool_name: raw.tool_name || raw.toolName || raw.command, agent_id: AGENT_ID }) + "\n";
|
|
7718
|
+
afs(resolve(".solongate", ".debug-guard-log"), debugLine);
|
|
7719
|
+
} catch {
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
let mappedToolName = raw.tool_name || raw.toolName || "";
|
|
7723
|
+
let mappedToolInput = raw.tool_input || raw.toolInput || raw.params || {};
|
|
7724
|
+
const data = {
|
|
7725
|
+
...raw,
|
|
7726
|
+
tool_name: mappedToolName,
|
|
7727
|
+
tool_input: mappedToolInput,
|
|
7728
|
+
tool_response: raw.tool_response || raw.toolResponse || {},
|
|
7729
|
+
cwd: raw.cwd || process.cwd(),
|
|
7730
|
+
session_id: raw.session_id || raw.sessionId || raw.conversation_id || ""
|
|
7731
|
+
};
|
|
7732
|
+
const args = data.tool_input;
|
|
7733
|
+
const toolName = data.tool_name || "";
|
|
7734
|
+
const hookCwd = data.cwd || process.cwd();
|
|
7735
|
+
let policy;
|
|
7736
|
+
let selfProtectEnabled = true;
|
|
7737
|
+
let securityCfg = null;
|
|
7738
|
+
const agentKey = (AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7739
|
+
const policyCacheFile = join(resolve(homedir(), ".solongate"), ".policy-cache-" + agentKey + ".json");
|
|
7740
|
+
const POLICY_TTL_MS = 1e4;
|
|
7702
7741
|
try {
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
if (
|
|
7708
|
-
|
|
7709
|
-
if (cached.policy)
|
|
7710
|
-
|
|
7711
|
-
if (
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7742
|
+
let dashboardPolicy = null;
|
|
7743
|
+
let staleCache = null;
|
|
7744
|
+
let refreshDue = true;
|
|
7745
|
+
try {
|
|
7746
|
+
if (existsSync(policyCacheFile)) {
|
|
7747
|
+
const cached = JSON.parse(readFileSync(policyCacheFile, "utf-8"));
|
|
7748
|
+
if (cached && cached.policy)
|
|
7749
|
+
staleCache = cached;
|
|
7750
|
+
if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
|
|
7751
|
+
refreshDue = false;
|
|
7752
|
+
if (cached.policy)
|
|
7753
|
+
dashboardPolicy = cached.policy;
|
|
7754
|
+
if (typeof cached.selfProtect === "boolean")
|
|
7755
|
+
selfProtectEnabled = cached.selfProtect;
|
|
7756
|
+
if (cached.security !== void 0)
|
|
7757
|
+
securityCfg = cached.security;
|
|
7758
|
+
if (cached.hookVersions)
|
|
7759
|
+
CLOUD_HOOK_VERSIONS = cached.hookVersions;
|
|
7760
|
+
}
|
|
7717
7761
|
}
|
|
7762
|
+
} catch {
|
|
7718
7763
|
}
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7764
|
+
if (refreshDue) {
|
|
7765
|
+
if (!dashboardPolicy && staleCache) {
|
|
7766
|
+
dashboardPolicy = staleCache.policy;
|
|
7767
|
+
if (typeof staleCache.selfProtect === "boolean")
|
|
7768
|
+
selfProtectEnabled = staleCache.selfProtect;
|
|
7769
|
+
if (staleCache.security !== void 0)
|
|
7770
|
+
securityCfg = staleCache.security;
|
|
7771
|
+
if (staleCache.hookVersions)
|
|
7772
|
+
CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
|
|
7773
|
+
}
|
|
7774
|
+
if (API_KEY) {
|
|
7775
|
+
try {
|
|
7776
|
+
const lock = join(resolve(homedir(), ".solongate"), ".policy-refresh-" + agentKey + ".lock");
|
|
7777
|
+
const due = !existsSync(lock) || Date.now() - statSync(lock).mtimeMs > 3e3;
|
|
7778
|
+
if (due) {
|
|
7779
|
+
try {
|
|
7780
|
+
writeFileSync(lock, String(Date.now()));
|
|
7781
|
+
} catch {
|
|
7782
|
+
}
|
|
7783
|
+
spawn(process.execPath, [process.argv[1], AGENT_TYPE, AGENT_NAME, "--sg-refresh-policy"], { detached: true, stdio: "ignore", env: process.env }).unref();
|
|
7784
|
+
}
|
|
7785
|
+
} catch {
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
}
|
|
7789
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7790
|
+
}
|
|
7791
|
+
if (dashboardPolicy) {
|
|
7792
|
+
policy = dashboardPolicy;
|
|
7793
|
+
} else {
|
|
7794
|
+
const candidates = [
|
|
7795
|
+
join(resolve(homedir(), ".solongate"), "policy.json"),
|
|
7796
|
+
resolve(hookCwd, "policy.json")
|
|
7797
|
+
];
|
|
7798
|
+
for (const p of candidates) {
|
|
7799
|
+
if (existsSync(p)) {
|
|
7736
7800
|
try {
|
|
7737
|
-
|
|
7801
|
+
policy = JSON.parse(readFileSync(p, "utf-8"));
|
|
7802
|
+
break;
|
|
7738
7803
|
} catch {
|
|
7739
7804
|
}
|
|
7740
|
-
spawn(process.execPath, [process.argv[1], AGENT_TYPE, AGENT_NAME, "--sg-refresh-policy"], { detached: true, stdio: "ignore", env: process.env }).unref();
|
|
7741
7805
|
}
|
|
7742
|
-
} catch {
|
|
7743
7806
|
}
|
|
7744
7807
|
}
|
|
7808
|
+
} catch {
|
|
7745
7809
|
}
|
|
7746
7810
|
if (process.env.SOLONGATE_DEBUG) {
|
|
7747
7811
|
}
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
resolve(hookCwd, "policy.json")
|
|
7754
|
-
];
|
|
7755
|
-
for (const p of candidates) {
|
|
7756
|
-
if (existsSync(p)) {
|
|
7757
|
-
try {
|
|
7758
|
-
policy = JSON.parse(readFileSync(p, "utf-8"));
|
|
7759
|
-
break;
|
|
7760
|
-
} catch {
|
|
7761
|
-
}
|
|
7762
|
-
}
|
|
7812
|
+
{
|
|
7813
|
+
const scope = policy && Array.isArray(policy.agents) && policy.agents.length > 0 ? policy.agents : ["*"];
|
|
7814
|
+
if (!scope.includes("*") && !scope.includes(AGENT_TYPE)) {
|
|
7815
|
+
allowTool();
|
|
7816
|
+
return;
|
|
7763
7817
|
}
|
|
7764
7818
|
}
|
|
7765
|
-
|
|
7766
|
-
}
|
|
7767
|
-
if (process.env.SOLONGATE_DEBUG) {
|
|
7768
|
-
}
|
|
7769
|
-
{
|
|
7770
|
-
const scope = policy && Array.isArray(policy.agents) && policy.agents.length > 0 ? policy.agents : ["*"];
|
|
7771
|
-
if (!scope.includes("*") && !scope.includes(AGENT_TYPE)) {
|
|
7772
|
-
allowTool();
|
|
7773
|
-
return;
|
|
7774
|
-
}
|
|
7775
|
-
}
|
|
7776
|
-
if (process.env.SOLONGATE_DEBUG) {
|
|
7777
|
-
}
|
|
7778
|
-
let reason = selfProtectEnabled ? tamperCheck(toolName, args) : null;
|
|
7779
|
-
if (!reason && securityCfg && securityCfg.ghost) {
|
|
7780
|
-
const ghostHit = ghostBlock(toolName, args, securityCfg.ghost);
|
|
7781
|
-
if (ghostHit) {
|
|
7782
|
-
try {
|
|
7783
|
-
writeDenyFlag(toolName);
|
|
7784
|
-
} catch {
|
|
7785
|
-
}
|
|
7786
|
-
writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: toolName, arguments: args, decision: "DENY", reason: "ghost path (hidden from agent)", permission: guessPermission(toolName), source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME, session_id: data.session_id || "", evaluation_time_ms: Date.now() - _evalStart });
|
|
7787
|
-
try {
|
|
7788
|
-
if (!localLogsOnly(securityCfg))
|
|
7789
|
-
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
7790
|
-
method: "POST",
|
|
7791
|
-
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
7792
|
-
body: JSON.stringify({
|
|
7793
|
-
tool: toolName,
|
|
7794
|
-
arguments: args,
|
|
7795
|
-
decision: "DENY",
|
|
7796
|
-
reason: "ghost path (hidden from agent)",
|
|
7797
|
-
permission: guessPermission(toolName),
|
|
7798
|
-
source: `${AGENT_TYPE}-guard`,
|
|
7799
|
-
agent_id: AGENT_TYPE,
|
|
7800
|
-
agent_name: AGENT_NAME,
|
|
7801
|
-
session_id: data.session_id || "",
|
|
7802
|
-
evaluation_time_ms: Date.now() - _evalStart
|
|
7803
|
-
}),
|
|
7804
|
-
signal: AbortSignal.timeout(3e3)
|
|
7805
|
-
});
|
|
7806
|
-
} catch {
|
|
7807
|
-
}
|
|
7808
|
-
await maybeSelfUpdate();
|
|
7809
|
-
if (AGENT_TYPE === "gemini-cli") {
|
|
7810
|
-
process.stdout.write(JSON.stringify({ decision: "deny", reason: ghostHit }));
|
|
7811
|
-
process.exit(0);
|
|
7812
|
-
}
|
|
7813
|
-
process.stderr.write(ghostHit);
|
|
7814
|
-
process.exit(2);
|
|
7819
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7815
7820
|
}
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7821
|
+
let reason = selfProtectEnabled ? tamperCheck(toolName, args) : null;
|
|
7822
|
+
if (!reason && securityCfg && securityCfg.ghost) {
|
|
7823
|
+
const ghostHit = ghostBlock(toolName, args, securityCfg.ghost);
|
|
7824
|
+
if (ghostHit) {
|
|
7825
|
+
try {
|
|
7826
|
+
writeDenyFlag(toolName);
|
|
7827
|
+
} catch {
|
|
7828
|
+
}
|
|
7829
|
+
writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: toolName, arguments: args, decision: "DENY", reason: "ghost path (hidden from agent)", permission: guessPermission(toolName), source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME, session_id: data.session_id || "", evaluation_time_ms: Date.now() - _evalStart });
|
|
7830
|
+
try {
|
|
7831
|
+
if (!localLogsOnly(securityCfg))
|
|
7832
|
+
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
7833
|
+
method: "POST",
|
|
7834
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
7835
|
+
body: JSON.stringify({
|
|
7836
|
+
tool: toolName,
|
|
7837
|
+
arguments: args,
|
|
7838
|
+
decision: "DENY",
|
|
7839
|
+
reason: "ghost path (hidden from agent)",
|
|
7840
|
+
permission: guessPermission(toolName),
|
|
7841
|
+
source: `${AGENT_TYPE}-guard`,
|
|
7842
|
+
agent_id: AGENT_TYPE,
|
|
7843
|
+
agent_name: AGENT_NAME,
|
|
7844
|
+
session_id: data.session_id || "",
|
|
7845
|
+
evaluation_time_ms: Date.now() - _evalStart
|
|
7846
|
+
}),
|
|
7847
|
+
signal: AbortSignal.timeout(3e3)
|
|
7848
|
+
});
|
|
7849
|
+
} catch {
|
|
7850
|
+
}
|
|
7819
7851
|
await maybeSelfUpdate();
|
|
7820
|
-
|
|
7852
|
+
if (AGENT_TYPE === "gemini-cli") {
|
|
7853
|
+
process.stdout.write(JSON.stringify({ decision: "deny", reason: ghostHit }));
|
|
7854
|
+
sgFinish(0);
|
|
7855
|
+
}
|
|
7856
|
+
process.stderr.write(ghostHit);
|
|
7857
|
+
sgFinish(2);
|
|
7858
|
+
}
|
|
7859
|
+
if (AGENT_TYPE !== "gemini-cli" && toolName === "Bash") {
|
|
7860
|
+
const rw = ghostListingRewrite(args, securityCfg.ghost);
|
|
7861
|
+
if (rw) {
|
|
7862
|
+
await maybeSelfUpdate();
|
|
7863
|
+
rewriteTool({ command: rw });
|
|
7864
|
+
}
|
|
7821
7865
|
}
|
|
7822
7866
|
}
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7867
|
+
if (!reason)
|
|
7868
|
+
reason = securityLayerCheck(toolName, args, securityCfg, agentKey);
|
|
7869
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7870
|
+
}
|
|
7871
|
+
let opaRoute = "white";
|
|
7872
|
+
if (reason) {
|
|
7873
|
+
opaRoute = "black";
|
|
7874
|
+
} else if (policy && policy.rules) {
|
|
7875
|
+
const opaResult = await evaluateWithOpa(policy, args, toolName, hookCwd);
|
|
7876
|
+
if (opaResult === void 0) {
|
|
7877
|
+
const legacy = evaluate(policy, args, toolName);
|
|
7878
|
+
if (typeof legacy === "string") {
|
|
7879
|
+
reason = legacy;
|
|
7880
|
+
opaRoute = "black";
|
|
7881
|
+
} else {
|
|
7882
|
+
opaRoute = "white";
|
|
7883
|
+
}
|
|
7884
|
+
} else if (typeof opaResult === "string") {
|
|
7885
|
+
reason = opaResult;
|
|
7837
7886
|
opaRoute = "black";
|
|
7838
7887
|
} else {
|
|
7839
7888
|
opaRoute = "white";
|
|
7840
7889
|
}
|
|
7841
|
-
} else if (typeof opaResult === "string") {
|
|
7842
|
-
reason = opaResult;
|
|
7843
|
-
opaRoute = "black";
|
|
7844
|
-
} else {
|
|
7845
|
-
opaRoute = "white";
|
|
7846
7890
|
}
|
|
7847
|
-
|
|
7848
|
-
process.stderr.write(`[SolonGate ROUTE] ${opaRoute.toUpperCase()} (${reason ? "block" : "allow"})
|
|
7891
|
+
process.stderr.write(`[SolonGate ROUTE] ${opaRoute.toUpperCase()} (${reason ? "block" : "allow"})
|
|
7849
7892
|
`);
|
|
7850
|
-
try {
|
|
7851
|
-
const _fd = resolve(".solongate");
|
|
7852
|
-
mkdirSync(_fd, { recursive: true });
|
|
7853
|
-
const _rec = { ms: Date.now() - _evalStart, ts: Date.now(), tool: toolName, session: data.session_id || "" };
|
|
7854
|
-
writeFileSync(join(_fd, ".last-eval"), JSON.stringify(_rec));
|
|
7855
7893
|
try {
|
|
7856
|
-
const
|
|
7857
|
-
|
|
7894
|
+
const _fd = resolve(".solongate");
|
|
7895
|
+
mkdirSync(_fd, { recursive: true });
|
|
7896
|
+
const _rec = { ms: Date.now() - _evalStart, ts: Date.now(), tool: toolName, session: data.session_id || "" };
|
|
7897
|
+
writeFileSync(join(_fd, ".last-eval"), JSON.stringify(_rec));
|
|
7858
7898
|
try {
|
|
7859
|
-
|
|
7860
|
-
|
|
7899
|
+
const ring = join(_fd, ".eval-ring.jsonl");
|
|
7900
|
+
appendFileSync(ring, JSON.stringify(_rec) + "\n");
|
|
7901
|
+
try {
|
|
7902
|
+
if (statSync(ring).size > 16384)
|
|
7903
|
+
writeFileSync(ring, readFileSync(ring, "utf-8").split("\n").filter(Boolean).slice(-50).join("\n") + "\n");
|
|
7904
|
+
} catch {
|
|
7905
|
+
}
|
|
7861
7906
|
} catch {
|
|
7862
7907
|
}
|
|
7863
7908
|
} catch {
|
|
7864
7909
|
}
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
} catch {
|
|
7910
|
+
if (reason) {
|
|
7911
|
+
if (true) {
|
|
7912
|
+
try {
|
|
7913
|
+
const logEntry = {
|
|
7914
|
+
tool: toolName,
|
|
7915
|
+
arguments: args,
|
|
7916
|
+
decision: "DENY",
|
|
7917
|
+
reason,
|
|
7918
|
+
permission: guessPermission(toolName),
|
|
7919
|
+
source: `${AGENT_TYPE}-guard`,
|
|
7920
|
+
agent_id: AGENT_TYPE,
|
|
7921
|
+
agent_name: AGENT_NAME,
|
|
7922
|
+
session_id: data.session_id || "",
|
|
7923
|
+
evaluation_time_ms: Date.now() - _evalStart
|
|
7924
|
+
};
|
|
7925
|
+
writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), ...logEntry });
|
|
7926
|
+
if (!localLogsOnly(securityCfg))
|
|
7927
|
+
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
7928
|
+
method: "POST",
|
|
7929
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
7930
|
+
body: JSON.stringify(logEntry),
|
|
7931
|
+
signal: AbortSignal.timeout(3e3)
|
|
7932
|
+
});
|
|
7933
|
+
} catch {
|
|
7934
|
+
}
|
|
7891
7935
|
}
|
|
7936
|
+
writeDenyFlag(toolName);
|
|
7937
|
+
await maybeSelfUpdate();
|
|
7938
|
+
blockTool(reason);
|
|
7892
7939
|
}
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7940
|
+
} catch {
|
|
7941
|
+
}
|
|
7942
|
+
await maybeSelfUpdate();
|
|
7943
|
+
allowTool();
|
|
7944
|
+
} catch (e) {
|
|
7945
|
+
if (e !== SG_DONE) {
|
|
7946
|
+
process.exitCode = process.exitCode || 0;
|
|
7896
7947
|
}
|
|
7897
|
-
} catch {
|
|
7898
7948
|
}
|
|
7899
|
-
|
|
7900
|
-
allowTool();
|
|
7901
|
-
});
|
|
7949
|
+
})();
|
package/hooks/guard.mjs
CHANGED
|
@@ -281,24 +281,52 @@ async function refreshPolicyCache() {
|
|
|
281
281
|
try { writeFileSync(cacheFile, JSON.stringify({ _ts: Date.now(), policy, selfProtect, security, hookVersions })); } catch {}
|
|
282
282
|
} catch {}
|
|
283
283
|
}
|
|
284
|
-
if (REFRESH_MODE) { refreshPolicyCache().finally(() => process.exit(0)); }
|
|
284
|
+
if (REFRESH_MODE) { refreshPolicyCache().finally(() => { process.exitCode = 0; try { setImmediate(() => { try { process.exit(0); } catch {} }); } catch { try { process.exit(0); } catch {} } }); }
|
|
285
285
|
|
|
286
286
|
// ── Per-tool block/allow output ──
|
|
287
287
|
// Response format depends on the agent:
|
|
288
288
|
// Claude Code: exit 2 + stderr = BLOCK, exit 0 = ALLOW
|
|
289
289
|
// Gemini CLI: {"decision": "deny/allow", "reason": "..."}
|
|
290
290
|
|
|
291
|
+
// Terminate the hook WITHOUT forcing process.exit(). On Windows + Node 24, calling
|
|
292
|
+
// process.exit() right after a fetch() (the cloud audit-log POST) aborts with
|
|
293
|
+
// `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c`:
|
|
294
|
+
// the fetch's DNS/socket teardown is still settling in libuv's threadpool and
|
|
295
|
+
// exit() double-closes the loop's async handle. The abort replaces exit code 2, so
|
|
296
|
+
// Claude Code sees a non-blocking hook failure and runs the tool anyway — the guard
|
|
297
|
+
// computes DENY but never blocks. Instead set process.exitCode and let the event
|
|
298
|
+
// loop drain and exit on its own. A latch preserves the FIRST code (so a later
|
|
299
|
+
// allowTool() reached on fall-through can't overwrite a block), SG_DONE unwinds the
|
|
300
|
+
// stack, and an unref'd backstop force-exits only if a handle is stuck — by then
|
|
301
|
+
// the threadpool has drained, so exit() is safe.
|
|
302
|
+
const SG_DONE = Symbol('sg-done');
|
|
303
|
+
let _sgDone = false;
|
|
304
|
+
function sgFinish(code) {
|
|
305
|
+
if (!_sgDone) {
|
|
306
|
+
_sgDone = true;
|
|
307
|
+
process.exitCode = code;
|
|
308
|
+
// Defer the real exit by ONE loop tick. process.exit() called synchronously in
|
|
309
|
+
// the same tick a fetch just settled aborts on Windows: a threadpool worker is
|
|
310
|
+
// still mid-uv_async_send when exit() closes the loop's async handle. setImmediate
|
|
311
|
+
// lets libuv drain that completion first, then we exit — ~0ms, and it still hard-
|
|
312
|
+
// exits (killing undici keep-alive sockets) instead of waiting on them.
|
|
313
|
+
try { setImmediate(() => { try { process.exit(process.exitCode || 0); } catch {} }); }
|
|
314
|
+
catch { try { process.exit(code); } catch {} }
|
|
315
|
+
}
|
|
316
|
+
throw SG_DONE;
|
|
317
|
+
}
|
|
318
|
+
|
|
291
319
|
function blockTool(reason) {
|
|
292
320
|
if (AGENT_TYPE === 'gemini-cli') {
|
|
293
321
|
process.stdout.write(JSON.stringify({
|
|
294
322
|
decision: 'deny',
|
|
295
323
|
reason: `[SolonGate] ${reason}`,
|
|
296
324
|
}));
|
|
297
|
-
|
|
325
|
+
sgFinish(0);
|
|
298
326
|
} else {
|
|
299
327
|
// Claude Code — exit code 2
|
|
300
328
|
process.stderr.write(reason);
|
|
301
|
-
|
|
329
|
+
sgFinish(2);
|
|
302
330
|
}
|
|
303
331
|
}
|
|
304
332
|
|
|
@@ -306,7 +334,7 @@ function allowTool() {
|
|
|
306
334
|
if (AGENT_TYPE === 'gemini-cli') {
|
|
307
335
|
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
308
336
|
}
|
|
309
|
-
|
|
337
|
+
sgFinish(0);
|
|
310
338
|
}
|
|
311
339
|
|
|
312
340
|
// Allow the tool but REPLACE its input (Claude Code `updatedInput`). Used by the
|
|
@@ -316,7 +344,7 @@ function rewriteTool(updatedInput) {
|
|
|
316
344
|
process.stdout.write(JSON.stringify({
|
|
317
345
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', updatedInput },
|
|
318
346
|
}));
|
|
319
|
-
|
|
347
|
+
sgFinish(0);
|
|
320
348
|
}
|
|
321
349
|
|
|
322
350
|
// Write flag file so stop.mjs knows a tool call (DENY) happened and doesn't log extra ALLOW
|
|
@@ -1402,8 +1430,17 @@ function readReferencedFiles(args, cwd) {
|
|
|
1402
1430
|
return out;
|
|
1403
1431
|
}
|
|
1404
1432
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1433
|
+
// Read stdin SYNCHRONOUSLY (fd 0) instead of via the process.stdin stream. On
|
|
1434
|
+
// Windows + Node 24, calling process.exit() from inside the stdin stream's 'end'
|
|
1435
|
+
// callback aborts with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
|
|
1436
|
+
// file src\win\async.c` — libuv double-closes the stdin pipe handle mid-teardown,
|
|
1437
|
+
// the hook crashes before its exit code lands, and Claude Code treats the DENY as
|
|
1438
|
+
// a non-blocking hook failure (so the block never applies). Reading fd 0 to EOF
|
|
1439
|
+
// synchronously never creates that pipe handle, and the exit below no longer runs
|
|
1440
|
+
// inside a stream callback, so the loop tears down cleanly.
|
|
1441
|
+
if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
1442
|
+
;(async () => {
|
|
1443
|
+
try {
|
|
1407
1444
|
// Background-refresh invocation has no tool call to evaluate — it already ran
|
|
1408
1445
|
// refreshPolicyCache() at startup; do nothing on the (empty) stdin.
|
|
1409
1446
|
if (REFRESH_MODE) return;
|
|
@@ -1596,9 +1633,9 @@ process.stdin.on('end', async () => {
|
|
|
1596
1633
|
});
|
|
1597
1634
|
} catch {}
|
|
1598
1635
|
await maybeSelfUpdate();
|
|
1599
|
-
if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit }));
|
|
1636
|
+
if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit })); sgFinish(0); }
|
|
1600
1637
|
process.stderr.write(ghostHit);
|
|
1601
|
-
|
|
1638
|
+
sgFinish(2);
|
|
1602
1639
|
}
|
|
1603
1640
|
// No direct hit: if this is a listing command, rewrite it so hidden
|
|
1604
1641
|
// entries are filtered out of its output (Claude Code only).
|
|
@@ -1709,4 +1746,9 @@ process.stdin.on('end', async () => {
|
|
|
1709
1746
|
} catch {}
|
|
1710
1747
|
await maybeSelfUpdate();
|
|
1711
1748
|
allowTool();
|
|
1712
|
-
})
|
|
1749
|
+
} catch (e) {
|
|
1750
|
+
// SG_DONE is the normal terminator (exitCode already set); anything else is an
|
|
1751
|
+
// unexpected error — fail OPEN (exit 0) so a hook bug never wedges the agent.
|
|
1752
|
+
if (e !== SG_DONE) { process.exitCode = process.exitCode || 0; }
|
|
1753
|
+
}
|
|
1754
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.81.
|
|
3
|
+
"version": "0.81.3",
|
|
4
4
|
"description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|