@solongate/proxy 0.79.0 → 0.80.0

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.
@@ -20,6 +20,12 @@ export interface GuardStatus {
20
20
  outdated_count: number;
21
21
  }
22
22
  export declare function getGuardStatus(): Promise<GuardStatus>;
23
+ export declare function getSelfProtection(): Promise<{
24
+ enabled: boolean;
25
+ }>;
26
+ export declare function setSelfProtection(enabled: boolean): Promise<{
27
+ enabled: boolean;
28
+ }>;
23
29
  export interface AlertRule {
24
30
  id: string;
25
31
  name: string;
@@ -204,8 +204,10 @@ __export(settings_exports, {
204
204
  getGuardStatus: () => getGuardStatus,
205
205
  getRateLimitHistory: () => getRateLimitHistory,
206
206
  getSecurityLayers: () => getSecurityLayers,
207
+ getSelfProtection: () => getSelfProtection,
207
208
  getWebhooks: () => getWebhooks,
208
- setSecurityLayers: () => setSecurityLayers
209
+ setSecurityLayers: () => setSecurityLayers,
210
+ setSelfProtection: () => setSelfProtection
209
211
  });
210
212
  function getSecurityLayers() {
211
213
  return request("GET", "/settings/security-layers");
@@ -222,6 +224,12 @@ function clearRateLimitHistory() {
222
224
  function getGuardStatus() {
223
225
  return request("GET", "/settings/guard-status");
224
226
  }
227
+ function getSelfProtection() {
228
+ return request("GET", "/settings/self-protection");
229
+ }
230
+ function setSelfProtection(enabled) {
231
+ return request("PUT", "/settings/self-protection", { body: { enabled } });
232
+ }
225
233
  function getAlerts() {
226
234
  return request("GET", "/settings/denial-alerts");
227
235
  }
package/dist/index.js CHANGED
@@ -6875,8 +6875,10 @@ __export(settings_exports, {
6875
6875
  getGuardStatus: () => getGuardStatus,
6876
6876
  getRateLimitHistory: () => getRateLimitHistory,
6877
6877
  getSecurityLayers: () => getSecurityLayers,
6878
+ getSelfProtection: () => getSelfProtection,
6878
6879
  getWebhooks: () => getWebhooks,
6879
- setSecurityLayers: () => setSecurityLayers
6880
+ setSecurityLayers: () => setSecurityLayers,
6881
+ setSelfProtection: () => setSelfProtection
6880
6882
  });
6881
6883
  function getSecurityLayers() {
6882
6884
  return request("GET", "/settings/security-layers");
@@ -6893,6 +6895,12 @@ function clearRateLimitHistory() {
6893
6895
  function getGuardStatus() {
6894
6896
  return request("GET", "/settings/guard-status");
6895
6897
  }
6898
+ function getSelfProtection() {
6899
+ return request("GET", "/settings/self-protection");
6900
+ }
6901
+ function setSelfProtection(enabled) {
6902
+ return request("PUT", "/settings/self-protection", { body: { enabled } });
6903
+ }
6896
6904
  function getAlerts() {
6897
6905
  return request("GET", "/settings/denial-alerts");
6898
6906
  }
@@ -7940,6 +7948,7 @@ function PoliciesPanel({ focused }) {
7940
7948
  const [pi, setPi] = useState3(0);
7941
7949
  const [ri, setRi] = useState3(0);
7942
7950
  const [fi, setFi] = useState3(0);
7951
+ const [vi, setVi] = useState3(0);
7943
7952
  const [rules, setRules] = useState3([]);
7944
7953
  const [mode, setMode] = useState3("denylist");
7945
7954
  const [dirty, setDirty] = useState3(false);
@@ -7948,6 +7957,10 @@ function PoliciesPanel({ focused }) {
7948
7957
  const [status, setStatus] = useState3(null);
7949
7958
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
7950
7959
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
7960
+ const versionsQ = useLoader(
7961
+ () => view === "versions" && selected ? api.policies.versions(selected.id, { limit: 50 }) : Promise.resolve(null),
7962
+ [view, selected?.id]
7963
+ );
7951
7964
  useEffect3(() => {
7952
7965
  if (detail.data) {
7953
7966
  setRules(detail.data.rules);
@@ -8022,10 +8035,41 @@ function PoliciesPanel({ focused }) {
8022
8035
  }
8023
8036
  } else if (input === "m") {
8024
8037
  mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
8038
+ } else if (input === "n") {
8039
+ const nr = { id: `rule-${Date.now()}`, description: "", effect: "DENY", priority: 100, toolPattern: "*", minimumTrustLevel: "UNTRUSTED", enabled: true };
8040
+ mutate([nr, ...rules]);
8041
+ setRi(0);
8042
+ setFi(0);
8043
+ setView("rule");
8044
+ } else if (input === "D") {
8045
+ setStatus("Dry-running\u2026");
8046
+ void api.policies.dryRun({ rules, mode }).then((res) => setStatus(`dry-run ${res.evaluated} calls \xB7 allow ${res.would_allow} / deny ${res.would_deny} \xB7 newly blocked ${res.newly_blocked} \xB7 newly allowed ${res.newly_allowed}`)).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
8047
+ } else if (input === "v") {
8048
+ setVi(0);
8049
+ setView("versions");
8025
8050
  } else if (input === "s") void save();
8026
8051
  else if (input === "x") discard();
8027
8052
  return;
8028
8053
  }
8054
+ if (view === "versions") {
8055
+ const vers = versionsQ.data?.versions ?? [];
8056
+ if (key.leftArrow) return void setView("rules");
8057
+ if (key.upArrow) setVi((n) => Math.max(0, n - 1));
8058
+ else if (key.downArrow) setVi((n) => Math.min(vers.length - 1, n + 1));
8059
+ else if (key.return) {
8060
+ const v = vers[vi];
8061
+ if (v && selected) {
8062
+ setStatus("Rolling back\u2026");
8063
+ void api.policies.rollback(selected.id, v.version).then((r) => {
8064
+ setStatus(`\u2713 rolled back to v${v.version} \u2192 new v${r.version}`);
8065
+ detail.reload();
8066
+ list6.reload();
8067
+ setView("rules");
8068
+ }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
8069
+ }
8070
+ }
8071
+ return;
8072
+ }
8029
8073
  const rule2 = rules[ri];
8030
8074
  if (!rule2) return setView("rules");
8031
8075
  const field = FIELDS[fi];
@@ -8070,7 +8114,7 @@ function PoliciesPanel({ focused }) {
8070
8114
  /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
8071
8115
  dirtyTag
8072
8116
  ] }),
8073
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
8117
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 n new \xB7 d del \xB7 m mode \xB7 D dry-run \xB7 v versions \xB7 s save \xB7 \u2190 back" }),
8074
8118
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
8075
8119
  Table,
8076
8120
  {
@@ -8096,6 +8140,33 @@ function PoliciesPanel({ focused }) {
8096
8140
  status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
8097
8141
  ] }) });
8098
8142
  }
8143
+ if (view === "versions") {
8144
+ const vers = versionsQ.data?.versions ?? [];
8145
+ return /* @__PURE__ */ jsx3(DataView, { loading: versionsQ.loading && !versionsQ.data, error: versionsQ.error, empty: !!versionsQ.data && vers.length === 0, emptyText: "No versions.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
8146
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate2(selected?.name ?? "", 28) }),
8147
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 select \xB7 enter roll back to that version \xB7 \u2190 back" }),
8148
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
8149
+ Table,
8150
+ {
8151
+ columns: [
8152
+ { header: "", width: 2 },
8153
+ { header: "VER", width: 5 },
8154
+ { header: "RULES", width: 5 },
8155
+ { header: "REASON", width: 34 },
8156
+ { header: "WHEN", width: 16 }
8157
+ ],
8158
+ rows: vers.map((v, i) => [
8159
+ { value: i === vi ? "\u25B8" : "", color: theme.accentBright },
8160
+ { value: `v${v.version}`, bold: i === vi },
8161
+ { value: String(v.rules_count), dim: true },
8162
+ { value: truncate2(v.reason || "\u2014", 34) },
8163
+ { value: truncate2(v.created_at, 16), dim: true }
8164
+ ])
8165
+ }
8166
+ ) }),
8167
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
8168
+ ] }) });
8169
+ }
8099
8170
  const rule = rules[ri];
