@solongate/proxy 0.83.54 → 0.83.56

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.
@@ -12,19 +12,6 @@ export interface AuditQuery {
12
12
  signal?: 'dlp' | 'ratelimit';
13
13
  }
14
14
  export declare function list(query?: AuditQuery): Promise<AuditList>;
15
- export declare function remove(ids: string[]): Promise<{
16
- deleted: {
17
- logs: number;
18
- agents: number;
19
- };
20
- }>;
21
- /** Deletes EVERY audit log of the project (agents/sessions are kept). */
22
- export declare function removeAll(): Promise<{
23
- deleted: {
24
- logs: number;
25
- agents: number;
26
- };
27
- }>;
28
15
  export declare function whitelist(id: string, scope?: 'exact' | 'tool'): Promise<{
29
16
  ok: true;
30
17
  deduped: boolean;
@@ -480,19 +480,11 @@ var audit_exports = {};
480
480
  __export(audit_exports, {
481
481
  block: () => block,
482
482
  list: () => list2,
483
- remove: () => remove2,
484
- removeAll: () => removeAll,
485
483
  whitelist: () => whitelist
486
484
  });
487
485
  function list2(query = {}) {
488
486
  return request("GET", "/audit-logs", { query });
489
487
  }
490
- function remove2(ids) {
491
- return request("DELETE", "/audit-logs", { body: { ids } });
492
- }
493
- function removeAll() {
494
- return request("DELETE", "/audit-logs", { body: { scope: "logs" } });
495
- }
496
488
  function whitelist(id, scope = "exact") {
497
489
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
498
490
  }
@@ -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) >>>";
@@ -7894,31 +7926,6 @@ function tailLines(file, maxBytes = 131072) {
7894
7926
  return [];
7895
7927
  }
7896
7928
  }
