@solongate/proxy 0.83.8 → 0.83.9
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 +0 -6
- package/dist/tui/index.js +0 -6
- package/hooks/audit.mjs +8 -6
- package/hooks/guard.bundled.mjs +32 -40
- package/hooks/guard.mjs +32 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12363,12 +12363,6 @@ function SettingsPanel({
|
|
|
12363
12363
|
] }, "h:" + sec)
|
|
12364
12364
|
);
|
|
12365
12365
|
lineKey.push("");
|
|
12366
|
-
if ((sec === "WEBHOOKS" || sec === "ALERTS") && local?.enabled) {
|
|
12367
|
-
lineEls.push(
|
|
12368
|
-
/* @__PURE__ */ jsx8(Text8, { wrap: "truncate", color: theme.warn, children: ` \u26A0 local logs on \u2014 real denials stay on this machine, so ${sec.toLowerCase()} do NOT fire${sec === "WEBHOOKS" ? " (t test still works)" : ""}` }, "warn:" + sec)
|
|
12369
|
-
);
|
|
12370
|
-
lineKey.push("");
|
|
12371
|
-
}
|
|
12372
12366
|
}
|
|
12373
12367
|
lineEls.push(/* @__PURE__ */ jsx8(Box8, { children: rowLine(r) }, keyOf(r)));
|
|
12374
12368
|
lineKey.push(keyOf(r));
|
package/dist/tui/index.js
CHANGED
|
@@ -5489,12 +5489,6 @@ function SettingsPanel({
|
|
|
5489
5489
|
] }, "h:" + sec)
|
|
5490
5490
|
);
|
|
5491
5491
|
lineKey.push("");
|
|
5492
|
-
if ((sec === "WEBHOOKS" || sec === "ALERTS") && local?.enabled) {
|
|
5493
|
-
lineEls.push(
|
|
5494
|
-
/* @__PURE__ */ jsx8(Text8, { wrap: "truncate", color: theme.warn, children: ` \u26A0 local logs on \u2014 real denials stay on this machine, so ${sec.toLowerCase()} do NOT fire${sec === "WEBHOOKS" ? " (t test still works)" : ""}` }, "warn:" + sec)
|
|
5495
|
-
);
|
|
5496
|
-
lineKey.push("");
|
|
5497
|
-
}
|
|
5498
5492
|
}
|
|
5499
5493
|
lineEls.push(/* @__PURE__ */ jsx8(Box8, { children: rowLine(r) }, keyOf(r)));
|
|
5500
5494
|
lineKey.push(keyOf(r));
|
package/hooks/audit.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { homedir } from 'node:os';
|
|
|
11
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 = 25;
|
|
15
15
|
|
|
16
16
|
function loadEnvKey(dir) {
|
|
17
17
|
try {
|
|
@@ -748,8 +748,11 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
748
748
|
try { rateLimitBurst = rateLimitObserveBurst(AGENT_ID, loadRateLimitObserve()); } catch { rateLimitBurst = false; }
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
// Local log storage (opt-in):
|
|
752
|
-
//
|
|
751
|
+
// Local log storage (opt-in) MIRRORS: the entry is appended to the user's
|
|
752
|
+
// chosen file AND still sent to the cloud. It used to divert instead, so a
|
|
753
|
+
// project with local logs on saw an empty dashboard with nothing to say the
|
|
754
|
+
// emptiness was deliberate — while the dataroom called the same setting
|
|
755
|
+
// "mirror every decision to a file".
|
|
753
756
|
const localLogs = loadLocalLogs();
|
|
754
757
|
if (localLogs) {
|
|
755
758
|
appendLocalLog(localLogs, {
|
|
@@ -759,9 +762,8 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
|
|
|
759
762
|
...(dlpMatches.length ? { dlp: dlpMatches } : {}),
|
|
760
763
|
...(rateLimitBurst ? { rate_limit_burst: true } : {}),
|
|
761
764
|
});
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
} else {
|
|
765
|
+
}
|
|
766
|
+
{
|
|
765
767
|
// Fire-and-forget: don't block tool execution waiting for API response
|
|
766
768
|
fetch(`${API_URL}/api/v1/audit-logs`, {
|
|
767
769
|
method: 'POST',
|
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -6536,7 +6536,7 @@ import { resolve, join, dirname, isAbsolute } from "node:path";
|
|
|
6536
6536
|
import { homedir } from "node:os";
|
|
6537
6537
|
import { gunzipSync } from "node:zlib";
|
|
6538
6538
|
import { createHash } from "node:crypto";
|
|
6539
|
-
var HOOK_VERSION =
|
|
6539
|
+
var HOOK_VERSION = 74;
|
|
6540
6540
|
function localLogsOnly(security) {
|
|
6541
6541
|
if (security !== void 0) {
|
|
6542
6542
|
const l = security && security.localLogs;
|
|
@@ -8428,13 +8428,12 @@ if (!REFRESH_MODE) {
|
|
|
8428
8428
|
} catch {
|
|
8429
8429
|
}
|
|
8430
8430
|
try {
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
});
|
|
8431
|
+
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
8432
|
+
method: "POST",
|
|
8433
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
8434
|
+
body: JSON.stringify(_logEntry),
|
|
8435
|
+
signal: AbortSignal.timeout(3e3)
|
|
8436
|
+
});
|
|
8438
8437
|
} catch {
|
|
8439
8438
|
}
|
|
8440
8439
|
if (AGENT_TYPE !== "codex")
|
|
@@ -8461,7 +8460,7 @@ if (!REFRESH_MODE) {
|
|
|
8461
8460
|
try {
|
|
8462
8461
|
if (existsSync(policyCacheFile)) {
|
|
8463
8462
|
const cached = JSON.parse(readFileSync(policyCacheFile, "utf-8"));
|
|
8464
|
-
if (cached
|
|
8463
|
+
if (cached)
|
|
8465
8464
|
staleCache = cached;
|
|
8466
8465
|
if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
|
|
8467
8466
|
refreshDue = false;
|
|
@@ -8546,35 +8545,31 @@ if (!REFRESH_MODE) {
|
|
|
8546
8545
|
}
|
|
8547
8546
|
writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: toolName, arguments: args, decision: "DENY", reason: "ghost path (hidden from agent)", permission: guessPermission(toolName), source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME, session_id: call.sessionId, evaluation_time_ms: Date.now() - _evalStart });
|
|
8548
8547
|
try {
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
});
|
|
8548
|
+
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
8549
|
+
method: "POST",
|
|
8550
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
8551
|
+
body: JSON.stringify({
|
|
8552
|
+
tool: toolName,
|
|
8553
|
+
arguments: args,
|
|
8554
|
+
decision: "DENY",
|
|
8555
|
+
reason: "ghost path (hidden from agent)",
|
|
8556
|
+
permission: guessPermission(toolName),
|
|
8557
|
+
source: `${AGENT_TYPE}-guard`,
|
|
8558
|
+
agent_id: AGENT_TYPE,
|
|
8559
|
+
agent_name: AGENT_NAME,
|
|
8560
|
+
session_id: call.sessionId,
|
|
8561
|
+
evaluation_time_ms: Date.now() - _evalStart
|
|
8562
|
+
}),
|
|
8563
|
+
signal: AbortSignal.timeout(3e3)
|
|
8564
|
+
});
|
|
8567
8565
|
} catch {
|
|
8568
8566
|
}
|
|
8569
|
-
await maybeSelfUpdate();
|
|
8570
8567
|
blockTool(ghostHit, true);
|
|
8571
8568
|
}
|
|
8572
8569
|
if (toolName === "Bash" || toolName === "run_command" || guessPermission(toolName) === "EXECUTE") {
|
|
8573
8570
|
const rw = ghostListingRewrite(args, securityCfg.ghost);
|
|
8574
|
-
if (rw)
|
|
8575
|
-
await maybeSelfUpdate();
|
|
8571
|
+
if (rw)
|
|
8576
8572
|
rewriteTool({ command: rw });
|
|
8577
|
-
}
|
|
8578
8573
|
}
|
|
8579
8574
|
}
|
|
8580
8575
|
if (!reason)
|
|
@@ -8608,7 +8603,6 @@ if (!REFRESH_MODE) {
|
|
|
8608
8603
|
reason = "Security layer (DLP): reading a file that contains a secret is blocked. Blocked by SolonGate.";
|
|
8609
8604
|
opaRoute = "black";
|
|
8610
8605
|
} else if (plan && plan.rewrite) {
|
|
8611
|
-
await maybeSelfUpdate();
|
|
8612
8606
|
rewriteTool(plan.rewrite);
|
|
8613
8607
|
}
|
|
8614
8608
|
}
|
|
@@ -8648,18 +8642,16 @@ if (!REFRESH_MODE) {
|
|
|
8648
8642
|
evaluation_time_ms: Date.now() - _evalStart
|
|
8649
8643
|
};
|
|
8650
8644
|
writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), ...logEntry });
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
});
|
|
8645
|
+
await fetch(API_URL + "/api/v1/audit-logs", {
|
|
8646
|
+
method: "POST",
|
|
8647
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
8648
|
+
body: JSON.stringify(logEntry),
|
|
8649
|
+
signal: AbortSignal.timeout(3e3)
|
|
8650
|
+
});
|
|
8658
8651
|
} catch {
|
|
8659
8652
|
}
|
|
8660
8653
|
}
|
|
8661
8654
|
writeDenyFlag(toolName);
|
|
8662
|
-
await maybeSelfUpdate();
|
|
8663
8655
|
blockTool(reason);
|
|
8664
8656
|
}
|
|
8665
8657
|
} catch {
|
package/hooks/guard.mjs
CHANGED
|
@@ -32,10 +32,13 @@ import { createHash } from 'node:crypto';
|
|
|
32
32
|
// the installed hook self-updates when the cloud version is higher (see
|
|
33
33
|
// maybeSelfUpdate). This is what makes guard fixes propagate without a manual
|
|
34
34
|
// reinstall — the same trust model as the OPA WASM this hook already runs.
|
|
35
|
-
const HOOK_VERSION =
|
|
35
|
+
const HOOK_VERSION = 74;
|
|
36
36
|
|
|
37
|
-
// True when local log storage is ON
|
|
38
|
-
//
|
|
37
|
+
// True when local log storage is ON — meaning a copy is written to this
|
|
38
|
+
// machine. It is a MIRROR, not a diversion: the cloud audit log is written
|
|
39
|
+
// either way. The dataroom always described it as "mirror every decision to a
|
|
40
|
+
// file"; the code diverted instead, so a project with local logs on had nothing
|
|
41
|
+
// in the dashboard and no way to tell that was deliberate.
|
|
39
42
|
//
|
|
40
43
|
// `undefined` is the ONLY value that means "we do not know yet". `null` is an
|
|
41
44
|
// answer: it is what the refresh writes when the API returns no security block,
|
|
@@ -125,14 +128,13 @@ function writeLocalLog(security, entry) {
|
|
|
125
128
|
const mark = accountMark();
|
|
126
129
|
if (mark) entry = { ...entry, acct: mark };
|
|
127
130
|
const l = security && security.localLogs;
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// never disappear, so fall back to the per-device default folder and keep it.
|
|
131
|
+
// No usable folder in the resolved config. The answer can still be "local is
|
|
132
|
+
// on" from the persisted marker alone, i.e. WITHOUT a resolved config (empty
|
|
133
|
+
// or cold policy cache, or a refresh that failed while the key was being
|
|
134
|
+
// rotated) — in which case keep the copy in the per-device default folder
|
|
135
|
+
// rather than dropping it.
|
|
134
136
|
if (!l || !l.enabled || typeof l.path !== 'string' || !l.path.trim()) {
|
|
135
|
-
if (!localLogsOnly(security)) return; //
|
|
137
|
+
if (!localLogsOnly(security)) return; // local logging is off — cloud only
|
|
136
138
|
const fallbackDir = resolve(homedir(), '.solongate', 'local-logs');
|
|
137
139
|
const fallbackLine = JSON.stringify(entry) + '\n';
|
|
138
140
|
const fallbackPayload = Buffer.from(JSON.stringify({ dir: fallbackDir, line: fallbackLine }), 'utf-8').toString('base64');
|
|
@@ -2424,7 +2426,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2424
2426
|
};
|
|
2425
2427
|
try { writeLocalLog(_sec, { ts: new Date().toISOString(), ..._logEntry }); } catch {}
|
|
2426
2428
|
try {
|
|
2427
|
-
|
|
2429
|
+
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
2428
2430
|
method: 'POST',
|
|
2429
2431
|
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
|
|
2430
2432
|
body: JSON.stringify(_logEntry),
|
|
@@ -2484,7 +2486,13 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2484
2486
|
try {
|
|
2485
2487
|
if (existsSync(policyCacheFile)) {
|
|
2486
2488
|
const cached = JSON.parse(readFileSync(policyCacheFile, 'utf-8'));
|
|
2487
|
-
|
|
2489
|
+
// Kept whenever it parses, NOT only when it carries a policy. The
|
|
2490
|
+
// security layers (DLP, rate limit, ghost) are configured separately
|
|
2491
|
+
// from the policy, so gating the last-known-good on a policy existing
|
|
2492
|
+
// meant a project with DLP on and no policy lost DLP entirely the
|
|
2493
|
+
// moment this cache went past its 10s TTL — which is almost always,
|
|
2494
|
+
// since it only refreshes on activity.
|
|
2495
|
+
if (cached) staleCache = cached;
|
|
2488
2496
|
if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
|
|
2489
2497
|
refreshDue = false;
|
|
2490
2498
|
if (cached.policy) dashboardPolicy = cached.policy;
|
|
@@ -2571,7 +2579,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2571
2579
|
try { writeDenyFlag(toolName); } catch {}
|
|
2572
2580
|
writeLocalLog(securityCfg, { ts: new Date().toISOString(), tool: toolName, arguments: args, decision: 'DENY', reason: 'ghost path (hidden from agent)', permission: guessPermission(toolName), source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME, session_id: call.sessionId, evaluation_time_ms: Date.now() - _evalStart });
|
|
2573
2581
|
try {
|
|
2574
|
-
|
|
2582
|
+
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
2575
2583
|
method: 'POST',
|
|
2576
2584
|
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
|
|
2577
2585
|
body: JSON.stringify({
|
|
@@ -2585,7 +2593,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2585
2593
|
signal: AbortSignal.timeout(3000),
|
|
2586
2594
|
});
|
|
2587
2595
|
} catch {}
|
|
2588
|
-
|
|
2596
|
+
// NOT maybeSelfUpdate() first: see the deny at the end of this flow.
|
|
2589
2597
|
// Stealth deny: ghostHit is a bare "No such file or directory", and the
|
|
2590
2598
|
// stealth flag keeps the adapter from adding any SolonGate branding, so
|
|
2591
2599
|
// the path looks like it simply is not there on every client.
|
|
@@ -2597,7 +2605,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2597
2605
|
// PreToolUse `overwrite` replaces the command before it runs (hooks.md).
|
|
2598
2606
|
if (toolName === 'Bash' || toolName === 'run_command' || guessPermission(toolName) === 'EXECUTE') {
|
|
2599
2607
|
const rw = ghostListingRewrite(args, securityCfg.ghost);
|
|
2600
|
-
if (rw)
|
|
2608
|
+
if (rw) rewriteTool({ command: rw }); // verdict first, update later
|
|
2601
2609
|
}
|
|
2602
2610
|
}
|
|
2603
2611
|
// Extra security layers run after tamper, before policy. Block reason wins
|
|
@@ -2667,8 +2675,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2667
2675
|
reason = 'Security layer (DLP): reading a file that contains a secret is blocked. Blocked by SolonGate.';
|
|
2668
2676
|
opaRoute = 'black';
|
|
2669
2677
|
} else if (plan && plan.rewrite) {
|
|
2670
|
-
|
|
2671
|
-
rewriteTool(plan.rewrite);
|
|
2678
|
+
rewriteTool(plan.rewrite); // verdict first, update later
|
|
2672
2679
|
}
|
|
2673
2680
|
}
|
|
2674
2681
|
|
|
@@ -2714,7 +2721,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2714
2721
|
writeLocalLog(securityCfg, { ts: new Date().toISOString(), ...logEntry });
|
|
2715
2722
|
// PI hook layer removed — piResult fields no longer attached.
|
|
2716
2723
|
// Local-only mode: keep the log on the user's machine, skip the cloud.
|
|
2717
|
-
|
|
2724
|
+
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
2718
2725
|
method: 'POST',
|
|
2719
2726
|
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
|
|
2720
2727
|
body: JSON.stringify(logEntry),
|
|
@@ -2723,7 +2730,13 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
2723
2730
|
} catch {}
|
|
2724
2731
|
}
|
|
2725
2732
|
writeDenyFlag(toolName);
|
|
2726
|
-
|
|
2733
|
+
// The verdict is emitted BEFORE the hook self-update, never after.
|
|
2734
|
+
// maybeSelfUpdate() fetches three hooks at up to 5s each, and the
|
|
2735
|
+
// whole process carries an 8s backstop that force-exits with
|
|
2736
|
+
// `process.exitCode || 0` — i.e. ALLOW. So a slow network on the update
|
|
2737
|
+
// path turned a decided DENY into a permitted call. Measured: 8043ms,
|
|
2738
|
+
// exit 0, on a call DLP had already refused. Updating is a background
|
|
2739
|
+
// nicety; it still runs on every allow, which is nearly every call.
|
|
2727
2740
|
blockTool(reason);
|
|
2728
2741
|
}
|
|
2729
2742
|
} catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.83.
|
|
3
|
+
"version": "0.83.9",
|
|
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": {
|