8100
8171
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
8101
8172
  /* @__PURE__ */ jsxs3(Box3, { children: [
@@ -8319,12 +8390,19 @@ function DlpPanel({ focused }) {
8319
8390
  const [adding, setAdding] = useState5(null);
8320
8391
  const [newName, setNewName] = useState5("");
8321
8392
  const [newRe, setNewRe] = useState5("");
8393
+ const [ghostOn, setGhostOn] = useState5(false);
8394
+ const spQ = useLoader(() => api.settings.getSelfProtection());
8395
+ const [selfProt, setSelfProt] = useState5(null);
8322
8396
  useEffect5(() => {
8323
8397
  if (q.data && !dlp) {
8324
8398
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
8325
8399
  setAvailable(q.data.availablePatterns);
8400
+ setGhostOn(q.data.layers.ghost.mode === "on");
8326
8401
  }
8327
8402
  }, [q.data, dlp]);
8403
+ useEffect5(() => {
8404
+ if (spQ.data && selfProt === null) setSelfProt(spQ.data.enabled);
8405
+ }, [spQ.data, selfProt]);
8328
8406
  const mutate = (next) => {
8329
8407
  setDlp(next);
8330
8408
  setDirty(true);
@@ -8334,14 +8412,27 @@ function DlpPanel({ focused }) {
8334
8412
  if (!dlp || !q.data) return;
8335
8413
  setStatus("Saving\u2026");
8336
8414
  try {
8337
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
8415
+ const ghost = { ...q.data.layers.ghost, mode: ghostOn ? "on" : "off" };
8416
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp, ghost });
8338
8417
  setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
8418
+ setGhostOn(res.layers.ghost.mode === "on");
8339
8419
  setDirty(false);
8340
8420
  setStatus("\u2713 Saved");
8341
8421
  } catch (e) {
8342
8422
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
8343
8423
  }
8344
8424
  };
8425
+ const toggleSelfProt = async () => {
8426
+ const next = !(selfProt ?? false);
8427
+ setSelfProt(next);
8428
+ try {
8429
+ await api.settings.setSelfProtection(next);
8430
+ setStatus(`\u2713 self-protection ${next ? "on" : "off"}`);
8431
+ } catch (e) {
8432
+ setSelfProt(!next);
8433
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
8434
+ }
8435
+ };
8345
8436
  const discard = () => {
8346
8437
  if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
8347
8438
  setDirty(false);
@@ -8371,7 +8462,12 @@ function DlpPanel({ focused }) {
8371
8462
  const ci = sel - available.length;
8372
8463
  mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
8373
8464
  setSel((n) => Math.max(0, n - 1));
8374
- } else if (input === "s") void save();
8465
+ } else if (input === "g") {
8466
+ setGhostOn((v) => !v);
8467
+ setDirty(true);
8468
+ setStatus(null);
8469
+ } else if (input === "P") void toggleSelfProt();
8470
+ else if (input === "s") void save();
8375
8471
  else if (input === "x") discard();
8376
8472
  },
8377
8473
  { isActive: focused && !adding }
@@ -8383,7 +8479,13 @@ function DlpPanel({ focused }) {
8383
8479
  /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
8384
8480
  dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
8385
8481
  ] }),
