@solongate/proxy 0.83.53 → 0.83.55

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.
@@ -87,6 +87,10 @@ function protectedTargets() {
87
87
  join(p.hooksDir, "audit.mjs"),
88
88
  join(p.hooksDir, "stop.mjs"),
89
89
  join(p.hooksDir, "shield.mjs"),
90
+ // The conversation record is locked with the rest. A guest who could edit
91
+ // it could decide what their host sees them say, which is the same class of
92
+ // problem as editing the guard.
93
+ join(p.hooksDir, "conversation.mjs"),
90
94
  p.configPath,
91
95
  p.settingsPath,
92
96
  p.antigravityHooksPath,
@@ -254,7 +258,7 @@ function removeAntigravityGuard(p) {
254
258
  } catch {
255
259
  }
256
260
  }
257
- var CODEX_EVENTS = ["PreToolUse", "PostToolUse", "Stop"];
261
+ var CODEX_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
258
262
  var CODEX_TIMEOUT_SEC = 30;
259
263
  function codexHookCommand(scriptAbs) {
260
264
  const nodeBin = process.execPath.replace(/\\/g, "/");
@@ -323,12 +327,17 @@ function installCodexGuard(p, hooksDir) {
323
327
  const script = {
324
328
  PreToolUse: "guard.mjs",
325
329
  PostToolUse: "audit.mjs",
326
- Stop: "stop.mjs"
330
+ UserPromptSubmit: "conversation.mjs",
331
+ // Codex takes one handler per event here, so the conversation record is
332
+ // what Stop runs. The Claude registration keeps both because its Stop
333
+ // already had a no-op registered that other machines are holding.
334
+ Stop: "conversation.mjs"
327
335
  };
328
336
  const status = {
329
337
  PreToolUse: "SolonGate policy check",
330
338
  PostToolUse: "SolonGate audit",
331
- Stop: "SolonGate audit"
339
+ UserPromptSubmit: "SolonGate conversation record",
340
+ Stop: "SolonGate conversation record"
332
341
  };
333
342
  for (const ev of CODEX_EVENTS) {
334
343
  const kept = (file.hooks[ev] ?? []).filter((g) => !isOurCodexGroup(g));
@@ -615,6 +624,7 @@ function installGlobalQuiet() {
615
624
  writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
616
625
  writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
617
626
  writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
627
+ writeFileSync(join(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
618
628
  writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
619
629
  let existing = {};
620
630
  if (existsSync(p.settingsPath)) {
@@ -634,7 +644,23 @@ function installGlobalQuiet() {
634
644
  hooks: {
635
645
  PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
636
646
  PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
637
- Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
647
+ // The two halves of a turn, for a machine in a fleet.
648
+ //
649
+ // UserPromptSubmit carries what the person typed. Stop carries
650
+ // last_assistant_message, which is the answer to THIS turn — the
651
+ // transcript on disk is flushed asynchronously and lags the live
652
+ // conversation, so reading that file instead would sometimes record the
653
+ // previous answer.
654
+ //
655
+ // Registering them does not start collecting anything: the server drops
656
+ // a turn from an account with no accepted fleet grant. That check is on
657
+ // the server on purpose, because a check on this side would live on the
658
+ // machine of the person it is about.
659
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }],
660
+ Stop: [
661
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] },
662
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }
663
+ ]
638
664
  }
639
665
  };
640
666
  writeFileSync(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
@@ -811,6 +837,7 @@ async function runGlobalInstall(opts = {}) {
811
837
  writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
812
838
  writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
813
839
  writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
840
+ writeFileSync(join(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
814
841
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
815
842
  installClaudeShim(join(p.hooksDir, "shield.mjs"));
816
843
  writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
@@ -831,6 +858,7 @@ async function runGlobalInstall(opts = {}) {
831
858
  const guardAbs = join(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
832
859
  const auditAbs = join(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
833
860
  const stopAbs = join(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
861
+ const convAbs = join(p.hooksDir, "conversation.mjs").replace(/\\/g, "/");
834
862
  const nodeBin = process.execPath.replace(/\\/g, "/");
835
863
  const call = process.platform === "win32" ? "& " : "";
836
864
  const hookCmd = (script) => `${call}"${nodeBin}" "${script}" claude-code "Claude Code"`;
@@ -839,7 +867,11 @@ async function runGlobalInstall(opts = {}) {
839
867
  hooks: {
840
868
  PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(guardAbs) }] }],
841
869
  PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(auditAbs) }] }],
842
- Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(stopAbs) }] }]
870
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(convAbs) }] }],
871
+ Stop: [
872
+ { matcher: "", hooks: [{ type: "command", command: hookCmd(stopAbs) }] },
873
+ { matcher: "", hooks: [{ type: "command", command: hookCmd(convAbs) }] }
874
+ ]
843
875
  }
