@solongate/proxy 0.81.64 → 0.81.66

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.
@@ -59,3 +59,13 @@
59
59
  {"ms":4,"ts":1784374838495,"tool":"Write","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
60
60
  {"ms":3,"ts":1784374848281,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
61
61
  {"ms":4,"ts":1784375224287,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
62
+ {"ms":5,"ts":1784375813890,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
63
+ {"ms":3,"ts":1784376258704,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
64
+ {"ms":6,"ts":1784376281596,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
65
+ {"ms":6,"ts":1784376313566,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
66
+ {"ms":6,"ts":1784376391108,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
67
+ {"ms":5,"ts":1784376414971,"tool":"Write","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
68
+ {"ms":3,"ts":1784376423302,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
69
+ {"ms":2,"ts":1784376491365,"tool":"Bash","session":"t2"}
70
+ {"ms":3,"ts":1784376536945,"tool":"Bash","session":"t2"}
71
+ {"ms":6,"ts":1784376555716,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
@@ -1 +1 @@
1
- {"tool":"Bash","ts":1784365527031}
1
+ {"tool":"Bash","ts":1784376536900}
@@ -1 +1 @@
1
- {"ms":4,"ts":1784375224287,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
1
+ {"ms":6,"ts":1784376555716,"tool":"Bash","session":"f5933c65-c315-4440-9afb-7136fce6b695"}
@@ -1 +1 @@
1
- 1784375211122
1
+ 1784376536991
@@ -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 = 38;
6539
+ var HOOK_VERSION = 40;
6540
6540
  function localLogsOnly(security) {
6541
6541
  if (security && typeof security === "object") {
6542
6542
  const l = security.localLogs;
@@ -6557,56 +6557,19 @@ function writeLocalMarker(security) {
6557
6557
  } catch {
6558
6558
  }
6559
6559
  }
6560
- var _invalidPathNoted = false;
6561
- function resolveLocalLogDir(rawPath) {
6562
- const dir = String(rawPath || "").trim().replace(/[\\/]+$/, "");
6563
- if (!dir)
6564
- return null;
6565
- if (isAbsolute(dir))
6566
- return dir;
6567
- const fallback = resolve(homedir(), ".solongate", "local-logs");
6568
- if (!_invalidPathNoted) {
6569
- _invalidPathNoted = true;
6570
- try {
6571
- mkdirSync(resolve(homedir(), ".solongate"), { recursive: true });
6572
- writeFileSync(
6573
- resolve(homedir(), ".solongate", ".local-logs-invalid-path"),
6574
- JSON.stringify({ configured: dir, fallback, ts: Date.now() })
6575
- );
6576
- } catch {
6577
- }
6578
- }
6579
- return fallback;
6580
- }
6581
6560
  function writeLocalLog(security, entry) {
6582
6561
  try {
6583
6562
  const l = security && security.localLogs;
6584
6563
  if (!l || !l.enabled || typeof l.path !== "string" || !l.path.trim())
6585
6564
  return;
6586
- const dir = resolveLocalLogDir(l.path);
6565
+ let dir = String(l.path).trim().replace(/[\\/]+$/, "");
6587
6566
  if (!dir)
6588
6567
  return;
6589
- try {
6590
- mkdirSync(dir, { recursive: true });
6591
- } catch {
6592
- }
6593
- const file = join(dir, "solongate-audit.jsonl");
6568
+ if (!isAbsolute(dir))
6569
+ dir = resolve(homedir(), ".solongate", "local-logs");
6594
6570
  const line = JSON.stringify(entry) + "\n";
6595
- for (let attempt = 0; attempt < 12; attempt++) {
6596
- try {
6597
- appendFileSync(file, line);
6598
- return;
6599
- } catch {
6600
- const _end = Date.now() + 2;
6601
- while (Date.now() < _end) {
6602
- }
6603
- }
6604
- }
6605
- try {
6606
- const payload = Buffer.from(JSON.stringify({ dir, line }), "utf-8").toString("base64");
6607
- spawn(process.execPath, [process.argv[1], "--sg-log-write", payload], { detached: true, stdio: "ignore" }).unref();
6608
- } catch {
6609
- }
6571
+ const payload = Buffer.from(JSON.stringify({ dir, line }), "utf-8").toString("base64");
6572
+ spawn(process.execPath, [process.argv[1], "--sg-log-write", payload], { detached: true, stdio: "ignore" }).unref();
6610
6573
  } catch {
6611
6574
  }
6612
6575
  }
@@ -7783,6 +7746,56 @@ if (!REFRESH_MODE) {
7783
7746
  };
7784
7747
  const args = data.tool_input;
7785
7748
  const toolName = data.tool_name || "";
7749
+ try {
7750
+ const _ak = (AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
7751
+ const _cf = join(resolve(homedir(), ".solongate"), ".policy-cache-" + _ak + ".json");
7752
+ let _selfProt = true, _sec = null, _cacheOk = false;
7753
+ try {
7754
+ const _c = JSON.parse(readFileSync(_cf, "utf-8"));
7755
+ _cacheOk = true;
7756
+ if (_c && typeof _c.selfProtect === "boolean")
7757
+ _selfProt = _c.selfProtect;
7758
+ if (_c && _c.security !== void 0)
7759
+ _sec = _c.security;
7760
+ } catch {
7761
+ }
7762
+ const _tr = _cacheOk && _selfProt ? tamperCheck(toolName, args) : null;
7763
+ if (_tr) {
7764
+ const _logEntry = {
7765
+ tool: toolName,
7766
+ arguments: args,
7767
+ decision: "DENY",
7768
+ reason: _tr,
7769
+ permission: guessPermission(toolName),
7770
+ source: `${AGENT_TYPE}-guard`,
7771
+ agent_id: AGENT_TYPE,
7772
+ agent_name: AGENT_NAME,
7773
+ session_id: data.session_id || "",
7774
+ evaluation_time_ms: Date.now() - _evalStart
7775
+ };
7776
+ try {
7777
+ writeLocalLog(_sec, { ts: (/* @__PURE__ */ new Date()).toISOString(), ..._logEntry });
7778
+ } catch {
7779
+ }
7780
+ try {
7781
+ if (!localLogsOnly(_sec))
7782
+ await fetch(API_URL + "/api/v1/audit-logs", {
7783
+ method: "POST",
7784
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
7785
+ body: JSON.stringify(_logEntry),
7786
+ signal: AbortSignal.timeout(3e3)
7787
+ });
7788
+ } catch {
7789
+ }
7790
+ process.stderr.write(`[SolonGate ROUTE] BLACK (block)
7791
+ `);
7792
+ writeDenyFlag(toolName);
7793
+ blockTool(_tr);
7794
+ }
7795
+ } catch (_e) {
7796
+ if (_e === SG_DONE)
7797
+ throw _e;
7798
+ }
7786
7799
  const hookCwd = data.cwd || process.cwd();
7787
7800
  let policy;
7788
7801
  let selfProtectEnabled = true;
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 = 38;
35
+ const HOOK_VERSION = 40;
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.
@@ -100,25 +100,19 @@ function writeLocalLog(security, entry) {
100
100
  try {
101
101
  const l = security && security.localLogs;
102
102
  if (!l || !l.enabled || typeof l.path !== 'string' || !l.path.trim()) return;
103
- const dir = resolveLocalLogDir(l.path);
103
+ // Resolve the dir with ZERO filesystem work (no mkdir, no marker write) so the
104
+ // detached spawn below is the FIRST syscall. Traced root cause: the earliest
105
+ // guard processes in a burst do extra startup work (policy refresh + WASM),
106
+ // exceed Claude Code's hook timeout, and get SIGKILLed partway THROUGH
107
+ // writeLocalLog — before any append. So we hand the write to a DETACHED child
108
+ // (own process group, survives the kill) IMMEDIATELY, as the very first action,
109
+ // and do it EVERY time (no inline append → no partial-write / duplicate risk).
110
+ let dir = String(l.path).trim().replace(/[\\/]+$/, '');
104
111
  if (!dir) return;
105
- try { mkdirSync(dir, { recursive: true }); } catch {}
106
- const file = join(dir, 'solongate-audit.jsonl');
112
+ if (!isAbsolute(dir)) dir = resolve(homedir(), '.solongate', 'local-logs');
107
113
  const line = JSON.stringify(entry) + '\n';
108
- // GUARANTEED write. Under a concurrent denial burst the append can transiently
109
- // throw (contention on the shared file while the dashboard reads it); this
110
- // hook process stays alive (it reaches blockTool), so retry the append a few
111
- // times with a short spin-backoff until it lands.
112
- for (let attempt = 0; attempt < 12; attempt++) {
113
- try { appendFileSync(file, line); return; }
114
- catch { const _end = Date.now() + 2; while (Date.now() < _end) { /* spin ~2ms */ } }
115
- }
116
- // Every inline attempt threw: hand off to a DETACHED child (own process group,
117
- // survives this process's death) as the final guarantee the entry is written.
118
- try {
119
- const payload = Buffer.from(JSON.stringify({ dir, line }), 'utf-8').toString('base64');
120
- spawn(process.execPath, [process.argv[1], '--sg-log-write', payload], { detached: true, stdio: 'ignore' }).unref();
121
- } catch {}
114
+ const payload = Buffer.from(JSON.stringify({ dir, line }), 'utf-8').toString('base64');
115
+ spawn(process.execPath, [process.argv[1], '--sg-log-write', payload], { detached: true, stdio: 'ignore' }).unref();
122
116
  } catch { /* best-effort */ }
123
117
  }
124
118
 
@@ -1568,6 +1562,54 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1568
1562
  const args = data.tool_input;
1569
1563
  const toolName = data.tool_name || '';
1570
1564
 
1565
+ // ── FAST tamper path (hook v40) ───────────────────────────────────────
1566
+ // Tamper protection is independent of the cloud policy, so decide it BEFORE
1567
+ // the slow policy resolution (cache read + WASM + background refresh). That
1568
+ // slow work made the earliest guard processes in a concurrent denial burst
1569
+ // exceed Claude Code's PreToolUse hook timeout and get SIGKILLed mid-write,
1570
+ // dropping the local log. Here we cheap-read the cache for selfProtect +
1571
+ // localLogs config and, on a tamper hit, log + block IMMEDIATELY — no WASM,
1572
+ // no refresh — so the process is fast and never killed. Falls through
1573
+ // untouched when there is no tamper hit.
1574
+ try {
1575
+ const _ak = (AGENT_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
1576
+ const _cf = join(resolve(homedir(), '.solongate'), '.policy-cache-' + _ak + '.json');
1577
+ let _selfProt = true, _sec = null, _cacheOk = false;
1578
+ try {
1579
+ const _c = JSON.parse(readFileSync(_cf, 'utf-8'));
1580
+ _cacheOk = true;
1581
+ if (_c && typeof _c.selfProtect === 'boolean') _selfProt = _c.selfProtect;
1582
+ if (_c && _c.security !== undefined) _sec = _c.security;
1583
+ } catch {}
1584
+ // Only fast-path when we actually read the cache (so _sec is the real
1585
+ // localLogs config). On a cache miss, fall through to the full flow, which
1586
+ // resolves the policy properly and still blocks tamper.
1587
+ const _tr = (_cacheOk && _selfProt) ? tamperCheck(toolName, args) : null;
1588
+ if (_tr) {
1589
+ const _logEntry = {
1590
+ tool: toolName, arguments: args,
1591
+ decision: 'DENY', reason: _tr,
1592
+ permission: guessPermission(toolName),
1593
+ source: `${AGENT_TYPE}-guard`,
1594
+ agent_id: AGENT_TYPE, agent_name: AGENT_NAME,
1595
+ session_id: data.session_id || '',
1596
+ evaluation_time_ms: Date.now() - _evalStart,
1597
+ };
1598
+ try { writeLocalLog(_sec, { ts: new Date().toISOString(), ..._logEntry }); } catch {}
1599
+ try {
1600
+ if (!localLogsOnly(_sec)) await fetch(API_URL + '/api/v1/audit-logs', {
1601
+ method: 'POST',
1602
+ headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
1603
+ body: JSON.stringify(_logEntry),
1604
+ signal: AbortSignal.timeout(3000),
1605
+ });
1606
+ } catch {}
1607
+ process.stderr.write(`[SolonGate ROUTE] BLACK (block)\n`);
1608
+ writeDenyFlag(toolName);
1609
+ blockTool(_tr); // throws SG_DONE — skips the slow policy path entirely
1610
+ }
1611
+ } catch (_e) { if (_e === SG_DONE) throw _e; /* else: fall through to full flow */ }
1612
+
1571
1613
  // (self-protection + PI hook layers removed per project decision)
1572
1614
 
1573
1615
  // Load policy. Priority:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.64",
3
+ "version": "0.81.66",
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": {