8386
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
8482
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 g ghost \xB7 P self-protect \xB7 s save" : "press \u2192 to edit" }),
8483
+ /* @__PURE__ */ jsxs5(Box5, { children: [
8484
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "ghost: " }),
8485
+ /* @__PURE__ */ jsx5(Text5, { color: ghostOn ? theme.ok : theme.dim, children: ghostOn ? "on" : "off" }),
8486
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " self-protection: " }),
8487
+ /* @__PURE__ */ jsx5(Text5, { color: selfProt ? theme.ok : theme.dim, children: selfProt === null ? "\u2026" : selfProt ? "on" : "off" })
8488
+ ] }),
8387
8489
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
8388
8490
  const on = enabled.has(p);
8389
8491
  const active2 = focused && sel === i;
@@ -8451,12 +8553,15 @@ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
8451
8553
  function StatsPanel({ active: active2 }) {
8452
8554
  const stats = useLoader(() => api.stats.get());
8453
8555
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
8556
+ const drift2 = useLoader(() => api.stats.drift(7));
8454
8557
  usePoll(() => {
8455
8558
  stats.reload();
8456
8559
  ts.reload();
8457
8560
  }, 5e3, active2);
8561
+ usePoll(drift2.reload, 3e4, active2);
8458
8562
  const s = stats.data;
8459
8563
  const points = ts.data?.timeseries ?? [];
8564
+ const driftRules = (drift2.data?.rules ?? []).slice(0, 5);
8460
8565
  return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
8461
8566
  /* @__PURE__ */ jsxs6(Box6, { children: [
8462
8567
  /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
@@ -8509,7 +8614,7 @@ function StatsPanel({ active: active2 }) {
8509
8614
  { header: "MS", width: 5 },
8510
8615
  { header: "WHEN", width: 6 }
8511
8616
  ],
8512
- rows: (s.recent_activity ?? []).slice(0, 8).map((a) => [
8617
+ rows: (s.recent_activity ?? []).slice(0, 6).map((a) => [
8513
8618
  { value: a.decision, color: decisionColor(a.decision) },
8514
8619
  { value: truncate2(a.tool_name, 20), color: theme.accent },
8515
8620
  { value: String(a.evaluation_time_ms ?? "\u2014"), dim: true },
@@ -8517,6 +8622,31 @@ function StatsPanel({ active: active2 }) {
8517
8622
  ])
8518
8623
  }
8519
8624
  )
8625
+ ] }),
8626
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
8627
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
8628
+ "Denial drift ",
8629
+ drift2.data ? `(7d: ${drift2.data.total_current} now vs ${drift2.data.total_previous} prev)` : ""
8630
+ ] }),
8631
+ driftRules.length ? /* @__PURE__ */ jsx6(
8632
+ Table,
8633
+ {
8634
+ columns: [
8635
+ { header: "NOW", width: 5 },
8636
+ { header: "PREV", width: 5 },
8637
+ { header: "\u0394", width: 6 },
8638
+ { header: "RULE", width: 22 },
8639
+ { header: "REASON", width: 26 }
8640
+ ],
8641
+ rows: driftRules.map((r) => [
8642
+ { value: String(r.current), bold: true },
8643
+ { value: String(r.previous), dim: true },
8644
+ { value: r.is_new ? "NEW" : r.spike ? `+${r.delta}` : String(r.delta), color: r.is_new ? theme.ok : r.spike ? theme.bad : void 0 },
8645
+ { value: truncate2(r.rule_id ?? "\u2014", 22), color: theme.accent },
8646
+ { value: truncate2(r.reason ?? r.last_tool ?? "\u2014", 26), dim: true }
8647
+ ])
8648
+ }
8649
+ ) : /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " no denials in window" })
8520
8650
  ] })