7897
- function deleteLocalEntry(at, tool, session) {
7898
- try {
7899
- const file = localLogFile();
7900
- const lines = readFileSync8(file, "utf-8").split("\n");
7901
- let removed = 0;
7902
- const kept = lines.filter((line) => {
7903
- if (!line.trim()) return false;
7904
- if (removed) return true;
7905
- try {
7906
- const j = JSON.parse(line);
7907
- const hit = Date.parse(j.ts ?? "") === at && (j.tool ?? "?") === tool && (!session || j.session_id === session);
7908
- if (hit) {
7909
- removed++;
7910
- return false;
7911
- }
7912
- } catch {
7913
- }
7914
- return true;
7915
- });
7916
- if (removed) writeFileSync6(file, kept.length ? kept.join("\n") + "\n" : "");
7917
- return removed;
7918
- } catch {
7919
- return 0;
7920
- }
7921
- }
7922
7929
  function clearLocalLog() {
7923
7930
  try {
7924
7931
  const file = localLogFile();
@@ -8463,19 +8470,11 @@ var audit_exports = {};
8463
8470
  __export(audit_exports, {
8464
8471
  block: () => block,
8465
8472
  list: () => list2,
8466
- remove: () => remove2,
8467
- removeAll: () => removeAll,
8468
8473
  whitelist: () => whitelist
8469
8474
  });
8470
8475
  function list2(query = {}) {
8471
8476
  return request("GET", "/audit-logs", { query });
8472
8477
  }
8473
- function remove2(ids) {
8474
- return request("DELETE", "/audit-logs", { body: { ids } });
8475
- }
8476
- function removeAll() {
8477
- return request("DELETE", "/audit-logs", { body: { scope: "logs" } });
8478
- }
8479
8478
  function whitelist(id, scope = "exact") {
8480
8479
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
8481
8480
  }
@@ -11006,7 +11005,6 @@ function AuditPanel({ active: active2, focused }) {
11006
11005
  const [si, setSi] = useState7(0);
11007
11006
  const [sessSearch, setSessSearch] = useState7("");
11008
11007
  const [sessSel, setSessSel] = useState7(0);
11009
- const [confirm, setConfirm] = useState7(null);
11010
11008
  const [msg, setMsg] = useState7(null);
11011
11009
  const [showHelp, setShowHelp] = useState7(false);
11012
11010
  const [frozen, setFrozen] = useState7(false);
@@ -11088,29 +11086,6 @@ function AuditPanel({ active: active2, focused }) {
11088
11086
  const currentSess = sessionsFiltered[Math.min(sessSel, Math.max(0, sessionsFiltered.length - 1))];
11089
11087
  const logsLoading = (source === "cloud" ? cloudQ.loading : localQ.loading) && !editing;
11090
11088
  const sessLoading = source === "cloud" ? agentsQ.loading : localQ.loading;
11091
- const doDelete = (kind) => {
11092
- setMsg({ text: "deleting\u2026", level: "ok" });
11093
- const run12 = async () => {
11094
- if (source === "cloud") {
11095
- if (kind === "one") {
11096
- if (!current) throw new Error("nothing selected");
11097
- await api.audit.remove([current.id]);
11098
- } else {
11099
- await api.audit.removeAll();
11100
- }
11101
- cloudQ.reload();
11102
- statsQ.reloadQuiet();
11103
- } else {
11104
- const n = kind === "one" && current ? deleteLocalEntry(current.at, current.tool, current.session) : kind === "all" ? clearLocalLog() : 0;
11105
- if (!n) throw new Error("entry not found in the local file");
11106
- localQ.reload();
11107
- }
11108
- };
11109
- run12().then(() => {
11110
- setMsg({ text: kind === "one" ? "\u2713 entry deleted" : `\u2713 ALL ${source} logs deleted`, level: "ok" });
11111
- toTop();
11112
- }).catch((e) => setMsg({ text: "\u2717 " + (e instanceof Error ? e.message : String(e)), level: "bad" }));
11113
- };
11114
11089
  const doExport = (kind) => {
11115
11090
  setMsg({ text: "exporting\u2026", level: "ok" });
11116
11091
  const run12 = async () => {
@@ -11161,8 +11136,7 @@ function AuditPanel({ active: active2, focused }) {
11161
11136
  return;
11162
11137
  }
11163
11138
  if (view === "logs" ? logsLoading : sessLoading) return;
11164
- if (confirm && input !== "x" && input !== "X") {
11165
- setConfirm(null);
11139
+ if (msg) {
11166
11140
  setMsg(null);
11167
11141
  }
11168
11142
  if (input === "s") {
@@ -11226,23 +11200,6 @@ function AuditPanel({ active: active2, focused }) {
11226
11200
  setGi((n) => (n + 1) % SIGNALS.length);
11227
11201
  setPage(0);
11228
11202
  toTop();
11229
- } else if (input === "x") {
11230
- if (!current) return;
11231
- if (confirm?.kind !== "one" || confirm.key !== current.id) {
11232
- setConfirm({ kind: "one", key: current.id });
11233
- setMsg({ text: `x = delete ONLY the selected entry: ${current.decision} ${current.tool} (${ago(current.at)} ago) \u2014 press x again`, level: "bad" });
11234
- return;
11235
- }
11236
- setConfirm(null);
11237
- doDelete("one");
11238
- } else if (input === "X") {
11239
- if (confirm?.kind !== "all") {
11240
- setConfirm({ kind: "all", key: "all" });
11241
- setMsg({ text: `\u26A0 X = delete ALL ${source} logs \u2014 every one of the ${total} matched entries! press X again`, level: "bad" });
11242
- return;
11243
- }
11244
- setConfirm(null);
11245
- doDelete("all");
11246
11203
  } else if (input === "e") doExport("page");
11247
11204
  else if (input === "E") doExport("all");
11248
11205
  else if (input === "t") setEditing("tool");
@@ -11556,8 +11513,6 @@ var init_Audit = __esm({
11556
11513
  ["g", "signal filter: all \u2192 dlp \u2192 ratelimit"],
11557
11514
  ["t / n", "tool / agent filter (type, enter done)"],
11558
11515
  ["/", "free-text search"],
11559
- ["x", "delete ONLY the selected entry (press x twice)"],
11560
- ["X", "delete ALL matched logs of the source (press X twice)"],
11561
11516
  ["e", "export this page \u2192 ~/.solongate/audit-export-<src>.jsonl"],
11562
11517
  ["E", "export ALL matched rows (cloud: up to 10k)"],
11563
11518
  ["c", "clear every filter (incl. session)"]
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) >>>";
@@ -998,31 +1024,6 @@ function tailLines(file, maxBytes = 131072) {
998
1024
  return [];
999
1025
  }
1000
1026
  }
1001
- function deleteLocalEntry(at, tool, session) {
1002
- try {
1003
- const file = localLogFile();
1004
- const lines = readFileSync2(file, "utf-8").split("\n");
1005
- let removed = 0;
1006
- const kept = lines.filter((line) => {
1007
- if (!line.trim()) return false;
1008
- if (removed) return true;
1009
- try {
1010
- const j = JSON.parse(line);
1011
- const hit = Date.parse(j.ts ?? "") === at && (j.tool ?? "?") === tool && (!session || j.session_id === session);
1012
- if (hit) {
1013
- removed++;
1014
- return false;
1015
- }
1016
- } catch {
1017
- }
1018
- return true;
1019
- });
1020
- if (removed) writeFileSync(file, kept.length ? kept.join("\n") + "\n" : "");
1021
- return removed;
1022
- } catch {
1023
- return 0;
1024
- }
1025
- }
1026
1027
  function clearLocalLog() {
1027
1028
  try {
1028
1029
  const file = localLogFile();
@@ -1519,19 +1520,11 @@ var audit_exports = {};
1519
1520
  __export(audit_exports, {
1520
1521
  block: () => block,
1521
1522
  list: () => list2,
1522
- remove: () => remove2,
1523
- removeAll: () => removeAll,
1524
1523
  whitelist: () => whitelist
1525
1524
  });
1526
1525
  function list2(query = {}) {
1527
1526
  return request("GET", "/audit-logs", { query });
1528
1527
  }
1529
- function remove2(ids) {
1530
- return request("DELETE", "/audit-logs", { body: { ids } });
1531
- }
1532
- function removeAll() {
1533
- return request("DELETE", "/audit-logs", { body: { scope: "logs" } });
1534
- }
1535
1528
  function whitelist(id, scope = "exact") {
1536
1529
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
1537
1530
  }
@@ -3977,8 +3970,6 @@ var AUDIT_HELP = [
3977
3970
  ["g", "signal filter: all \u2192 dlp \u2192 ratelimit"],
3978
3971
  ["t / n", "tool / agent filter (type, enter done)"],
3979
3972
  ["/", "free-text search"],
3980
- ["x", "delete ONLY the selected entry (press x twice)"],
3981
- ["X", "delete ALL matched logs of the source (press X twice)"],
3982
3973
  ["e", "export this page \u2192 ~/.solongate/audit-export-<src>.jsonl"],
3983
3974
  ["E", "export ALL matched rows (cloud: up to 10k)"],
3984
3975
  ["c", "clear every filter (incl. session)"]
@@ -4029,7 +4020,6 @@ function AuditPanel({ active: active2, focused }) {
4029
4020
  const [si, setSi] = useState7(0);
4030
4021
  const [sessSearch, setSessSearch] = useState7("");
4031
4022
  const [sessSel, setSessSel] = useState7(0);
4032
- const [confirm, setConfirm] = useState7(null);
4033
4023
  const [msg, setMsg] = useState7(null);
4034
4024
  const [showHelp, setShowHelp] = useState7(false);
4035
4025
  const [frozen, setFrozen] = useState7(false);
@@ -4111,29 +4101,6 @@ function AuditPanel({ active: active2, focused }) {
4111
4101
  const currentSess = sessionsFiltered[Math.min(sessSel, Math.max(0, sessionsFiltered.length - 1))];
4112
4102
  const logsLoading = (source === "cloud" ? cloudQ.loading : localQ.loading) && !editing;
4113
4103
  const sessLoading = source === "cloud" ? agentsQ.loading : localQ.loading;
4114
- const doDelete = (kind) => {
4115
- setMsg({ text: "deleting\u2026", level: "ok" });
4116
- const run = async () => {
4117
- if (source === "cloud") {
4118
- if (kind === "one") {
4119
- if (!current) throw new Error("nothing selected");
4120
- await api.audit.remove([current.id]);
4121
- } else {
4122
- await api.audit.removeAll();
4123
- }
4124
- cloudQ.reload();
4125
- statsQ.reloadQuiet();
4126
- } else {
4127
- const n = kind === "one" && current ? deleteLocalEntry(current.at, current.tool, current.session) : kind === "all" ? clearLocalLog() : 0;
4128
- if (!n) throw new Error("entry not found in the local file");
4129
- localQ.reload();
4130
- }
4131
- };
4132
- run().then(() => {
4133
- setMsg({ text: kind === "one" ? "\u2713 entry deleted" : `\u2713 ALL ${source} logs deleted`, level: "ok" });
4134
- toTop();
4135
- }).catch((e) => setMsg({ text: "\u2717 " + (e instanceof Error ? e.message : String(e)), level: "bad" }));
4136
- };
4137
4104
  const doExport = (kind) => {
4138
4105
  setMsg({ text: "exporting\u2026", level: "ok" });
4139
4106
  const run = async () => {
@@ -4184,8 +4151,7 @@ function AuditPanel({ active: active2, focused }) {
4184
4151
  return;
4185
4152
  }
4186
4153
  if (view === "logs" ? logsLoading : sessLoading) return;
4187
- if (confirm && input !== "x" && input !== "X") {
4188
- setConfirm(null);
4154
+ if (msg) {
4189
4155
  setMsg(null);
4190
4156
  }
4191
4157
  if (input === "s") {
@@ -4249,23 +4215,6 @@ function AuditPanel({ active: active2, focused }) {
4249
4215
  setGi((n) => (n + 1) % SIGNALS.length);
4250
4216
  setPage(0);
4251
4217
  toTop();
4252
- } else if (input === "x") {
4253
- if (!current) return;
4254
- if (confirm?.kind !== "one" || confirm.key !== current.id) {
4255
- setConfirm({ kind: "one", key: current.id });
4256
- setMsg({ text: `x = delete ONLY the selected entry: ${current.decision} ${current.tool} (${ago(current.at)} ago) \u2014 press x again`, level: "bad" });
4257
- return;
4258
- }
4259
- setConfirm(null);
4260
- doDelete("one");
4261
- } else if (input === "X") {
4262
- if (confirm?.kind !== "all") {
4263
- setConfirm({ kind: "all", key: "all" });
4264
- setMsg({ text: `\u26A0 X = delete ALL ${source} logs \u2014 every one of the ${total} matched entries! press X again`, level: "bad" });
4265
- return;
4266
- }
4267
- setConfirm(null);
4268
- doDelete("all");
4269
4218
  } else if (input === "e") doExport("page");
4270
4219
  else if (input === "E") doExport("all");
4271
4220
  else if (input === "t") setEditing("tool");
@@ -77,8 +77,6 @@ export interface LocalLogLine {
77
77
  matched_rule_id?: string;
78
78
  rate_limit_burst?: boolean;
79
79
  }
80
- /** Delete ONE matching line (ts + tool [+ session]) from the local log file. */
81
- export declare function deleteLocalEntry(at: number, tool: string, session?: string | null): number;
82
80
  /**
83
81
  * Empty the local log file. Returns how many lines were removed.
84
82
  *
@@ -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.54",
3
+ "version": "0.83.56",
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.54",
65
- "@solongate/guard-linux-arm64": "0.83.54",
66
- "@solongate/guard-darwin-x64": "0.83.54",
67
- "@solongate/guard-darwin-arm64": "0.83.54",
68
- "@solongate/guard-win32-x64": "0.83.54",
69
- "@solongate/guard-win32-arm64": "0.83.54"
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",