@solongate/proxy 0.81.2 → 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 CHANGED
@@ -531,7 +531,12 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
531
531
  // on the write's flush callback (and on the fire-and-forget audit POST).
532
532
  let flushed = (typeof EMITTED_PAYLOAD !== 'string');
533
533
  let fetchDone = false;
534
- const maybeExit = () => { if (flushed && fetchDone) process.exit(0); };
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 {} }); } };
535
540
  if (typeof EMITTED_PAYLOAD === 'string') {
536
541
  try { process.stdout.write(EMITTED_PAYLOAD, () => { flushed = true; maybeExit(); }); }
537
542
  catch { flushed = true; }
@@ -581,9 +586,11 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
581
586
  signal: AbortSignal.timeout(5000),
582
587
  }).catch(() => {}).finally(() => { fetchDone = true; maybeExit(); });
583
588
  }
584
- // Hard backstop: exit even if the write callback or fetch never settles.
585
- setTimeout(() => process.exit(0), 3000);
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);
586
592
  } catch {
587
- process.exit(0);
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 {} }
588
595
  }
589
596
  })();
@@ -6757,7 +6757,44 @@ async function refreshPolicyCache() {
6757
6757
  }
6758
6758
  }
6759
6759
  if (REFRESH_MODE) {
6760
- refreshPolicyCache().finally(() => process.exit(0));
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
- process.exit(0);
6805
+ sgFinish(0);
6769
6806
  } else {
6770
6807
  process.stderr.write(reason);
6771
- process.exit(2);
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
- process.exit(0);
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
- process.exit(0);
6821
+ sgFinish(0);
6785
6822
  }
6786
6823
  function writeDenyFlag(toolName) {
6787
6824
  try {
@@ -7661,246 +7698,252 @@ if (!REFRESH_MODE) {
7661
7698
  }
7662
7699
  }
7663
7700
  (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
7701
  try {
7674
- const raw = JSON.parse(input);
7702
+ if (REFRESH_MODE)
7703
+ return;
7675
7704
  if (process.env.SOLONGATE_DEBUG) {
7676
- try {
7677
- const { appendFileSync: afs, mkdirSync: mds } = await import("node:fs");
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);
7681
- } catch {
7682
- }
7683
7705
  }
7684
- let mappedToolName = raw.tool_name || raw.toolName || "";
7685
- let mappedToolInput = raw.tool_input || raw.toolInput || raw.params || {};
7686
- const data = {
7687
- ...raw,
7688
- tool_name: mappedToolName,
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;
7706
+ if (!API_KEY) {
7707
+ allowTool();
7708
+ return;
7709
+ }
7710
+ const _evalStart = Date.now();
7703
7711
  try {
7704
- let dashboardPolicy = null;
7705
- let staleCache = null;
7706
- let refreshDue = true;
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;
7707
7741
  try {
7708
- if (existsSync(policyCacheFile)) {
7709
- const cached = JSON.parse(readFileSync(policyCacheFile, "utf-8"));
7710
- if (cached && cached.policy)
7711
- staleCache = cached;
7712
- if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
7713
- refreshDue = false;
7714
- if (cached.policy)
7715
- dashboardPolicy = cached.policy;
7716
- if (typeof cached.selfProtect === "boolean")
7717
- selfProtectEnabled = cached.selfProtect;
7718
- if (cached.security !== void 0)
7719
- securityCfg = cached.security;
7720
- if (cached.hookVersions)
7721
- CLOUD_HOOK_VERSIONS = cached.hookVersions;
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
+ }
7722
7761
  }
7762
+ } catch {
7723
7763
  }
7724
- } catch {
7725
- }
7726
- if (refreshDue) {
7727
- if (!dashboardPolicy && staleCache) {
7728
- dashboardPolicy = staleCache.policy;
7729
- if (typeof staleCache.selfProtect === "boolean")
7730
- selfProtectEnabled = staleCache.selfProtect;
7731
- if (staleCache.security !== void 0)
7732
- securityCfg = staleCache.security;
7733
- if (staleCache.hookVersions)
7734
- CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
7735
- }
7736
- if (API_KEY) {
7737
- try {
7738
- const lock = join(resolve(homedir(), ".solongate"), ".policy-refresh-" + agentKey + ".lock");
7739
- const due = !existsSync(lock) || Date.now() - statSync(lock).mtimeMs > 3e3;
7740
- if (due) {
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)) {
7741
7800
  try {
7742
- writeFileSync(lock, String(Date.now()));
7801
+ policy = JSON.parse(readFileSync(p, "utf-8"));
7802
+ break;
7743
7803
  } catch {
7744
7804
  }
7745
- spawn(process.execPath, [process.argv[1], AGENT_TYPE, AGENT_NAME, "--sg-refresh-policy"], { detached: true, stdio: "ignore", env: process.env }).unref();
7746
7805
  }
7747
- } catch {
7748
7806
  }
7749
7807
  }
7808
+ } catch {
7750
7809
  }
7751
7810
  if (process.env.SOLONGATE_DEBUG) {
7752
7811
  }
7753
- if (dashboardPolicy) {
7754
- policy = dashboardPolicy;
7755
- } else {
7756
- const candidates = [
7757
- join(resolve(homedir(), ".solongate"), "policy.json"),
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
- }
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;
7768
7817
  }
7769
7818
  }