8521
8651
  ] }) : null });
8522
8652
  }
@@ -8742,7 +8872,16 @@ function App() {
8742
8872
  const { exit } = useApp();
8743
8873
  const [section, setSection] = useState7(0);
8744
8874
  const [focus, setFocus] = useState7("nav");
8875
+ const [help, setHelp] = useState7(false);
8745
8876
  useInput6((input, key) => {
8877
+ if (help) {
8878
+ setHelp(false);
8879
+ return;
8880
+ }
8881
+ if (input === "?" && focus === "nav") {
8882
+ setHelp(true);
8883
+ return;
8884
+ }
8746
8885
  if (focus === "nav") {
8747
8886
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
8748
8887
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
@@ -8756,6 +8895,7 @@ function App() {
8756
8895
  const cols = process.stdout.columns ?? 100;
8757
8896
  const rows = process.stdout.rows ?? 30;
8758
8897
  const current = SECTIONS[section];
8898
+ if (help) return /* @__PURE__ */ jsx8(HelpOverlay, { cols, rows });
8759
8899
  if (current.label === "Live" && focus === "panel") {
8760
8900
  return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
8761
8901
  }
@@ -8766,10 +8906,23 @@ function App() {
8766
8906
  /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
8767
8907
  /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" }) })
8768
8908
  ] }),
8769
- /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
8909
+ /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["?", "help"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
8770
8910
  ] });
8771
8911
  }
