@solongate/proxy 0.83.1 → 0.83.3

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/dist/index.js CHANGED
@@ -6923,12 +6923,18 @@ var init_global_install = __esm({
6923
6923
  // src/self-update.ts
6924
6924
  var self_update_exports = {};
6925
6925
  __export(self_update_exports, {
6926
+ adminInstallCommand: () => adminInstallCommand,
6927
+ autoUpdateEnabled: () => autoUpdateEnabled,
6928
+ autoUpdateForcedByEnv: () => autoUpdateForcedByEnv,
6926
6929
  currentVersion: () => currentVersion,
6927
6930
  latestVersion: () => latestVersion,
6928
6931
  maybeSelfUpdate: () => maybeSelfUpdate,
6929
6932
  newerThan: () => newerThan,
6933
+ runAutoUpdateCommand: () => runAutoUpdateCommand,
6930
6934
  runUpdateCommand: () => runUpdateCommand,
6931
- tuiUpdateFlow: () => tuiUpdateFlow
6935
+ setAutoUpdate: () => setAutoUpdate,
6936
+ tuiUpdateFlow: () => tuiUpdateFlow,
6937
+ updateNow: () => updateNow
6932
6938
  });
6933
6939
  import { execFile, execFileSync as execFileSync3, spawn } from "child_process";
6934
6940
  import { mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
@@ -6950,6 +6956,20 @@ function writeState(s) {
6950
6956
  } catch {
6951
6957
  }
6952
6958
  }
6959
+ function autoUpdateEnabled() {
6960
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
6961
+ if (env === "1" || env === "true" || env === "on" || env === "yes") return true;
6962
+ if (env === "0" || env === "false" || env === "off" || env === "no") return false;
6963
+ return readState().auto === true;
6964
+ }
6965
+ function autoUpdateForcedByEnv() {
6966
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
6967
+ return ["1", "true", "on", "yes", "0", "false", "off", "no"].includes(env);
6968
+ }
6969
+ function setAutoUpdate(on) {
6970
+ const s = readState();
6971
+ writeState(on ? { ...s, auto: true, needsAdmin: void 0 } : { ...s, auto: false });
6972
+ }
6953
6973
  function currentVersion() {
6954
6974
  try {
6955
6975
  const pkg = JSON.parse(readFileSync5(join5(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf-8"));
@@ -7015,11 +7035,18 @@ async function runUpdateCommand() {
7015
7035
  }
7016
7036
  if (newerThan(latest, cur)) {
7017
7037
  out2(` updating ${cur} -> ${latest} ...`);
7018
- const ok = await runGlobalInstall2(latest);
7019
- if (!ok) {
7020
- out2(" update failed. Try: npm i -g @solongate/proxy@latest");
7038
+ const r = await runGlobalInstall2(latest);
7039
+ if (!r.ok) {
7040
+ if (r.needsAdmin) {
7041
+ writeState({ ...readState(), needsAdmin: latest });
7042
+ out2(` npm could not write to the global folder (it needs admin rights).`);
7043
+ out2(` Run: ${adminInstallCommand()}`);
7044
+ } else {
7045
+ out2(" update failed. Try: npm i -g @solongate/proxy@latest");
7046
+ }
7021
7047
  return 1;
7022
7048
  }
7049
+ writeState({ ...readState(), needsAdmin: void 0 });
7023
7050
  out2(` \u2713 updated to ${latest} (open a new session to use it)`);
7024
7051
  } else {
7025
7052
  out2(` \u2713 already up to date`);
@@ -7049,21 +7076,61 @@ function runGlobalInstall2(version) {
7049
7076
  ["install", "-g", `${PKG}@${version}`],
7050
7077
  { timeout: 3e5, windowsHide: true, shell: process.platform === "win32" },
7051
7078
  (err2, stdout, stderr) => {
7079
+ const output = `${stdout}
7080
+ ${stderr}`;
7052
7081
  try {
7053
7082
  writeFileSync4(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err2 ? "FAILED" : "ok"}
7054
- ${stdout}
7055
- ${stderr}
7083
+ ${output}
7056
7084
  `, { flag: "a" });
7057
7085
  } catch {
7058
7086
  }
7059
- resolve6(!err2);
7087
+ resolve6({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
7060
7088
  }
7061
7089
  );
7062
7090
  } catch {
7063
- resolve6(false);
7091
+ resolve6({ ok: false, needsAdmin: false });
7064
7092
  }
7065
7093
  });
7066
7094
  }
7095
+ function adminInstallCommand() {
7096
+ return process.platform === "win32" ? `npm i -g ${PKG}@latest (in an Administrator terminal)` : `sudo npm i -g ${PKG}@latest`;
7097
+ }
7098
+ async function updateNow() {
7099
+ const cur = currentVersion();
7100
+ const latest = await fetchLatest();
7101
+ if (!latest) return { status: "unreachable", version: cur };
7102
+ if (!newerThan(latest, cur)) return { status: "current", version: cur };
7103
+ const r = await runGlobalInstall2(latest);
7104
+ if (r.ok) {
7105
+ writeState({ ...readState(), installed: latest, needsAdmin: void 0 });
7106
+ return { status: "updated", version: latest };
7107
+ }
7108
+ if (r.needsAdmin) {
7109
+ writeState({ ...readState(), needsAdmin: latest });
7110
+ return { status: "needs-admin", version: latest };
7111
+ }
7112
+ return { status: "failed", version: latest };
7113
+ }
7114
+ function runAutoUpdateCommand(arg) {
7115
+ const out2 = (s) => void process.stderr.write(s + "\n");
7116
+ const envForced = autoUpdateForcedByEnv();
7117
+ if (arg === void 0) {
7118
+ out2(` auto-update is ${autoUpdateEnabled() ? "on" : "off"}${envForced ? " (forced by SOLONGATE_AUTO_UPDATE)" : ""}`);
7119
+ out2(" change it with: solongate update auto on|off");
7120
+ return 0;
7121
+ }
7122
+ const v = arg.trim().toLowerCase();
7123
+ const on = ["on", "1", "true", "yes", "enable", "enabled"].includes(v);
7124
+ const off = ["off", "0", "false", "no", "disable", "disabled"].includes(v);
7125
+ if (!on && !off) {
7126
+ out2(` unknown value "${arg}" \u2014 use: solongate update auto on|off`);
7127
+ return 1;
7128
+ }
7129
+ setAutoUpdate(on);
7130
+ out2(on ? " \u2713 auto-update on \u2014 new versions install in the background" : " \u2713 auto-update off \u2014 update with: solongate update");
7131
+ if (envForced) out2(` note: SOLONGATE_AUTO_UPDATE is set and overrides this while it stays set`);
7132
+ return 0;
7133
+ }
7067
7134
  async function tuiUpdateFlow(onStatus) {
7068
7135
  try {
7069
7136
  const current = currentVersion();
@@ -7075,10 +7142,22 @@ async function tuiUpdateFlow(onStatus) {
7075
7142
  onStatus({ kind: "updated", version: latest });
7076
7143
  return;
7077
7144
  }
7145
+ if (!autoUpdateEnabled()) {
7146
+ onStatus({ kind: "available", version: latest });
7147
+ return;
7148
+ }
7149
+ if (readState().needsAdmin === latest) {
7150
+ onStatus({ kind: "needs-admin", version: latest });
7151
+ return;
7152
+ }
7078
7153
  onStatus({ kind: "updating", version: latest });
7079
- if (await runGlobalInstall2(latest)) {
7080
- writeState({ ...readState(), installed: latest });
7154
+ const r = await runGlobalInstall2(latest);
7155
+ if (r.ok) {
7156
+ writeState({ ...readState(), installed: latest, needsAdmin: void 0 });
7081
7157
  onStatus({ kind: "updated", version: latest });
7158
+ } else if (r.needsAdmin) {
7159
+ writeState({ ...readState(), needsAdmin: latest });
7160
+ onStatus({ kind: "needs-admin", version: latest });
7082
7161
  }
7083
7162
  } catch {
7084
7163
  }
@@ -7097,17 +7176,20 @@ function maybeSelfUpdate(notify = (l) => process.stderr.write(l + "\n")) {
7097
7176
  }
7098
7177
  if (!latest || !newerThan(latest, current)) return;
7099
7178
  const attempts = readState().attempts ?? {};
7100
- if (now - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS) {
7101
- writeState({ ...readState(), attempts: { [latest]: now } });
7102
- if (spawnGlobalInstall(latest)) {
7103
- notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 updating in the background, next run uses it${c.reset}`);
7104
- }
7179
+ if (now - (attempts[latest] ?? 0) < ATTEMPT_EVERY_MS) return;
7180
+ writeState({ ...readState(), attempts: { [latest]: now } });
7181
+ if (!autoUpdateEnabled()) {
7182
+ notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 run ${c.reset}${c.cyan}solongate update${c.reset}`);
7183
+ return;
7184
+ }
7185
+ if (spawnGlobalInstall(latest)) {
7186
+ notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 updating in the background, next run uses it${c.reset}`);
7105
7187
  }
7106
7188
  } catch {
7107
7189
  }
7108
7190
  })();
7109
7191
  }
7110
- var PKG, CHECK_EVERY_MS, ATTEMPT_EVERY_MS, STATE_FILE, LOG_FILE;
7192
+ var PKG, CHECK_EVERY_MS, ATTEMPT_EVERY_MS, STATE_FILE, LOG_FILE, NEEDS_ADMIN_RE;
7111
7193
  var init_self_update = __esm({
7112
7194
  "src/self-update.ts"() {
7113
7195
  "use strict";
@@ -7117,6 +7199,7 @@ var init_self_update = __esm({
7117
7199
  ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
7118
7200
  STATE_FILE = join5(homedir3(), ".solongate", ".self-update.json");
7119
7201
  LOG_FILE = join5(homedir3(), ".solongate", "self-update.log");
7202
+ NEEDS_ADMIN_RE = /\bEACCES\b|\bEPERM\b|permission denied|operation not permitted/i;
7120
7203
  }
7121
7204
  });
7122
7205
 
@@ -11458,6 +11541,8 @@ function SettingsPanel({
11458
11541
  const [editor, setEditor] = useState8(null);
11459
11542
  const [latest, setLatest] = useState8(null);
11460
11543
  const [diagBusy, setDiagBusy] = useState8(null);
11544
+ const [autoUp, setAutoUp] = useState8(() => autoUpdateEnabled());
11545
+ const [updBusy, setUpdBusy] = useState8(false);
11461
11546
  const [diag, setDiag] = useState8(null);
11462
11547
  const [diagStep, setDiagStep] = useState8(0);
11463
11548
  const DIAG_STEPS = {
@@ -11515,10 +11600,10 @@ function SettingsPanel({
11515
11600
  }, []);
11516
11601
  useEffect7(() => {
11517
11602
  const loggingIn = login && login.phase !== "done" && login.phase !== "error";
11518
- if (!loggingIn && !diagBusy) return;
11603
+ if (!loggingIn && !diagBusy && !updBusy) return;
11519
11604
  const t = setInterval(() => setTick((n) => n + 1), 120);
11520
11605
  return () => clearInterval(t);
11521
- }, [login, diagBusy]);
11606
+ }, [login, diagBusy, updBusy]);
11522
11607
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
11523
11608
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
11524
11609
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
@@ -11552,6 +11637,8 @@ function SettingsPanel({
11552
11637
  { kind: "self" },
11553
11638
  { kind: "doctor" },
11554
11639
  { kind: "repair" },
11640
+ { kind: "cli-update" },
11641
+ { kind: "auto-update" },
11555
11642
  { kind: "ll-enabled" },
11556
11643
  { kind: "ll-path" },
11557
11644
  { kind: "ll-server" },
@@ -11731,6 +11818,34 @@ function SettingsPanel({
11731
11818
  } else if (r.kind === "self") {
11732
11819
  if (!selfProt) return;
11733
11820
  run10(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
11821
+ } else if (r.kind === "cli-update") {
11822
+ if (updBusy) return;
11823
+ setUpdBusy(true);
11824
+ setMsg({ text: "updating\u2026", level: "ok" });
11825
+ void (async () => {
11826
+ try {
11827
+ const res = await updateNow();
11828
+ if (res.status === "updated") setMsg({ text: `\u2713 v${res.version} installed \xB7 restart (q, then solongate) to apply`, level: "ok" });
11829
+ else if (res.status === "current") setMsg({ text: `\u2713 already on the latest version (v${res.version})`, level: "ok" });
11830
+ else if (res.status === "needs-admin") setMsg({ text: `\u2717 npm needs admin rights here \u2014 run: ${adminInstallCommand()}`, level: "bad" });
11831
+ else if (res.status === "unreachable") setMsg({ text: "\u2717 could not reach the npm registry", level: "bad" });
11832
+ else setMsg({ text: `\u2717 update to v${res.version} failed \u2014 see ~/.solongate/self-update.log`, level: "bad" });
11833
+ setLatest(await latestVersion());
11834
+ } finally {
11835
+ setUpdBusy(false);
11836
+ }
11837
+ })();
11838
+ } else if (r.kind === "auto-update") {
11839
+ if (autoUpdateForcedByEnv()) {
11840
+ setMsg({ text: "SOLONGATE_AUTO_UPDATE is set \u2014 unset it to change this here", level: "bad" });
11841
+ return;
11842
+ }
11843
+ const next = !autoUp;
11844
+ setAutoUpdate(next);
11845
+ setAutoUp(next);
11846
+ setMsg(
11847
+ next ? { text: "\u2713 auto-update on \u2014 new versions install in the background", level: "ok" } : { text: "\u2713 auto-update off \u2014 update from this row or with: solongate update", level: "ok" }
11848
+ );
11734
11849
  } else if (r.kind === "ll-enabled") {
11735
11850
  if (!local) return;
11736
11851
  if (!local.enabled && !local.path.trim()) {
@@ -11829,7 +11944,7 @@ function SettingsPanel({
11829
11944
  if (ok) clearLocalLog();
11830
11945
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
11831
11946
  refreshAccounts();
11832
- } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
11947
+ } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "auto-update" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
11833
11948
  if (cur.kind === "alert") run10(cur.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(cur.rule.id, !cur.rule.enabled), alertQ.reload);
11834
11949
  else activate(cur);
11835
11950
  }
@@ -12041,6 +12156,22 @@ function SettingsPanel({
12041
12156
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "repair".padEnd(11) }),
12042
12157
  diagBusy === "repair" ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: `${spin} restoring protection\u2026` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "restore every protection file after tampering or deletion \xB7 enter runs it" })
12043
12158
  ] });
12159
+ case "cli-update": {
12160
+ const behind = latest ? newerThan(latest, ver) : false;
12161
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
12162
+ cursor(r),
12163
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "version".padEnd(11) }),
12164
+ /* @__PURE__ */ jsx8(Text8, { color: behind ? theme.warn : theme.ok, children: `v${ver}` }),
12165
+ updBusy ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: ` ${spin} updating\u2026` }) : behind ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` v${latest} on npm \xB7 enter updates now` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: latest ? " latest \xB7 enter checks again" : " enter checks npm and updates" })
12166
+ ] });
12167
+ }
12168
+ case "auto-update":
12169
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
12170
+ cursor(r),
12171
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "auto".padEnd(11) }),
12172
+ onOff(autoUp),
12173
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: autoUpdateForcedByEnv() ? " set by SOLONGATE_AUTO_UPDATE" : autoUp ? " installs new versions in the background \xB7 enter toggles" : " off: nothing installs on its own (npm -g may need sudo) \xB7 enter toggles" })
12174
+ ] });
12044
12175
  case "ll-enabled":
