@solongate/proxy 0.82.55 → 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.
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 = 16;
14
+ const HOOK_VERSION = 17;
15
15
 
16
16
  function loadEnvKey(dir) {
17
17
  try {
@@ -131,6 +131,15 @@ function ghostMatch(targetPath, patterns) {
131
131
  continue;
132
132
  }
133
133
  if (re.test(norm)) return true;
134
+ // A path ghost must also hide the file when a tool lists it by BARE name —
135
+ // `ls dir/` prints just "secret-plan.txt" (no slash), which a slash-pattern
136
+ // like `*/secret-plan.txt` would otherwise miss. Match the pattern's last
137
+ // segment against the target's basename.
138
+ const patBase = pat.slice(pat.lastIndexOf('/') + 1);
139
+ if (patBase && patBase !== pat) {
140
+ const reBase = ghostGlobToRegExp(patBase);
141
+ if (reBase && reBase.test(base)) return true;
142
+ }
134
143
  }
135
144
  return false;
136
145
  }
@@ -151,6 +160,10 @@ function ghostStripLines(text, pats) {
151
160
  const trimmed = line.trim();
152
161
  if (!trimmed) { kept.push(line); continue; }
153
162
  if (ghostMatch(trimmed, pats)) continue; // full-path listing line
163
+ // grep -n / ripgrep output is `path:line:content` (or `path:content`) — the
164
+ // path is the first colon field. Drop the whole match line if it's a ghost.
165
+ const firstField = trimmed.split(':')[0];
166
+ if (firstField && firstField !== trimmed && ghostMatch(firstField, pats)) continue;
154
167
  const toks = trimmed.split(/\s+/);
155
168
  const anyHit = toks.some((t) => ghostMatch(ghostCleanToken(t), pats));
156
169
  if (!anyHit) { kept.push(line); continue; }
@@ -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.55",
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": {