8772
- var SECTIONS, BANNER_HEX;
8912
+ function HelpOverlay({ cols, rows }) {
8913
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 2, paddingTop: 1, children: [
8914
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accentBright, children: "SolonGate \u2014 keyboard shortcuts" }),
8915
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: HELP.map(([group, keys]) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
8916
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accent, children: group }),
8917
+ keys.map(([k, desc]) => /* @__PURE__ */ jsxs8(Text8, { children: [
8918
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: (" " + k).padEnd(16) }),
8919
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: desc })
8920
+ ] }, k))
8921
+ ] }, group)) }),
8922
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "press any key to close" })
8923
+ ] });
8924
+ }
8925
+ var SECTIONS, BANNER_HEX, HELP;
8773
8926
  var init_App = __esm({
8774
8927
  "src/tui/App.tsx"() {
8775
8928
  "use strict";
@@ -8791,6 +8944,12 @@ var init_App = __esm({
8791
8944
  { label: "Audit", Panel: AuditPanel }
8792
8945
  ];
8793
8946
  BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
8947
+ HELP = [
8948
+ ["Global", [["\u2191\u2193", "move between sections"], ["\u2192 / enter", "open a section"], ["\u2190 / esc", "back to the menu"], ["?", "this help"], ["q", "quit"]]],
8949
+ ["Live", [["\u2191\u2193", "select a stream row"], ["w", "whitelist the selected DENY"], ["b", "block the selected ALLOW"], ["d / x / r", "filter denies / dlp / rate-limit"], ["f", "local / cloud filter"], ["/", "search"], ["s", "sessions (\u2191\u2193 pick, enter open)"], ["space", "copy mode (freeze)"]]],
8950
+ ["Policies", [["\u2191\u2193", "browse / select"], ["enter", "open rules \u2192 open a rule"], ["space", "toggle a rule on/off"], ["e", "flip effect"], ["n", "new rule"], ["d", "delete rule"], ["m", "flip mode"], ["D", "dry-run the draft"], ["v", "versions / rollback"], ["s", "save"], ["x", "discard"]]],
8951
+ ["Rate limit / DLP", [["\u2191\u2193", "field / pattern"], ["\u2190\u2192", "adjust (shift = \xB110)"], ["space", "toggle pattern"], ["m", "mode"], ["a / d", "add / remove custom"], ["g", "ghost mode"], ["P", "self-protection"], ["s", "save"]]]
8952
+ ];
8794
8953
  }
8795
8954
  });
8796
8955
 
package/dist/tui/index.js CHANGED
@@ -333,8 +333,10 @@ __export(settings_exports, {
333
333
  getGuardStatus: () => getGuardStatus,
334
334
  getRateLimitHistory: () => getRateLimitHistory,
335
335
  getSecurityLayers: () => getSecurityLayers,
336
+ getSelfProtection: () => getSelfProtection,
336
337
  getWebhooks: () => getWebhooks,
337
- setSecurityLayers: () => setSecurityLayers
338
+ setSecurityLayers: () => setSecurityLayers,
339
+ setSelfProtection: () => setSelfProtection
338
340
  });
339
341
  function getSecurityLayers() {
340
342
  return request("GET", "/settings/security-layers");
@@ -351,6 +353,12 @@ function clearRateLimitHistory() {
351
353
  function getGuardStatus() {
352
354
  return request("GET", "/settings/guard-status");
353
355
  }
356
+ function getSelfProtection() {
357
+ return request("GET", "/settings/self-protection");
358
+ }
359
+ function setSelfProtection(enabled) {
360
+ return request("PUT", "/settings/self-protection", { body: { enabled } });
361
+ }
354
362
  function getAlerts() {
355
363
  return request("GET", "/settings/denial-alerts");
356
364
  }
@@ -1347,6 +1355,7 @@ function PoliciesPanel({ focused }) {
1347
1355
  const [pi, setPi] = useState3(0);
1348
1356
  const [ri, setRi] = useState3(0);
1349
1357
  const [fi, setFi] = useState3(0);
1358
+ const [vi, setVi] = useState3(0);
1350
1359
  const [rules, setRules] = useState3([]);
1351
1360
  const [mode, setMode] = useState3("denylist");
1352
1361
  const [dirty, setDirty] = useState3(false);
@@ -1355,6 +1364,10 @@ function PoliciesPanel({ focused }) {
1355
1364
  const [status, setStatus] = useState3(null);
1356
1365
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
1357
1366
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
1367
+ const versionsQ = useLoader(
1368
+ () => view === "versions" && selected ? api.policies.versions(selected.id, { limit: 50 }) : Promise.resolve(null),
1369
+ [view, selected?.id]
1370
+ );
1358
1371
  useEffect3(() => {
1359
1372
  if (detail.data) {
1360
1373
  setRules(detail.data.rules);
@@ -1429,10 +1442,41 @@ function PoliciesPanel({ focused }) {
1429
1442
  }
1430
1443
  } else if (input === "m") {
1431
1444
  mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
1445
+ } else if (input === "n") {
1446
+ const nr = { id: `rule-${Date.now()}`, description: "", effect: "DENY", priority: 100, toolPattern: "*", minimumTrustLevel: "UNTRUSTED", enabled: true };
1447
+ mutate([nr, ...rules]);
1448
+ setRi(0);
1449
+ setFi(0);
1450
+ setView("rule");
1451
+ } else if (input === "D") {
1452
+ setStatus("Dry-running\u2026");
1453
+ void api.policies.dryRun({ rules, mode }).then((res) => setStatus(`dry-run ${res.evaluated} calls \xB7 allow ${res.would_allow} / deny ${res.would_deny} \xB7 newly blocked ${res.newly_blocked} \xB7 newly allowed ${res.newly_allowed}`)).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
1454
+ } else if (input === "v") {
1455
+ setVi(0);
1456
+ setView("versions");
1432
1457
  } else if (input === "s") void save();
1433
1458
  else if (input === "x") discard();
1434
1459
  return;
1435
1460
  }
1461
+ if (view === "versions") {
1462
+ const vers = versionsQ.data?.versions ?? [];
1463
+ if (key.leftArrow) return void setView("rules");
1464
+ if (key.upArrow) setVi((n) => Math.max(0, n - 1));
1465
+ else if (key.downArrow) setVi((n) => Math.min(vers.length - 1, n + 1));
1466
+ else if (key.return) {
1467
+ const v = vers[vi];
1468
+ if (v && selected) {
1469
+ setStatus("Rolling back\u2026");
1470
+ void api.policies.rollback(selected.id, v.version).then((r) => {
1471
+ setStatus(`\u2713 rolled back to v${v.version} \u2192 new v${r.version}`);
1472
+ detail.reload();
1473
+ list5.reload();
1474
+ setView("rules");
1475
+ }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
1476
+ }
1477
+ }
1478
+ return;
1479
+ }
1436
1480
  const rule2 = rules[ri];
1437
1481
  if (!rule2) return setView("rules");
1438
1482
  const field = FIELDS[fi];
@@ -1477,7 +1521,7 @@ function PoliciesPanel({ focused }) {
1477
1521
  /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
1478
1522
  dirtyTag
1479
1523
  ] }),
1480
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
1524
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 n new \xB7 d del \xB7 m mode \xB7 D dry-run \xB7 v versions \xB7 s save \xB7 \u2190 back" }),
1481
1525
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
1482
1526
  Table,
1483
1527
  {
@@ -1503,6 +1547,33 @@ function PoliciesPanel({ focused }) {
1503
1547
  status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
1504
1548
  ] }) });
1505
1549
  }
1550
+ if (view === "versions") {
1551
+ const vers = versionsQ.data?.versions ?? [];
1552
+ return /* @__PURE__ */ jsx3(DataView, { loading: versionsQ.loading && !versionsQ.data, error: versionsQ.error, empty: !!versionsQ.data && vers.length === 0, emptyText: "No versions.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1553
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
1554
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 select \xB7 enter roll back to that version \xB7 \u2190 back" }),
1555
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
1556
+ Table,
1557
+ {
1558
+ columns: [
1559
+ { header: "", width: 2 },
1560
+ { header: "VER", width: 5 },
1561
+ { header: "RULES", width: 5 },
1562
+ { header: "REASON", width: 34 },
1563
+ { header: "WHEN", width: 16 }
1564
+ ],
1565
+ rows: vers.map((v, i) => [
1566
+ { value: i === vi ? "\u25B8" : "", color: theme.accentBright },
1567
+ { value: `v${v.version}`, bold: i === vi },
1568
+ { value: String(v.rules_count), dim: true },
1569
+ { value: truncate(v.reason || "\u2014", 34) },
1570
+ { value: truncate(v.created_at, 16), dim: true }
1571
+ ])
1572
+ }
1573
+ ) }),
1574
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
1575
+ ] }) });
1576
+ }
1506
1577
  const rule = rules[ri];