12045
12176
  return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
12046
12177
  cursor(r),
@@ -12093,10 +12224,11 @@ function SettingsPanel({
12093
12224
  ] });
12094
12225
  }
12095
12226
  };
12096
- const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
12227
+ const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "cli-update" || r.kind === "auto-update" ? "UPDATES" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
12097
12228
  const SECTION_DESC = {
12098
12229
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
12099
12230
  PROTECTION: "guard hook: enter install/update \xB7 d remove \xB7 self-protection \xB7 doctor + repair",
12231
+ UPDATES: "the solongate CLI itself \xB7 background auto-update is off unless you turn it on",
12100
12232
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
12101
12233
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
12102
12234
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
@@ -12165,7 +12297,7 @@ function SettingsPanel({
12165
12297
  lineEls.push(
12166
12298
  /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", color: theme.dim, children: [
12167
12299
  `solongate v${ver}`,
12168
- latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 auto-updating` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
12300
+ latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 ${autoUp ? "auto-updating" : "UPDATES above"}` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
12169
12301
  ] }, "ver")
12170
12302
  );
12171
12303
  lineKey.push("");
@@ -12394,7 +12526,7 @@ function App() {
12394
12526
  // a row off whichever panel is open.
12395
12527
  /* @__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
12528
  ) : 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
12529
+ 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` }) : update2.kind === "available" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} out \xB7 Settings \u2192 UPDATES` }) : update2.kind === "needs-admin" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} needs admin rights \xB7 Settings \u2192 UPDATES` }) : null
12398
12530
  ] }),
12399
12531
  /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
12400
12532
  /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexShrink: 0, width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
@@ -17026,6 +17158,7 @@ function printHelp() {
17026
17158
  head("Setup & status");
17027
17159
  cmd("solongate", "open the dataroom UI (login, policies, audit, settings)");
17028
17160
  cmd("update", "update SolonGate to the newest version and refresh the guard");
17161
+ cmd("update auto on|off", "background auto-update (default off \u2014 on macOS npm -g often needs sudo)");
17029
17162
  cmd("repair", "restore the guard + hook + settings files if they were deleted or disarmed");
17030
17163
  cmd("doctor", "health check: login, policy, guard, local logs");
17031
17164
  cmd("doctor --json", "the same health check as machine-readable JSON");
@@ -17173,6 +17306,10 @@ async function main() {
17173
17306
  return;
17174
17307
  }
17175
17308
  if (subcommand === "update") {
17309
+ if (process.argv[3] === "auto") {
17310
+ const { runAutoUpdateCommand: runAutoUpdateCommand2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
17311
+ process.exit(runAutoUpdateCommand2(process.argv[4]));
17312
+ }
17176
17313
  const { runUpdateCommand: runUpdateCommand2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
17177
17314
  process.exit(await runUpdateCommand2());
17178
17315
  }
@@ -1,30 +1,64 @@
1
+ /**
2
+ * Is the background updater turned on? Default: NO — see the file header.
3
+ * SOLONGATE_AUTO_UPDATE=on|off (1/0, true/false) overrides the stored setting
4
+ * for one run, so a CI image or a managed fleet can force either way without
5
+ * writing to the user's home directory.
6
+ */
7
+ export declare function autoUpdateEnabled(): boolean;
8
+ /** True when the env var is deciding, so the UI can say the row is overridden. */
9
+ export declare function autoUpdateForcedByEnv(): boolean;
10
+ /** Turn the background updater on/off. Clears the "npm refused" memo on enable. */
11
+ export declare function setAutoUpdate(on: boolean): void;
1
12
  export declare function currentVersion(): string;
2
13
  /** true when `b` is a strictly newer semver than `a` (numeric triples only). */
3
14
  export declare function newerThan(b: string, a: string): boolean;
4
15
  /** The latest version published to npm, or null if it can't be reached. */
5
16
  export declare function latestVersion(): Promise<string | null>;
6
- /** Foreground global install — resolves true on success, output → update log. */
7
17
  /**
8
- * `solongate update` — explicit, visible update instead of waiting for the
9
- * background auto-update: installs the newest CLI globally, then refreshes the
10
- * installed guard hooks. Returns a process exit code.
18
+ * `solongate update` — the normal way to update, and the ONLY way when the
19
+ * background updater is off (which is the default): installs the newest CLI
20
+ * globally, then refreshes the installed guard hooks. Returns a process exit
21
+ * code.
11
22
  */
12
23
  export declare function runUpdateCommand(): Promise<number>;
24
+ /** The exact command that fixes a root-owned global prefix. */
25
+ export declare function adminInstallCommand(): string;
26
+ /**
27
+ * Install the newest version NOW, whatever the auto-update setting says — the
28
+ * dataroom's UPDATES row and `solongate update` both run through here.
29
+ */
30
+ export declare function updateNow(): Promise<{
31
+ status: 'updated' | 'current' | 'needs-admin' | 'unreachable' | 'failed';
32
+ version: string;
33
+ }>;
34
+ /**
35
+ * `solongate update auto [on|off]` — show or change the background updater.
36
+ * Kept separate from `runUpdateCommand` so `solongate update` stays "update me
37
+ * now" and never changes a setting as a side effect.
38
+ */
39
+ export declare function runAutoUpdateCommand(arg?: string): number;
13
40
  /** What the dataroom shows about the updater. */
14
41
  export type UpdateStatus = {
15
42
  kind: 'idle';
43
+ } | {
44
+ kind: 'available';
45
+ version: string;
16
46
  } | {
17
47
  kind: 'updating';
18
48
  version: string;
19
49
  } | {
20
50
  kind: 'updated';
21
51
  version: string;
52
+ } | {
53
+ kind: 'needs-admin';
54
+ version: string;
22
55
  };
23
56
  /**
24
- * The dataroom's updater (Claude Code-style): check the registry EVERY time the
25
- * TUI opens (and periodically while it stays open), install the new version in
26
- * the background, then tell the user to restart. `onStatus` drives the in-app
27
- * status line; nothing is ever written to the terminal directly.
57
+ * The dataroom's updater: check the registry EVERY time the TUI opens (and
58
+ * periodically while it stays open). With auto-update ON, install in the
59
+ * background and ask for a restart; with it OFF (the default), only report that
60
+ * a version is available. `onStatus` drives the in-app status line; nothing is
61
+ * ever written to the terminal directly.
28
62
  */
29
63
  export declare function tuiUpdateFlow(onStatus: (s: UpdateStatus) => void): Promise<void>;
30
64
  /**
package/dist/tui/index.js CHANGED
@@ -4504,6 +4504,7 @@ var CHECK_EVERY_MS = 30 * 60 * 1e3;
4504
4504
  var ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
4505
4505
  var STATE_FILE2 = join9(homedir9(), ".solongate", ".self-update.json");
4506
4506
  var LOG_FILE2 = join9(homedir9(), ".solongate", "self-update.log");
4507
+ var NEEDS_ADMIN_RE = /\bEACCES\b|\bEPERM\b|permission denied|operation not permitted/i;
4507
4508
  function readState2() {
4508
4509
  try {
4509
4510
  const s = JSON.parse(readFileSync7(STATE_FILE2, "utf-8"));
@@ -4519,6 +4520,20 @@ function writeState2(s) {
4519
4520
  } catch {
4520
4521
  }
4521
4522
  }
4523
+ function autoUpdateEnabled() {
4524
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
4525
+ if (env === "1" || env === "true" || env === "on" || env === "yes") return true;
4526
+ if (env === "0" || env === "false" || env === "off" || env === "no") return false;
4527
+ return readState2().auto === true;
4528
+ }
4529
+ function autoUpdateForcedByEnv() {
4530
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
4531
+ return ["1", "true", "on", "yes", "0", "false", "off", "no"].includes(env);
4532
+ }
4533
+ function setAutoUpdate(on) {
4534
+ const s = readState2();
4535
+ writeState2(on ? { ...s, auto: true, needsAdmin: void 0 } : { ...s, auto: false });
4536
+ }
4522
4537
  function currentVersion() {
4523
4538
  try {
4524
4539
  const pkg = JSON.parse(readFileSync7(join9(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
@@ -4564,21 +4579,41 @@ function runGlobalInstall(version) {
4564
4579
  ["install", "-g", `${PKG}@${version}`],
4565
4580
  { timeout: 3e5, windowsHide: true, shell: process.platform === "win32" },
4566
4581
  (err2, stdout, stderr) => {
4582
+ const output = `${stdout}
4583
+ ${stderr}`;
4567
4584
  try {
4568
4585
  writeFileSync7(LOG_FILE2, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err2 ? "FAILED" : "ok"}
4569
- ${stdout}
4570
- ${stderr}
4586
+ ${output}
4571
4587
  `, { flag: "a" });
4572
4588
  } catch {
4573
4589
  }
4574
- resolve3(!err2);
4590
+ resolve3({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
4575
4591
  }
4576
4592
  );
4577
4593
  } catch {
4578
- resolve3(false);
4594
+ resolve3({ ok: false, needsAdmin: false });
4579
4595
  }
4580
4596
  });
4581
4597
  }
4598
+ function adminInstallCommand() {
4599
+ return process.platform === "win32" ? `npm i -g ${PKG}@latest (in an Administrator terminal)` : `sudo npm i -g ${PKG}@latest`;
4600
+ }
4601
+ async function updateNow() {
4602
+ const cur = currentVersion();
4603
+ const latest = await fetchLatest();
4604
+ if (!latest) return { status: "unreachable", version: cur };
4605
+ if (!newerThan(latest, cur)) return { status: "current", version: cur };
4606
+ const r = await runGlobalInstall(latest);
4607
+ if (r.ok) {
4608
+ writeState2({ ...readState2(), installed: latest, needsAdmin: void 0 });
4609
+ return { status: "updated", version: latest };
4610
+ }
4611
+ if (r.needsAdmin) {
4612
+ writeState2({ ...readState2(), needsAdmin: latest });
4613
+ return { status: "needs-admin", version: latest };
4614
+ }
4615
+ return { status: "failed", version: latest };
4616
+ }
4582
4617
  async function tuiUpdateFlow(onStatus) {
4583
4618
  try {
4584
4619
  const current = currentVersion();
@@ -4590,10 +4625,22 @@ async function tuiUpdateFlow(onStatus) {
4590
4625
  onStatus({ kind: "updated", version: latest });
4591
4626
  return;
4592
4627
  }
4628
+ if (!autoUpdateEnabled()) {
4629
+ onStatus({ kind: "available", version: latest });
4630
+ return;
4631
+ }
4632
+ if (readState2().needsAdmin === latest) {
4633
+ onStatus({ kind: "needs-admin", version: latest });
4634
+ return;
4635
+ }
4593
4636
  onStatus({ kind: "updating", version: latest });
4594
- if (await runGlobalInstall(latest)) {
4595
- writeState2({ ...readState2(), installed: latest });
4637
+ const r = await runGlobalInstall(latest);
4638
+ if (r.ok) {
4639
+ writeState2({ ...readState2(), installed: latest, needsAdmin: void 0 });
4596
4640
  onStatus({ kind: "updated", version: latest });
4641
+ } else if (r.needsAdmin) {
4642
+ writeState2({ ...readState2(), needsAdmin: latest });
4643
+ onStatus({ kind: "needs-admin", version: latest });
4597
4644
  }
4598
4645
  } catch {
4599
4646
  }
@@ -4654,6 +4701,8 @@ function SettingsPanel({
4654
4701
  const [editor, setEditor] = useState8(null);
4655
4702
  const [latest, setLatest] = useState8(null);
4656
4703
  const [diagBusy, setDiagBusy] = useState8(null);
4704
+ const [autoUp, setAutoUp] = useState8(() => autoUpdateEnabled());
4705
+ const [updBusy, setUpdBusy] = useState8(false);
4657
4706
  const [diag, setDiag] = useState8(null);
4658
4707
  const [diagStep, setDiagStep] = useState8(0);
4659
4708
  const DIAG_STEPS = {
@@ -4711,10 +4760,10 @@ function SettingsPanel({
4711
4760
  }, []);
4712
4761
  useEffect7(() => {
4713
4762
  const loggingIn = login && login.phase !== "done" && login.phase !== "error";
4714
- if (!loggingIn && !diagBusy) return;
4763
+ if (!loggingIn && !diagBusy && !updBusy) return;
4715
4764
  const t = setInterval(() => setTick((n) => n + 1), 120);
4716
4765
  return () => clearInterval(t);
4717
- }, [login, diagBusy]);
4766
+ }, [login, diagBusy, updBusy]);
4718
4767
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
4719
4768
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
4720
4769
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
@@ -4748,6 +4797,8 @@ function SettingsPanel({
4748
4797
  { kind: "self" },
4749
4798
  { kind: "doctor" },
4750
4799
  { kind: "repair" },
4800
+ { kind: "cli-update" },
4801
+ { kind: "auto-update" },
4751
4802
  { kind: "ll-enabled" },
4752
4803
  { kind: "ll-path" },
4753
4804
  { kind: "ll-server" },
@@ -4927,6 +4978,34 @@ function SettingsPanel({
4927
4978
  } else if (r.kind === "self") {
4928
4979
  if (!selfProt) return;
4929
4980
  run(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
4981
+ } else if (r.kind === "cli-update") {
4982
+ if (updBusy) return;
4983
+ setUpdBusy(true);
4984
+ setMsg({ text: "updating\u2026", level: "ok" });
4985
+ void (async () => {
4986
+ try {
4987
+ const res = await updateNow();
4988
+ if (res.status === "updated") setMsg({ text: `\u2713 v${res.version} installed \xB7 restart (q, then solongate) to apply`, level: "ok" });
4989
+ else if (res.status === "current") setMsg({ text: `\u2713 already on the latest version (v${res.version})`, level: "ok" });
4990
+ else if (res.status === "needs-admin") setMsg({ text: `\u2717 npm needs admin rights here \u2014 run: ${adminInstallCommand()}`, level: "bad" });
4991
+ else if (res.status === "unreachable") setMsg({ text: "\u2717 could not reach the npm registry", level: "bad" });
4992
+ else setMsg({ text: `\u2717 update to v${res.version} failed \u2014 see ~/.solongate/self-update.log`, level: "bad" });
4993
+ setLatest(await latestVersion());
4994
+ } finally {
4995
+ setUpdBusy(false);
4996
+ }
4997
+ })();
4998
+ } else if (r.kind === "auto-update") {
4999
+ if (autoUpdateForcedByEnv()) {
5000
+ setMsg({ text: "SOLONGATE_AUTO_UPDATE is set \u2014 unset it to change this here", level: "bad" });
5001
+ return;
5002
+ }
5003
+ const next = !autoUp;
5004
+ setAutoUpdate(next);
5005
+ setAutoUp(next);
5006
+ setMsg(
5007
+ next ? { text: "\u2713 auto-update on \u2014 new versions install in the background", level: "ok" } : { text: "\u2713 auto-update off \u2014 update from this row or with: solongate update", level: "ok" }
5008
+ );
4930
5009
  } else if (r.kind === "ll-enabled") {
4931
5010
  if (!local) return;
4932
5011
  if (!local.enabled && !local.path.trim()) {
@@ -5025,7 +5104,7 @@ function SettingsPanel({
5025
5104
  if (ok) clearLocalLog();
5026
5105
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
5027
5106
  refreshAccounts();
5028
- } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
5107
+ } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "auto-update" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
5029
5108
  if (cur.kind === "alert") run(cur.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(cur.rule.id, !cur.rule.enabled), alertQ.reload);
5030
5109
  else activate(cur);
5031
5110
  }
@@ -5237,6 +5316,22 @@ function SettingsPanel({
5237
5316
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "repair".padEnd(11) }),
5238
5317
  diagBusy === "repair" ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: `${spin} restoring protection\u2026` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "restore every protection file after tampering or deletion \xB7 enter runs it" })
5239
5318
  ] });
5319
+ case "cli-update": {
5320
+ const behind = latest ? newerThan(latest, ver) : false;
5321
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5322
+ cursor(r),
5323
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "version".padEnd(11) }),
5324
+ /* @__PURE__ */ jsx8(Text8, { color: behind ? theme.warn : theme.ok, children: `v${ver}` }),
5325
+ updBusy ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: ` ${spin} updating\u2026` }) : behind ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` v${latest} on npm \xB7 enter updates now` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: latest ? " latest \xB7 enter checks again" : " enter checks npm and updates" })
5326
+ ] });
5327
+ }
5328
+ case "auto-update":
5329
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5330
+ cursor(r),
5331
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "auto".padEnd(11) }),
5332
+ onOff(autoUp),
5333
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: autoUpdateForcedByEnv() ? " set by SOLONGATE_AUTO_UPDATE" : autoUp ? " installs new versions in the background \xB7 enter toggles" : " off: nothing installs on its own (npm -g may need sudo) \xB7 enter toggles" })
5334
+ ] });
5240
5335
  case "ll-enabled":