7770
- } catch {
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);
7819
+ if (process.env.SOLONGATE_DEBUG) {
7820
7820
  }
7821
- if (AGENT_TYPE !== "gemini-cli" && toolName === "Bash") {
7822
- const rw = ghostListingRewrite(args, securityCfg.ghost);
7823
- if (rw) {
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
+ }
7824
7851
  await maybeSelfUpdate();
7825
- rewriteTool({ command: rw });
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
+ }
7826
7865
  }
7827
7866
  }
7828
- }
7829
- if (!reason)
7830
- reason = securityLayerCheck(toolName, args, securityCfg, agentKey);
7831
- if (process.env.SOLONGATE_DEBUG) {
7832
- }
7833
- let opaRoute = "white";
7834
- if (reason) {
7835
- opaRoute = "black";
7836
- } else if (policy && policy.rules) {
7837
- const opaResult = await evaluateWithOpa(policy, args, toolName, hookCwd);
7838
- if (opaResult === void 0) {
7839
- const legacy = evaluate(policy, args, toolName);
7840
- if (typeof legacy === "string") {
7841
- reason = legacy;
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;
7842
7886
  opaRoute = "black";
7843
7887
  } else {
7844
7888
  opaRoute = "white";
7845
7889
  }
7846
- } else if (typeof opaResult === "string") {
7847
- reason = opaResult;
7848
- opaRoute = "black";
7849
- } else {
7850
- opaRoute = "white";
7851
7890
  }
7852
- }
7853
- process.stderr.write(`[SolonGate ROUTE] ${opaRoute.toUpperCase()} (${reason ? "block" : "allow"})
7891
+ process.stderr.write(`[SolonGate ROUTE] ${opaRoute.toUpperCase()} (${reason ? "block" : "allow"})
7854
7892
  `);
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
7893
  try {
7861
- const ring = join(_fd, ".eval-ring.jsonl");
7862
- appendFileSync(ring, JSON.stringify(_rec) + "\n");
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));
7863
7898
  try {
7864
- if (statSync(ring).size > 16384)
7865
- writeFileSync(ring, readFileSync(ring, "utf-8").split("\n").filter(Boolean).slice(-50).join("\n") + "\n");
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
+ }
7866
7906
  } catch {
7867
7907
  }
7868
7908
  } catch {
7869
7909
  }
7870
- } catch {
7871
- }
7872
- if (reason) {
7873
- if (true) {
7874
- try {
7875
- const logEntry = {
7876
- tool: toolName,
7877
- arguments: args,
7878
- decision: "DENY",
7879
- reason,
7880
- permission: guessPermission(toolName),
7881
- source: `${AGENT_TYPE}-guard`,
7882
- agent_id: AGENT_TYPE,
7883
- agent_name: AGENT_NAME,
7884
- session_id: data.session_id || "",
7885
- evaluation_time_ms: Date.now() - _evalStart
7886
- };
7887
- writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), ...logEntry });
7888
- if (!localLogsOnly(securityCfg))
7889
- await fetch(API_URL + "/api/v1/audit-logs", {
7890
- method: "POST",
7891
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
7892
- body: JSON.stringify(logEntry),
7893
- signal: AbortSignal.timeout(3e3)
7894
- });
7895
- } 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
+ }
7896
7935
  }
7936
+ writeDenyFlag(toolName);
7937
+ await maybeSelfUpdate();
7938
+ blockTool(reason);
7897
7939
  }
7898
- writeDenyFlag(toolName);
7899
- await maybeSelfUpdate();
7900
- blockTool(reason);
7940
+ } catch {
7941
+ }
7942
+ await maybeSelfUpdate();
7943
+ allowTool();
7944
+ } catch (e) {
7945
+ if (e !== SG_DONE) {
7946
+ process.exitCode = process.exitCode || 0;
7901
7947
  }
7902
- } catch {
7903
7948
  }
7904
- await maybeSelfUpdate();
7905
- allowTool();
7906
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
- process.exit(0);
325
+ sgFinish(0);
298
326
  } else {
299
327
  // Claude Code — exit code 2
300
328
  process.stderr.write(reason);
301
- process.exit(2);
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
- process.exit(0);
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
- process.exit(0);
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
@@ -1412,6 +1440,7 @@ function readReferencedFiles(args, cwd) {
1412
1440
  // inside a stream callback, so the loop tears down cleanly.
1413
1441
  if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1414
1442
  ;(async () => {
1443
+ try {
1415
1444
  // Background-refresh invocation has no tool call to evaluate — it already ran
1416
1445
  // refreshPolicyCache() at startup; do nothing on the (empty) stdin.
1417
1446
  if (REFRESH_MODE) return;
@@ -1604,9 +1633,9 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1604
1633
  });
1605
1634
  } catch {}
1606
1635
  await maybeSelfUpdate();
1607
- if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit })); process.exit(0); }
1636
+ if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit })); sgFinish(0); }
1608
1637
  process.stderr.write(ghostHit);
1609
- process.exit(2);
1638
+ sgFinish(2);
1610
1639
  }
1611
1640
  // No direct hit: if this is a listing command, rewrite it so hidden
1612
1641
  // entries are filtered out of its output (Claude Code only).
@@ -1717,4 +1746,9 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1717
1746
  } catch {}
1718
1747
  await maybeSelfUpdate();
1719
1748
  allowTool();
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
+ }
1720
1754
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.2",
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": {