@solongate/proxy 0.81.33 → 0.81.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -7409,6 +7409,7 @@ function StreamLine({ e, loc, selected }) {
|
|
|
7409
7409
|
/* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
|
|
7410
7410
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate2(e.agent ?? "-", 11).padEnd(12) }),
|
|
7411
7411
|
e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
|
|
7412
|
+
e.burst ? /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "RL! " }) : null,
|
|
7412
7413
|
e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate2(e.rule, 14) + " " }) : null,
|
|
7413
7414
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
|
|
7414
7415
|
] });
|
|
@@ -9258,6 +9259,7 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
9258
9259
|
/* @__PURE__ */ jsx6(Detail, { label: "agent", value: current.agent ?? "\u2014" }),
|
|
9259
9260
|
/* @__PURE__ */ jsx6(Detail, { label: "session", value: current.session ?? "\u2014" }),
|
|
9260
9261
|
/* @__PURE__ */ jsx6(Detail, { label: "dlp", value: current.dlp.length ? current.dlp.join(", ") : "none", color: current.dlp.length ? theme.bad : void 0 }),
|
|
9262
|
+
/* @__PURE__ */ jsx6(Detail, { label: "ratelimit", value: current.burst ? "BURST \u2014 over the limit for this agent" : "no", color: current.burst ? theme.bad : void 0 }),
|
|
9261
9263
|
/* @__PURE__ */ jsx6(Detail, { label: "when", value: new Date(current.at).toLocaleString() + (current.evalMs != null ? ` \xB7 eval ${current.evalMs}ms` : "") }),
|
|
9262
9264
|
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: `arguments \u2014 full \xB7 ${argLines.length} lines${maxScroll ? ` \xB7 \u25BC${maxScroll - off} more \xB7 \u2191\u2193 scroll` : ""}` }),
|
|
9263
9265
|
/* @__PURE__ */ jsx6(Box6, { flexDirection: "column", height: argBudget, overflow: "hidden", children: argWin.map((l, i) => /* @__PURE__ */ jsx6(Text6, { wrap: "truncate", children: l || " " }, off + i)) }),
|
|
@@ -9372,7 +9374,7 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
9372
9374
|
const maxStart = Math.max(0, pageRows.length - listRows);
|
|
9373
9375
|
const start = Math.min(Math.max(0, selClamped - Math.floor((listRows - 1) / 2)), maxStart);
|
|
9374
9376
|
const windowed = pageRows.slice(start, start + listRows);
|
|
9375
|
-
const reasonW = Math.min(
|
|
9377
|
+
const reasonW = Math.min(58, Math.max(12, cols - 71));
|
|
9376
9378
|
return /* @__PURE__ */ jsx6(DataView, { loading: logsLoading && !frozen, error: source === "cloud" ? cloudQ.error : localQ.error, children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
9377
9379
|
strip,
|
|
9378
9380
|
copyBanner,
|
|
@@ -9417,7 +9419,7 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
9417
9419
|
{ header: "TOOL", width: 18 },
|
|
9418
9420
|
{ header: "AGENT", width: 14 },
|
|
9419
9421
|
{ header: "REASON", width: reasonW },
|
|
9420
|
-
{ header: "
|
|
9422
|
+
{ header: "SIGNAL", width: 8 },
|
|
9421
9423
|
{ header: "WHEN", width: 6 }
|
|
9422
9424
|
],
|
|
9423
9425
|
rows: windowed.map((e, i) => rowFor(e, start + i === selClamped && focused, reasonW))
|
|
@@ -9431,13 +9433,14 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
9431
9433
|
] }) });
|
|
9432
9434
|
}
|
|
9433
9435
|
function rowFor(e, active2, reasonW) {
|
|
9436
|
+
const sig = [e.dlp.length ? `DLP${e.dlp.length > 1 ? e.dlp.length : ""}` : "", e.burst ? "RL" : ""].filter(Boolean).join(" ");
|
|
9434
9437
|
return [
|
|
9435
9438
|
{ value: active2 ? "\u25B8" : "", color: theme.accentBright },
|
|
9436
9439
|
{ value: e.decision, color: decisionColor(e.decision) },
|
|
9437
9440
|
{ value: truncate2(e.tool, 18), color: theme.accent },
|
|
9438
9441
|
{ value: truncate2(e.agent ?? "\u2014", 14), dim: true },
|
|
9439
9442
|
{ value: truncate2(e.reason ?? e.args ?? "\u2014", reasonW) },
|
|
9440
|
-
{ value:
|
|
9443
|
+
{ value: sig || "\xB7", color: sig ? theme.bad : void 0, dim: !sig },
|
|
9441
9444
|
{ value: ago(e.at), dim: true }
|
|
9442
9445
|
];
|
|
9443
9446
|
}
|
|
@@ -9848,7 +9851,7 @@ var init_App = __esm({
|
|
|
9848
9851
|
{ label: "Audit", Panel: AuditPanel },
|
|
9849
9852
|
{ label: "Settings", Panel: SettingsPanel }
|
|
9850
9853
|
];
|
|
9851
|
-
BANNER_HEX = ["
|
|
9854
|
+
BANNER_HEX = ["white", "white", "white", "white", "white", "white"];
|
|
9852
9855
|
HELP = [
|
|
9853
9856
|
["Global", [["\u2191\u2193", "move between sections"], ["\u2192 / enter", "open a section"], ["\u2190 / esc", "back to the menu"], ["?", "this help"], ["q", "quit"]]],
|
|
9854
9857
|
["Live", [["\u2191\u2193", "select a stream row"], ["enter", "full entry content"], ["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)"]]],
|
package/dist/tui/index.js
CHANGED
|
@@ -802,6 +802,7 @@ function StreamLine({ e, loc, selected }) {
|
|
|
802
802
|
/* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
|
|
803
803
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent ?? "-", 11).padEnd(12) }),
|
|
804
804
|
e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
|
|
805
|
+
e.burst ? /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "RL! " }) : null,
|
|
805
806
|
e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate(e.rule, 14) + " " }) : null,
|
|
806
807
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
|
|
807
808
|
] });
|
|
@@ -2665,6 +2666,7 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
2665
2666
|
/* @__PURE__ */ jsx6(Detail, { label: "agent", value: current.agent ?? "\u2014" }),
|
|
2666
2667
|
/* @__PURE__ */ jsx6(Detail, { label: "session", value: current.session ?? "\u2014" }),
|
|
2667
2668
|
/* @__PURE__ */ jsx6(Detail, { label: "dlp", value: current.dlp.length ? current.dlp.join(", ") : "none", color: current.dlp.length ? theme.bad : void 0 }),
|
|
2669
|
+
/* @__PURE__ */ jsx6(Detail, { label: "ratelimit", value: current.burst ? "BURST \u2014 over the limit for this agent" : "no", color: current.burst ? theme.bad : void 0 }),
|
|
2668
2670
|
/* @__PURE__ */ jsx6(Detail, { label: "when", value: new Date(current.at).toLocaleString() + (current.evalMs != null ? ` \xB7 eval ${current.evalMs}ms` : "") }),
|
|
2669
2671
|
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: `arguments \u2014 full \xB7 ${argLines.length} lines${maxScroll ? ` \xB7 \u25BC${maxScroll - off} more \xB7 \u2191\u2193 scroll` : ""}` }),
|
|
2670
2672
|
/* @__PURE__ */ jsx6(Box6, { flexDirection: "column", height: argBudget, overflow: "hidden", children: argWin.map((l, i) => /* @__PURE__ */ jsx6(Text6, { wrap: "truncate", children: l || " " }, off + i)) }),
|
|
@@ -2779,7 +2781,7 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
2779
2781
|
const maxStart = Math.max(0, pageRows.length - listRows);
|
|
2780
2782
|
const start = Math.min(Math.max(0, selClamped - Math.floor((listRows - 1) / 2)), maxStart);
|
|
2781
2783
|
const windowed = pageRows.slice(start, start + listRows);
|
|
2782
|
-
const reasonW = Math.min(
|
|
2784
|
+
const reasonW = Math.min(58, Math.max(12, cols - 71));
|
|
2783
2785
|
return /* @__PURE__ */ jsx6(DataView, { loading: logsLoading && !frozen, error: source === "cloud" ? cloudQ.error : localQ.error, children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
2784
2786
|
strip,
|
|
2785
2787
|
copyBanner,
|
|
@@ -2824,7 +2826,7 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
2824
2826
|
{ header: "TOOL", width: 18 },
|
|
2825
2827
|
{ header: "AGENT", width: 14 },
|
|
2826
2828
|
{ header: "REASON", width: reasonW },
|
|
2827
|
-
{ header: "
|
|
2829
|
+
{ header: "SIGNAL", width: 8 },
|
|
2828
2830
|
{ header: "WHEN", width: 6 }
|
|
2829
2831
|
],
|
|
2830
2832
|
rows: windowed.map((e, i) => rowFor(e, start + i === selClamped && focused, reasonW))
|
|
@@ -2838,13 +2840,14 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
2838
2840
|
] }) });
|
|
2839
2841
|
}
|
|
2840
2842
|
function rowFor(e, active2, reasonW) {
|
|
2843
|
+
const sig = [e.dlp.length ? `DLP${e.dlp.length > 1 ? e.dlp.length : ""}` : "", e.burst ? "RL" : ""].filter(Boolean).join(" ");
|
|
2841
2844
|
return [
|
|
2842
2845
|
{ value: active2 ? "\u25B8" : "", color: theme.accentBright },
|
|
2843
2846
|
{ value: e.decision, color: decisionColor(e.decision) },
|
|
2844
2847
|
{ value: truncate(e.tool, 18), color: theme.accent },
|
|
2845
2848
|
{ value: truncate(e.agent ?? "\u2014", 14), dim: true },
|
|
2846
2849
|
{ value: truncate(e.reason ?? e.args ?? "\u2014", reasonW) },
|
|
2847
|
-
{ value:
|
|
2850
|
+
{ value: sig || "\xB7", color: sig ? theme.bad : void 0, dim: !sig },
|
|
2848
2851
|
{ value: ago(e.at), dim: true }
|
|
2849
2852
|
];
|
|
2850
2853
|
}
|
|
@@ -3082,7 +3085,7 @@ var SECTIONS = [
|
|
|
3082
3085
|
{ label: "Audit", Panel: AuditPanel },
|
|
3083
3086
|
{ label: "Settings", Panel: SettingsPanel }
|
|
3084
3087
|
];
|
|
3085
|
-
var BANNER_HEX = ["
|
|
3088
|
+
var BANNER_HEX = ["white", "white", "white", "white", "white", "white"];
|
|
3086
3089
|
function Banner({ cols }) {
|
|
3087
3090
|
const wide = cols >= 82;
|
|
3088
3091
|
if (!wide) {
|
|
@@ -2,3 +2,7 @@
|
|
|
2
2
|
{"ms":4,"ts":1784136544239,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
3
3
|
{"ms":2,"ts":1784136555335,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
4
4
|
{"ms":4,"ts":1784136601528,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
5
|
+
{"ms":2,"ts":1784137482457,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
6
|
+
{"ms":4,"ts":1784137524268,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
7
|
+
{"ms":5,"ts":1784138159970,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
8
|
+
{"ms":2,"ts":1784138167398,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"tool":"Bash","ts":
|
|
1
|
+
{"tool":"Bash","ts":1784138162061}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"ms":
|
|
1
|
+
{"ms":2,"ts":1784138167398,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1784138162060
|
package/hooks/audit.mjs
CHANGED
|
@@ -8,10 +8,10 @@ import { readFileSync, existsSync, writeFileSync, mkdirSync, appendFileSync } fr
|
|
|
8
8
|
import { resolve, join, isAbsolute } from 'node:path';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
|
|
11
|
-
// Bump on every audit
|
|
11
|
+
// Bump on every audit hook change. The cloud serves the newest version; the guard
|
|
12
12
|
// hook installs it on its next run (no re-login needed). See guard.mjs
|
|
13
13
|
// fetchAndInstallHook / maybeSelfUpdate.
|
|
14
|
-
const HOOK_VERSION =
|
|
14
|
+
const HOOK_VERSION = 16;
|
|
15
15
|
|
|
16
16
|
function loadEnvKey(dir) {
|
|
17
17
|
try {
|
|
@@ -268,6 +268,81 @@ function loadDlpRedact() {
|
|
|
268
268
|
} catch { return null; }
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// ── DETECT-mode observation ──
|
|
272
|
+
// In DETECT mode the guard doesn't block (and doesn't log) — the call is ALLOWED
|
|
273
|
+
// and reaches THIS PostToolUse hook. To make "detect" mean observe-AND-record
|
|
274
|
+
// (not silent), we scan the ARGUMENTS for DLP hits and flag rate-limit bursts
|
|
275
|
+
// here, on the ALLOW entry. Block-mode hits are DENIED upstream and carry their
|
|
276
|
+
// own reason, so this only fires for detect-mode ALLOWs. Everything is wrapped
|
|
277
|
+
// fail-safe: any error just omits the field.
|
|
278
|
+
|
|
279
|
+
// Which DLP patterns match the stringified tool arguments (uses the enabled set
|
|
280
|
+
// the guard cached; block+detect both populate dlpRedact). Names only.
|
|
281
|
+
function dlpScanArgs(argsSummary, dlpCfg) {
|
|
282
|
+
try {
|
|
283
|
+
if (!dlpCfg || !Array.isArray(dlpCfg.patterns) || !dlpCfg.patterns.length) return [];
|
|
284
|
+
const text = typeof argsSummary === 'string' ? argsSummary : JSON.stringify(argsSummary || {});
|
|
285
|
+
if (!text) return [];
|
|
286
|
+
const enabled = new Set(dlpCfg.patterns);
|
|
287
|
+
const hits = [];
|
|
288
|
+
for (const p of DLP_PATTERNS) {
|
|
289
|
+
if (!enabled.has(p.name)) continue;
|
|
290
|
+
try { p.re.lastIndex = 0; if (p.re.test(text)) hits.push(p.name); } catch { /* skip */ }
|
|
291
|
+
if (hits.length >= 5) break;
|
|
292
|
+
}
|
|
293
|
+
for (const c of (dlpCfg.custom || [])) {
|
|
294
|
+
try { if (c && c.re && dlpGlobToRe(c.re, 'gi').test(text)) hits.push(c.name || 'custom'); } catch { /* skip */ }
|
|
295
|
+
if (hits.length >= 5) break;
|
|
296
|
+
}
|
|
297
|
+
return hits;
|
|
298
|
+
} catch { return []; }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Detect-mode rate-limit config (delivered only when mode === 'detect').
|
|
302
|
+
function loadRateLimitObserve() {
|
|
303
|
+
try {
|
|
304
|
+
const sel = (process.env.SOLONGATE_AGENT_ID || process.argv[2] || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
305
|
+
const f = resolve(homedir(), '.solongate', '.policy-cache-' + sel + '.json');
|
|
306
|
+
if (!existsSync(f)) return null;
|
|
307
|
+
const c = JSON.parse(readFileSync(f, 'utf-8'));
|
|
308
|
+
const r = c && c.security && c.security.rateLimitObserve;
|
|
309
|
+
return r && typeof r === 'object' ? r : null;
|
|
310
|
+
} catch { return null; }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Sliding-window burst check for DETECT mode. Uses a SEPARATE stamps file from
|
|
314
|
+
// the guard's enforcement file so observation never interferes with blocking.
|
|
315
|
+
// Returns true when this call is at/over an enabled window's limit.
|
|
316
|
+
function rateLimitObserveBurst(agentKey, limits) {
|
|
317
|
+
try {
|
|
318
|
+
if (!limits) return false;
|
|
319
|
+
const key = String(agentKey || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
320
|
+
const file = resolve(homedir(), '.solongate', '.ratelimit-observe-' + key + '.json');
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
let stamps = [];
|
|
323
|
+
if (existsSync(file)) { try { stamps = JSON.parse(readFileSync(file, 'utf-8')); } catch { stamps = []; } }
|
|
324
|
+
if (!Array.isArray(stamps)) stamps = [];
|
|
325
|
+
stamps = stamps.filter((t) => typeof t === 'number' && now - t < 86400000);
|
|
326
|
+
if (stamps.length > 50000) stamps = stamps.slice(-50000);
|
|
327
|
+
const windows = [
|
|
328
|
+
{ key: 'perDay', ms: 86400000 },
|
|
329
|
+
{ key: 'perHour', ms: 3600000 },
|
|
330
|
+
{ key: 'perMinute', ms: 60000 },
|
|
331
|
+
];
|
|
332
|
+
let burst = false;
|
|
333
|
+
for (const w of windows) {
|
|
334
|
+
const limit = limits[w.key];
|
|
335
|
+
if (limit > 0) {
|
|
336
|
+
const count = stamps.reduce((n, t) => (now - t < w.ms ? n + 1 : n), 0);
|
|
337
|
+
if (count >= limit) { burst = true; break; }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
stamps.push(now);
|
|
341
|
+
try { writeFileSync(file, JSON.stringify(stamps)); } catch { /* best-effort */ }
|
|
342
|
+
return burst;
|
|
343
|
+
} catch { return false; }
|
|
344
|
+
}
|
|
345
|
+
|
|
271
346
|
// Local log storage: the user can opt to keep a full copy of every audit entry
|
|
272
347
|
// in a file of their choosing (set from the dashboard survey / Settings, then
|
|
273
348
|
// delivered to us via the same policy cache the guard writes). We append one
|
|
@@ -560,6 +635,17 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
560
635
|
const permission = guessPermission(toolName);
|
|
561
636
|
const evaluationTimeMs = readLastEvalMs(toolName, sessionId);
|
|
562
637
|
|
|
638
|
+
// DETECT-mode observation: record DLP matches / rate-limit bursts on this
|
|
639
|
+
// ALLOW entry so "detect" means observe-AND-log (not silent). Fail-safe —
|
|
640
|
+
// any error leaves the fields off. Only meaningful on ALLOW (block-mode hits
|
|
641
|
+
// are DENIED upstream with their own reason).
|
|
642
|
+
let dlpMatches = [];
|
|
643
|
+
let rateLimitBurst = false;
|
|
644
|
+
if (decision === 'ALLOW') {
|
|
645
|
+
try { dlpMatches = dlpScanArgs(argsSummary, loadDlpRedact()); } catch { dlpMatches = []; }
|
|
646
|
+
try { rateLimitBurst = rateLimitObserveBurst(AGENT_ID, loadRateLimitObserve()); } catch { rateLimitBurst = false; }
|
|
647
|
+
}
|
|
648
|
+
|
|
563
649
|
// Local log storage (opt-in): when ON, logs are kept LOCAL ONLY — we append
|
|
564
650
|
// this entry to the user's chosen file and do NOT send it to the cloud.
|
|
565
651
|
const localLogs = loadLocalLogs();
|
|
@@ -568,6 +654,8 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
568
654
|
ts: new Date().toISOString(),
|
|
569
655
|
tool: toolName, arguments: argsSummary, decision, reason, permission,
|
|
570
656
|
evaluation_time_ms: evaluationTimeMs, agent_id: AGENT_ID, agent_name: AGENT_NAME, session_id: sessionId,
|
|
657
|
+
...(dlpMatches.length ? { dlp: dlpMatches } : {}),
|
|
658
|
+
...(rateLimitBurst ? { rate_limit_burst: true } : {}),
|
|
571
659
|
});
|
|
572
660
|
fetchDone = true;
|
|
573
661
|
maybeExit();
|
|
@@ -594,6 +682,8 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
594
682
|
agent_id: AGENT_ID,
|
|
595
683
|
agent_name: AGENT_NAME,
|
|
596
684
|
session_id: sessionId,
|
|
685
|
+
...(dlpMatches.length ? { dlp: dlpMatches } : {}),
|
|
686
|
+
...(rateLimitBurst ? { rate_limit_burst: true } : {}),
|
|
597
687
|
}),
|
|
598
688
|
signal: AbortSignal.timeout(5000),
|
|
599
689
|
}).catch(() => {}).finally(() => { fetchDone = true; maybeExit(); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.81.
|
|
3
|
+
"version": "0.81.35",
|
|
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": {
|