5241
5336
  return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5242
5337
  cursor(r),
@@ -5289,10 +5384,11 @@ function SettingsPanel({
5289
5384
  ] });
5290
5385
  }
5291
5386
  };
5292
- const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
5387
+ const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "cli-update" || r.kind === "auto-update" ? "UPDATES" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
5293
5388
  const SECTION_DESC = {
5294
5389
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
5295
5390
  PROTECTION: "guard hook: enter install/update \xB7 d remove \xB7 self-protection \xB7 doctor + repair",
5391
+ UPDATES: "the solongate CLI itself \xB7 background auto-update is off unless you turn it on",
5296
5392
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
5297
5393
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
5298
5394
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
@@ -5361,7 +5457,7 @@ function SettingsPanel({
5361
5457
  lineEls.push(
5362
5458
  /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", color: theme.dim, children: [
5363
5459
  `solongate v${ver}`,
5364
- latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 auto-updating` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
5460
+ latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 ${autoUp ? "auto-updating" : "UPDATES above"}` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
5365
5461
  ] }, "ver")
5366
5462
  );
5367
5463
  lineKey.push("");
@@ -5566,7 +5662,7 @@ function App() {
5566
5662
  // a row off whichever panel is open.
5567
5663
  /* @__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
5664
  ) : 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
5665
+ 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` }) : update2.kind === "available" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} out \xB7 Settings \u2192 UPDATES` }) : update2.kind === "needs-admin" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} needs admin rights \xB7 Settings \u2192 UPDATES` }) : null
5570
5666
  ] }),
5571
5667
  /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
5572
5668
  /* @__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.1",
3
+ "version": "0.83.3",
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": {