@solongate/proxy 0.83.8 → 0.83.10

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 CHANGED
@@ -11,7 +11,7 @@ import { homedir } from 'node:os';
11
11
  // Bump on every audit hook change. The cloud serves the newest version; the guard
12
12
  // hook installs it on its next run (no re-login needed). See guard.mjs
13
13
  // fetchAndInstallHook / maybeSelfUpdate.
14
- const HOOK_VERSION = 24;
14
+ const HOOK_VERSION = 26;
15
15
 
16
16
  function loadEnvKey(dir) {
17
17
  try {
@@ -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 = 73;
6539
+ var HOOK_VERSION = 75;
6540
6540
  function localLogsOnly(security) {
6541
6541
  if (security !== void 0) {
6542
6542
  const l = security && security.localLogs;
@@ -8461,7 +8461,7 @@ if (!REFRESH_MODE) {
8461
8461
  try {
8462
8462
  if (existsSync(policyCacheFile)) {
8463
8463
  const cached = JSON.parse(readFileSync(policyCacheFile, "utf-8"));
8464
- if (cached && cached.policy)
8464
+ if (cached)
8465
8465
  staleCache = cached;
8466
8466
  if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
8467
8467
  refreshDue = false;
@@ -8566,15 +8566,12 @@ if (!REFRESH_MODE) {
8566
8566
  });
8567
8567
  } catch {
8568
8568
  }
8569
- await maybeSelfUpdate();
8570
8569
  blockTool(ghostHit, true);
8571
8570
  }
8572
8571
  if (toolName === "Bash" || toolName === "run_command" || guessPermission(toolName) === "EXECUTE") {
8573
8572
  const rw = ghostListingRewrite(args, securityCfg.ghost);
8574
- if (rw) {
8575
- await maybeSelfUpdate();
8573
+ if (rw)
8576
8574
  rewriteTool({ command: rw });
8577
- }
8578
8575
  }
8579
8576
  }
8580
8577
  if (!reason)
@@ -8608,7 +8605,6 @@ if (!REFRESH_MODE) {
8608
8605
  reason = "Security layer (DLP): reading a file that contains a secret is blocked. Blocked by SolonGate.";
8609
8606
  opaRoute = "black";
8610
8607
  } else if (plan && plan.rewrite) {
8611
- await maybeSelfUpdate();
8612
8608
  rewriteTool(plan.rewrite);
8613
8609
  }
8614
8610
  }
@@ -8659,7 +8655,6 @@ if (!REFRESH_MODE) {
8659
8655
  }
8660
8656
  }
8661
8657
  writeDenyFlag(toolName);
8662
- await maybeSelfUpdate();
8663
8658
  blockTool(reason);
8664
8659
  }
