@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 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
- process.stdin.on('data', c => input += c);
396
- process.stdin.on('end', async () => {
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
- 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 {} }); } };
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
- 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);
581
592
  } catch {
582
- 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 {} }
583
595
  }
584
- });
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 {
@@ -7654,248 +7691,259 @@ function readReferencedFiles(args, cwd) {
7654
7691
  }
7655
7692
  return out;
7656
7693
  }
7657
- process.stdin.on("data", (c) => input += c);
7658
- process.stdin.on("end", async () => {
7659
- if (REFRESH_MODE)
7660
- return;
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
- const _evalStart = Date.now();
7699
+ }
7700
+ (async () => {
7668
7701
  try {
7669
- const raw = JSON.parse(input);
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
- let mappedToolName = raw.tool_name || raw.toolName || "";
7680
- let mappedToolInput = raw.tool_input || raw.toolInput || raw.params || {};
7681
- const data = {
7682
- ...raw,
7683
- tool_name: mappedToolName,
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
- let dashboardPolicy = null;
7700
- let staleCache = null;
7701
- 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;
7702
7741
  try {
7703
- if (existsSync(policyCacheFile)) {
7704
- const cached = JSON.parse(readFileSync(policyCacheFile, "utf-8"));
7705
- if (cached && cached.policy)
7706
- staleCache = cached;
7707
- if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
7708
- refreshDue = false;
7709
- if (cached.policy)
7710
- dashboardPolicy = cached.policy;
7711
- if (typeof cached.selfProtect === "boolean")
7712
- selfProtectEnabled = cached.selfProtect;
7713
- if (cached.security !== void 0)
7714
- securityCfg = cached.security;
7715
- if (cached.hookVersions)
7716
- 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
+ }
7717
7761
  }
7762
+ } catch {
7718
7763
  }
7719
- } catch {
7720
- }
7721
- if (refreshDue) {
7722
- if (!dashboardPolicy && staleCache) {
7723
- dashboardPolicy = staleCache.policy;
7724
- if (typeof staleCache.selfProtect === "boolean")
7725
- selfProtectEnabled = staleCache.selfProtect;
7726
- if (staleCache.security !== void 0)
7727
- securityCfg = staleCache.security;
7728
- if (staleCache.hookVersions)
7729
- CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
7730
- }
7731
- if (API_KEY) {
7732
- try {
7733
- const lock = join(resolve(homedir(), ".solongate"), ".policy-refresh-" + agentKey + ".lock");
7734
- const due = !existsSync(lock) || Date.now() - statSync(lock).mtimeMs > 3e3;
7735
- 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)) {
7736
7800
  try {
7737
- writeFileSync(lock, String(Date.now()));
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
- if (dashboardPolicy) {
7749
- policy = dashboardPolicy;
7750
- } else {
7751
- const candidates = [
7752
- join(resolve(homedir(), ".solongate"), "policy.json"),
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
- } catch {
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
- if (AGENT_TYPE !== "gemini-cli" && toolName === "Bash") {
7817
- const rw = ghostListingRewrite(args, securityCfg.ghost);
7818
- 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
+ }
7819
7851
  await maybeSelfUpdate();
7820
- 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
+ }
7821
7865
  }
7822
7866
  }
7823
- }
7824
- if (!reason)
7825
- reason = securityLayerCheck(toolName, args, securityCfg, agentKey);
7826
- if (process.env.SOLONGATE_DEBUG) {
7827
- }
7828
- let opaRoute = "white";
7829
- if (reason) {
7830
- opaRoute = "black";
7831
- } else if (policy && policy.rules) {
7832
- const opaResult = await evaluateWithOpa(policy, args, toolName, hookCwd);
7833
- if (opaResult === void 0) {
7834
- const legacy = evaluate(policy, args, toolName);
7835
- if (typeof legacy === "string") {
7836
- 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;
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 ring = join(_fd, ".eval-ring.jsonl");
7857
- 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));
7858
7898
  try {
7859
- if (statSync(ring).size > 16384)
7860
- 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
+ }
7861
7906
  } catch {
7862
7907
  }
7863
7908
  } catch {
7864
7909
  }
7865
- } catch {
7866
- }
7867
- if (reason) {
7868
- if (true) {
7869
- try {
7870
- const logEntry = {
7871
- tool: toolName,
7872
- arguments: args,
7873
- decision: "DENY",
7874
- reason,
7875
- permission: guessPermission(toolName),
7876
- source: `${AGENT_TYPE}-guard`,
7877
- agent_id: AGENT_TYPE,
7878
- agent_name: AGENT_NAME,
7879
- session_id: data.session_id || "",
7880
- evaluation_time_ms: Date.now() - _evalStart
7881
- };
7882
- writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), ...logEntry });
7883
- if (!localLogsOnly(securityCfg))
7884
- await fetch(API_URL + "/api/v1/audit-logs", {
7885
- method: "POST",
7886
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
7887
- body: JSON.stringify(logEntry),
7888
- signal: AbortSignal.timeout(3e3)
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
- writeDenyFlag(toolName);
7894
- await maybeSelfUpdate();
7895
- blockTool(reason);
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
- await maybeSelfUpdate();
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
- 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
@@ -1402,8 +1430,17 @@ function readReferencedFiles(args, cwd) {
1402
1430
  return out;
1403
1431
  }
1404
1432
 
1405
- process.stdin.on('data', c => input += c);
1406
- process.stdin.on('end', async () => {
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 })); process.exit(0); }
1636
+ if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit })); sgFinish(0); }
1600
1637
  process.stderr.write(ghostHit);
1601
- process.exit(2);
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.1",
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": {