844
876
  };
845
877
  writeFileSync(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
package/dist/index.js CHANGED
@@ -6300,6 +6300,10 @@ function protectedTargets() {
6300
6300
  join4(p.hooksDir, "audit.mjs"),
6301
6301
  join4(p.hooksDir, "stop.mjs"),
6302
6302
  join4(p.hooksDir, "shield.mjs"),
6303
+ // The conversation record is locked with the rest. A guest who could edit
6304
+ // it could decide what their host sees them say, which is the same class of
6305
+ // problem as editing the guard.
6306
+ join4(p.hooksDir, "conversation.mjs"),
6303
6307
  p.configPath,
6304
6308
  p.settingsPath,
6305
6309
  p.antigravityHooksPath,
@@ -6533,12 +6537,17 @@ function installCodexGuard(p, hooksDir) {
6533
6537
  const script = {
6534
6538
  PreToolUse: "guard.mjs",
6535
6539
  PostToolUse: "audit.mjs",
6536
- Stop: "stop.mjs"
6540
+ UserPromptSubmit: "conversation.mjs",
6541
+ // Codex takes one handler per event here, so the conversation record is
6542
+ // what Stop runs. The Claude registration keeps both because its Stop
6543
+ // already had a no-op registered that other machines are holding.
6544
+ Stop: "conversation.mjs"
6537
6545
  };
6538
6546
  const status = {
6539
6547
  PreToolUse: "SolonGate policy check",
6540
6548
  PostToolUse: "SolonGate audit",
6541
- Stop: "SolonGate audit"
6549
+ UserPromptSubmit: "SolonGate conversation record",
6550
+ Stop: "SolonGate conversation record"
6542
6551
  };
6543
6552
  for (const ev of CODEX_EVENTS) {
6544
6553
  const kept = (file.hooks[ev] ?? []).filter((g) => !isOurCodexGroup(g));
@@ -6824,6 +6833,7 @@ function installGlobalQuiet() {
6824
6833
  writeFileSync3(join4(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
6825
6834
  writeFileSync3(join4(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
6826
6835
  writeFileSync3(join4(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
6836
+ writeFileSync3(join4(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
6827
6837
  writeFileSync3(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
6828
6838
  let existing = {};
6829
6839
  if (existsSync3(p.settingsPath)) {
@@ -6843,7 +6853,23 @@ function installGlobalQuiet() {
6843
6853
  hooks: {
6844
6854
  PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
6845
6855
  PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
6846
- Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
6856
+ // The two halves of a turn, for a machine in a fleet.
6857
+ //
6858
+ // UserPromptSubmit carries what the person typed. Stop carries
6859
+ // last_assistant_message, which is the answer to THIS turn — the
6860
+ // transcript on disk is flushed asynchronously and lags the live
6861
+ // conversation, so reading that file instead would sometimes record the
6862
+ // previous answer.
6863
+ //
6864
+ // Registering them does not start collecting anything: the server drops
6865
+ // a turn from an account with no accepted fleet grant. That check is on
6866
+ // the server on purpose, because a check on this side would live on the
6867
+ // machine of the person it is about.
6868
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }],
6869
+ Stop: [
6870
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] },
6871
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }
6872
+ ]
6847
6873
  }
6848
6874
  };
6849
6875
  writeFileSync3(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
@@ -7018,6 +7044,7 @@ async function runGlobalInstall(opts = {}) {
7018
7044
  writeFileSync3(join4(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
7019
7045
  writeFileSync3(join4(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
7020
7046
  writeFileSync3(join4(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
7047
+ writeFileSync3(join4(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
7021
7048
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
7022
7049
  installClaudeShim(join4(p.hooksDir, "shield.mjs"));
7023
7050
  writeFileSync3(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
@@ -7038,6 +7065,7 @@ async function runGlobalInstall(opts = {}) {
7038
7065
  const guardAbs = join4(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
7039
7066
  const auditAbs = join4(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
7040
7067
  const stopAbs = join4(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
7068
+ const convAbs = join4(p.hooksDir, "conversation.mjs").replace(/\\/g, "/");
7041
7069
  const nodeBin = process.execPath.replace(/\\/g, "/");
7042
7070
  const call = process.platform === "win32" ? "& " : "";
7043
7071
  const hookCmd = (script) => `${call}"${nodeBin}" "${script}" claude-code "Claude Code"`;
@@ -7046,7 +7074,11 @@ async function runGlobalInstall(opts = {}) {
7046
7074
  hooks: {
7047
7075
  PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(guardAbs) }] }],
7048
7076
  PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(auditAbs) }] }],
7049
- Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(stopAbs) }] }]
7077
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(convAbs) }] }],
7078
+ Stop: [
7079
+ { matcher: "", hooks: [{ type: "command", command: hookCmd(stopAbs) }] },
7080
+ { matcher: "", hooks: [{ type: "command", command: hookCmd(convAbs) }] }
7081
+ ]
7050
7082
  }
7051
7083
  };
7052
7084
  writeFileSync3(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
@@ -7093,7 +7125,7 @@ var init_global_install = __esm({
7093
7125
  __dirname = dirname(fileURLToPath(import.meta.url));
7094
7126
  HOOKS_DIR = resolve3(__dirname, "..", "hooks");
7095
7127
  ANTIGRAVITY_GROUP = "solongate-guard";
7096
- CODEX_EVENTS = ["PreToolUse", "PostToolUse", "Stop"];
7128
+ CODEX_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
7097
7129
  CODEX_TIMEOUT_SEC = 30;
7098
7130
  SCRATCH_FILES = /* @__PURE__ */ new Set([".eval-ring.jsonl", ".last-eval", ".last-deny", ".last-tool-call", ".debug-guard-log"]);
7099
7131
  SHIM_BEGIN = "# >>> SolonGate shield (auto secret redaction) >>>";
package/dist/tui/index.js CHANGED
@@ -95,6 +95,10 @@ function protectedTargets() {
95
95
  join3(p.hooksDir, "audit.mjs"),
96
96
  join3(p.hooksDir, "stop.mjs"),
97
97
  join3(p.hooksDir, "shield.mjs"),
98
+ // The conversation record is locked with the rest. A guest who could edit
99
+ // it could decide what their host sees them say, which is the same class of
100
+ // problem as editing the guard.
101
+ join3(p.hooksDir, "conversation.mjs"),
98
102
  p.configPath,
99
103
  p.settingsPath,
100
104
  p.antigravityHooksPath,
@@ -320,12 +324,17 @@ function installCodexGuard(p, hooksDir) {
320
324
  const script = {
321
325
  PreToolUse: "guard.mjs",
322
326
  PostToolUse: "audit.mjs",
323
- Stop: "stop.mjs"
327
+ UserPromptSubmit: "conversation.mjs",
328
+ // Codex takes one handler per event here, so the conversation record is
329
+ // what Stop runs. The Claude registration keeps both because its Stop
330
+ // already had a no-op registered that other machines are holding.
331
+ Stop: "conversation.mjs"
324
332
  };
325
333
  const status = {
326
334
  PreToolUse: "SolonGate policy check",
327
335
  PostToolUse: "SolonGate audit",
328
- Stop: "SolonGate audit"
336
+ UserPromptSubmit: "SolonGate conversation record",
337
+ Stop: "SolonGate conversation record"
329
338
  };
330
339
  for (const ev of CODEX_EVENTS) {
331
340
  const kept = (file.hooks[ev] ?? []).filter((g) => !isOurCodexGroup(g));
@@ -541,6 +550,7 @@ function installGlobalQuiet() {
541
550
  writeFileSync2(join3(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
542
551
  writeFileSync2(join3(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
543
552
  writeFileSync2(join3(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
553
+ writeFileSync2(join3(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
544
554
  writeFileSync2(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
545
555
  let existing = {};
546
556
  if (existsSync2(p.settingsPath)) {
@@ -560,7 +570,23 @@ function installGlobalQuiet() {
560
570
  hooks: {
561
571
  PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
562
572
  PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
563
- Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
573
+ // The two halves of a turn, for a machine in a fleet.
574
+ //
575
+ // UserPromptSubmit carries what the person typed. Stop carries
576
+ // last_assistant_message, which is the answer to THIS turn — the
577
+ // transcript on disk is flushed asynchronously and lags the live
578
+ // conversation, so reading that file instead would sometimes record the
579
+ // previous answer.
580
+ //
581
+ // Registering them does not start collecting anything: the server drops
582
+ // a turn from an account with no accepted fleet grant. That check is on
583
+ // the server on purpose, because a check on this side would live on the
584
+ // machine of the person it is about.
585
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }],
586
+ Stop: [
587
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] },
588
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }
589
+ ]
564
590
  }
565
591
  };
566
592
  writeFileSync2(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
@@ -677,7 +703,7 @@ var init_global_install = __esm({
677
703
  __dirname = dirname(fileURLToPath(import.meta.url));
678
704
  HOOKS_DIR = resolve(__dirname, "..", "hooks");
679
705
  ANTIGRAVITY_GROUP = "solongate-guard";
680
- CODEX_EVENTS = ["PreToolUse", "PostToolUse", "Stop"];
706
+ CODEX_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
681
707
  CODEX_TIMEOUT_SEC = 30;
682
708
  SCRATCH_FILES = /* @__PURE__ */ new Set([".eval-ring.jsonl", ".last-eval", ".last-deny", ".last-tool-call", ".debug-guard-log"]);
683
709
  SHIM_BEGIN = "# >>> SolonGate shield (auto secret redaction) >>>";
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SolonGate Conversation Hook (UserPromptSubmit + Stop)
4
+ *
5
+ * What a person wrote to their agent, and what it wrote back, for a machine
6
+ * that is in a FLEET. Nothing else in this package records either.
7
+ *
8
+ * WHY THIS IS NOT A TRANSCRIPT READER. Every hook event carries a
9
+ * transcript_path, and the whole conversation is in that file. Reading it would
10
+ * mean tracking a byte offset per session on disk, re-reading a growing file on
11
+ * every turn, and racing the writer: the transcript is flushed asynchronously
12
+ * and lags the live conversation, which the documentation says out loud when it
13
+ * tells you to use last_assistant_message for the current turn instead. Two
14
+ * events carry exactly what is needed, once per turn, with no offset to keep:
15
+ *
16
+ * UserPromptSubmit → user_input what the person typed
17
+ * Stop → last_assistant_message what the agent said back
18
+ *
19
+ * WHAT IS SENT, AND WHEN NOTHING IS. Three gates, cheapest first:
20
+ *
21
+ * 1. No credential → nothing. The same rule every hook here follows.
22
+ * 2. Local-logs mode → nothing. Somebody who has asked for their logs to stay
23
+ * on their machine has asked about this more than about anything else.
24
+ * 3. Not in a fleet → the SERVER drops it. This hook cannot know whether a
25
+ * grant exists, and asking would be a round trip before every send; the
26
+ * API answers 204 and the row is never written. That check is on the
27
+ * server deliberately: a check that lived here would live on the machine
28
+ * of the person it is about.
29
+ *
30
+ * SECRETS ARE REMOVED BEFORE THE TEXT LEAVES. The guard already strips secrets
31
+ * out of tool results; shipping the same secret because somebody pasted it into
32
+ * a prompt would be a leak with this product's name on it. The redaction uses
33
+ * the pattern set the guard cached, so it is the customer's configured set and
34
+ * not a second opinion, and the row is flagged so a host reading a masked line
35
+ * knows it was masked rather than typed that way.
36
+ *
37
+ * Fire-and-forget, and every failure is silent. This hook must never delay a
38
+ * turn and must never be the reason one fails: it records something ABOUT the
39
+ * work, and the work matters more.
40
+ */
41
+ import { readFileSync, existsSync } from 'node:fs';
42
+ import { resolve } from 'node:path';
43
+ import { homedir } from 'node:os';
44
+
45
+ // Bump on every change to this file, alongside the other hooks.
46
+ const HOOK_VERSION = 1;
47
+
48
+ const AGENT_ID = (process.env.SOLONGATE_AGENT_ID || process.argv[2] || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
49
+
50
+ function loadGlobalCloudConfig() {
51
+ try {
52
+ const p = resolve(homedir(), '.solongate', 'cloud-guard.json');
53
+ if (!existsSync(p)) return {};
54
+ const cfg = JSON.parse(readFileSync(p, 'utf-8'));
55
+ return (cfg && typeof cfg === 'object') ? cfg : {};
56
+ } catch { return {}; }
57
+ }
58
+
59
+ function loadPolicyCache() {
60
+ try {
61
+ const f = resolve(homedir(), '.solongate', '.policy-cache-' + AGENT_ID + '.json');
62
+ if (!existsSync(f)) return null;
63
+ return JSON.parse(readFileSync(f, 'utf-8'));
64
+ } catch { return null; }
65
+ }
66
+
67
+ // The pattern set the guard cached, so redaction here is the customer's own
68
+ // configuration rather than a second opinion invented in this file.
69
+ const DLP_PATTERNS = [
70
+ { name: 'AWS key', re: /AKIA[0-9A-Z]{16}/g },
71
+ { name: 'GitHub token', re: /gh[pousr]_[A-Za-z0-9]{36,}/g },
72
+ { name: 'OpenAI key', re: /sk-[A-Za-z0-9]{20,}/g },
73
+ { name: 'Anthropic key', re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
74
+ { name: 'Slack token', re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
75
+ { name: 'Stripe key', re: /[rs]k_(live|test)_[A-Za-z0-9]{16,}/g },
76
+ { name: 'SendGrid key', re: /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g },
77
+ { name: 'Twilio key', re: /SK[0-9a-fA-F]{32}/g },
78
+ { name: 'npm token', re: /npm_[A-Za-z0-9]{36}/g },
79
+ { name: 'JWT', re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
80
+ { name: 'Bearer token', re: /bearer\s+[A-Za-z0-9._-]{20,}/gi },
81
+ // Private keys are masked whatever the configured set says. A PEM block in a
82
+ // prompt is the one thing that must not reach a host's screen because
83
+ // somebody had not ticked a box.
84
+ { name: 'Private key block', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, always: true },
85
+ ];
86
+
87
+ // Redact what the configured set names, plus what is always redacted.
88
+ // Returns the text and whether anything was masked.
89
+ function redact(text, cache) {
90
+ let out = String(text || '');
91
+ let masked = false;
92
+ let enabled = null;
93
+ try {
94
+ const d = cache && cache.security && cache.security.dlpRedact;
95
+ if (d && Array.isArray(d.patterns)) enabled = new Set(d.patterns);
96
+ } catch { enabled = null; }
97
+
98
+ for (const p of DLP_PATTERNS) {
99
+ if (!p.always && (!enabled || !enabled.has(p.name))) continue;
100
+ try {
101
+ p.re.lastIndex = 0;
102
+ if (!p.re.test(out)) continue;
103
+ p.re.lastIndex = 0;
104
+ out = out.replace(p.re, '[redacted: ' + p.name + ']');
105
+ masked = true;
106
+ } catch { /* a pattern that will not run leaves the text alone */ }
107
+ }
108
+ return { text: out, masked };
109
+ }
110
+
111
+ // Local-logs mode means the person has asked for their records to stay on their
112
+ // machine. Of everything this package sends, this is the one they meant most.
113
+ function localLogsOn(cache) {
114
+ try {
115
+ const v = cache && cache.security && cache.security.localLogs;
116
+ return !!(v && (v === true || v.enabled === true || v.path));
117
+ } catch { return false; }
118
+ }
119
+
120
+ function readStdin() {
121
+ try { return readFileSync(0, 'utf-8'); } catch { return ''; }
122
+ }
123
+
124
+ (async () => {
125
+ // Whatever happens below, this hook allows the turn. It has no opinion about
126
+ // whether the work should proceed; it is a record of it.
127
+ process.exitCode = 0;
128
+
129
+ let data = {};
130
+ try { data = JSON.parse(readStdin() || '{}'); } catch { return; }
131
+
132
+ const event = data.hook_event_name || data.hookEventName || '';
133
+ // The two halves. Nothing else is listened for: PreToolUse and PostToolUse
134
+ // already have hooks and are about tool calls rather than about words.
135
+ let role = '';
136
+ let body = '';
137
+ if (event === 'UserPromptSubmit') {
138
+ role = 'prompt';
139
+ body = data.user_input || data.prompt || '';
140
+ } else if (event === 'Stop' || event === 'SubagentStop') {
141
+ role = 'reply';
142
+ // The field the documentation points at for the CURRENT turn. The
143
+ // transcript on disk lags the live conversation, so reading it here would
144
+ // sometimes record the previous answer.
145
+ body = data.last_assistant_message || '';
146
+ } else {
147
+ return;
148
+ }
149
+ if (!String(body).trim()) return;
150
+
151
+ const sessionId = data.session_id || data.sessionId || data.conversation_id || '';
152
+ if (!sessionId) return;
153
+
154
+ const cfg = loadGlobalCloudConfig();
155
+ const apiKey = process.env.SOLONGATE_API_KEY || cfg.apiKey || '';
156
+ const apiUrl = process.env.SOLONGATE_API_URL || cfg.apiUrl || 'https://api.solongate.com';
157
+ if (!apiKey) return;
158
+
159
+ const cache = loadPolicyCache();
160
+ if (localLogsOn(cache)) return;
161
+
162
+ const cleaned = redact(body, cache);
163
+
164
+ try {
165
+ await fetch(`${apiUrl}/api/v1/conversations`, {
166
+ method: 'POST',
167
+ headers: {
168
+ 'Authorization': `Bearer ${apiKey}`,
169
+ 'Content-Type': 'application/json',
170
+ },
171
+ body: JSON.stringify({
172
+ session_id: sessionId,
173
+ role,
174
+ body: cleaned.text,
175
+ redacted: cleaned.masked,
176
+ agent_id: AGENT_ID,
177
+ agent_name: data.agent_name || '',
178
+ source: `${AGENT_ID}-hook`,
179
+ hook_version: HOOK_VERSION,
180
+ }),
181
+ });
182
+ } catch { /* silent: a record that could not be sent must not fail a turn */ }
183
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.53",
3
+ "version": "0.83.55",
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": {
@@ -61,12 +61,12 @@
61
61
  "node": ">=20.0.0"
62
62
  },
63
63
  "optionalDependencies": {
64
- "@solongate/guard-linux-x64": "0.83.52",
65
- "@solongate/guard-linux-arm64": "0.83.52",
66
- "@solongate/guard-darwin-x64": "0.83.52",
67
- "@solongate/guard-darwin-arm64": "0.83.52",
68
- "@solongate/guard-win32-x64": "0.83.52",
69
- "@solongate/guard-win32-arm64": "0.83.52"
64
+ "@solongate/guard-linux-x64": "0.83.55",
65
+ "@solongate/guard-linux-arm64": "0.83.55",
66
+ "@solongate/guard-darwin-x64": "0.83.55",
67
+ "@solongate/guard-darwin-arm64": "0.83.55",
68
+ "@solongate/guard-win32-x64": "0.83.55",
69
+ "@solongate/guard-win32-arm64": "0.83.55"
70
70
  },
71
71
  "dependencies": {
72
72
  "@modelcontextprotocol/sdk": "^1.26.0",