@solongate/proxy 0.59.0 → 0.59.2

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.
@@ -6535,7 +6535,7 @@ import { resolve, join, dirname, isAbsolute } from "node:path";
6535
6535
  import { homedir } from "node:os";
6536
6536
  import { gunzipSync } from "node:zlib";
6537
6537
  import { createHash } from "node:crypto";
6538
- var HOOK_VERSION = 30;
6538
+ var HOOK_VERSION = 31;
6539
6539
  function localLogsOnly(security) {
6540
6540
  const l = security && security.localLogs;
6541
6541
  return !!(l && l.enabled && typeof l.path === "string" && l.path.trim());
@@ -7443,7 +7443,7 @@ function extractWasmFromBundle(buf) {
7443
7443
  }
7444
7444
  throw new Error("Unknown OPA bundle format");
7445
7445
  }
7446
- var OPA_WASM_TTL_MS = 3e4;
7446
+ var OPA_WASM_TTL_MS = 24 * 60 * 60 * 1e3;
7447
7447
  async function getOpaWasmBytes(policy) {
7448
7448
  if (!policy || !policy.id)
7449
7449
  return null;
@@ -7466,7 +7466,7 @@ async function getOpaWasmBytes(policy) {
7466
7466
  try {
7467
7467
  const res = await fetch(
7468
7468
  API_URL + "/api/v1/policies/" + encodeURIComponent(policy.id) + "/wasm",
7469
- { headers: AUTH_HEADERS, signal: AbortSignal.timeout(8e3) }
7469
+ { headers: AUTH_HEADERS, signal: AbortSignal.timeout(2500) }
7470
7470
  );
7471
7471
  if (!res.ok)
7472
7472
  return stale;
@@ -7646,12 +7646,15 @@ process.stdin.on("end", async () => {
7646
7646
  let securityCfg = null;
7647
7647
  const agentKey = (AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
7648
7648
  const policyCacheFile = join(resolve(homedir(), ".solongate"), ".policy-cache-" + agentKey + ".json");
7649
- const POLICY_TTL_MS = 3e3;
7649
+ const POLICY_TTL_MS = 1e4;
7650
7650
  try {
7651
7651
  let dashboardPolicy = null;
7652
+ let staleCache = null;
7652
7653
  try {
7653
7654
  if (existsSync(policyCacheFile)) {
7654
7655
  const cached = JSON.parse(readFileSync(policyCacheFile, "utf-8"));
7656
+ if (cached && cached.policy)
7657
+ staleCache = cached;
7655
7658
  if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
7656
7659
  if (cached.policy)
7657
7660
  dashboardPolicy = cached.policy;
@@ -7667,7 +7670,7 @@ process.stdin.on("end", async () => {
7667
7670
  }
7668
7671
  if (!dashboardPolicy) {
7669
7672
  try {
7670
- 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) });
7673
+ 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) });
7671
7674
  if (res.ok) {
7672
7675
  const body = await res.json();
7673
7676
  if (typeof body?.self_protection_enabled === "boolean")
@@ -7685,6 +7688,15 @@ process.stdin.on("end", async () => {
7685
7688
  }
7686
7689
  } catch {
7687
7690
  }
7691
+ if (!dashboardPolicy && staleCache) {
7692
+ dashboardPolicy = staleCache.policy;
7693
+ if (typeof staleCache.selfProtect === "boolean")
7694
+ selfProtectEnabled = staleCache.selfProtect;
7695
+ if (staleCache.security !== void 0)
7696
+ securityCfg = staleCache.security;
7697
+ if (staleCache.hookVersions)
7698
+ CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
7699
+ }
7688
7700
  }
7689
7701
  if (process.env.SOLONGATE_DEBUG) {
7690
7702
  }
package/hooks/guard.mjs CHANGED
@@ -31,7 +31,7 @@ import { createHash } from 'node:crypto';
31
31
  // the installed hook self-updates when the cloud version is higher (see
32
32
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
33
33
  // reinstall — the same trust model as the OPA WASM this hook already runs.
34
- const HOOK_VERSION = 30;
34
+ const HOOK_VERSION = 31;
35
35
 
36
36
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
37
37
  // nothing is sent to the cloud audit log.
@@ -1138,7 +1138,12 @@ function extractWasmFromBundle(buf) {
1138
1138
  // Fetches the compiled WASM for this policy from the API, with a local cache
1139
1139
  // keyed by a fingerprint of the policy so updates propagate. Returns the raw
1140
1140
  // policy.wasm bytes (Uint8Array) or null when unavailable.
1141
- const OPA_WASM_TTL_MS = 30_000;
1141
+ // The WASM cache is keyed by `fp`, a fingerprint of the policy rules+mode, so a
1142
+ // real policy change invalidates it immediately regardless of age. The TTL is
1143
+ // therefore only a backstop for a server-side recompile that keeps the same
1144
+ // rules — a rare event — so we keep it long (24h) instead of re-downloading the
1145
+ // bundle every 30s on the hot path.
1146
+ const OPA_WASM_TTL_MS = 24 * 60 * 60 * 1000;
1142
1147
  async function getOpaWasmBytes(policy) {
1143
1148
  if (!policy || !policy.id) return null;
1144
1149
  const fp = djb2(JSON.stringify(policy.rules || []) + '|' + (policy.mode || ''));
@@ -1165,7 +1170,7 @@ async function getOpaWasmBytes(policy) {
1165
1170
  try {
1166
1171
  const res = await fetch(
1167
1172
  API_URL + '/api/v1/policies/' + encodeURIComponent(policy.id) + '/wasm',
1168
- { headers: AUTH_HEADERS, signal: AbortSignal.timeout(8000) },
1173
+ { headers: AUTH_HEADERS, signal: AbortSignal.timeout(2500) },
1169
1174
  );
1170
1175
  if (!res.ok) return stale; // API has no compiled WASM right now → last known good
1171
1176
  const bundle = Buffer.from(await res.arrayBuffer());
@@ -1426,13 +1431,26 @@ process.stdin.on('end', async () => {
1426
1431
  // don't share a stale cached policy.
1427
1432
  const agentKey = (AGENT_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
1428
1433
  const policyCacheFile = join(resolve(homedir(), '.solongate'), '.policy-cache-' + agentKey + '.json');
1429
- const POLICY_TTL_MS = 3_000;
1434
+ // Serve the cached policy without any network for TTL seconds. This is a HOT
1435
+ // PATH — it runs before every tool call. The original 3s TTL was slow not
1436
+ // because of the number but because it paired an 8s-timeout blocking fetch
1437
+ // with an every-3s cadence (and stacked behind the WASM fetch + a shared
1438
+ // rate limit). With those fixed — 2s timeout, WASM decoupled (24h/fp),
1439
+ // last-known-good stale fallback — a short TTL is cheap again: one small
1440
+ // ~200ms GET on a single call per window, others read the cache with no
1441
+ // network. So keep freshness tight (10s propagation) at negligible cost.
1442
+ const POLICY_TTL_MS = 10_000;
1430
1443
  try {
1431
1444
  let dashboardPolicy = null;
1445
+ // Last-known-good cache, kept even past the TTL so we can fall back on it
1446
+ // when the API is slow/unreachable instead of blocking or dropping the
1447
+ // policy. `fresh` gates whether we skip the network entirely.
1448
+ let staleCache = null;
1432
1449
  // Try cache first
1433
1450
  try {
1434
1451
  if (existsSync(policyCacheFile)) {
1435
1452
  const cached = JSON.parse(readFileSync(policyCacheFile, 'utf-8'));
1453
+ if (cached && cached.policy) staleCache = cached;
1436
1454
  if (cached && cached._ts && Date.now() - cached._ts < POLICY_TTL_MS) {
1437
1455
  if (cached.policy) dashboardPolicy = cached.policy;
1438
1456
  if (typeof cached.selfProtect === 'boolean') selfProtectEnabled = cached.selfProtect;
@@ -1445,8 +1463,10 @@ process.stdin.on('end', async () => {
1445
1463
  if (!dashboardPolicy) {
1446
1464
  try {
1447
1465
  // Report our installed version (hv) so the dashboard can show whether
1448
- // this guard is on the latest build.
1449
- 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) });
1466
+ // this guard is on the latest build. Short timeout: a stale cached
1467
+ // policy (below) is a safe fallback, so there's no reason to block the
1468
+ // tool for 8s waiting on a slow cloud — fail fast to last-known-good.
1469
+ 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) });
1450
1470
  if (res.ok) {
1451
1471
  const body = await res.json();
1452
1472
  // Capture the self-protection flag even when no cloud policy is set.
@@ -1457,6 +1477,14 @@ process.stdin.on('end', async () => {
1457
1477
  try { writeFileSync(policyCacheFile, JSON.stringify({ _ts: Date.now(), policy: dashboardPolicy || null, selfProtect: selfProtectEnabled, security: securityCfg, hookVersions: CLOUD_HOOK_VERSIONS })); } catch {}
1458
1478
  }
1459
1479
  } catch {}
1480
+ // API slow/unreachable → keep enforcing the last-known-good policy rather
1481
+ // than blocking or silently falling through to the local default.
1482
+ if (!dashboardPolicy && staleCache) {
1483
+ dashboardPolicy = staleCache.policy;
1484
+ if (typeof staleCache.selfProtect === 'boolean') selfProtectEnabled = staleCache.selfProtect;
1485
+ if (staleCache.security !== undefined) securityCfg = staleCache.security;
1486
+ if (staleCache.hookVersions) CLOUD_HOOK_VERSIONS = staleCache.hookVersions;
1487
+ }
1460
1488
  }
1461
1489
 
1462
1490
  if (process.env.SOLONGATE_DEBUG) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.59.0",
3
+ "version": "0.59.2",
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": {