1507
1578
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1508
1579
  /* @__PURE__ */ jsxs3(Box3, { children: [
@@ -1686,12 +1757,19 @@ function DlpPanel({ focused }) {
1686
1757
  const [adding, setAdding] = useState5(null);
1687
1758
  const [newName, setNewName] = useState5("");
1688
1759
  const [newRe, setNewRe] = useState5("");
1760
+ const [ghostOn, setGhostOn] = useState5(false);
1761
+ const spQ = useLoader(() => api.settings.getSelfProtection());
1762
+ const [selfProt, setSelfProt] = useState5(null);
1689
1763
  useEffect5(() => {
1690
1764
  if (q.data && !dlp) {
1691
1765
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
1692
1766
  setAvailable(q.data.availablePatterns);
1767
+ setGhostOn(q.data.layers.ghost.mode === "on");
1693
1768
  }
1694
1769
  }, [q.data, dlp]);
1770
+ useEffect5(() => {
1771
+ if (spQ.data && selfProt === null) setSelfProt(spQ.data.enabled);
1772
+ }, [spQ.data, selfProt]);
1695
1773
  const mutate = (next) => {
1696
1774
  setDlp(next);
1697
1775
  setDirty(true);
@@ -1701,14 +1779,27 @@ function DlpPanel({ focused }) {
1701
1779
  if (!dlp || !q.data) return;
1702
1780
  setStatus("Saving\u2026");
1703
1781
  try {
1704
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
1782
+ const ghost = { ...q.data.layers.ghost, mode: ghostOn ? "on" : "off" };
1783
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp, ghost });
1705
1784
  setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
1785
+ setGhostOn(res.layers.ghost.mode === "on");
1706
1786
  setDirty(false);
1707
1787
  setStatus("\u2713 Saved");
1708
1788
  } catch (e) {
1709
1789
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1710
1790
  }
1711
1791
  };
1792
+ const toggleSelfProt = async () => {
1793
+ const next = !(selfProt ?? false);
1794
+ setSelfProt(next);
1795
+ try {
1796
+ await api.settings.setSelfProtection(next);
1797
+ setStatus(`\u2713 self-protection ${next ? "on" : "off"}`);
1798
+ } catch (e) {
1799
+ setSelfProt(!next);
1800
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1801
+ }
1802
+ };
1712
1803
  const discard = () => {
1713
1804
  if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
1714
1805
  setDirty(false);
@@ -1738,7 +1829,12 @@ function DlpPanel({ focused }) {
1738
1829
  const ci = sel - available.length;
1739
1830
  mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
1740
1831
  setSel((n) => Math.max(0, n - 1));
1741
- } else if (input === "s") void save();
1832
+ } else if (input === "g") {
1833
+ setGhostOn((v) => !v);
1834
+ setDirty(true);
1835
+ setStatus(null);
1836
+ } else if (input === "P") void toggleSelfProt();
1837
+ else if (input === "s") void save();
1742
1838
  else if (input === "x") discard();
1743
1839
  },
1744
1840
  { isActive: focused && !adding }
@@ -1750,7 +1846,13 @@ function DlpPanel({ focused }) {
1750
1846
  /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
1751
1847
  dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
1752
1848
  ] }),
