@solongate/proxy 0.83.41 → 0.83.42
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 +72 -0
- package/hooks/guard.mjs +77 -0
- package/package.json +7 -7
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -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
|
@@ -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.
|
|
3
|
+
"version": "0.83.42",
|
|
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.
|
|
65
|
-
"@solongate/guard-linux-arm64": "0.83.
|
|
66
|
-
"@solongate/guard-darwin-x64": "0.83.
|
|
67
|
-
"@solongate/guard-darwin-arm64": "0.83.
|
|
68
|
-
"@solongate/guard-win32-x64": "0.83.
|
|
69
|
-
"@solongate/guard-win32-arm64": "0.83.
|
|
64
|
+
"@solongate/guard-linux-x64": "0.83.42",
|
|
65
|
+
"@solongate/guard-linux-arm64": "0.83.42",
|
|
66
|
+
"@solongate/guard-darwin-x64": "0.83.42",
|
|
67
|
+
"@solongate/guard-darwin-arm64": "0.83.42",
|
|
68
|
+
"@solongate/guard-win32-x64": "0.83.42",
|
|
69
|
+
"@solongate/guard-win32-arm64": "0.83.42"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.26.0",
|