@solongate/proxy 0.83.17 → 0.83.19

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
@@ -8726,6 +8726,7 @@ function LivePanel({ active: active2 }) {
8726
8726
  const setting = localLogsSetting();
8727
8727
  setLocalOn(setting.enabled);
8728
8728
  setLocalPath(setting);
8729
+ if (!setting.enabled) return;
8729
8730
  const lines = tailLines(setting.file);
8730
8731
  if (!lines.length) return;
8731
8732
  const fresh = [];
@@ -8988,9 +8989,7 @@ function LivePanel({ active: active2 }) {
8988
8989
  }))
8989
8990
  ];
8990
8991
  sessRef.current = combinedSess;
8991
- const localSessIds = new Set(localSess.keys());
8992
- if (ring?.session) localSessIds.add(ring.session);
8993
- const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
8992
+ const isLoc = (e) => e.source === "local";
8994
8993
  const q = search.trim().toLowerCase();
8995
8994
  const matches = (e) => !q || `${e.tool} ${e.agent ?? ""} ${e.decision} ${e.permission} ${e.rule ?? ""} ${e.detail}`.toLowerCase().includes(q);
8996
8995
  const signalOk = (e) => signal === "none" ? true : signal === "deny" ? e.decision !== "ALLOW" : signal === "dlp" ? e.dlp : e.burst;
@@ -9557,14 +9556,14 @@ function LivePanel({ active: active2 }) {
9557
9556
  filter === "all" ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
9558
9557
  spin,
9559
9558
  " awaiting traffic\u2026"
9560
- ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "LOC (this machine)" : "CLD (other devices)"} calls in the buffer \xB7 f switches origin` })
9559
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "LOC (local log)" : "CLD (cloud)"} calls in the buffer \xB7 f switches source` })
9561
9560
  ) : null,
9562
9561
  windowed.map((e, i) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e), selected: clampedScroll + i === selClamped }, e.id))
9563
9562
  ] }),
9564
9563
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
9565
9564
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", bold: true, children: " LIVE " }),
9566
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local-log ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 origin ${filter === "all" ? "all" : filter === "local" ? "LOC only" : "CLD only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
9567
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f origin \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
9565
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local-log ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 source ${filter === "all" ? "all" : filter === "local" ? "LOC only" : "CLD only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
9566
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f source \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
9568
9567
  ] })
9569
9568
  ] });
9570
9569
  }
