@solongate/proxy 0.59.3 → 0.59.4
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/hooks/guard.bundled.mjs +62 -20
- package/hooks/guard.mjs +57 -20
- package/package.json +1 -1
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -6531,11 +6531,12 @@ var init_src = __esm({
|
|
|
6531
6531
|
|
|
6532
6532
|
// packages/proxy/hooks/guard.mjs
|
|
6533
6533
|
import { readFileSync, existsSync, statSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync } from "node:fs";
|
|
6534
|
+
import { spawn } from "node:child_process";
|
|
6534
6535
|
import { resolve, join, dirname, isAbsolute } from "node:path";
|
|
6535
6536
|
import { homedir } from "node:os";
|
|
6536
6537
|
import { gunzipSync } from "node:zlib";
|
|
6537
6538
|
import { createHash } from "node:crypto";
|
|
6538
|
-
var HOOK_VERSION =
|
|
6539
|
+
var HOOK_VERSION = 33;
|
|
6539
6540
|
function localLogsOnly(security) {
|
|
6540
6541
|
const l = security && security.localLogs;
|
|
6541
6542
|
return !!(l && l.enabled && typeof l.path === "string" && l.path.trim());
|
|
@@ -6713,6 +6714,51 @@ var AGENT_TYPE = process.argv[2] || "claude-code";
|
|
|
6713
6714
|
var POLICY_SELECTOR = process.env.SOLONGATE_AGENT_ID || "";
|
|
6714
6715
|
var AGENT_ID = POLICY_SELECTOR || AGENT_TYPE;
|
|
6715
6716
|
var AGENT_NAME = process.env.SOLONGATE_AGENT_NAME || process.argv[3] || AGENT_TYPE;
|
|
6717
|
+
var REFRESH_MODE = process.argv.includes("--sg-refresh-policy");
|
|
6718
|
+
async function refreshPolicyCache() {
|
|
6719
|
+
try {
|
|
6720
|
+
const agentKey = (AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
6721
|
+
const cacheFile = join(resolve(homedir(), ".solongate"), ".policy-cache-" + agentKey + ".json");
|
|
6722
|
+
let selfProtect = true, security = null, hookVersions = null, policy = null;
|
|
6723
|
+
try {
|
|
6724
|
+
if (existsSync(cacheFile)) {
|
|
6725
|
+
const c = JSON.parse(readFileSync(cacheFile, "utf-8"));
|
|
6726
|
+
if (c) {
|
|
6727
|
+
policy = c.policy ?? null;
|
|
6728
|
+
if (typeof c.selfProtect === "boolean")
|
|
6729
|
+
selfProtect = c.selfProtect;
|
|
6730
|
+
if (c.security !== void 0)
|
|
6731
|
+
security = c.security;
|
|
6732
|
+
if (c.hookVersions)
|
|
6733
|
+
hookVersions = c.hookVersions;
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
} catch {
|
|
6737
|
+
}
|
|
6738
|
+
try {
|
|
6739
|
+
const res = await fetch(API_URL + "/api/v1/policies/active?agent_id=" + encodeURIComponent(AGENT_ID || "") + "&hv=" + HOOK_VERSION, { headers: AUTH_HEADERS, signal: AbortSignal.timeout(8e3) });
|
|
6740
|
+
if (res.ok) {
|
|
6741
|
+
const body = await res.json();
|
|
6742
|
+
if (typeof body?.self_protection_enabled === "boolean")
|
|
6743
|
+
selfProtect = body.self_protection_enabled;
|
|
6744
|
+
if (body?.security !== void 0)
|
|
6745
|
+
security = body.security;
|
|
6746
|
+
if (body?.hook_versions && typeof body.hook_versions === "object")
|
|
6747
|
+
hookVersions = body.hook_versions;
|
|
6748
|
+
policy = body && body.policy ? body.policy : null;
|
|
6749
|
+
}
|
|
6750
|
+
} catch {
|
|
6751
|
+
}
|
|
6752
|
+
try {
|
|
6753
|
+
writeFileSync(cacheFile, JSON.stringify({ _ts: Date.now(), policy, selfProtect, security, hookVersions }));
|
|
6754
|
+
} catch {
|
|
6755
|
+
}
|
|
6756
|
+
} catch {
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
if (REFRESH_MODE) {
|
|
6760
|
+
refreshPolicyCache().finally(() => process.exit(0));
|
|
6761
|
+
}
|
|
6716
6762
|
function blockTool(reason) {
|
|
6717
6763
|
if (AGENT_TYPE === "gemini-cli") {
|
|
6718
6764
|
process.stdout.write(JSON.stringify({
|
|
@@ -7610,6 +7656,8 @@ function readReferencedFiles(args, cwd) {
|
|
|
7610
7656
|
}
|
|
7611
7657
|
process.stdin.on("data", (c) => input += c);
|
|
7612
7658
|
process.stdin.on("end", async () => {
|
|
7659
|
+
if (REFRESH_MODE)
|
|
7660
|
+
return;
|
|
7613
7661
|
if (process.env.SOLONGATE_DEBUG) {
|
|
7614
7662
|
}
|
|
7615
7663
|
if (!API_KEY) {
|
|
@@ -7671,21 +7719,6 @@ process.stdin.on("end", async () => {
|
|
|
7671
7719
|
} catch {
|
|
7672
7720
|
}
|
|
7673
7721
|
if (refreshDue) {
|
|
7674
|
-
try {
|
|
7675
|
-
const res = await fetch(API_URL + "/api/v1/policies/active?agent_id=" + encodeURIComponent(AGENT_ID || "") + "&hv=" + HOOK_VERSION, { headers: AUTH_HEADERS, signal: AbortSignal.timeout(2e3) });
|
|
7676
|
-
if (res.ok) {
|
|
7677
|
-
const body = await res.json();
|
|
7678
|
-
if (typeof body?.self_protection_enabled === "boolean")
|
|
7679
|
-
selfProtectEnabled = body.self_protection_enabled;
|
|
7680
|
-
if (body?.security !== void 0)
|
|
7681
|
-
securityCfg = body.security;
|
|
7682
|
-
if (body?.hook_versions && typeof body.hook_versions === "object")
|
|
7683
|
-
CLOUD_HOOK_VERSIONS = body.hook_versions;
|
|
7684
|
-
if (body && body.policy)
|
|
7685
|
-
dashboardPolicy = body.policy;
|
|
7686
|
-
}
|
|
7687
|
-
} catch {
|
|
7688
|
-
}
|
|
7689
7722
|
if (!dashboardPolicy && staleCache) {
|
|
7690
7723
|
dashboardPolicy = staleCache.policy;
|
|
7691
7724
|
if (typeof staleCache.selfProtect === "boolean")
|
|
@@ -7695,10 +7728,19 @@ process.stdin.on("end", async () => {
|
|
|
7695
7728
|
if (staleCache.hookVersions)
|
|
7696
7729
|
CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
|
|
7697
7730
|
}
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7731
|
+
if (API_KEY) {
|
|
7732
|
+
try {
|
|
7733
|
+
const lock = join(resolve(homedir(), ".solongate"), ".policy-refresh-" + agentKey + ".lock");
|
|
7734
|
+
const due = !existsSync(lock) || Date.now() - statSync(lock).mtimeMs > 3e3;
|
|
7735
|
+
if (due) {
|
|
7736
|
+
try {
|
|
7737
|
+
writeFileSync(lock, String(Date.now()));
|
|
7738
|
+
} catch {
|
|
7739
|
+
}
|
|
7740
|
+
spawn(process.execPath, [process.argv[1], AGENT_TYPE, AGENT_NAME, "--sg-refresh-policy"], { detached: true, stdio: "ignore", env: process.env }).unref();
|
|
7741
|
+
}
|
|
7742
|
+
} catch {
|
|
7743
|
+
}
|
|
7702
7744
|
}
|
|
7703
7745
|
}
|
|
7704
7746
|
if (process.env.SOLONGATE_DEBUG) {
|
package/hooks/guard.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* Auto-installed by: npx @solongate/proxy init --global
|
|
23
23
|
*/
|
|
24
24
|
import { readFileSync, existsSync, statSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync } from 'node:fs';
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
25
26
|
import { resolve, join, dirname, isAbsolute } from 'node:path';
|
|
26
27
|
import { homedir } from 'node:os';
|
|
27
28
|
import { gunzipSync } from 'node:zlib';
|
|
@@ -31,7 +32,7 @@ import { createHash } from 'node:crypto';
|
|
|
31
32
|
// the installed hook self-updates when the cloud version is higher (see
|
|
32
33
|
// maybeSelfUpdate). This is what makes guard fixes propagate without a manual
|
|
33
34
|
// reinstall — the same trust model as the OPA WASM this hook already runs.
|
|
34
|
-
const HOOK_VERSION =
|
|
35
|
+
const HOOK_VERSION = 33;
|
|
35
36
|
|
|
36
37
|
// True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
|
|
37
38
|
// nothing is sent to the cloud audit log.
|
|
@@ -248,6 +249,40 @@ const POLICY_SELECTOR = process.env.SOLONGATE_AGENT_ID || '';
|
|
|
248
249
|
const AGENT_ID = POLICY_SELECTOR || AGENT_TYPE;
|
|
249
250
|
const AGENT_NAME = process.env.SOLONGATE_AGENT_NAME || process.argv[3] || AGENT_TYPE;
|
|
250
251
|
|
|
252
|
+
// ── Background policy refresh (stale-while-revalidate) ──
|
|
253
|
+
// The hot path NEVER blocks on the network: when the policy cache is stale it
|
|
254
|
+
// serves the cached (or local) policy INSTANTLY and spawns this detached mode to
|
|
255
|
+
// fetch a fresh policy and rewrite the cache for the NEXT call. Result: fast tool
|
|
256
|
+
// calls AND ~one-call propagation of policy changes — no long-TTL tradeoff.
|
|
257
|
+
const REFRESH_MODE = process.argv.includes('--sg-refresh-policy');
|
|
258
|
+
async function refreshPolicyCache() {
|
|
259
|
+
try {
|
|
260
|
+
const agentKey = (AGENT_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
261
|
+
const cacheFile = join(resolve(homedir(), '.solongate'), '.policy-cache-' + agentKey + '.json');
|
|
262
|
+
// Preserve existing fields so a failed fetch never erases the last-known-good.
|
|
263
|
+
let selfProtect = true, security = null, hookVersions = null, policy = null;
|
|
264
|
+
try {
|
|
265
|
+
if (existsSync(cacheFile)) {
|
|
266
|
+
const c = JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
267
|
+
if (c) { policy = c.policy ?? null; if (typeof c.selfProtect === 'boolean') selfProtect = c.selfProtect; if (c.security !== undefined) security = c.security; if (c.hookVersions) hookVersions = c.hookVersions; }
|
|
268
|
+
}
|
|
269
|
+
} catch {}
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch(API_URL + '/api/v1/policies/active?agent_id=' + encodeURIComponent(AGENT_ID || '') + '&hv=' + HOOK_VERSION, { headers: AUTH_HEADERS, signal: AbortSignal.timeout(8000) });
|
|
272
|
+
if (res.ok) {
|
|
273
|
+
const body = await res.json();
|
|
274
|
+
if (typeof body?.self_protection_enabled === 'boolean') selfProtect = body.self_protection_enabled;
|
|
275
|
+
if (body?.security !== undefined) security = body.security;
|
|
276
|
+
if (body?.hook_versions && typeof body.hook_versions === 'object') hookVersions = body.hook_versions;
|
|
277
|
+
policy = (body && body.policy) ? body.policy : null;
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
280
|
+
// Always advance _ts (success or failure) so the hot path backs off between refreshes.
|
|
281
|
+
try { writeFileSync(cacheFile, JSON.stringify({ _ts: Date.now(), policy, selfProtect, security, hookVersions })); } catch {}
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
if (REFRESH_MODE) { refreshPolicyCache().finally(() => process.exit(0)); }
|
|
285
|
+
|
|
251
286
|
// ── Per-tool block/allow output ──
|
|
252
287
|
// Response format depends on the agent:
|
|
253
288
|
// Claude Code: exit 2 + stderr = BLOCK, exit 0 = ALLOW
|
|
@@ -1369,6 +1404,9 @@ function readReferencedFiles(args, cwd) {
|
|
|
1369
1404
|
|
|
1370
1405
|
process.stdin.on('data', c => input += c);
|
|
1371
1406
|
process.stdin.on('end', async () => {
|
|
1407
|
+
// Background-refresh invocation has no tool call to evaluate — it already ran
|
|
1408
|
+
// refreshPolicyCache() at startup; do nothing on the (empty) stdin.
|
|
1409
|
+
if (REFRESH_MODE) return;
|
|
1372
1410
|
// No policy selected => no enforcement. A plain launch (no SOLONGATE_AGENT_ID)
|
|
1373
1411
|
// is intentionally unrestricted.
|
|
1374
1412
|
if (process.env.SOLONGATE_DEBUG) {
|
|
@@ -1466,32 +1504,31 @@ process.stdin.on('end', async () => {
|
|
|
1466
1504
|
}
|
|
1467
1505
|
}
|
|
1468
1506
|
} catch {}
|
|
1469
|
-
//
|
|
1507
|
+
// Cache stale → DO NOT block the tool on the network. Serve the last-known-good
|
|
1508
|
+
// policy right now (or the local policy.json fallback below if there is none)
|
|
1509
|
+
// and kick off a DETACHED background refresh that rewrites the cache for the
|
|
1510
|
+
// NEXT call. Stale-while-revalidate: tool calls stay instant (~ms), yet a
|
|
1511
|
+
// policy change still lands within ~one call, because the fetch runs in
|
|
1512
|
+
// parallel instead of on the hot path.
|
|
1470
1513
|
if (refreshDue) {
|
|
1471
|
-
try {
|
|
1472
|
-
const res = await fetch(API_URL + '/api/v1/policies/active?agent_id=' + encodeURIComponent(AGENT_ID || '') + '&hv=' + HOOK_VERSION, { headers: AUTH_HEADERS, signal: AbortSignal.timeout(2000) });
|
|
1473
|
-
if (res.ok) {
|
|
1474
|
-
const body = await res.json();
|
|
1475
|
-
if (typeof body?.self_protection_enabled === 'boolean') selfProtectEnabled = body.self_protection_enabled;
|
|
1476
|
-
if (body?.security !== undefined) securityCfg = body.security;
|
|
1477
|
-
if (body?.hook_versions && typeof body.hook_versions === 'object') CLOUD_HOOK_VERSIONS = body.hook_versions;
|
|
1478
|
-
if (body && body.policy) dashboardPolicy = body.policy;
|
|
1479
|
-
}
|
|
1480
|
-
} catch {}
|
|
1481
|
-
// API slow/unreachable → keep enforcing the last-known-good policy.
|
|
1482
1514
|
if (!dashboardPolicy && staleCache) {
|
|
1483
1515
|
dashboardPolicy = staleCache.policy;
|
|
1484
1516
|
if (typeof staleCache.selfProtect === 'boolean') selfProtectEnabled = staleCache.selfProtect;
|
|
1485
1517
|
if (staleCache.security !== undefined) securityCfg = staleCache.security;
|
|
1486
1518
|
if (staleCache.hookVersions) CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
|
|
1487
1519
|
}
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1520
|
+
// Debounce: at most one background refresh in flight per ~3s, so a burst of
|
|
1521
|
+
// stale calls (or several agents sharing this cache) doesn't spawn a swarm.
|
|
1522
|
+
if (API_KEY) {
|
|
1523
|
+
try {
|
|
1524
|
+
const lock = join(resolve(homedir(), '.solongate'), '.policy-refresh-' + agentKey + '.lock');
|
|
1525
|
+
const due = !existsSync(lock) || (Date.now() - statSync(lock).mtimeMs) > 3000;
|
|
1526
|
+
if (due) {
|
|
1527
|
+
try { writeFileSync(lock, String(Date.now())); } catch {}
|
|
1528
|
+
spawn(process.execPath, [process.argv[1], AGENT_TYPE, AGENT_NAME, '--sg-refresh-policy'], { detached: true, stdio: 'ignore', env: process.env }).unref();
|
|
1529
|
+
}
|
|
1530
|
+
} catch {}
|
|
1531
|
+
}
|
|
1495
1532
|
}
|
|
1496
1533
|
|
|
1497
1534
|
if (process.env.SOLONGATE_DEBUG) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.4",
|
|
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": {
|