@solongate/proxy 0.83.19 → 0.83.20

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.
@@ -6578,7 +6578,7 @@ function sweepLegacyFlagDir() {
6578
6578
  } catch {
6579
6579
  }
6580
6580
  }
6581
- var HOOK_VERSION = 79;
6581
+ var HOOK_VERSION = 80;
6582
6582
  function localLogsOnly(security) {
6583
6583
  if (security !== void 0) {
6584
6584
  const l = security && security.localLogs;
@@ -6606,6 +6606,17 @@ function accountMark() {
6606
6606
  return "";
6607
6607
  }
6608
6608
  }
6609
+ function postAuditDetached(entry) {
6610
+ try {
6611
+ const payload = Buffer.from(JSON.stringify({
6612
+ url: API_URL + "/api/v1/audit-logs",
6613
+ headers: AUTH_HEADERS,
6614
+ body: entry
6615
+ }), "utf-8").toString("base64");
6616
+ spawn(process.execPath, [process.argv[1], "--sg-audit-post", payload], { detached: true, stdio: "ignore" }).unref();
6617
+ } catch {
6618
+ }
6619
+ }
6609
6620
  function writeLocalLog(security, entry) {
6610
6621
  try {
6611
6622
  const mark = accountMark();
@@ -6818,6 +6829,28 @@ var AGENT_TYPE = process.argv[2] || "claude-code";
6818
6829
  var POLICY_SELECTOR = process.env.SOLONGATE_AGENT_ID || "";
6819
6830
  var AGENT_ID = POLICY_SELECTOR || AGENT_TYPE;
6820
6831
  var AGENT_NAME = process.env.SOLONGATE_AGENT_NAME || process.argv[3] || AGENT_TYPE;
6832
+ {
6833
+ const _ai = process.argv.indexOf("--sg-audit-post");
6834
+ if (_ai !== -1) {
6835
+ try {
6836
+ const _raw = Buffer.from(process.argv[_ai + 1] || "", "base64").toString("utf-8");
6837
+ const _p = JSON.parse(_raw);
6838
+ if (_p && _p.url && _p.body) {
6839
+ fetch(_p.url, {
6840
+ method: "POST",
6841
+ headers: { "Content-Type": "application/json", ..._p.headers || {} },
6842
+ body: JSON.stringify(_p.body),
6843
+ signal: AbortSignal.timeout(1e4)
6844
+ }).catch(() => {
6845
+ }).finally(() => process.exit(0));
6846
+ setTimeout(() => process.exit(0), 11e3).unref();
6847
+ } else
6848
+ process.exit(0);
6849
+ } catch {
6850
+ process.exit(0);
6851
+ }
6852
+ }
6853
+ }
6821
6854
  {
6822
6855
  const _wi = process.argv.indexOf("--sg-log-write");
6823
6856
  if (_wi !== -1) {
@@ -8511,12 +8544,7 @@ if (!REFRESH_MODE) {
8511
8544
  }
8512
8545
  try {
8513
8546
  if (!localLogsOnly(_sec))
8514
- await fetch(API_URL + "/api/v1/audit-logs", {
8515
- method: "POST",
8516
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
8517
- body: JSON.stringify(_logEntry),
8518
- signal: AbortSignal.timeout(3e3)
8519
- });
8547
+ postAuditDetached(_logEntry);
8520
8548
  } catch {
8521
8549
  }
8522
8550
  if (AGENT_TYPE !== "codex")
@@ -8629,22 +8657,17 @@ if (!REFRESH_MODE) {
8629
8657
  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: call.sessionId, evaluation_time_ms: Date.now() - _evalStart });
8630
8658
  try {
8631
8659
  if (!localLogsOnly(securityCfg))
8632
- await fetch(API_URL + "/api/v1/audit-logs", {
8633
- method: "POST",
8634
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
8635
- body: JSON.stringify({
8636
- tool: toolName,
8637
- arguments: args,
8638
- decision: "DENY",
8639
- reason: "ghost path (hidden from agent)",
8640
- permission: guessPermission(toolName),
8641
- source: `${AGENT_TYPE}-guard`,
8642
- agent_id: AGENT_TYPE,
8643
- agent_name: AGENT_NAME,
8644
- session_id: call.sessionId,
8645
- evaluation_time_ms: Date.now() - _evalStart
8646
- }),
8647
- signal: AbortSignal.timeout(3e3)
8660
+ postAuditDetached({
8661
+ tool: toolName,
8662
+ arguments: args,
8663
+ decision: "DENY",
8664
+ reason: "ghost path (hidden from agent)",
8665
+ permission: guessPermission(toolName),
8666
+ source: `${AGENT_TYPE}-guard`,
8667
+ agent_id: AGENT_TYPE,
8668
+ agent_name: AGENT_NAME,
8669
+ session_id: call.sessionId,
8670
+ evaluation_time_ms: Date.now() - _evalStart
8648
8671
  });
8649
8672
  } catch {
8650
8673
  }
@@ -8728,12 +8751,7 @@ if (!REFRESH_MODE) {
8728
8751
  };
8729
8752
  writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), ...logEntry });
8730
8753
  if (!localLogsOnly(securityCfg))
8731
- await fetch(API_URL + "/api/v1/audit-logs", {
8732
- method: "POST",
8733
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
8734
- body: JSON.stringify(logEntry),
8735
- signal: AbortSignal.timeout(3e3)
8736
- });
8754
+ postAuditDetached(logEntry);
8737
8755
  } catch {
8738
8756
  }
8739
8757
  }
package/hooks/guard.mjs CHANGED
@@ -81,7 +81,7 @@ import { createHash } from 'node:crypto';
81
81
  // the installed hook self-updates when the cloud version is higher (see
82
82
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
83
83
  // reinstall — the same trust model as the OPA WASM this hook already runs.
84
- const HOOK_VERSION = 79;
84
+ const HOOK_VERSION = 80;
85
85
 
86
86
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
87
87
  // nothing is sent to the cloud audit log — local and cloud are one or the
@@ -170,6 +170,25 @@ function accountMark() {
170
170
  }
171
171
  }
