@solongate/proxy 0.83.0 → 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.
@@ -8,5 +8,13 @@ export interface Me {
8
8
  name?: string;
9
9
  slug?: string;
10
10
  } | null;
11
+ /**
12
+ * Present only for a guest account, and carrying only whether the allowance is
13
+ * spent. The size of it and how much is left are the operator's numbers and
14
+ * are not sent here.
15
+ */
16
+ guest?: {
17
+ exhausted?: boolean;
18
+ } | null;
11
19
  }
12
20
  export declare function me(): Promise<Me>;
package/dist/index.js CHANGED
@@ -12288,6 +12288,21 @@ function App() {
12288
12288
  const rawEmail = cur?.email ?? "";
12289
12289
  const isGuestAccount2 = rawEmail.endsWith("@anonymous.solongate.invalid");
12290
12290
  const acctLabel2 = !cur ? "not logged in" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
12291
+ const [guestSpent, setGuestSpent] = useState9(false);
12292
+ useEffect8(() => {
12293
+ if (!isGuestAccount2) {
12294
+ setGuestSpent(false);
12295
+ return;
12296
+ }
12297
+ let alive = true;
12298
+ api.auth.me().then((r) => {
12299
+ if (alive) setGuestSpent(r.guest?.exhausted === true);
12300
+ }).catch(() => {
12301
+ });
12302
+ return () => {
12303
+ alive = false;
12304
+ };
12305
+ }, [isGuestAccount2, cur?.apiKey]);
12291
12306
  useEffect8(() => {
12292
12307
  if (!cur || cur.email) return;
12293
12308
  let alive = true;
@@ -12373,8 +12388,13 @@ function App() {
12373
12388
  /* @__PURE__ */ jsxs9(Text9, { wrap: "truncate", children: [
12374
12389
  /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "account: " }),
12375
12390
  /* @__PURE__ */ jsx9(Text9, { color: locked ? theme.warn : theme.accentBright, bold: true, children: acctLabel2 }),
12376
- locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
12377
- update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \u2014 restart (q, then solongate) to apply` }) : null
12391
+ locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : guestSpent ? (
12392
+ // Said on this line rather than on a new one: every panel budgets its
12393
+ // height against a fixed shell, so one more line up here silently clips
12394
+ // a row off whichever panel is open.
12395
+ /* @__PURE__ */ jsx9(Text9, { color: theme.bad, bold: true, children: " \xB7 allowance used up \xB7 every call denied \xB7 create an account to carry on" })
12396
+ ) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
12397
+ update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \xB7 restart (q, then solongate) to apply` }) : null
12378
12398
  ] }),
12379
12399
  /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
12380
12400
  /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexShrink: 0, width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
package/dist/tui/index.js CHANGED
@@ -5460,6 +5460,21 @@ function App() {
5460
5460
  const rawEmail = cur?.email ?? "";
5461
5461
  const isGuestAccount2 = rawEmail.endsWith("@anonymous.solongate.invalid");
5462
5462
  const acctLabel2 = !cur ? "not logged in" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
5463
+ const [guestSpent, setGuestSpent] = useState9(false);
5464
+ useEffect8(() => {
5465
+ if (!isGuestAccount2) {
5466
+ setGuestSpent(false);
5467
+ return;
5468
+ }
5469
+ let alive = true;
5470
+ api.auth.me().then((r) => {
5471
+ if (alive) setGuestSpent(r.guest?.exhausted === true);
5472
+ }).catch(() => {
5473
+ });
5474
+ return () => {
5475
+ alive = false;
5476
+ };
5477
+ }, [isGuestAccount2, cur?.apiKey]);
5463
5478
  useEffect8(() => {
5464
5479
  if (!cur || cur.email) return;
5465
5480
  let alive = true;
@@ -5545,8 +5560,13 @@ function App() {
5545
5560
  /* @__PURE__ */ jsxs9(Text9, { wrap: "truncate", children: [
5546
5561
  /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "account: " }),
5547
5562
  /* @__PURE__ */ jsx9(Text9, { color: locked ? theme.warn : theme.accentBright, bold: true, children: acctLabel2 }),
5548
- locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
5549
- update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \u2014 restart (q, then solongate) to apply` }) : null
5563
+ locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : guestSpent ? (
5564
+ // Said on this line rather than on a new one: every panel budgets its
5565
+ // height against a fixed shell, so one more line up here silently clips
5566
+ // a row off whichever panel is open.
5567
+ /* @__PURE__ */ jsx9(Text9, { color: theme.bad, bold: true, children: " \xB7 allowance used up \xB7 every call denied \xB7 create an account to carry on" })
5568
+ ) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
5569
+ update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \xB7 restart (q, then solongate) to apply` }) : null
5550
5570
  ] }),
5551
5571
  /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
5552
5572
  /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexShrink: 0, width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
@@ -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.0",
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": {