@@ -9613,7 +9612,7 @@ var init_Live = __esm({
9613
9612
  ["w", "whitelist the selected DENY (adds ALLOW rule)"],
9614
9613
  ["b", "block the selected ALLOW (adds DENY rule)"],
9615
9614
  ["d / x / r", "filter: denies / dlp hits / rate-limit bursts"],
9616
- ["f", "origin: all \u2192 LOC (this machine) \u2192 CLD (other devices)"],
9615
+ ["f", "source: all \u2192 LOC (this machine's local log) \u2192 CLD (cloud)"],
9617
9616
  ["/", "live search (tool, agent, command\u2026) \xB7 enter done"],
9618
9617
  ["s", "session picker"],
9619
9618
  ["l", "layers detail (rate limit \xB7 dlp \xB7 ghost \xB7 guard)"],
@@ -11,11 +11,10 @@ export declare function PaneTitle({ label, extra, width }: {
11
11
  }): JSX.Element;
12
12
  /**
13
13
  * One tool-stream row, shared verbatim by Live (rolling buffer) and Audit (full
14
- * history). `loc` tags where the call HAPPENED: LOC (this machine) vs CLD
15
- * (another paired device). Note this is ORIGIN, not storage a call made here
16
- * reads LOC even when local logging is off and the record went to the cloud.
17
- * The storage setting is spelled `local-log` in the footer so the two are not
18
- * the same word. `selected` highlights + shows the ▸ cursor.
14
+ * history). `loc` tags where the record IS: LOC (a line in this machine's
15
+ * local log) vs CLD (the cloud audit log). With local storage off nothing is
16
+ * LOC, which is the whole point of the setting. `selected` highlights + shows
17
+ * the cursor.
19
18
  * Columns are fixed-width and NEVER conditional so scrolling can't reshuffle the
20
19
  * line and glitch the frame.
21
20
  */
package/dist/tui/index.js CHANGED
@@ -1746,7 +1746,7 @@ var LIVE_HELP = [
1746
1746
  ["w", "whitelist the selected DENY (adds ALLOW rule)"],
1747
1747
  ["b", "block the selected ALLOW (adds DENY rule)"],
1748
1748
  ["d / x / r", "filter: denies / dlp hits / rate-limit bursts"],
1749
- ["f", "origin: all \u2192 LOC (this machine) \u2192 CLD (other devices)"],
1749
+ ["f", "source: all \u2192 LOC (this machine's local log) \u2192 CLD (cloud)"],
1750
1750
  ["/", "live search (tool, agent, command\u2026) \xB7 enter done"],
1751
1751
  ["s", "session picker"],
1752
1752
  ["l", "layers detail (rate limit \xB7 dlp \xB7 ghost \xB7 guard)"],
@@ -1841,6 +1841,7 @@ function LivePanel({ active: active2 }) {
1841
1841
  const setting = localLogsSetting();
1842
1842
  setLocalOn(setting.enabled);
1843
1843
  setLocalPath(setting);
1844
+ if (!setting.enabled) return;
1844
1845
  const lines = tailLines(setting.file);
1845
1846
  if (!lines.length) return;
1846
1847
  const fresh = [];
@@ -2103,9 +2104,7 @@ function LivePanel({ active: active2 }) {
2103
2104
  }))
2104
2105
  ];
2105
2106
  sessRef.current = combinedSess;
2106
- const localSessIds = new Set(localSess.keys());
2107
- if (ring?.session) localSessIds.add(ring.session);
2108
- const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
2107
+ const isLoc = (e) => e.source === "local";
2109
2108
  const q = search.trim().toLowerCase();
2110
2109
  const matches = (e) => !q || `${e.tool} ${e.agent ?? ""} ${e.decision} ${e.permission} ${e.rule ?? ""} ${e.detail}`.toLowerCase().includes(q);
2111
2110
  const signalOk = (e) => signal === "none" ? true : signal === "deny" ? e.decision !== "ALLOW" : signal === "dlp" ? e.dlp : e.burst;
@@ -2672,14 +2671,14 @@ function LivePanel({ active: active2 }) {
2672
2671
  filter === "all" ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
2673
2672
  spin,
2674
2673
  " awaiting traffic\u2026"
2675
- ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "LOC (this machine)" : "CLD (other devices)"} calls in the buffer \xB7 f switches origin` })
2674
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "LOC (local log)" : "CLD (cloud)"} calls in the buffer \xB7 f switches source` })
2676
2675
  ) : null,
2677
2676
  windowed.map((e, i) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e), selected: clampedScroll + i === selClamped }, e.id))
2678
2677
  ] }),
2679
2678
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
2680
2679
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", bold: true, children: " LIVE " }),
2681
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local-log ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 origin ${filter === "all" ? "all" : filter === "local" ? "LOC only" : "CLD only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
2682
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f origin \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
2680
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local-log ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 source ${filter === "all" ? "all" : filter === "local" ? "LOC only" : "CLD only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
2681
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f source \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
2683
2682
  ] })
2684
2683
  ] });
2685
2684
  }
