@solongate/proxy 0.82.56 → 0.82.57

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 = 47;
6539
+ var HOOK_VERSION = 48;
6540
6540
  function localLogsOnly(security) {
6541
6541
  if (security && typeof security === "object") {
6542
6542
  const l = security.localLogs;
@@ -7161,6 +7161,73 @@ function extractTargetPaths(args) {
7161
7161
  }
7162
7162
  return out;
7163
7163
  }
7164
+ function ghostGlobToRe(glob) {
7165
+ let re = "";
7166
+ for (let i = 0; i < glob.length; i++) {
7167
+ const c = glob[i];
7168
+ if (c === "*") {
7169
+ if (glob[i + 1] === "*") {
7170
+ re += ".*";
7171
+ i++;
7172
+ } else
7173
+ re += "[^/]*";
7174
+ } else if (c === "?")
7175
+ re += "[^/]";
7176
+ else if ("\\^$.|+()[]{}".indexOf(c) !== -1)
7177
+ re += "\\" + c;
7178
+ else
7179
+ re += c;
7180
+ }
7181
+ try {
7182
+ return new RegExp("^" + re + "$");
7183
+ } catch {
7184
+ return null;
7185
+ }
7186
+ }
7187
+ function ghostPathMatch(p, patterns) {
7188
+ if (!p)
7189
+ return false;
7190
+ const norm = String(p).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
7191
+ if (!norm)
7192
+ return false;
7193
+ const base = norm.split("/").filter(Boolean).pop() || norm;
7194
+ for (let pat of patterns) {
7195
+ pat = String(pat || "").trim().toLowerCase();
7196
+ if (!pat)
7197
+ continue;
7198
+ const re = ghostGlobToRe(pat);
7199
+ if (re && (re.test(norm) || re.test(base)))
7200
+ return true;
7201
+ const patBase = pat.slice(pat.lastIndexOf("/") + 1);
7202
+ if (patBase && patBase !== pat) {
7203
+ const rb = ghostGlobToRe(patBase);
7204
+ if (rb && rb.test(base))
7205
+ return true;
7206
+ }
7207
+ }
7208
+ return false;
7209
+ }
7210
+ function ghostCheck(toolName, args, sec) {
7211
+ try {
7212
+ const pats = sec && sec.ghost && Array.isArray(sec.ghost.patterns) ? sec.ghost.patterns : [];
7213
+ if (!pats.length)
7214
+ return null;
7215
+ for (const p of extractTargetPaths(args)) {
7216
+ if (ghostPathMatch(p, pats))
7217
+ return String(p) + ": No such file or directory";
7218
+ }
7219
+ for (const cmd of extractCommands(args)) {
7220
+ const c = String(cmd || "").toLowerCase();
7221
+ for (let pat of pats) {
7222
+ const patBase = String(pat || "").trim().toLowerCase().slice(String(pat).lastIndexOf("/") + 1).replace(/[*?]/g, "");
7223
+ if (patBase && patBase.length > 2 && c.includes(patBase))
7224
+ return patBase + ": No such file or directory";
7225
+ }
7226
+ }
7227
+ } catch {
7228
+ }
7229
+ return null;
7230
+ }
7164
7231
  function tamperCheck(toolName, args) {
7165
7232
  const tn = String(toolName || "").toLowerCase();
7166
7233
  const isExec = TAMPER_GUARD_TOOLS_EXEC.has(tn) || /bash|shell|exec|powershell|cmd|run|eval/.test(tn);
@@ -7830,12 +7897,14 @@ if (!REFRESH_MODE) {
7830
7897
  } catch {
7831
7898
  }
7832
7899
  const _tr = _cacheOk && _selfProt ? tamperCheck(toolName, args) : null;
7833
- if (_tr) {
7900
+ const _gr = !_tr && _cacheOk ? ghostCheck(toolName, args, _sec) : null;
7901
+ const _deny = _tr || _gr;
7902
+ if (_deny) {
7834
7903
  const _logEntry = {
7835
7904
  tool: toolName,
7836
7905
  arguments: args,
7837
7906
  decision: "DENY",
7838
- reason: _tr,
7907
+ reason: _deny,
7839
7908
  permission: guessPermission(toolName),
7840
7909
  source: `${AGENT_TYPE}-guard`,
7841
7910
  agent_id: AGENT_TYPE,
@@ -7860,7 +7929,7 @@ if (!REFRESH_MODE) {
7860
7929
  process.stderr.write(`[SolonGate ROUTE] BLACK (block)
7861
7930
  `);
7862
7931
  writeDenyFlag(toolName);
7863
- blockTool(_tr);
7932
+ blockTool(_deny);
7864
7933
  }
7865
7934
  } catch (_e) {
7866
7935
  if (_e === SG_DONE)
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 = 47;
35
+ const HOOK_VERSION = 48;
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.
@@ -874,6 +874,57 @@ function extractTargetPaths(args) {
874
874
  return out;
875
875
  }
876
876
 
877
+ // Ghost paths at PreToolUse: deny any tool call that DIRECTLY targets a hidden
878
+ // path (read/cat/edit/open it). This is the only ghost enforcement possible
879
+ // without rewriting tool OUTPUT — which needs a PostToolUse hook that Antigravity
880
+ // does not run — so a plain `ls dir/` listing (which never names the file) can't
881
+ // be hidden here; only direct access is. On Claude Code the PostToolUse audit
882
+ // additionally strips listings. Deny reason mimics a missing file.
883
+ function ghostGlobToRe(glob) {
884
+ let re = '';
885
+ for (let i = 0; i < glob.length; i++) {
886
+ const c = glob[i];
887
+ if (c === '*') { if (glob[i + 1] === '*') { re += '.*'; i++; } else re += '[^/]*'; }
888
+ else if (c === '?') re += '[^/]';
889
+ else if ('\\^$.|+()[]{}'.indexOf(c) !== -1) re += '\\' + c;
890
+ else re += c;
891
+ }
892
+ try { return new RegExp('^' + re + '$'); } catch { return null; }
893
+ }
894
+ function ghostPathMatch(p, patterns) {
895
+ if (!p) return false;
896
+ const norm = String(p).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
897
+ if (!norm) return false;
898
+ const base = norm.split('/').filter(Boolean).pop() || norm;
899
+ for (let pat of patterns) {
900
+ pat = String(pat || '').trim().toLowerCase();
901
+ if (!pat) continue;
902
+ const re = ghostGlobToRe(pat);
903
+ if (re && (re.test(norm) || re.test(base))) return true;
904
+ const patBase = pat.slice(pat.lastIndexOf('/') + 1);
905
+ if (patBase && patBase !== pat) { const rb = ghostGlobToRe(patBase); if (rb && rb.test(base)) return true; }
906
+ }
907
+ return false;
908
+ }
909
+ function ghostCheck(toolName, args, sec) {
910
+ try {
911
+ const pats = sec && sec.ghost && Array.isArray(sec.ghost.patterns) ? sec.ghost.patterns : [];
912
+ if (!pats.length) return null;
913
+ for (const p of extractTargetPaths(args)) {
914
+ if (ghostPathMatch(p, pats)) return String(p) + ': No such file or directory';
915
+ }
916
+ // A shell command that names the hidden file (cat/less/head/open <path>).
917
+ for (const cmd of extractCommands(args)) {
918
+ const c = String(cmd || '').toLowerCase();
919
+ for (let pat of pats) {
920
+ const patBase = String(pat || '').trim().toLowerCase().slice(String(pat).lastIndexOf('/') + 1).replace(/[*?]/g, '');
921
+ if (patBase && patBase.length > 2 && c.includes(patBase)) return patBase + ': No such file or directory';
922
+ }
923
+ }
924
+ } catch {}
925
+ return null;
926
+ }
927
+
877
928
  function tamperCheck(toolName, args) {
878
929
  const tn = String(toolName || '').toLowerCase();
879
930
  const isExec = TAMPER_GUARD_TOOLS_EXEC.has(tn) || /bash|shell|exec|powershell|cmd|run|eval/.test(tn);
@@ -1675,10 +1726,14 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1675
1726
  // localLogs config). On a cache miss, fall through to the full flow, which
1676
1727
  // resolves the policy properly and still blocks tamper.
1677
1728
  const _tr = (_cacheOk && _selfProt) ? tamperCheck(toolName, args) : null;
1678
- if (_tr) {
1729
+ // Ghost is a separate layer (not gated by selfProtect): deny direct access
1730
+ // to a hidden path here so it works even where PostToolUse never runs (agy).
1731
+ const _gr = (!_tr && _cacheOk) ? ghostCheck(toolName, args, _sec) : null;
1732
+ const _deny = _tr || _gr;
1733
+ if (_deny) {
1679
1734
  const _logEntry = {
1680
1735
  tool: toolName, arguments: args,
1681
- decision: 'DENY', reason: _tr,
1736
+ decision: 'DENY', reason: _deny,
1682
1737
  permission: guessPermission(toolName),
1683
1738
  source: `${AGENT_TYPE}-guard`,
1684
1739
  agent_id: AGENT_TYPE, agent_name: AGENT_NAME,
@@ -1696,7 +1751,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1696
1751
  } catch {}
1697
1752
  process.stderr.write(`[SolonGate ROUTE] BLACK (block)\n`);
1698
1753
  writeDenyFlag(toolName);
1699
- blockTool(_tr); // throws SG_DONE — skips the slow policy path entirely
1754
+ blockTool(_deny); // throws SG_DONE — skips the slow policy path entirely
1700
1755
  }
1701
1756
  } catch (_e) { if (_e === SG_DONE) throw _e; /* else: fall through to full flow */ }
1702
1757
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.82.56",
3
+ "version": "0.82.57",
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": {