172
172
 
173
+ /**
174
+ * Record a denial in the cloud WITHOUT making the agent wait for it.
175
+ *
176
+ * The verdict is the product; the audit line is bookkeeping. Awaiting the POST
177
+ * put a full network round trip between "blocked" and the agent hearing it —
178
+ * measured at 1643ms per denial, against 78ms with the API unreachable. Handing
179
+ * it to a detached child keeps the record and gives the time back.
180
+ */
181
+ function postAuditDetached(entry) {
182
+ try {
183
+ const payload = Buffer.from(JSON.stringify({
184
+ url: API_URL + '/api/v1/audit-logs',
185
+ headers: AUTH_HEADERS,
186
+ body: entry,
187
+ }), 'utf-8').toString('base64');
188
+ spawn(process.execPath, [process.argv[1], '--sg-audit-post', payload], { detached: true, stdio: 'ignore' }).unref();
189
+ } catch { /* the denial still stands; only the record is at risk */ }
190
+ }
191
+
173
192
  function writeLocalLog(security, entry) {
174
193
  try {
175
194
  const mark = accountMark();
@@ -460,6 +479,31 @@ const AGENT_NAME = process.env.SOLONGATE_AGENT_NAME || process.argv[3] || AGENT_
460
479
  // lands even if Claude Code kills the parent hook mid-write during a concurrent
461
480
  // denial burst. Handled FIRST and exits immediately — the child never loads the
462
481
  // heavy guard logic below.
482
+ // Survivable CLOUD writer, same idea. A denial used to `await` its audit POST
483
+ // before telling the agent it was blocked, so the agent sat waiting on a network
484
+ // round trip to be told "no": measured at 1643ms per denial against 78ms with
485
+ // the API unreachable. The record still has to land, so it is handed to a
486
+ // detached child rather than dropped — the verdict goes out at once and the POST
487
+ // finishes on its own.
488
+ {
489
+ const _ai = process.argv.indexOf('--sg-audit-post');
490
+ if (_ai !== -1) {
491
+ try {
492
+ const _raw = Buffer.from(process.argv[_ai + 1] || '', 'base64').toString('utf-8');
493
+ const _p = JSON.parse(_raw);
494
+ if (_p && _p.url && _p.body) {
495
+ fetch(_p.url, {
496
+ method: 'POST',
497
+ headers: { 'Content-Type': 'application/json', ...(_p.headers || {}) },
498
+ body: JSON.stringify(_p.body),
499
+ signal: AbortSignal.timeout(10000),
500
+ }).catch(() => {}).finally(() => process.exit(0));
501
+ setTimeout(() => process.exit(0), 11000).unref();
502
+ } else process.exit(0);
503
+ } catch { process.exit(0); }
504
+ }
505
+ }
506
+
463
507
  {
464
508
  const _wi = process.argv.indexOf('--sg-log-write');
465
509
  if (_wi !== -1) {
@@ -2523,12 +2567,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2523
2567
  };
2524
2568
  try { writeLocalLog(_sec, { ts: new Date().toISOString(), ..._logEntry }); } catch {}
2525
2569
  try {
2526
- if (!localLogsOnly(_sec)) await fetch(API_URL + '/api/v1/audit-logs', {
2527
- method: 'POST',
2528
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
2529
- body: JSON.stringify(_logEntry),
2530
- signal: AbortSignal.timeout(3000),
2531
- });
2570
+ if (!localLogsOnly(_sec)) postAuditDetached(_logEntry);
2532
2571
  } catch {}
2533
2572
  // Not on Codex: its block reason IS the hook's stderr (see the ROUTE
2534
2573
  // write at the end of the slow path for the full rationale).
@@ -2676,18 +2715,13 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2676
2715
  try { writeDenyFlag(toolName); } catch {}
2677
2716
  writeLocalLog(securityCfg, { ts: 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: call.sessionId, evaluation_time_ms: Date.now() - _evalStart });
2678
2717
  try {
2679
- if (!localLogsOnly(securityCfg)) await fetch(API_URL + '/api/v1/audit-logs', {
2680
- method: 'POST',
2681
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
2682
- body: JSON.stringify({
2683
- tool: toolName, arguments: args, decision: 'DENY',
2684
- reason: 'ghost path (hidden from agent)',
2685
- permission: guessPermission(toolName),
2686
- source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME,
2687
- session_id: call.sessionId,
2688
- evaluation_time_ms: Date.now() - _evalStart,
2689
- }),
2690
- signal: AbortSignal.timeout(3000),
2718
+ if (!localLogsOnly(securityCfg)) postAuditDetached({
2719
+ tool: toolName, arguments: args, decision: 'DENY',
2720
+ reason: 'ghost path (hidden from agent)',
2721
+ permission: guessPermission(toolName),
2722
+ source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME,
2723
+ session_id: call.sessionId,
2724
+ evaluation_time_ms: Date.now() - _evalStart,
2691
2725
  });
2692
2726
  } catch {}
2693
2727
  // NOT maybeSelfUpdate() first: see the deny at the end of this flow.
@@ -2819,12 +2853,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2819
2853
  writeLocalLog(securityCfg, { ts: new Date().toISOString(), ...logEntry });
2820
2854
  // PI hook layer removed — piResult fields no longer attached.
2821
2855
  // Local-only mode: keep the log on the user's machine, skip the cloud.
2822
- if (!localLogsOnly(securityCfg)) await fetch(API_URL + '/api/v1/audit-logs', {
2823
- method: 'POST',
2824
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
2825
- body: JSON.stringify(logEntry),
2826
- signal: AbortSignal.timeout(3000),
2827
- });
2856
+ if (!localLogsOnly(securityCfg)) postAuditDetached(logEntry);
2828
2857
  } catch {}
2829
2858
  }
2830
2859
  writeDenyFlag(toolName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.19",
3
+ "version": "0.83.20",
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": {