@solongate/proxy 0.81.2 → 0.81.4
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 +16 -4
- package/hooks/guard.bundled.mjs +242 -207
- package/hooks/guard.mjs +47 -7
- package/package.json +1 -1
package/hooks/audit.mjs
CHANGED
|
@@ -399,6 +399,10 @@ let input = '';
|
|
|
399
399
|
// EOF avoids creating that handle, so the exits below tear down cleanly.
|
|
400
400
|
try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
401
401
|
;(async () => {
|
|
402
|
+
// Safety backstop ONLY: the normal path exits by natural drain. Force-exit if the
|
|
403
|
+
// loop fails to drain — 8s > the 5s fetch timeout, so the threadpool is idle and
|
|
404
|
+
// exit() can't hit the Windows UV_HANDLE_CLOSING abort.
|
|
405
|
+
try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {}
|
|
402
406
|
try {
|
|
403
407
|
const data = JSON.parse(input);
|
|
404
408
|
let EMITTED_PAYLOAD = null;
|
|
@@ -531,7 +535,13 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
531
535
|
// on the write's flush callback (and on the fire-and-forget audit POST).
|
|
532
536
|
let flushed = (typeof EMITTED_PAYLOAD !== 'string');
|
|
533
537
|
let fetchDone = false;
|
|
534
|
-
|
|
538
|
+
// Set the exit code and let the event loop drain naturally — do NOT call
|
|
539
|
+
// process.exit(). On Windows + Node 24, process.exit() in the same tick the
|
|
540
|
+
// audit-log fetch settled aborts with `Assertion failed: !(handle->flags &
|
|
541
|
+
// UV_HANDLE_CLOSING), file src\win\async.c` (a threadpool DNS worker is still
|
|
542
|
+
// mid-uv_async_send when exit() force-closes the loop's async handle). undici
|
|
543
|
+
// unrefs idle sockets, so Node exits on its own ~1ms after the fetch settles.
|
|
544
|
+
const maybeExit = () => { if (flushed && fetchDone) { process.exitCode = 0; } };
|
|
535
545
|
if (typeof EMITTED_PAYLOAD === 'string') {
|
|
536
546
|
try { process.stdout.write(EMITTED_PAYLOAD, () => { flushed = true; maybeExit(); }); }
|
|
537
547
|
catch { flushed = true; }
|
|
@@ -581,9 +591,11 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
581
591
|
signal: AbortSignal.timeout(5000),
|
|
582
592
|
}).catch(() => {}).finally(() => { fetchDone = true; maybeExit(); });
|
|
583
593
|
}
|
|
584
|
-
//
|
|
585
|
-
|
|
594
|
+
// Safety backstop ONLY (unref'd, 8s > the 5s fetch timeout): the normal path
|
|
595
|
+
// exits by natural drain ~1ms after the fetch settles. This fires only if the
|
|
596
|
+
// loop somehow fails to drain — by 8s the threadpool is idle, so exit() is safe.
|
|
597
|
+
try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {}
|
|
586
598
|
} catch {
|
|
587
|
-
process.
|
|
599
|
+
process.exitCode = 0;
|
|
588
600
|
}
|
|
589
601
|
})();
|
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -6757,7 +6757,27 @@ async function refreshPolicyCache() {
|
|
|
6757
6757
|
}
|
|
6758
6758
|
}
|
|
6759
6759
|
if (REFRESH_MODE) {
|
|
6760
|
-
|
|
6760
|
+
try {
|
|
6761
|
+
setTimeout(() => {
|
|
6762
|
+
try {
|
|
6763
|
+
process.exit(process.exitCode || 0);
|
|
6764
|
+
} catch {
|
|
6765
|
+
}
|
|
6766
|
+
}, 8e3).unref();
|
|
6767
|
+
} catch {
|
|
6768
|
+
}
|
|
6769
|
+
refreshPolicyCache().finally(() => {
|
|
6770
|
+
process.exitCode = 0;
|
|
6771
|
+
});
|
|
6772
|
+
}
|
|
6773
|
+
var SG_DONE = Symbol("sg-done");
|
|
6774
|
+
var _sgDone = false;
|
|
6775
|
+
function sgFinish(code) {
|
|
6776
|
+
if (!_sgDone) {
|
|
6777
|
+
_sgDone = true;
|
|
6778
|
+
process.exitCode = code;
|
|
6779
|
+
}
|
|
6780
|
+
throw SG_DONE;
|
|
6761
6781
|
}
|
|
6762
6782
|
function blockTool(reason) {
|
|
6763
6783
|
if (AGENT_TYPE === "gemini-cli") {
|
|
@@ -6765,23 +6785,23 @@ function blockTool(reason) {
|
|
|
6765
6785
|
decision: "deny",
|
|
6766
6786
|
reason: `[SolonGate] ${reason}`
|
|
6767
6787
|
}));
|
|
6768
|
-
|
|
6788
|
+
sgFinish(0);
|
|
6769
6789
|
} else {
|
|
6770
6790
|
process.stderr.write(reason);
|
|
6771
|
-
|
|
6791
|
+
sgFinish(2);
|
|
6772
6792
|
}
|
|
6773
6793
|
}
|
|
6774
6794
|
function allowTool() {
|
|
6775
6795
|
if (AGENT_TYPE === "gemini-cli") {
|
|
6776
6796
|
process.stdout.write(JSON.stringify({ decision: "allow" }));
|
|
6777
6797
|
}
|
|
6778
|
-
|
|
6798
|
+
sgFinish(0);
|
|
6779
6799
|
}
|
|
6780
6800
|
function rewriteTool(updatedInput) {
|
|
6781
6801
|
process.stdout.write(JSON.stringify({
|
|
6782
6802
|
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput }
|
|
6783
6803
|
}));
|
|
6784
|
-
|
|
6804
|
+
sgFinish(0);
|
|
6785
6805
|
}
|
|
6786
6806
|
function writeDenyFlag(toolName) {
|
|
6787
6807
|
try {
|
|
@@ -7661,246 +7681,261 @@ if (!REFRESH_MODE) {
|
|
|
7661
7681
|
}
|
|
7662
7682
|
}
|
|
7663
7683
|
(async () => {
|
|
7664
|
-
if (REFRESH_MODE)
|
|
7665
|
-
return;
|
|
7666
|
-
if (process.env.SOLONGATE_DEBUG) {
|
|
7667
|
-
}
|
|
7668
|
-
if (!API_KEY) {
|
|
7669
|
-
allowTool();
|
|
7670
|
-
return;
|
|
7671
|
-
}
|
|
7672
|
-
const _evalStart = Date.now();
|
|
7673
7684
|
try {
|
|
7674
|
-
|
|
7675
|
-
if (process.env.SOLONGATE_DEBUG) {
|
|
7685
|
+
setTimeout(() => {
|
|
7676
7686
|
try {
|
|
7677
|
-
|
|
7678
|
-
mds(resolve(".solongate"), { recursive: true });
|
|
7679
|
-
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";
|
|
7680
|
-
afs(resolve(".solongate", ".debug-guard-log"), debugLine);
|
|
7687
|
+
process.exit(process.exitCode || 0);
|
|
7681
7688
|
} catch {
|
|
7682
7689
|
}
|
|
7690
|
+
}, 8e3).unref();
|
|
7691
|
+
} catch {
|
|
7692
|
+
}
|
|
7693
|
+
try {
|
|
7694
|
+
if (REFRESH_MODE)
|
|
7695
|
+
return;
|
|
7696
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7683
7697
|
}
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
tool_input: mappedToolInput,
|
|
7690
|
-
tool_response: raw.tool_response || raw.toolResponse || {},
|
|
7691
|
-
cwd: raw.cwd || process.cwd(),
|
|
7692
|
-
session_id: raw.session_id || raw.sessionId || raw.conversation_id || ""
|
|
7693
|
-
};
|
|
7694
|
-
const args = data.tool_input;
|
|
7695
|
-
const toolName = data.tool_name || "";
|
|
7696
|
-
const hookCwd = data.cwd || process.cwd();
|
|
7697
|
-
let policy;
|
|
7698
|
-
let selfProtectEnabled = true;
|
|
7699
|
-
let securityCfg = null;
|
|
7700
|
-
const agentKey = (AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7701
|
-
const policyCacheFile = join(resolve(homedir(), ".solongate"), ".policy-cache-" + agentKey + ".json");
|
|
7702
|
-
const POLICY_TTL_MS = 1e4;
|
|
7698
|
+
if (!API_KEY) {
|
|
7699
|
+
allowTool();
|
|
7700
|
+
return;
|
|
7701
|
+
}
|
|
7702
|
+
const _evalStart = Date.now();
|
|
7703
7703
|
try {
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7704
|
+
const raw = JSON.parse(input);
|
|
7705
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7706
|
+
try {
|
|
7707
|
+
const { appendFileSync: afs, mkdirSync: mds } = await import("node:fs");
|
|
7708
|
+
mds(resolve(".solongate"), { recursive: true });
|
|
7709
|
+
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";
|
|
7710
|
+
afs(resolve(".solongate", ".debug-guard-log"), debugLine);
|
|
7711
|
+
} catch {
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
let mappedToolName = raw.tool_name || raw.toolName || "";
|
|
7715
|
+
let mappedToolInput = raw.tool_input || raw.toolInput || raw.params || {};
|
|
7716
|
+
const data = {
|
|
7717
|
+
...raw,
|
|
7718
|
+
tool_name: mappedToolName,
|
|
7719
|
+
tool_input: mappedToolInput,
|
|
7720
|
+
tool_response: raw.tool_response || raw.toolResponse || {},
|
|
7721
|
+
cwd: raw.cwd || process.cwd(),
|
|
7722
|
+
session_id: raw.session_id || raw.sessionId || raw.conversation_id || ""
|
|
7723
|
+
};
|
|
7724
|
+
const args = data.tool_input;
|
|
7725
|
+
const toolName = data.tool_name || "";
|
|
7726
|
+
const hookCwd = data.cwd || process.cwd();
|
|
7727
|
+
let policy;
|
|
7728
|
+
let selfProtectEnabled = true;
|
|
7729
|
+
let securityCfg = null;
|
|
7730
|
+
const agentKey = (AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7731
|
+
const policyCacheFile = join(resolve(homedir(), ".solongate"), ".policy-cache-" + agentKey + ".json");
|
|
7732
|
+
const POLICY_TTL_MS = 1e4;
|
|
7707
7733
|
try {
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
if (
|
|
7713
|
-
|
|
7714
|
-
if (cached.policy)
|
|
7715
|
-
|
|
7716
|
-
if (
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7734
|
+
let dashboardPolicy = null;
|
|
7735
|
+
let staleCache = null;
|
|
7736
|
+
let refreshDue = true;
|
|
7737
|
+
try {
|
|
7738
|
+
if (existsSync(policyCacheFile)) {
|
|
7739
|
+
const cached = JSON.parse(readFileSync(policyCacheFile, "utf-8"));
|
|
7740
|
+
if (cached && cached.policy)
|
|
7741
|
+
staleCache = cached;
|
|
7742
|
+
if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
|
|
7743
|
+
refreshDue = false;
|
|
7744
|
+
if (cached.policy)
|
|
7745
|
+
dashboardPolicy = cached.policy;
|
|
7746
|
+
if (typeof cached.selfProtect === "boolean")
|
|
7747
|
+
selfProtectEnabled = cached.selfProtect;
|
|
7748
|
+
if (cached.security !== void 0)
|
|
7749
|
+
securityCfg = cached.security;
|
|
7750
|
+
if (cached.hookVersions)
|
|
7751
|
+
CLOUD_HOOK_VERSIONS = cached.hookVersions;
|
|
7752
|
+
}
|
|
7722
7753
|
}
|
|
7754
|
+
} catch {
|
|
7723
7755
|
}
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7756
|
+
if (refreshDue) {
|
|
7757
|
+
if (!dashboardPolicy && staleCache) {
|
|
7758
|
+
dashboardPolicy = staleCache.policy;
|
|
7759
|
+
if (typeof staleCache.selfProtect === "boolean")
|
|
7760
|
+
selfProtectEnabled = staleCache.selfProtect;
|
|
7761
|
+
if (staleCache.security !== void 0)
|
|
7762
|
+
securityCfg = staleCache.security;
|
|
7763
|
+
if (staleCache.hookVersions)
|
|
7764
|
+
CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
|
|
7765
|
+
}
|
|
7766
|
+
if (API_KEY) {
|
|
7767
|
+
try {
|
|
7768
|
+
const lock = join(resolve(homedir(), ".solongate"), ".policy-refresh-" + agentKey + ".lock");
|
|
7769
|
+
const due = !existsSync(lock) || Date.now() - statSync(lock).mtimeMs > 3e3;
|
|
7770
|
+
if (due) {
|
|
7771
|
+
try {
|
|
7772
|
+
writeFileSync(lock, String(Date.now()));
|
|
7773
|
+
} catch {
|
|
7774
|
+
}
|
|
7775
|
+
spawn(process.execPath, [process.argv[1], AGENT_TYPE, AGENT_NAME, "--sg-refresh-policy"], { detached: true, stdio: "ignore", env: process.env }).unref();
|
|
7776
|
+
}
|
|
7777
|
+
} catch {
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7782
|
+
}
|
|
7783
|
+
if (dashboardPolicy) {
|
|
7784
|
+
policy = dashboardPolicy;
|
|
7785
|
+
} else {
|
|
7786
|
+
const candidates = [
|
|
7787
|
+
join(resolve(homedir(), ".solongate"), "policy.json"),
|
|
7788
|
+
resolve(hookCwd, "policy.json")
|
|
7789
|
+
];
|
|
7790
|
+
for (const p of candidates) {
|
|
7791
|
+
if (existsSync(p)) {
|
|
7741
7792
|
try {
|
|
7742
|
-
|
|
7793
|
+
policy = JSON.parse(readFileSync(p, "utf-8"));
|
|
7794
|
+
break;
|
|
7743
7795
|
} catch {
|
|
7744
7796
|
}
|
|
7745
|
-
spawn(process.execPath, [process.argv[1], AGENT_TYPE, AGENT_NAME, "--sg-refresh-policy"], { detached: true, stdio: "ignore", env: process.env }).unref();
|
|
7746
7797
|
}
|
|
7747
|
-
} catch {
|
|
7748
7798
|
}
|
|
7749
7799
|
}
|
|
7800
|
+
} catch {
|
|
7750
7801
|
}
|
|
7751
7802
|
if (process.env.SOLONGATE_DEBUG) {
|
|
7752
7803
|
}
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
resolve(hookCwd, "policy.json")
|
|
7759
|
-
];
|
|
7760
|
-
for (const p of candidates) {
|
|
7761
|
-
if (existsSync(p)) {
|
|
7762
|
-
try {
|
|
7763
|
-
policy = JSON.parse(readFileSync(p, "utf-8"));
|
|
7764
|
-
break;
|
|
7765
|
-
} catch {
|
|
7766
|
-
}
|
|
7767
|
-
}
|
|
7804
|
+
{
|
|
7805
|
+
const scope = policy && Array.isArray(policy.agents) && policy.agents.length > 0 ? policy.agents : ["*"];
|
|
7806
|
+
if (!scope.includes("*") && !scope.includes(AGENT_TYPE)) {
|
|
7807
|
+
allowTool();
|
|
7808
|
+
return;
|
|
7768
7809
|
}
|
|
7769
7810
|
}
|
|
7770
|
-
|
|
7771
|
-
}
|
|
7772
|
-
if (process.env.SOLONGATE_DEBUG) {
|
|
7773
|
-
}
|
|
7774
|
-
{
|
|
7775
|
-
const scope = policy && Array.isArray(policy.agents) && policy.agents.length > 0 ? policy.agents : ["*"];
|
|
7776
|
-
if (!scope.includes("*") && !scope.includes(AGENT_TYPE)) {
|
|
7777
|
-
allowTool();
|
|
7778
|
-
return;
|
|
7779
|
-
}
|
|
7780
|
-
}
|
|
7781
|
-
if (process.env.SOLONGATE_DEBUG) {
|
|
7782
|
-
}
|
|
7783
|
-
let reason = selfProtectEnabled ? tamperCheck(toolName, args) : null;
|
|
7784
|
-
if (!reason && securityCfg && securityCfg.ghost) {
|
|
7785
|
-
const ghostHit = ghostBlock(toolName, args, securityCfg.ghost);
|
|
7786
|
-
if (ghostHit) {
|
|
7787
|
-
try {
|
|
7788
|
-
writeDenyFlag(toolName);
|
|
7789
|
-
} catch {
|
|
7790
|
-
}
|
|
7791
|
-
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 });
|
|
7792
|
-
try {
|
|
7793
|
-
if (!localLogsOnly(securityCfg))
|
|
7794
|
-
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
7795
|
-
method: "POST",
|
|
7796
|
-
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
7797
|
-
body: JSON.stringify({
|
|
7798
|
-
tool: toolName,
|
|
7799
|
-
arguments: args,
|
|
7800
|
-
decision: "DENY",
|
|
7801
|
-
reason: "ghost path (hidden from agent)",
|
|
7802
|
-
permission: guessPermission(toolName),
|
|
7803
|
-
source: `${AGENT_TYPE}-guard`,
|
|
7804
|
-
agent_id: AGENT_TYPE,
|
|
7805
|
-
agent_name: AGENT_NAME,
|
|
7806
|
-
session_id: data.session_id || "",
|
|
7807
|
-
evaluation_time_ms: Date.now() - _evalStart
|
|
7808
|
-
}),
|
|
7809
|
-
signal: AbortSignal.timeout(3e3)
|
|
7810
|
-
});
|
|
7811
|
-
} catch {
|
|
7812
|
-
}
|
|
7813
|
-
await maybeSelfUpdate();
|
|
7814
|
-
if (AGENT_TYPE === "gemini-cli") {
|
|
7815
|
-
process.stdout.write(JSON.stringify({ decision: "deny", reason: ghostHit }));
|
|
7816
|
-
process.exit(0);
|
|
7817
|
-
}
|
|
7818
|
-
process.stderr.write(ghostHit);
|
|
7819
|
-
process.exit(2);
|
|
7811
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7820
7812
|
}
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7813
|
+
let reason = selfProtectEnabled ? tamperCheck(toolName, args) : null;
|
|
7814
|
+
if (!reason && securityCfg && securityCfg.ghost) {
|
|
7815
|
+
const ghostHit = ghostBlock(toolName, args, securityCfg.ghost);
|
|
7816
|
+
if (ghostHit) {
|
|
7817
|
+
try {
|
|
7818
|
+
writeDenyFlag(toolName);
|
|
7819
|
+
} catch {
|
|
7820
|
+
}
|
|
7821
|
+
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 });
|
|
7822
|
+
try {
|
|
7823
|
+
if (!localLogsOnly(securityCfg))
|
|
7824
|
+
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
7825
|
+
method: "POST",
|
|
7826
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
7827
|
+
body: JSON.stringify({
|
|
7828
|
+
tool: toolName,
|
|
7829
|
+
arguments: args,
|
|
7830
|
+
decision: "DENY",
|
|
7831
|
+
reason: "ghost path (hidden from agent)",
|
|
7832
|
+
permission: guessPermission(toolName),
|
|
7833
|
+
source: `${AGENT_TYPE}-guard`,
|
|
7834
|
+
agent_id: AGENT_TYPE,
|
|
7835
|
+
agent_name: AGENT_NAME,
|
|
7836
|
+
session_id: data.session_id || "",
|
|
7837
|
+
evaluation_time_ms: Date.now() - _evalStart
|
|
7838
|
+
}),
|
|
7839
|
+
signal: AbortSignal.timeout(3e3)
|
|
7840
|
+
});
|
|
7841
|
+
} catch {
|
|
7842
|
+
}
|
|
7824
7843
|
await maybeSelfUpdate();
|
|
7825
|
-
|
|
7844
|
+
if (AGENT_TYPE === "gemini-cli") {
|
|
7845
|
+
process.stdout.write(JSON.stringify({ decision: "deny", reason: ghostHit }));
|
|
7846
|
+
sgFinish(0);
|
|
7847
|
+
}
|
|
7848
|
+
process.stderr.write(ghostHit);
|
|
7849
|
+
sgFinish(2);
|
|
7850
|
+
}
|
|
7851
|
+
if (AGENT_TYPE !== "gemini-cli" && toolName === "Bash") {
|
|
7852
|
+
const rw = ghostListingRewrite(args, securityCfg.ghost);
|
|
7853
|
+
if (rw) {
|
|
7854
|
+
await maybeSelfUpdate();
|
|
7855
|
+
rewriteTool({ command: rw });
|
|
7856
|
+
}
|
|
7826
7857
|
}
|
|
7827
7858
|
}
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7859
|
+
if (!reason)
|
|
7860
|
+
reason = securityLayerCheck(toolName, args, securityCfg, agentKey);
|
|
7861
|
+
if (process.env.SOLONGATE_DEBUG) {
|
|
7862
|
+
}
|
|
7863
|
+
let opaRoute = "white";
|
|
7864
|
+
if (reason) {
|
|
7865
|
+
opaRoute = "black";
|
|
7866
|
+
} else if (policy && policy.rules) {
|
|
7867
|
+
const opaResult = await evaluateWithOpa(policy, args, toolName, hookCwd);
|
|
7868
|
+
if (opaResult === void 0) {
|
|
7869
|
+
const legacy = evaluate(policy, args, toolName);
|
|
7870
|
+
if (typeof legacy === "string") {
|
|
7871
|
+
reason = legacy;
|
|
7872
|
+
opaRoute = "black";
|
|
7873
|
+
} else {
|
|
7874
|
+
opaRoute = "white";
|
|
7875
|
+
}
|
|
7876
|
+
} else if (typeof opaResult === "string") {
|
|
7877
|
+
reason = opaResult;
|
|
7842
7878
|
opaRoute = "black";
|
|
7843
7879
|
} else {
|
|
7844
7880
|
opaRoute = "white";
|
|
7845
7881
|
}
|
|
7846
|
-
} else if (typeof opaResult === "string") {
|
|
7847
|
-
reason = opaResult;
|
|
7848
|
-
opaRoute = "black";
|
|
7849
|
-
} else {
|
|
7850
|
-
opaRoute = "white";
|
|
7851
7882
|
}
|
|
7852
|
-
|
|
7853
|
-
process.stderr.write(`[SolonGate ROUTE] ${opaRoute.toUpperCase()} (${reason ? "block" : "allow"})
|
|
7883
|
+
process.stderr.write(`[SolonGate ROUTE] ${opaRoute.toUpperCase()} (${reason ? "block" : "allow"})
|
|
7854
7884
|
`);
|
|
7855
|
-
try {
|
|
7856
|
-
const _fd = resolve(".solongate");
|
|
7857
|
-
mkdirSync(_fd, { recursive: true });
|
|
7858
|
-
const _rec = { ms: Date.now() - _evalStart, ts: Date.now(), tool: toolName, session: data.session_id || "" };
|
|
7859
|
-
writeFileSync(join(_fd, ".last-eval"), JSON.stringify(_rec));
|
|
7860
7885
|
try {
|
|
7861
|
-
const
|
|
7862
|
-
|
|
7886
|
+
const _fd = resolve(".solongate");
|
|
7887
|
+
mkdirSync(_fd, { recursive: true });
|
|
7888
|
+
const _rec = { ms: Date.now() - _evalStart, ts: Date.now(), tool: toolName, session: data.session_id || "" };
|
|
7889
|
+
writeFileSync(join(_fd, ".last-eval"), JSON.stringify(_rec));
|
|
7863
7890
|
try {
|
|
7864
|
-
|
|
7865
|
-
|
|
7891
|
+
const ring = join(_fd, ".eval-ring.jsonl");
|
|
7892
|
+
appendFileSync(ring, JSON.stringify(_rec) + "\n");
|
|
7893
|
+
try {
|
|
7894
|
+
if (statSync(ring).size > 16384)
|
|
7895
|
+
writeFileSync(ring, readFileSync(ring, "utf-8").split("\n").filter(Boolean).slice(-50).join("\n") + "\n");
|
|
7896
|
+
} catch {
|
|
7897
|
+
}
|
|
7866
7898
|
} catch {
|
|
7867
7899
|
}
|
|
7868
7900
|
} catch {
|
|
7869
7901
|
}
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
} catch {
|
|
7902
|
+
if (reason) {
|
|
7903
|
+
if (true) {
|
|
7904
|
+
try {
|
|
7905
|
+
const logEntry = {
|
|
7906
|
+
tool: toolName,
|
|
7907
|
+
arguments: args,
|
|
7908
|
+
decision: "DENY",
|
|
7909
|
+
reason,
|
|
7910
|
+
permission: guessPermission(toolName),
|
|
7911
|
+
source: `${AGENT_TYPE}-guard`,
|
|
7912
|
+
agent_id: AGENT_TYPE,
|
|
7913
|
+
agent_name: AGENT_NAME,
|
|
7914
|
+
session_id: data.session_id || "",
|
|
7915
|
+
evaluation_time_ms: Date.now() - _evalStart
|
|
7916
|
+
};
|
|
7917
|
+
writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), ...logEntry });
|
|
7918
|
+
if (!localLogsOnly(securityCfg))
|
|
7919
|
+
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
7920
|
+
method: "POST",
|
|
7921
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
7922
|
+
body: JSON.stringify(logEntry),
|
|
7923
|
+
signal: AbortSignal.timeout(3e3)
|
|
7924
|
+
});
|
|
7925
|
+
} catch {
|
|
7926
|
+
}
|
|
7896
7927
|
}
|
|
7928
|
+
writeDenyFlag(toolName);
|
|
7929
|
+
await maybeSelfUpdate();
|
|
7930
|
+
blockTool(reason);
|
|
7897
7931
|
}
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7932
|
+
} catch {
|
|
7933
|
+
}
|
|
7934
|
+
await maybeSelfUpdate();
|
|
7935
|
+
allowTool();
|
|
7936
|
+
} catch (e) {
|
|
7937
|
+
if (e !== SG_DONE) {
|
|
7938
|
+
process.exitCode = process.exitCode || 0;
|
|
7901
7939
|
}
|
|
7902
|
-
} catch {
|
|
7903
7940
|
}
|
|
7904
|
-
await maybeSelfUpdate();
|
|
7905
|
-
allowTool();
|
|
7906
7941
|
})();
|
package/hooks/guard.mjs
CHANGED
|
@@ -281,24 +281,54 @@ 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.
|
|
284
|
+
if (REFRESH_MODE) { try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {} refreshPolicyCache().finally(() => { process.exitCode = 0; }); }
|
|
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
|
+
// Terminate WITHOUT ever calling process.exit() on the hot path. On Windows + Node
|
|
305
|
+
// 24, process.exit() called in (or right after) the same tick a cloud fetch()
|
|
306
|
+
// settled aborts with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
|
|
307
|
+
// file src\win\async.c`: a libuv threadpool worker (DNS) is still mid-uv_async_send
|
|
308
|
+
// when exit() force-closes the loop's async handle. The abort replaces exit code 2,
|
|
309
|
+
// so Claude Code sees a non-blocking hook failure and runs the tool anyway (guard
|
|
310
|
+
// logs DENY, never blocks). Instead we just set process.exitCode and let the event
|
|
311
|
+
// loop drain — undici unrefs idle sockets, so Node exits on its own within ~1ms of
|
|
312
|
+
// the work finishing, cleanly (no forced teardown → no abort). A latch preserves
|
|
313
|
+
// the FIRST code so a later allowTool() on fall-through can't overwrite a block; an
|
|
314
|
+
// unref'd backstop armed at the top of the handler is the only place exit() may run,
|
|
315
|
+
// and only long after every fetch has settled.
|
|
316
|
+
function sgFinish(code) {
|
|
317
|
+
if (!_sgDone) { _sgDone = true; process.exitCode = code; }
|
|
318
|
+
throw SG_DONE;
|
|
319
|
+
}
|
|
320
|
+
|
|
291
321
|
function blockTool(reason) {
|
|
292
322
|
if (AGENT_TYPE === 'gemini-cli') {
|
|
293
323
|
process.stdout.write(JSON.stringify({
|
|
294
324
|
decision: 'deny',
|
|
295
325
|
reason: `[SolonGate] ${reason}`,
|
|
296
326
|
}));
|
|
297
|
-
|
|
327
|
+
sgFinish(0);
|
|
298
328
|
} else {
|
|
299
329
|
// Claude Code — exit code 2
|
|
300
330
|
process.stderr.write(reason);
|
|
301
|
-
|
|
331
|
+
sgFinish(2);
|
|
302
332
|
}
|
|
303
333
|
}
|
|
304
334
|
|
|
@@ -306,7 +336,7 @@ function allowTool() {
|
|
|
306
336
|
if (AGENT_TYPE === 'gemini-cli') {
|
|
307
337
|
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
308
338
|
}
|
|
309
|
-
|
|
339
|
+
sgFinish(0);
|
|
310
340
|
}
|
|
311
341
|
|
|
312
342
|
// Allow the tool but REPLACE its input (Claude Code `updatedInput`). Used by the
|
|
@@ -316,7 +346,7 @@ function rewriteTool(updatedInput) {
|
|
|
316
346
|
process.stdout.write(JSON.stringify({
|
|
317
347
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', updatedInput },
|
|
318
348
|
}));
|
|
319
|
-
|
|
349
|
+
sgFinish(0);
|
|
320
350
|
}
|
|
321
351
|
|
|
322
352
|
// Write flag file so stop.mjs knows a tool call (DENY) happened and doesn't log extra ALLOW
|
|
@@ -1412,6 +1442,11 @@ function readReferencedFiles(args, cwd) {
|
|
|
1412
1442
|
// inside a stream callback, so the loop tears down cleanly.
|
|
1413
1443
|
if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
1414
1444
|
;(async () => {
|
|
1445
|
+
// Safety backstop ONLY: the normal path exits by natural drain (~1ms after work
|
|
1446
|
+
// finishes). If the loop somehow fails to drain, force-exit — 8s is well past every
|
|
1447
|
+
// fetch timeout (≤3s) so the threadpool is idle and exit() can't hit the abort.
|
|
1448
|
+
try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {}
|
|
1449
|
+
try {
|
|
1415
1450
|
// Background-refresh invocation has no tool call to evaluate — it already ran
|
|
1416
1451
|
// refreshPolicyCache() at startup; do nothing on the (empty) stdin.
|
|
1417
1452
|
if (REFRESH_MODE) return;
|
|
@@ -1604,9 +1639,9 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
1604
1639
|
});
|
|
1605
1640
|
} catch {}
|
|
1606
1641
|
await maybeSelfUpdate();
|
|
1607
|
-
if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit }));
|
|
1642
|
+
if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit })); sgFinish(0); }
|
|
1608
1643
|
process.stderr.write(ghostHit);
|
|
1609
|
-
|
|
1644
|
+
sgFinish(2);
|
|
1610
1645
|
}
|
|
1611
1646
|
// No direct hit: if this is a listing command, rewrite it so hidden
|
|
1612
1647
|
// entries are filtered out of its output (Claude Code only).
|
|
@@ -1717,4 +1752,9 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
1717
1752
|
} catch {}
|
|
1718
1753
|
await maybeSelfUpdate();
|
|
1719
1754
|
allowTool();
|
|
1755
|
+
} catch (e) {
|
|
1756
|
+
// SG_DONE is the normal terminator (exitCode already set); anything else is an
|
|
1757
|
+
// unexpected error — fail OPEN (exit 0) so a hook bug never wedges the agent.
|
|
1758
|
+
if (e !== SG_DONE) { process.exitCode = process.exitCode || 0; }
|
|
1759
|
+
}
|
|
1720
1760
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.81.
|
|
3
|
+
"version": "0.81.4",
|
|
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": {
|