8665
8660
  } catch {
package/hooks/guard.mjs CHANGED
@@ -32,10 +32,11 @@ 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 = 73;
35
+ const HOOK_VERSION = 75;
36
36
 
37
37
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
38
- // nothing is sent to the cloud audit log.
38
+ // nothing is sent to the cloud audit log — local and cloud are one or the
39
+ // other, never both.
39
40
  //
40
41
  // `undefined` is the ONLY value that means "we do not know yet". `null` is an
41
42
  // answer: it is what the refresh writes when the API returns no security block,
@@ -125,14 +126,13 @@ function writeLocalLog(security, entry) {
125
126
  const mark = accountMark();
126
127
  if (mark) entry = { ...entry, acct: mark };
127
128
  const l = security && security.localLogs;
128
- // The cloud POST is skipped whenever localLogsOnly() says this device is in
129
- // local-only mode and that answer can come from the persisted marker alone,
130
- // i.e. WITHOUT a resolved config (empty/cold policy cache, or a refresh that
131
- // failed while the key was being rotated). In that state this function used
132
- // to return early: no cloud write, no local write, entry gone. A denial must
133
- // never disappear, so fall back to the per-device default folder and keep it.
129
+ // No usable folder in the resolved config. The answer can still be "local is
130
+ // on" from the persisted marker alone, i.e. WITHOUT a resolved config (empty
131
+ // or cold policy cache, or a refresh that failed while the key was being
132
+ // rotated) in which case keep the copy in the per-device default folder
133
+ // rather than dropping it.
134
134
  if (!l || !l.enabled || typeof l.path !== 'string' || !l.path.trim()) {
135
- if (!localLogsOnly(security)) return; // cloud is handling it nothing to do
135
+ if (!localLogsOnly(security)) return; // local logging is offcloud only
136
136
  const fallbackDir = resolve(homedir(), '.solongate', 'local-logs');
137
137
  const fallbackLine = JSON.stringify(entry) + '\n';
138
138
  const fallbackPayload = Buffer.from(JSON.stringify({ dir: fallbackDir, line: fallbackLine }), 'utf-8').toString('base64');
@@ -2484,7 +2484,13 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2484
2484
  try {
2485
2485
  if (existsSync(policyCacheFile)) {
2486
2486
  const cached = JSON.parse(readFileSync(policyCacheFile, 'utf-8'));
2487
- if (cached && cached.policy) staleCache = cached;
2487
+ // Kept whenever it parses, NOT only when it carries a policy. The
2488
+ // security layers (DLP, rate limit, ghost) are configured separately
2489
+ // from the policy, so gating the last-known-good on a policy existing
2490
+ // meant a project with DLP on and no policy lost DLP entirely the
2491
+ // moment this cache went past its 10s TTL — which is almost always,
2492
+ // since it only refreshes on activity.
2493
+ if (cached) staleCache = cached;
2488
2494
  if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
2489
2495
  refreshDue = false;
2490
2496
  if (cached.policy) dashboardPolicy = cached.policy;
@@ -2585,7 +2591,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2585
2591
  signal: AbortSignal.timeout(3000),
2586
2592
  });
2587
2593
  } catch {}
2588
- await maybeSelfUpdate();
2594
+ // NOT maybeSelfUpdate() first: see the deny at the end of this flow.
2589
2595
  // Stealth deny: ghostHit is a bare "No such file or directory", and the
2590
2596
  // stealth flag keeps the adapter from adding any SolonGate branding, so
2591
2597
  // the path looks like it simply is not there on every client.
@@ -2597,7 +2603,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2597
2603
  // PreToolUse `overwrite` replaces the command before it runs (hooks.md).
2598
2604
  if (toolName === 'Bash' || toolName === 'run_command' || guessPermission(toolName) === 'EXECUTE') {
2599
2605
  const rw = ghostListingRewrite(args, securityCfg.ghost);
2600
- if (rw) { await maybeSelfUpdate(); rewriteTool({ command: rw }); }
2606
+ if (rw) rewriteTool({ command: rw }); // verdict first, update later
2601
2607
  }
2602
2608
  }
2603
2609
  // Extra security layers run after tamper, before policy. Block reason wins
@@ -2667,8 +2673,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2667
2673
  reason = 'Security layer (DLP): reading a file that contains a secret is blocked. Blocked by SolonGate.';
2668
2674
  opaRoute = 'black';
2669
2675
  } else if (plan && plan.rewrite) {
2670
- await maybeSelfUpdate();
2671
- rewriteTool(plan.rewrite);
2676
+ rewriteTool(plan.rewrite); // verdict first, update later
2672
2677
  }
2673
2678
  }
2674
2679
 
@@ -2723,7 +2728,13 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2723
2728
  } catch {}
2724
2729
  }
2725
2730
  writeDenyFlag(toolName);
2726
- await maybeSelfUpdate();
2731
+ // The verdict is emitted BEFORE the hook self-update, never after.
2732
+ // maybeSelfUpdate() fetches three hooks at up to 5s each, and the
2733
+ // whole process carries an 8s backstop that force-exits with
2734
+ // `process.exitCode || 0` — i.e. ALLOW. So a slow network on the update
2735
+ // path turned a decided DENY into a permitted call. Measured: 8043ms,
2736
+ // exit 0, on a call DLP had already refused. Updating is a background
2737
+ // nicety; it still runs on every allow, which is nearly every call.
2727
2738
  blockTool(reason);
2728
2739
  }
2729
2740
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.8",
3
+ "version": "0.83.10",
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": {