package/hooks/audit.mjs CHANGED
@@ -40,7 +40,7 @@ function projectFlagDir() {
40
40
  // Bump on every audit hook change. The cloud serves the newest version; the guard
41
41
  // hook installs it on its next run (no re-login needed). See guard.mjs
42
42
  // fetchAndInstallHook / maybeSelfUpdate.
43
- const HOOK_VERSION = 27;
43
+ const HOOK_VERSION = 28;
44
44
 
45
45
  function loadEnvKey(dir) {
46
46
  try {
@@ -724,13 +724,6 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
724
724
  : v;
725
725
  }
726
726
 
727
- // Write flag so stop.mjs knows tool calls happened (skip text-only ALLOW)
728
- try {
729
- const flagDir = projectFlagDir();
730
- mkdirSync(flagDir, { recursive: true });
731
- writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
732
- } catch {}
733
-
734
727
  // Codex CANNOT rewrite tool output from PostToolUse: its output struct is
735
728
  // deny_unknown_fields and the only rewrite field it defines
736
729
  // (updatedMCPToolOutput) is explicitly rejected as unsupported — so emitting
@@ -6578,7 +6578,7 @@ function sweepLegacyFlagDir() {
6578
6578
  } catch {
6579
6579
  }
6580
6580
  }
6581
- var HOOK_VERSION = 78;
6581
+ var HOOK_VERSION = 79;
6582
6582
  function localLogsOnly(security) {
6583
6583
  if (security !== void 0) {
6584
6584
  const l = security && security.localLogs;
@@ -7096,7 +7096,6 @@ function writeDenyFlag(toolName) {
7096
7096
  try {
7097
7097
  const flagDir = projectFlagDir();
7098
7098
  mkdirSync(flagDir, { recursive: true });
7099
- writeFileSync(join(flagDir, ".last-tool-call"), Date.now().toString());
7100
7099
  writeFileSync(join(flagDir, ".last-deny"), JSON.stringify({
7101
7100
  tool: toolName,
7102
7101
  ts: Date.now(),
package/hooks/guard.mjs CHANGED
@@ -81,7 +81,7 @@ import { createHash } from 'node:crypto';
81
81
  // the installed hook self-updates when the cloud version is higher (see
82
82
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
83
83
  // reinstall — the same trust model as the OPA WASM this hook already runs.
84
- const HOOK_VERSION = 78;
84
+ const HOOK_VERSION = 79;
85
85
 
86
86
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
87
87
  // nothing is sent to the cloud audit log — local and cloud are one or the
@@ -826,12 +826,12 @@ function callFingerprint(s) {
826
826
  return h.toString(16);
827
827
  }
828
828
 
829
- // Write flag file so stop.mjs knows a tool call (DENY) happened and doesn't log extra ALLOW
829
+ // Records WHICH call was denied, so the PostToolUse audit hook does not log a
830
+ // second, ALLOW-looking entry for a call the guard already blocked.
830
831
  function writeDenyFlag(toolName) {
831
832
  try {
832
833
  const flagDir = projectFlagDir();
833
834
  mkdirSync(flagDir, { recursive: true });
834
- writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
835
835
  // Deny-specific flag: audit.mjs (PostToolUse) reads it to avoid logging a
836
836
  // second, ALLOW-looking entry for a call the guard already denied.
837
837
  //
package/hooks/stop.mjs CHANGED
@@ -1,91 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * SolonGate Stop Hook (Stop event)
4
- * Logs a single ALLOW when Claude responds WITHOUT any tool calls.
5
- * If tool calls were made, audit.mjs already logged them this hook skips.
6
- * Auto-installed by: npx @solongate/proxy login
3
+ * SolonGate Stop Hook (Stop event) — intentionally does nothing.
4
+ *
5
+ * It used to POST one synthetic audit row named `_response` whenever a turn
6
+ * ended with no tool calls, to record "the agent replied with text only". That
7
+ * was wrong twice over:
8
+ *
9
+ * - `_response` is not a tool, and it sat in the TOOL CALL log. It showed up
10
+ * as a row in the live stream and as an entry in the dashboard's top-tools
11
+ * list, and it inflated the call count with something no policy can act on.
12
+ * - it decided whether to write by looking for a flag file left by the audit
13
+ * hook, so any drift in where that flag lived turned "text-only turn" into
14
+ * "every turn" — which is exactly what happened when the per-call scratch
15
+ * moved out of the working directory.
16
+ *
17
+ * The registration is kept (it costs one process exit per turn and removing it
18
+ * would need every client's hook config rewritten), but nothing is logged. If a
19
+ * text-only turn is ever worth recording, it belongs in its own place, not in
20
+ * the record of what the agent was allowed to run.
7
21
  */
8
- import { readFileSync, existsSync, unlinkSync, writeFileSync } from 'node:fs';
9
- import { resolve, join } from 'node:path';
10
- import { homedir } from 'node:os';
11
-
12
- function loadEnvKey(dir) {
13
- try {
14
- const envPath = resolve(dir, '.env');
15
- if (!existsSync(envPath)) return {};
16
- const lines = readFileSync(envPath, 'utf-8').split('\n');
17
- const env = {};
18
- for (const line of lines) {
19
- const m = line.match(/^([A-Z_]+)=(.*)$/);
20
- if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
21
- }
22
- return env;
23
- } catch { return {}; }
24
- }
25
-
26
- // Global cloud config written at login (~/.solongate/cloud-guard.json). This hook
27
- // used to read ONLY the environment and a project env file, so on a normally
28
- // paired device it silently logged nothing — and where an env file did exist, a
29
- // stale key in it shadowed the login. Same order as the other hooks now:
30
- // process env -> login -> project env file.
31
- function loadGlobalCloudConfig() {
32
- try {
33
- const p = resolve(homedir(), '.solongate', 'cloud-guard.json');
34
- if (!existsSync(p)) return {};
35
- const cfg = JSON.parse(readFileSync(p, 'utf-8'));
36
- return (cfg && typeof cfg === 'object') ? cfg : {};
37
- } catch { return {}; }
38
- }
39
-
40
- const dotenv = loadEnvKey(process.cwd());
41
- const globalCfg = loadGlobalCloudConfig();
42
- const API_KEY = process.env.SOLONGATE_API_KEY || globalCfg.apiKey || dotenv.SOLONGATE_API_KEY || '';
43
- const API_URL = process.env.SOLONGATE_API_URL || globalCfg.apiUrl || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
44
-
45
- // Agent identity from CLI args: node stop.mjs <agent_id> <agent_name>
46
- const AGENT_ID = process.argv[2] || 'claude-code';
47
- const AGENT_NAME = process.argv[3] || 'Claude Code';
48
-
49
- if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
50
-
51
- // Flag file: audit.mjs writes this when a tool call was logged in this turn.
52
- // If the flag exists, tool calls were made → skip (already logged).
53
- // If the flag doesn't exist, no tool calls → log 1 ALLOW for the text response.
54
- const flagDir = resolve('.solongate');
55
- const flagFile = join(flagDir, '.last-tool-call');
56
-
57
- let input = '';
58
- process.stdin.on('data', c => input += c);
59
- process.stdin.on('end', async () => {
60
- try {
61
- // Check if tool calls were made in this turn
62
- if (existsSync(flagFile)) {
63
- // Tool calls happened → audit.mjs already logged them. Clean up flag.
64
- try { unlinkSync(flagFile); } catch {}
65
- process.exit(0);
66
- }
67
-
68
- // No tool calls → log 1 ALLOW for the text-only response (fire-and-forget)
69
- fetch(`${API_URL}/api/v1/audit-logs`, {
70
- method: 'POST',
71
- headers: {
72
- 'Authorization': `Bearer ${API_KEY}`,
73
- 'Content-Type': 'application/json',
74
- },
75
- body: JSON.stringify({
76
- tool: '_response',
77
- arguments: {},
78
- decision: 'ALLOW',
79
- reason: 'text response (no tool calls)',
80
- source: `${AGENT_ID}-hook`,
81
- evaluationTimeMs: 0,
82
- agent_id: AGENT_ID,
83
- agent_name: AGENT_NAME,
84
- }),
85
- signal: AbortSignal.timeout(5000),
86
- }).catch(() => {}).finally(() => process.exit(0));
87
- setTimeout(() => process.exit(0), 3000);
88
- } catch {
89
- process.exit(0);
90
- }
91
- });
22
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.17",
3
+ "version": "0.83.19",
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": {