@solongate/proxy 0.83.1 → 0.83.2

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 = 69;
6539
+ var HOOK_VERSION = 70;
6540
6540
  function localLogsOnly(security) {
6541
6541
  if (security && typeof security === "object") {
6542
6542
  const l = security.localLogs;
@@ -7951,13 +7951,52 @@ function rateLimitCheck(agentKey, limits) {
7951
7951
  return null;
7952
7952
  }
7953
7953
  }
7954
+ function quotaTally(used) {
7955
+ const dir = resolve(homedir(), ".solongate");
7956
+ const markFile = join(dir, ".quota-mark");
7957
+ const tallyFile = join(dir, ".quota-tally");
7958
+ let n = 0;
7959
+ try {
7960
+ let mark = null;
7961
+ if (existsSync(markFile)) {
7962
+ try {
7963
+ mark = readFileSync(markFile, "utf-8").trim();
7964
+ } catch {
7965
+ }
7966
+ }
7967
+ if (mark !== String(used)) {
7968
+ writeFileSync(markFile, String(used));
7969
+ writeFileSync(tallyFile, "");
7970
+ } else if (existsSync(tallyFile)) {
7971
+ try {
7972
+ n = statSync(tallyFile).size;
7973
+ } catch {
7974
+ }
7975
+ }
7976
+ } catch {
7977
+ }
7978
+ return {
7979
+ n,
7980
+ add() {
7981
+ try {
7982
+ appendFileSync(tallyFile, ".");
7983
+ } catch {
7984
+ }
7985
+ }
7986
+ };
7987
+ }
7954
7988
  function securityLayerCheck(toolName, args, cfg, agentKey) {
7955
7989
  if (!cfg)
7956
7990
  return null;
7957
7991
  try {
7958
7992
  const q = cfg.guestQuota;
7959
- if (q && q.exhausted) {
7960
- return `Guest allowance used up (${q.limit} tool calls). Create an account at https://auth.solongate.com to carry on. Your project, its policy and everything recorded carry over, and this device keeps working. To stop guarding this machine instead: solongate, Settings, guard, then press d.`;
7993
+ if (q) {
7994
+ const tally = quotaTally(q.used ?? 0);
7995
+ const spent = (q.used ?? 0) + tally.n;
7996
+ if (q.exhausted || spent >= q.limit) {
7997
+ return `Guest allowance used up (${q.limit} tool calls). Create an account at https://auth.solongate.com to carry on. Your project, its policy and everything recorded carry over, and this device keeps working. To stop guarding this machine instead: solongate, Settings, guard, then press d.`;
7998
+ }
7999
+ tally.add();
7961
8000
  }
7962
8001
  if (cfg.dlpBlock) {
7963
8002
  const hit = dlpScan(args, cfg.dlpBlock);
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 = 69;
35
+ const HOOK_VERSION = 70;
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.
@@ -1761,6 +1761,40 @@ function rateLimitCheck(agentKey, limits) {
1761
1761
  }
1762
1762
  }
1763
1763
 
1764
+ // The allowance figure the API sends is built from audit rows, and those are
1765
+ // written AFTER a call runs. Under a burst of parallel tool calls every hook
1766
+ // therefore reads the same pre-burst figure, and the allowance overshoots by
1767
+ // roughly a batch before the count catches up.
1768
+ //
1769
+ // The guard is the thing making the calls, so it can close that gap without
1770
+ // another round trip: it tallies what it has already let through since the
1771
+ // server's figure last moved, and adds it in. One byte is appended per call and
1772
+ // the file's LENGTH is the count, because many hooks run at once and a
1773
+ // read-modify-write would lose increments; an O_APPEND write of a single byte
1774
+ // does not interleave.
1775
+ function quotaTally(used) {
1776
+ const dir = resolve(homedir(), '.solongate');
1777
+ const markFile = join(dir, '.quota-mark');
1778
+ const tallyFile = join(dir, '.quota-tally');
1779
+ let n = 0;
1780
+ try {
1781
+ let mark = null;
1782
+ if (existsSync(markFile)) { try { mark = readFileSync(markFile, 'utf-8').trim(); } catch {} }
1783
+ // A figure that has moved means the audit rows caught up and absorbed the
1784
+ // local tally, so it starts again from there.
1785
+ if (mark !== String(used)) {
1786
+ writeFileSync(markFile, String(used));
1787
+ writeFileSync(tallyFile, '');
1788
+ } else if (existsSync(tallyFile)) {
1789
+ try { n = statSync(tallyFile).size; } catch {}
1790
+ }
1791
+ } catch { /* a missing tally only costs accuracy near the edge, never the gate */ }
1792
+ return {
1793
+ n,
1794
+ add() { try { appendFileSync(tallyFile, '.'); } catch {} },
1795
+ };
1796
+ }
1797
+
1764
1798
  // Runs all enabled enforcement layers; returns a deny reason or null (allow).
1765
1799
  function securityLayerCheck(toolName, args, cfg, agentKey) {
1766
1800
  if (!cfg) return null;
@@ -1771,8 +1805,17 @@ function securityLayerCheck(toolName, args, cfg, agentKey) {
1771
1805
  // that is stopped and told why. Checked first, so no other layer can let a
1772
1806
  // call through after the allowance is gone.
1773
1807
  const q = cfg.guestQuota;
1774
- if (q && q.exhausted) {
1775
- return `Guest allowance used up (${q.limit} tool calls). Create an account at https://auth.solongate.com to carry on. Your project, its policy and everything recorded carry over, and this device keeps working. To stop guarding this machine instead: solongate, Settings, guard, then press d.`;
1808
+ if (q) {
1809
+ // The server's figure plus what this device has run since, so a burst
1810
+ // cannot spend past the allowance while the audit rows are still in flight.
1811
+ const tally = quotaTally(q.used ?? 0);
1812
+ const spent = (q.used ?? 0) + tally.n;
1813
+ if (q.exhausted || spent >= q.limit) {
1814
+ return `Guest allowance used up (${q.limit} tool calls). Create an account at https://auth.solongate.com to carry on. Your project, its policy and everything recorded carry over, and this device keeps working. To stop guarding this machine instead: solongate, Settings, guard, then press d.`;
1815
+ }
1816
+ // Counted here rather than after the verdict, because a call the policy
1817
+ // goes on to deny is still recorded and still spends the allowance.
1818
+ tally.add();
1776
1819
  }
1777
1820
  if (cfg.dlpBlock) {
1778
1821
  const hit = dlpScan(args, cfg.dlpBlock);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.1",
3
+ "version": "0.83.2",
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": {