@solongate/proxy 0.83.41 → 0.83.43

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.
@@ -6579,7 +6579,7 @@ function sweepLegacyFlagDir() {
6579
6579
  } catch {
6580
6580
  }
6581
6581
  }
6582
- var HOOK_VERSION = 81;
6582
+ var HOOK_VERSION = 82;
6583
6583
  var SG_REFRESH_ARG = process.argv.includes("--sg-refresh-policy");
6584
6584
  var SG_STDIN = SG_REFRESH_ARG ? "" : (() => {
6585
6585
  try {
@@ -7402,6 +7402,72 @@ function extractPaths(args, isExec) {
7402
7402
  }
7403
7403
  return paths;
7404
7404
  }
7405
+ var SEARCH_WALK_MAX_FILES = 2e3;
7406
+ var SEARCH_WALK_MAX_DEPTH = 8;
7407
+ var SEARCH_SKIP_DIRS = /* @__PURE__ */ new Set([
7408
+ ".git",
7409
+ "node_modules",
7410
+ ".venv",
7411
+ "venv",
7412
+ "vendor",
7413
+ "dist",
7414
+ "build",
7415
+ "target",
7416
+ "__pycache__",
7417
+ ".next",
7418
+ ".turbo"
7419
+ ]);
7420
+ function isContentSearchTool(tool) {
7421
+ const n = String(tool || "").toLowerCase();
7422
+ if (n.includes("websearch") || n.includes("web_search"))
7423
+ return false;
7424
+ return n.includes("grep") || n.includes("search") || n.includes("ripgrep");
7425
+ }
7426
+ function walkSearchRoot(root, budget, depth, out) {
7427
+ if (budget.n <= 0 || depth > SEARCH_WALK_MAX_DEPTH)
7428
+ return;
7429
+ let entries;
7430
+ try {
7431
+ entries = readdirSync(root, { withFileTypes: true });
7432
+ } catch {
7433
+ return;
7434
+ }
7435
+ for (const e of entries) {
7436
+ if (budget.n <= 0)
7437
+ return;
7438
+ const p = root + "/" + e.name;
7439
+ if (e.isDirectory()) {
7440
+ if (SEARCH_SKIP_DIRS.has(e.name))
7441
+ continue;
7442
+ walkSearchRoot(p, budget, depth + 1, out);
7443
+ continue;
7444
+ }
7445
+ budget.n--;
7446
+ out.push(p);
7447
+ }
7448
+ }
7449
+ function expandSearchRoots(tool, args, cwd) {
7450
+ if (!isContentSearchTool(tool) || !args || typeof args !== "object")
7451
+ return [];
7452
+ const base = cwd || process.cwd();
7453
+ const out = [];
7454
+ const budget = { n: SEARCH_WALK_MAX_FILES };
7455
+ for (const v of scanStrings(args)) {
7456
+ if (budget.n <= 0)
7457
+ break;
7458
+ if (/^https?:\/\//i.test(v) || !/[/\\]/.test(v))
7459
+ continue;
7460
+ const root = v.startsWith("/") ? v : base + "/" + v;
7461
+ try {
7462
+ if (!statSync(root).isDirectory())
7463
+ continue;
7464
+ } catch {
7465
+ continue;
7466
+ }
7467
+ walkSearchRoot(root.replace(/\/+$/, ""), budget, 0, out);
7468
+ }
7469
+ return out;
7470
+ }
7405
7471
  function expandCommandGlobs(args, cwd) {
7406
7472
  const out = [];
7407
7473
  try {
@@ -8431,6 +8497,12 @@ async function evaluateWithOpa(policy, args, toolName, cwd) {
8431
8497
  input2.filenames.push(bn);
8432
8498
  }
8433
8499
  }
8500
+ for (const sp of expandSearchRoots(toolName, expandedArgs, cwd)) {
8501
+ input2.paths.push(sp);
8502
+ const bn = sp.split("/").pop();
8503
+ if (bn)
8504
+ input2.filenames.push(bn);
8505
+ }
8434
8506
  if (process.env.SOLONGATE_DEBUG) {
8435
8507
  }
8436
8508
  const results = opaPolicy.evaluate(input2);
package/hooks/guard.mjs CHANGED
@@ -82,7 +82,7 @@ import { createHash } from 'node:crypto';
82
82
  // the installed hook self-updates when the cloud version is higher (see
83
83
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
84
84
  // reinstall — the same trust model as the OPA WASM this hook already runs.
85
- const HOOK_VERSION = 81;
85
+ const HOOK_VERSION = 82;
86
86
 
87
87
  // ── The Go guard, when there is one and it is the right one ──────────────────
88
88
  //
@@ -1346,6 +1346,67 @@ function extractPaths(args, isExec) {
1346
1346
  // the glob to `staging.env` here lets the rule see (and block) it, regardless of
1347
1347
  // WHICH reader the model used. Bounded to a single directory level; returns real
1348
1348
  // absolute paths. cwd is the agent's working dir (see hook payload).
1349
+ // The files a content-returning search rooted at these arguments could return.
1350
+ //
1351
+ // Bounded and fail-open by construction: this is hardening on top of the
1352
+ // literal match, never the thing standing between a call and a decision, so an
1353
+ // unreadable tree or an exhausted budget leaves the literal match on its own
1354
+ // rather than denying.
1355
+ const SEARCH_WALK_MAX_FILES = 2000;
1356
+ const SEARCH_WALK_MAX_DEPTH = 8;
1357
+ const SEARCH_SKIP_DIRS = new Set([
1358
+ '.git', 'node_modules', '.venv', 'venv', 'vendor', 'dist', 'build', 'target',
1359
+ '__pycache__', '.next', '.turbo',
1360
+ ]);
1361
+
1362
+ // `websearch` reaches the network, not the disk, and guessPermission already
1363
+ // puts it in NETWORK.
1364
+ function isContentSearchTool(tool) {
1365
+ const n = String(tool || '').toLowerCase();
1366
+ if (n.includes('websearch') || n.includes('web_search')) return false;
1367
+ return n.includes('grep') || n.includes('search') || n.includes('ripgrep');
1368
+ }
1369
+
1370
+ function walkSearchRoot(root, budget, depth, out) {
1371
+ if (budget.n <= 0 || depth > SEARCH_WALK_MAX_DEPTH) return;
1372
+ let entries;
1373
+ try {
1374
+ entries = readdirSync(root, { withFileTypes: true });
1375
+ } catch {
1376
+ return;
1377
+ }
1378
+ for (const e of entries) {
1379
+ if (budget.n <= 0) return;
1380
+ const p = root + '/' + e.name;
1381
+ if (e.isDirectory()) {
1382
+ if (SEARCH_SKIP_DIRS.has(e.name)) continue;
1383
+ walkSearchRoot(p, budget, depth + 1, out);
1384
+ continue;
1385
+ }
1386
+ budget.n--;
1387
+ out.push(p);
1388
+ }
1389
+ }
1390
+
1391
+ function expandSearchRoots(tool, args, cwd) {
1392
+ if (!isContentSearchTool(tool) || !args || typeof args !== 'object') return [];
1393
+ const base = cwd || process.cwd();
1394
+ const out = [];
1395
+ const budget = { n: SEARCH_WALK_MAX_FILES };
1396
+ for (const v of scanStrings(args)) {
1397
+ if (budget.n <= 0) break;
1398
+ if (/^https?:\/\//i.test(v) || !/[/\\]/.test(v)) continue;
1399
+ const root = v.startsWith('/') ? v : base + '/' + v;
1400
+ try {
1401
+ if (!statSync(root).isDirectory()) continue;
1402
+ } catch {
1403
+ continue;
1404
+ }
1405
+ walkSearchRoot(root.replace(/\/+$/, ''), budget, 0, out);
1406
+ }
1407
+ return out;
1408
+ }
1409
+
1349
1410
  function expandCommandGlobs(args, cwd) {
1350
1411
  const out = [];
1351
1412
  try {
@@ -2463,6 +2524,22 @@ async function evaluateWithOpa(policy, args, toolName, cwd) {
2463
2524
  if (bn) input.filenames.push(bn);
2464
2525
  }
2465
2526
  }
2527
+ // A grep-style tool is a READ of every file under its root, and its
2528
+ // arguments say none of that: they carry the root and a query, never the
2529
+ // files whose contents come back. So the guard saw a search of a directory
2530
+ // no rule mentions, matched nothing, and allowed it — the rule held against
2531
+ // the read tool and was walked past by the search tool beside it.
2532
+ //
2533
+ // Measured on Antigravity: view_file on a denied path is refused, and
2534
+ // grep_search rooted at the workspace returns the same file's contents.
2535
+ //
2536
+ // Same move as the glob expansion above: resolve what the call will actually
2537
+ // reach so the rules already written can see it. Every bound fails OPEN.
2538
+ for (const sp of expandSearchRoots(toolName, expandedArgs, cwd)) {
2539
+ input.paths.push(sp);
2540
+ const bn = sp.split('/').pop();
2541
+ if (bn) input.filenames.push(bn);
2542
+ }
2466
2543
  if (process.env.SOLONGATE_DEBUG) {
2467
2544
  }
2468
2545
  const results = opaPolicy.evaluate(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.41",
3
+ "version": "0.83.43",
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": {
@@ -61,12 +61,12 @@
61
61
  "node": ">=20.0.0"
62
62
  },
63
63
  "optionalDependencies": {
64
- "@solongate/guard-linux-x64": "0.83.41",
65
- "@solongate/guard-linux-arm64": "0.83.41",
66
- "@solongate/guard-darwin-x64": "0.83.41",
67
- "@solongate/guard-darwin-arm64": "0.83.41",
68
- "@solongate/guard-win32-x64": "0.83.41",
69
- "@solongate/guard-win32-arm64": "0.83.41"
64
+ "@solongate/guard-linux-x64": "0.83.43",
65
+ "@solongate/guard-linux-arm64": "0.83.43",
66
+ "@solongate/guard-darwin-x64": "0.83.43",
67
+ "@solongate/guard-darwin-arm64": "0.83.43",
68
+ "@solongate/guard-win32-x64": "0.83.43",
69
+ "@solongate/guard-win32-arm64": "0.83.43"
70
70
  },
71
71
  "dependencies": {
72
72
  "@modelcontextprotocol/sdk": "^1.26.0",