1753
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
1849
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 g ghost \xB7 P self-protect \xB7 s save" : "press \u2192 to edit" }),
1850
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1851
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "ghost: " }),
1852
+ /* @__PURE__ */ jsx5(Text5, { color: ghostOn ? theme.ok : theme.dim, children: ghostOn ? "on" : "off" }),
1853
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " self-protection: " }),
1854
+ /* @__PURE__ */ jsx5(Text5, { color: selfProt ? theme.ok : theme.dim, children: selfProt === null ? "\u2026" : selfProt ? "on" : "off" })
1855
+ ] }),
1754
1856
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
1755
1857
  const on = enabled.has(p);
1756
1858
  const active2 = focused && sel === i;
@@ -1807,12 +1909,15 @@ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1807
1909
  function StatsPanel({ active: active2 }) {
1808
1910
  const stats = useLoader(() => api.stats.get());
1809
1911
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
1912
+ const drift2 = useLoader(() => api.stats.drift(7));
1810
1913
  usePoll(() => {
1811
1914
  stats.reload();
1812
1915
  ts.reload();
1813
1916
  }, 5e3, active2);
1917
+ usePoll(drift2.reload, 3e4, active2);
1814
1918
  const s = stats.data;
1815
1919
  const points = ts.data?.timeseries ?? [];
1920
+ const driftRules = (drift2.data?.rules ?? []).slice(0, 5);
1816
1921
  return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1817
1922
  /* @__PURE__ */ jsxs6(Box6, { children: [
1818
1923
  /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
@@ -1865,7 +1970,7 @@ function StatsPanel({ active: active2 }) {
1865
1970
  { header: "MS", width: 5 },
1866
1971
  { header: "WHEN", width: 6 }
1867
1972
  ],
1868
- rows: (s.recent_activity ?? []).slice(0, 8).map((a) => [
1973
+ rows: (s.recent_activity ?? []).slice(0, 6).map((a) => [
1869
1974
  { value: a.decision, color: decisionColor(a.decision) },
1870
1975
  { value: truncate(a.tool_name, 20), color: theme.accent },
1871
1976
  { value: String(a.evaluation_time_ms ?? "\u2014"), dim: true },
@@ -1873,6 +1978,31 @@ function StatsPanel({ active: active2 }) {
1873
1978
  ])
1874
1979
  }
1875
1980
  )
1981
+ ] }),
1982
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1983
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
1984
+ "Denial drift ",
1985
+ drift2.data ? `(7d: ${drift2.data.total_current} now vs ${drift2.data.total_previous} prev)` : ""
1986
+ ] }),
1987
+ driftRules.length ? /* @__PURE__ */ jsx6(
1988
+ Table,
1989
+ {
1990
+ columns: [
1991
+ { header: "NOW", width: 5 },
1992
+ { header: "PREV", width: 5 },
1993
+ { header: "\u0394", width: 6 },
1994
+ { header: "RULE", width: 22 },
1995
+ { header: "REASON", width: 26 }
1996
+ ],
1997
+ rows: driftRules.map((r) => [
1998
+ { value: String(r.current), bold: true },
1999
+ { value: String(r.previous), dim: true },
2000
+ { value: r.is_new ? "NEW" : r.spike ? `+${r.delta}` : String(r.delta), color: r.is_new ? theme.ok : r.spike ? theme.bad : void 0 },
2001
+ { value: truncate(r.rule_id ?? "\u2014", 22), color: theme.accent },
2002
+ { value: truncate(r.reason ?? r.last_tool ?? "\u2014", 26), dim: true }
2003
+ ])
2004
+ }
2005
+ ) : /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " no denials in window" })
1876
2006
  ] })
