@solongate/proxy 0.82.17 → 0.82.18

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.
@@ -6536,7 +6536,7 @@ import { resolve, join, dirname, isAbsolute } from "node:path";
6536
6536
  import { homedir } from "node:os";
6537
6537
  import { gunzipSync } from "node:zlib";
6538
6538
  import { createHash } from "node:crypto";
6539
- var HOOK_VERSION = 45;
6539
+ var HOOK_VERSION = 46;
6540
6540
  function localLogsOnly(security) {
6541
6541
  if (security && typeof security === "object") {
6542
6542
  const l = security.localLogs;
@@ -6789,6 +6789,7 @@ if (REFRESH_MODE) {
6789
6789
  }
6790
6790
  var SG_DONE = Symbol("sg-done");
6791
6791
  var _sgDone = false;
6792
+ var _decisionEmitted = false;
6792
6793
  function sgFinish(code) {
6793
6794
  if (!_sgDone) {
6794
6795
  _sgDone = true;
@@ -6798,34 +6799,46 @@ function sgFinish(code) {
6798
6799
  }
6799
6800
  function blockTool(reason) {
6800
6801
  if (AGENT_TYPE === "antigravity") {
6801
- process.stdout.write(JSON.stringify({
6802
- allow_tool: false,
6803
- deny_reason: `[SolonGate] ${reason}`
6804
- }));
6802
+ if (!_decisionEmitted) {
6803
+ _decisionEmitted = true;
6804
+ process.stdout.write(JSON.stringify({
6805
+ allow_tool: false,
6806
+ deny_reason: `[SolonGate] ${reason}`
6807
+ }));
6808
+ }
6805
6809
  sgFinish(0);
6806
6810
  } else {
6807
6811
  const msg = reason || "[SolonGate] Blocked by policy";
6808
- process.stdout.write(JSON.stringify({
6809
- hookSpecificOutput: {
6810
- hookEventName: "PreToolUse",
6811
- permissionDecision: "deny",
6812
- permissionDecisionReason: msg
6813
- }
6814
- }));
6815
- process.stderr.write(msg + "\n");
6812
+ if (!_decisionEmitted) {
6813
+ _decisionEmitted = true;
6814
+ process.stdout.write(JSON.stringify({
6815
+ hookSpecificOutput: {
6816
+ hookEventName: "PreToolUse",
6817
+ permissionDecision: "deny",
6818
+ permissionDecisionReason: msg
6819
+ }
6820
+ }));
6821
+ process.stderr.write(msg + "\n");
6822
+ }
6816
6823
  sgFinish(2);
6817
6824
  }
6818
6825
  }
6819
6826
  function allowTool() {
6820
6827
  if (AGENT_TYPE === "antigravity") {
6821
- process.stdout.write(JSON.stringify({ allow_tool: true }));
6828
+ if (!_decisionEmitted) {
6829
+ _decisionEmitted = true;
6830
+ process.stdout.write(JSON.stringify({ allow_tool: true }));
6831
+ }
6822
6832
  }
6823
6833
  sgFinish(0);
6824
6834
  }
6825
6835
  function rewriteTool(updatedInput) {
6826
- process.stdout.write(JSON.stringify({
6827
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput }
6828
- }));
6836
+ if (!_decisionEmitted) {
6837
+ _decisionEmitted = true;
6838
+ process.stdout.write(JSON.stringify({
6839
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput }
6840
+ }));
6841
+ }
6829
6842
  sgFinish(0);
6830
6843
  }
6831
6844
  function writeDenyFlag(toolName) {
@@ -7966,11 +7979,17 @@ if (!REFRESH_MODE) {
7966
7979
  }
7967
7980
  await maybeSelfUpdate();
7968
7981
  if (AGENT_TYPE === "antigravity") {
7969
- process.stdout.write(JSON.stringify({ allow_tool: false, deny_reason: ghostHit }));
7982
+ if (!_decisionEmitted) {
7983
+ _decisionEmitted = true;
7984
+ process.stdout.write(JSON.stringify({ allow_tool: false, deny_reason: ghostHit }));
7985
+ }
7970
7986
  sgFinish(0);
7971
7987
  }
7972
- process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: ghostHit } }));
7973
- process.stderr.write(ghostHit + "\n");
7988
+ if (!_decisionEmitted) {
7989
+ _decisionEmitted = true;
7990
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: ghostHit } }));
7991
+ process.stderr.write(ghostHit + "\n");
7992
+ }
7974
7993
  sgFinish(2);
7975
7994
  }
