@solongate/proxy 0.81.3 → 0.81.5
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 -11
- package/hooks/guard.bundled.mjs +27 -29
- package/hooks/guard.mjs +36 -17
- 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,12 +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
|
-
//
|
|
535
|
-
//
|
|
536
|
-
// `Assertion failed: !(handle->flags &
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
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; } };
|
|
540
545
|
if (typeof EMITTED_PAYLOAD === 'string') {
|
|
541
546
|
try { process.stdout.write(EMITTED_PAYLOAD, () => { flushed = true; maybeExit(); }); }
|
|
542
547
|
catch { flushed = true; }
|
|
@@ -586,11 +591,11 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
586
591
|
signal: AbortSignal.timeout(5000),
|
|
587
592
|
}).catch(() => {}).finally(() => { fetchDone = true; maybeExit(); });
|
|
588
593
|
}
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
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 {}
|
|
592
598
|
} catch {
|
|
593
|
-
|
|
594
|
-
try { setImmediate(() => { try { process.exit(0); } catch {} }); } catch { try { process.exit(0); } catch {} }
|
|
599
|
+
process.exitCode = 0;
|
|
595
600
|
}
|
|
596
601
|
})();
|
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -6757,21 +6757,17 @@ async function refreshPolicyCache() {
|
|
|
6757
6757
|
}
|
|
6758
6758
|
}
|
|
6759
6759
|
if (REFRESH_MODE) {
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
try {
|
|
6763
|
-
setImmediate(() => {
|
|
6764
|
-
try {
|
|
6765
|
-
process.exit(0);
|
|
6766
|
-
} catch {
|
|
6767
|
-
}
|
|
6768
|
-
});
|
|
6769
|
-
} catch {
|
|
6760
|
+
try {
|
|
6761
|
+
setTimeout(() => {
|
|
6770
6762
|
try {
|
|
6771
|
-
process.exit(0);
|
|
6763
|
+
process.exit(process.exitCode || 0);
|
|
6772
6764
|
} catch {
|
|
6773
6765
|
}
|
|
6774
|
-
}
|
|
6766
|
+
}, 8e3).unref();
|
|
6767
|
+
} catch {
|
|
6768
|
+
}
|
|
6769
|
+
refreshPolicyCache().finally(() => {
|
|
6770
|
+
process.exitCode = 0;
|
|
6775
6771
|
});
|
|
6776
6772
|
}
|
|
6777
6773
|
var SG_DONE = Symbol("sg-done");
|
|
@@ -6780,19 +6776,6 @@ function sgFinish(code) {
|
|
|
6780
6776
|
if (!_sgDone) {
|
|
6781
6777
|
_sgDone = true;
|
|
6782
6778
|
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
6779
|
}
|
|
6797
6780
|
throw SG_DONE;
|
|
6798
6781
|
}
|
|
@@ -6804,8 +6787,14 @@ function blockTool(reason) {
|
|
|
6804
6787
|
}));
|
|
6805
6788
|
sgFinish(0);
|
|
6806
6789
|
} else {
|
|
6807
|
-
process.
|
|
6808
|
-
|
|
6790
|
+
process.stdout.write(JSON.stringify({
|
|
6791
|
+
hookSpecificOutput: {
|
|
6792
|
+
hookEventName: "PreToolUse",
|
|
6793
|
+
permissionDecision: "deny",
|
|
6794
|
+
permissionDecisionReason: reason
|
|
6795
|
+
}
|
|
6796
|
+
}));
|
|
6797
|
+
sgFinish(0);
|
|
6809
6798
|
}
|
|
6810
6799
|
}
|
|
6811
6800
|
function allowTool() {
|
|
@@ -7698,6 +7687,15 @@ if (!REFRESH_MODE) {
|
|
|
7698
7687
|
}
|
|
7699
7688
|
}
|
|
7700
7689
|
(async () => {
|
|
7690
|
+
try {
|
|
7691
|
+
setTimeout(() => {
|
|
7692
|
+
try {
|
|
7693
|
+
process.exit(process.exitCode || 0);
|
|
7694
|
+
} catch {
|
|
7695
|
+
}
|
|
7696
|
+
}, 8e3).unref();
|
|
7697
|
+
} catch {
|
|
7698
|
+
}
|
|
7701
7699
|
try {
|
|
7702
7700
|
if (REFRESH_MODE)
|
|
7703
7701
|
return;
|
|
@@ -7853,8 +7851,8 @@ if (!REFRESH_MODE) {
|
|
|
7853
7851
|
process.stdout.write(JSON.stringify({ decision: "deny", reason: ghostHit }));
|
|
7854
7852
|
sgFinish(0);
|
|
7855
7853
|
}
|
|
7856
|
-
process.
|
|
7857
|
-
sgFinish(
|
|
7854
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: ghostHit } }));
|
|
7855
|
+
sgFinish(0);
|
|
7858
7856
|
}
|
|
7859
7857
|
if (AGENT_TYPE !== "gemini-cli" && toolName === "Bash") {
|
|
7860
7858
|
const rw = ghostListingRewrite(args, securityCfg.ghost);
|
package/hooks/guard.mjs
CHANGED
|
@@ -281,7 +281,7 @@ 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) {
|
|
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:
|
|
@@ -301,18 +301,20 @@ if (REFRESH_MODE) { refreshPolicyCache().finally(() => { process.exitCode = 0; t
|
|
|
301
301
|
// the threadpool has drained, so exit() is safe.
|
|
302
302
|
const SG_DONE = Symbol('sg-done');
|
|
303
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.
|
|
304
316
|
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
|
-
}
|
|
317
|
+
if (!_sgDone) { _sgDone = true; process.exitCode = code; }
|
|
316
318
|
throw SG_DONE;
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -324,9 +326,20 @@ function blockTool(reason) {
|
|
|
324
326
|
}));
|
|
325
327
|
sgFinish(0);
|
|
326
328
|
} else {
|
|
327
|
-
// Claude Code —
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
// Claude Code — signal the block via the JSON PreToolUse protocol on stdout,
|
|
330
|
+
// NOT exit code 2. On Windows the hook runs through PowerShell, which does not
|
|
331
|
+
// reliably propagate a native process's exit code back to Claude Code, so an
|
|
332
|
+
// `exit 2` block was being seen as a "non-blocking status code" and the tool ran
|
|
333
|
+
// anyway. Claude only processes this JSON on exit 0 (JSON is ignored if you exit
|
|
334
|
+
// 2), so we MUST exit 0 here. This is shell-independent and works everywhere.
|
|
335
|
+
process.stdout.write(JSON.stringify({
|
|
336
|
+
hookSpecificOutput: {
|
|
337
|
+
hookEventName: 'PreToolUse',
|
|
338
|
+
permissionDecision: 'deny',
|
|
339
|
+
permissionDecisionReason: reason,
|
|
340
|
+
},
|
|
341
|
+
}));
|
|
342
|
+
sgFinish(0);
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
|
|
@@ -1440,6 +1453,10 @@ function readReferencedFiles(args, cwd) {
|
|
|
1440
1453
|
// inside a stream callback, so the loop tears down cleanly.
|
|
1441
1454
|
if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
1442
1455
|
;(async () => {
|
|
1456
|
+
// Safety backstop ONLY: the normal path exits by natural drain (~1ms after work
|
|
1457
|
+
// finishes). If the loop somehow fails to drain, force-exit — 8s is well past every
|
|
1458
|
+
// fetch timeout (≤3s) so the threadpool is idle and exit() can't hit the abort.
|
|
1459
|
+
try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {}
|
|
1443
1460
|
try {
|
|
1444
1461
|
// Background-refresh invocation has no tool call to evaluate — it already ran
|
|
1445
1462
|
// refreshPolicyCache() at startup; do nothing on the (empty) stdin.
|
|
@@ -1634,8 +1651,10 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
1634
1651
|
} catch {}
|
|
1635
1652
|
await maybeSelfUpdate();
|
|
1636
1653
|
if (AGENT_TYPE === 'gemini-cli') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: ghostHit })); sgFinish(0); }
|
|
1637
|
-
|
|
1638
|
-
|
|
1654
|
+
// Claude Code: JSON deny on stdout + exit 0 (see blockTool — exit codes are
|
|
1655
|
+
// not reliable through PowerShell on Windows; JSON is ignored on exit 2).
|
|
1656
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ghostHit } }));
|
|
1657
|
+
sgFinish(0);
|
|
1639
1658
|
}
|
|
1640
1659
|
// No direct hit: if this is a listing command, rewrite it so hidden
|
|
1641
1660
|
// entries are filtered out of its output (Claude Code only).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.81.
|
|
3
|
+
"version": "0.81.5",
|
|
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": {
|