1877
2007
  ] }) : null });
1878
2008
  }
@@ -2086,7 +2216,16 @@ function App() {
2086
2216
  const { exit } = useApp();
2087
2217
  const [section, setSection] = useState7(0);
2088
2218
  const [focus, setFocus] = useState7("nav");
2219
+ const [help, setHelp] = useState7(false);
2089
2220
  useInput6((input, key) => {
2221
+ if (help) {
2222
+ setHelp(false);
2223
+ return;
2224
+ }
2225
+ if (input === "?" && focus === "nav") {
2226
+ setHelp(true);
2227
+ return;
2228
+ }
2090
2229
  if (focus === "nav") {
2091
2230
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
2092
2231
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
@@ -2100,6 +2239,7 @@ function App() {
2100
2239
  const cols = process.stdout.columns ?? 100;
2101
2240
  const rows = process.stdout.rows ?? 30;
2102
2241
  const current = SECTIONS[section];
2242
+ if (help) return /* @__PURE__ */ jsx8(HelpOverlay, { cols, rows });
2103
2243
  if (current.label === "Live" && focus === "panel") {
2104
2244
  return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
2105
2245
  }
@@ -2110,7 +2250,26 @@ function App() {
2110
2250
  /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
2111
2251
  /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" }) })
2112
2252
  ] }),
2113
- /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
2253
+ /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["?", "help"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
2254
+ ] });
2255
+ }
2256
+ var HELP = [
2257
+ ["Global", [["\u2191\u2193", "move between sections"], ["\u2192 / enter", "open a section"], ["\u2190 / esc", "back to the menu"], ["?", "this help"], ["q", "quit"]]],
2258
+ ["Live", [["\u2191\u2193", "select a stream row"], ["w", "whitelist the selected DENY"], ["b", "block the selected ALLOW"], ["d / x / r", "filter denies / dlp / rate-limit"], ["f", "local / cloud filter"], ["/", "search"], ["s", "sessions (\u2191\u2193 pick, enter open)"], ["space", "copy mode (freeze)"]]],
2259
+ ["Policies", [["\u2191\u2193", "browse / select"], ["enter", "open rules \u2192 open a rule"], ["space", "toggle a rule on/off"], ["e", "flip effect"], ["n", "new rule"], ["d", "delete rule"], ["m", "flip mode"], ["D", "dry-run the draft"], ["v", "versions / rollback"], ["s", "save"], ["x", "discard"]]],
2260
+ ["Rate limit / DLP", [["\u2191\u2193", "field / pattern"], ["\u2190\u2192", "adjust (shift = \xB110)"], ["space", "toggle pattern"], ["m", "mode"], ["a / d", "add / remove custom"], ["g", "ghost mode"], ["P", "self-protection"], ["s", "save"]]]
2261
+ ];
2262
+ function HelpOverlay({ cols, rows }) {
2263
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 2, paddingTop: 1, children: [
2264
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accentBright, children: "SolonGate \u2014 keyboard shortcuts" }),
2265
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: HELP.map(([group, keys]) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
2266
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accent, children: group }),
2267
+ keys.map(([k, desc]) => /* @__PURE__ */ jsxs8(Text8, { children: [
2268
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: (" " + k).padEnd(16) }),
2269
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: desc })
2270
+ ] }, k))
2271
+ ] }, group)) }),
2272
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "press any key to close" })
2114
2273
  ] });
2115
2274
  }
2116
2275
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
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": {