7976
7995
  if (AGENT_TYPE !== "antigravity" && toolName === "Bash") {
package/hooks/guard.mjs CHANGED
@@ -32,7 +32,7 @@ import { createHash } from 'node:crypto';
32
32
  // the installed hook self-updates when the cloud version is higher (see
33
33
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
34
34
  // reinstall — the same trust model as the OPA WASM this hook already runs.
35
- const HOOK_VERSION = 45;
35
+ const HOOK_VERSION = 46;
36
36
 
37
37
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
38
38
  // nothing is sent to the cloud audit log.
@@ -365,6 +365,14 @@ if (REFRESH_MODE) { try { setTimeout(() => { try { process.exit(process.exitCode
365
365
  // the threadpool has drained, so exit() is safe.
366
366
  const SG_DONE = Symbol('sg-done');
367
367
  let _sgDone = false;
368
+ // The decision JSON must be written to stdout AT MOST ONCE. The handler can reach
369
+ // a terminal allowTool() even after a block already emitted its deny (the SG_DONE
370
+ // unwind is swallowed on some paths), which for Claude Code was harmless
371
+ // (allowTool writes nothing there) but for Antigravity appended a second
372
+ // {"allow_tool":true} right after {"allow_tool":false} — agy then parsed the
373
+ // trailing allow (or failed on the double object) and ran the blocked tool. This
374
+ // latch guarantees the FIRST decision is the only one on stdout.
375
+ let _decisionEmitted = false;
368
376
  // Terminate WITHOUT ever calling process.exit() on the hot path. On Windows + Node
369
377
  // 24, process.exit() called in (or right after) the same tick a cloud fetch()
370
378
  // settled aborts with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
@@ -384,10 +392,13 @@ function sgFinish(code) {
384
392
 
385
393
  function blockTool(reason) {
386
394
  if (AGENT_TYPE === 'antigravity') {
387
- process.stdout.write(JSON.stringify({
388
- allow_tool: false,
389
- deny_reason: `[SolonGate] ${reason}`,
390
- }));
395
+ if (!_decisionEmitted) {
396
+ _decisionEmitted = true;
397
+ process.stdout.write(JSON.stringify({
398
+ allow_tool: false,
399
+ deny_reason: `[SolonGate] ${reason}`,
400
+ }));
401
+ }
391
402
  sgFinish(0);
392
403
  } else {
393
404
  // Claude Code — emit the JSON PreToolUse deny on stdout AND exit 2 (reason on
@@ -400,21 +411,27 @@ function blockTool(reason) {
400
411
  // propagate reliably; Claude Code parses the JSON instead). Reason is never
401
412
  // empty — an empty deny becomes an is_error tool_result the API rejects.
402
413
  const msg = reason || '[SolonGate] Blocked by policy';
403
- process.stdout.write(JSON.stringify({
404
- hookSpecificOutput: {
405
- hookEventName: 'PreToolUse',
406
- permissionDecision: 'deny',
407
- permissionDecisionReason: msg,
408
- },
409
- }));
410
- process.stderr.write(msg + '\n');
414
+ if (!_decisionEmitted) {
415
+ _decisionEmitted = true;
416
+ process.stdout.write(JSON.stringify({
417
+ hookSpecificOutput: {
418
+ hookEventName: 'PreToolUse',
419
+ permissionDecision: 'deny',
420
+ permissionDecisionReason: msg,
421
+ },
422
+ }));
423
+ process.stderr.write(msg + '\n');
424
+ }
411
425
  sgFinish(2);
412
426
  }
413
427
  }
414
428
 
415
429
  function allowTool() {
416
430
  if (AGENT_TYPE === 'antigravity') {
417
- process.stdout.write(JSON.stringify({ allow_tool: true }));
431
+ if (!_decisionEmitted) {
432
+ _decisionEmitted = true;
433
+ process.stdout.write(JSON.stringify({ allow_tool: true }));
434
+ }
418
435
  }
419
436
  sgFinish(0);
420
437
  }
@@ -423,9 +440,12 @@ function allowTool() {
423
440
  // ghost layer to rewrite a listing command so hidden entries are filtered out of
424
441
  // its output — the agent never sees them. Claude Code only.
425
442
  function rewriteTool(updatedInput) {
426
- process.stdout.write(JSON.stringify({
427
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', updatedInput },
428
- }));
443
+ if (!_decisionEmitted) {
444
+ _decisionEmitted = true;
445
+ process.stdout.write(JSON.stringify({
446
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', updatedInput },
447
+ }));
448
+ }
429
449
  sgFinish(0);
430
450
  }
431
451
 
@@ -1821,13 +1841,16 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1821
1841
  });
1822
1842
  } catch {}
1823
1843
  await maybeSelfUpdate();
1824
- if (AGENT_TYPE === 'antigravity') { process.stdout.write(JSON.stringify({ allow_tool: false, deny_reason: ghostHit })); sgFinish(0); }
1844
+ if (AGENT_TYPE === 'antigravity') { if (!_decisionEmitted) { _decisionEmitted = true; process.stdout.write(JSON.stringify({ allow_tool: false, deny_reason: ghostHit })); } sgFinish(0); }
1825
1845
  // Claude Code: JSON deny on stdout + exit 2 (reason on stderr). Exit 2 is
1826
1846
  // the only block enforced in auto-accept mode (see blockTool); the JSON is
1827
1847
  // the Windows fallback. ghostHit is a bare "No such file or directory", so
1828
1848
  // the stderr text keeps the stealth (looks like the path simply isn't there).
1829
- process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ghostHit } }));
1830
- process.stderr.write(ghostHit + '\n');
1849
+ if (!_decisionEmitted) {
1850
+ _decisionEmitted = true;
1851
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ghostHit } }));
1852
+ process.stderr.write(ghostHit + '\n');
1853
+ }
1831
1854
  sgFinish(2);
1832
1855
  }
1833
1856
  // No direct hit: if this is a listing command, rewrite it so hidden
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.82.17",
3
+ "version": "0.82.18",
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": {