@node9/proxy 1.31.0 → 1.33.0
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/cli.js +1344 -855
- package/dist/cli.mjs +1329 -840
- package/dist/dashboard.mjs +78 -15
- package/dist/index.js +86 -17
- package/dist/index.mjs +86 -17
- package/package.json +1 -1
package/dist/dashboard.mjs
CHANGED
|
@@ -244,6 +244,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
244
244
|
if (f === PARSE_FAIL) return command;
|
|
245
245
|
try {
|
|
246
246
|
const strips = [];
|
|
247
|
+
const rewrites = [];
|
|
248
|
+
const msgSpans = /* @__PURE__ */ new Set();
|
|
247
249
|
syntax.Walk(f, (node) => {
|
|
248
250
|
if (!node) return false;
|
|
249
251
|
const n = node;
|
|
@@ -259,25 +261,46 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
259
261
|
if (nextParts.length !== 1) continue;
|
|
260
262
|
const quotedNode = nextParts[0];
|
|
261
263
|
const nt = syntax.NodeType(quotedNode);
|
|
264
|
+
const markStrip = () => {
|
|
265
|
+
const s = next.Pos().Offset();
|
|
266
|
+
const e = next.End().Offset();
|
|
267
|
+
strips.push([s, e]);
|
|
268
|
+
msgSpans.add(`${s}:${e}`);
|
|
269
|
+
};
|
|
262
270
|
if (nt === "SglQuoted") {
|
|
263
|
-
|
|
271
|
+
markStrip();
|
|
264
272
|
} else if (nt === "DblQuoted") {
|
|
265
273
|
const innerParts = quotedNode.Parts || [];
|
|
266
274
|
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
267
275
|
if (allLit) {
|
|
268
|
-
|
|
276
|
+
markStrip();
|
|
269
277
|
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
270
|
-
|
|
278
|
+
markStrip();
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
}
|
|
282
|
+
for (const arg of args) {
|
|
283
|
+
const s = arg.Pos().Offset();
|
|
284
|
+
const e = arg.End().Offset();
|
|
285
|
+
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
286
|
+
const resolved = resolveWordLiteral(arg);
|
|
287
|
+
if (resolved === null) continue;
|
|
288
|
+
const source = command.slice(s, e);
|
|
289
|
+
if (resolved === source) continue;
|
|
290
|
+
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
291
|
+
rewrites.push([s, e, resolved]);
|
|
292
|
+
}
|
|
274
293
|
return true;
|
|
275
294
|
});
|
|
276
|
-
|
|
277
|
-
|
|
295
|
+
const edits = [
|
|
296
|
+
...strips.map(([s, e]) => [s, e, '""']),
|
|
297
|
+
...rewrites
|
|
298
|
+
];
|
|
299
|
+
if (edits.length === 0) return command;
|
|
300
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
278
301
|
let result = command;
|
|
279
|
-
for (const [
|
|
280
|
-
result = result.slice(0,
|
|
302
|
+
for (const [s, e, rep] of edits) {
|
|
303
|
+
result = result.slice(0, s) + rep + result.slice(e);
|
|
281
304
|
}
|
|
282
305
|
return result;
|
|
283
306
|
} catch {
|
|
@@ -386,20 +409,38 @@ function extractLiteralArgs(callExpr) {
|
|
|
386
409
|
}
|
|
387
410
|
return { name, flags, paths };
|
|
388
411
|
}
|
|
412
|
+
function resolveWordLiteral(w) {
|
|
413
|
+
const parts = w?.Parts || [];
|
|
414
|
+
let s = "";
|
|
415
|
+
for (const p of parts) {
|
|
416
|
+
const t = syntax.NodeType(p);
|
|
417
|
+
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
418
|
+
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
419
|
+
else if (t === "DblQuoted") {
|
|
420
|
+
const inner = p.Parts || [];
|
|
421
|
+
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
422
|
+
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
423
|
+
} else {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return s;
|
|
428
|
+
}
|
|
389
429
|
function analyzeFsOperation(command) {
|
|
390
|
-
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
fsOpCache.
|
|
394
|
-
fsOpCache.
|
|
430
|
+
const normalized = normalizeCommandForPolicy(command);
|
|
431
|
+
if (!FS_OP_PRESCREEN_RE.test(normalized)) return null;
|
|
432
|
+
if (fsOpCache.has(normalized)) {
|
|
433
|
+
const hit = fsOpCache.get(normalized) ?? null;
|
|
434
|
+
fsOpCache.delete(normalized);
|
|
435
|
+
fsOpCache.set(normalized, hit);
|
|
395
436
|
return hit;
|
|
396
437
|
}
|
|
397
|
-
const computed = analyzeFsOperationImpl(
|
|
438
|
+
const computed = analyzeFsOperationImpl(normalized);
|
|
398
439
|
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
399
440
|
const oldest = fsOpCache.keys().next().value;
|
|
400
441
|
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
401
442
|
}
|
|
402
|
-
fsOpCache.set(
|
|
443
|
+
fsOpCache.set(normalized, computed);
|
|
403
444
|
return computed;
|
|
404
445
|
}
|
|
405
446
|
function analyzeFsOperationImpl(command) {
|
|
@@ -1219,7 +1260,11 @@ var init_dist = __esm({
|
|
|
1219
1260
|
"shield:project-jail:block-read-ssh",
|
|
1220
1261
|
"shield:project-jail:block-read-aws",
|
|
1221
1262
|
"shield:project-jail:block-read-env",
|
|
1222
|
-
"shield:project-jail:review-read-credentials"
|
|
1263
|
+
"shield:project-jail:review-read-credentials",
|
|
1264
|
+
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1265
|
+
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1266
|
+
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1267
|
+
"review-drop-truncate-shell"
|
|
1223
1268
|
]);
|
|
1224
1269
|
FS_OP_CACHE_MAX = 5e3;
|
|
1225
1270
|
fsOpCache = /* @__PURE__ */ new Map();
|
|
@@ -2659,6 +2704,22 @@ var init_cost_codex = __esm({
|
|
|
2659
2704
|
}
|
|
2660
2705
|
});
|
|
2661
2706
|
|
|
2707
|
+
// src/cost-gemini.ts
|
|
2708
|
+
var init_cost_gemini = __esm({
|
|
2709
|
+
"src/cost-gemini.ts"() {
|
|
2710
|
+
"use strict";
|
|
2711
|
+
init_litellm();
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2715
|
+
// src/cost-copilot.ts
|
|
2716
|
+
var init_cost_copilot = __esm({
|
|
2717
|
+
"src/cost-copilot.ts"() {
|
|
2718
|
+
"use strict";
|
|
2719
|
+
init_litellm();
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2662
2723
|
// src/costSync.ts
|
|
2663
2724
|
function decodeProjectDirName(dirName) {
|
|
2664
2725
|
return dirName.replace(/-/g, "/");
|
|
@@ -2671,6 +2732,8 @@ var init_costSync = __esm({
|
|
|
2671
2732
|
init_audit();
|
|
2672
2733
|
init_litellm();
|
|
2673
2734
|
init_cost_codex();
|
|
2735
|
+
init_cost_gemini();
|
|
2736
|
+
init_cost_copilot();
|
|
2674
2737
|
SYNC_INTERVAL_MS = 10 * 60 * 1e3;
|
|
2675
2738
|
}
|
|
2676
2739
|
});
|
package/dist/index.js
CHANGED
|
@@ -140,6 +140,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
140
140
|
const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
|
|
141
141
|
const dlpFields = meta?.dlpPattern ? { dlpPattern: meta.dlpPattern, dlpSample: meta.dlpSample } : {};
|
|
142
142
|
const cloudLinkField = meta?.cloudRequestId ? { cloudRequestId: meta.cloudRequestId } : {};
|
|
143
|
+
const workingDirField = meta?.workingDir ? { workingDir: meta.workingDir } : {};
|
|
144
|
+
const shell = process.env.SHELL ? import_path.default.basename(process.env.SHELL) : void 0;
|
|
145
|
+
const shellTypeField = shell ? { shellType: shell } : {};
|
|
143
146
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
144
147
|
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
145
148
|
// makes the JSONL easy to eyeball.
|
|
@@ -153,11 +156,14 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
153
156
|
...ruleNameField,
|
|
154
157
|
...dlpFields,
|
|
155
158
|
...cloudLinkField,
|
|
159
|
+
...workingDirField,
|
|
160
|
+
...shellTypeField,
|
|
156
161
|
...testRun,
|
|
157
162
|
agent: meta?.agent,
|
|
158
163
|
mcpServer: meta?.mcpServer,
|
|
159
164
|
sessionId: meta?.sessionId,
|
|
160
|
-
hostname: import_os.default.hostname()
|
|
165
|
+
hostname: import_os.default.hostname(),
|
|
166
|
+
platform: import_os.default.platform()
|
|
161
167
|
});
|
|
162
168
|
}
|
|
163
169
|
function appendConfigAudit(entry) {
|
|
@@ -1033,6 +1039,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1033
1039
|
if (f === PARSE_FAIL) return command;
|
|
1034
1040
|
try {
|
|
1035
1041
|
const strips = [];
|
|
1042
|
+
const rewrites = [];
|
|
1043
|
+
const msgSpans = /* @__PURE__ */ new Set();
|
|
1036
1044
|
syntax.Walk(f, (node) => {
|
|
1037
1045
|
if (!node) return false;
|
|
1038
1046
|
const n = node;
|
|
@@ -1048,25 +1056,46 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1048
1056
|
if (nextParts.length !== 1) continue;
|
|
1049
1057
|
const quotedNode = nextParts[0];
|
|
1050
1058
|
const nt = syntax.NodeType(quotedNode);
|
|
1059
|
+
const markStrip = () => {
|
|
1060
|
+
const s = next.Pos().Offset();
|
|
1061
|
+
const e = next.End().Offset();
|
|
1062
|
+
strips.push([s, e]);
|
|
1063
|
+
msgSpans.add(`${s}:${e}`);
|
|
1064
|
+
};
|
|
1051
1065
|
if (nt === "SglQuoted") {
|
|
1052
|
-
|
|
1066
|
+
markStrip();
|
|
1053
1067
|
} else if (nt === "DblQuoted") {
|
|
1054
1068
|
const innerParts = quotedNode.Parts || [];
|
|
1055
1069
|
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
1056
1070
|
if (allLit) {
|
|
1057
|
-
|
|
1071
|
+
markStrip();
|
|
1058
1072
|
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
1059
|
-
|
|
1073
|
+
markStrip();
|
|
1060
1074
|
}
|
|
1061
1075
|
}
|
|
1062
1076
|
}
|
|
1077
|
+
for (const arg of args) {
|
|
1078
|
+
const s = arg.Pos().Offset();
|
|
1079
|
+
const e = arg.End().Offset();
|
|
1080
|
+
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
1081
|
+
const resolved = resolveWordLiteral(arg);
|
|
1082
|
+
if (resolved === null) continue;
|
|
1083
|
+
const source = command.slice(s, e);
|
|
1084
|
+
if (resolved === source) continue;
|
|
1085
|
+
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
1086
|
+
rewrites.push([s, e, resolved]);
|
|
1087
|
+
}
|
|
1063
1088
|
return true;
|
|
1064
1089
|
});
|
|
1065
|
-
|
|
1066
|
-
|
|
1090
|
+
const edits = [
|
|
1091
|
+
...strips.map(([s, e]) => [s, e, '""']),
|
|
1092
|
+
...rewrites
|
|
1093
|
+
];
|
|
1094
|
+
if (edits.length === 0) return command;
|
|
1095
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
1067
1096
|
let result = command;
|
|
1068
|
-
for (const [
|
|
1069
|
-
result = result.slice(0,
|
|
1097
|
+
for (const [s, e, rep] of edits) {
|
|
1098
|
+
result = result.slice(0, s) + rep + result.slice(e);
|
|
1070
1099
|
}
|
|
1071
1100
|
return result;
|
|
1072
1101
|
} catch {
|
|
@@ -1231,8 +1260,35 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
1231
1260
|
"shield:project-jail:block-read-ssh",
|
|
1232
1261
|
"shield:project-jail:block-read-aws",
|
|
1233
1262
|
"shield:project-jail:block-read-env",
|
|
1234
|
-
"shield:project-jail:review-read-credentials"
|
|
1263
|
+
"shield:project-jail:review-read-credentials",
|
|
1264
|
+
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1265
|
+
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1266
|
+
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1267
|
+
"review-drop-truncate-shell"
|
|
1268
|
+
]);
|
|
1269
|
+
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
1270
|
+
"psql",
|
|
1271
|
+
"mysql",
|
|
1272
|
+
"mariadb",
|
|
1273
|
+
"sqlite3",
|
|
1274
|
+
"sqlplus",
|
|
1275
|
+
"cockroach",
|
|
1276
|
+
"clickhouse-client",
|
|
1277
|
+
"mongo",
|
|
1278
|
+
"mongosh"
|
|
1235
1279
|
]);
|
|
1280
|
+
var SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
|
|
1281
|
+
function analyzeSqlDestructive(command) {
|
|
1282
|
+
if (!SQL_DDL_RE.test(command)) return null;
|
|
1283
|
+
const { actions } = analyzeShellCommand(command);
|
|
1284
|
+
if (!actions.some((a) => SQL_DB_CLIS.has(a))) return null;
|
|
1285
|
+
return {
|
|
1286
|
+
ruleName: "review-drop-truncate-shell",
|
|
1287
|
+
verdict: "review",
|
|
1288
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
1289
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1236
1292
|
function isProtectedHomePath(rawPath) {
|
|
1237
1293
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
1238
1294
|
let underHome = false;
|
|
@@ -1481,19 +1537,20 @@ function extractShellDestinations(command) {
|
|
|
1481
1537
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1482
1538
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1483
1539
|
function analyzeFsOperation(command) {
|
|
1484
|
-
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
1487
|
-
fsOpCache.
|
|
1488
|
-
fsOpCache.
|
|
1540
|
+
const normalized = normalizeCommandForPolicy(command);
|
|
1541
|
+
if (!FS_OP_PRESCREEN_RE.test(normalized)) return null;
|
|
1542
|
+
if (fsOpCache.has(normalized)) {
|
|
1543
|
+
const hit = fsOpCache.get(normalized) ?? null;
|
|
1544
|
+
fsOpCache.delete(normalized);
|
|
1545
|
+
fsOpCache.set(normalized, hit);
|
|
1489
1546
|
return hit;
|
|
1490
1547
|
}
|
|
1491
|
-
const computed = analyzeFsOperationImpl(
|
|
1548
|
+
const computed = analyzeFsOperationImpl(normalized);
|
|
1492
1549
|
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
1493
1550
|
const oldest = fsOpCache.keys().next().value;
|
|
1494
1551
|
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
1495
1552
|
}
|
|
1496
|
-
fsOpCache.set(
|
|
1553
|
+
fsOpCache.set(normalized, computed);
|
|
1497
1554
|
return computed;
|
|
1498
1555
|
}
|
|
1499
1556
|
function analyzeFsOperationImpl(command) {
|
|
@@ -2186,6 +2243,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2186
2243
|
ruleDescription: fsVerdict.reason
|
|
2187
2244
|
};
|
|
2188
2245
|
}
|
|
2246
|
+
const sqlVerdict = analyzeSqlDestructive(bashCommand);
|
|
2247
|
+
if (sqlVerdict) {
|
|
2248
|
+
return {
|
|
2249
|
+
decision: sqlVerdict.verdict,
|
|
2250
|
+
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2251
|
+
reason: sqlVerdict.reason,
|
|
2252
|
+
tier: 2,
|
|
2253
|
+
ruleName: sqlVerdict.ruleName,
|
|
2254
|
+
ruleDescription: sqlVerdict.description
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2189
2257
|
}
|
|
2190
2258
|
if (config.policy.smartRules.length > 0) {
|
|
2191
2259
|
const matchedRule = config.policy.smartRules.find(
|
|
@@ -4915,7 +4983,8 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4915
4983
|
}
|
|
4916
4984
|
return _authorizeHeadlessCore(toolName, args, meta, options);
|
|
4917
4985
|
}
|
|
4918
|
-
async function _authorizeHeadlessCore(toolName, args,
|
|
4986
|
+
async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
4987
|
+
const meta = options?.cwd && !metaArg?.workingDir ? { ...metaArg, workingDir: options.cwd } : metaArg;
|
|
4919
4988
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
4920
4989
|
const pauseState = checkPause();
|
|
4921
4990
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
package/dist/index.mjs
CHANGED
|
@@ -121,6 +121,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
121
121
|
const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
|
|
122
122
|
const dlpFields = meta?.dlpPattern ? { dlpPattern: meta.dlpPattern, dlpSample: meta.dlpSample } : {};
|
|
123
123
|
const cloudLinkField = meta?.cloudRequestId ? { cloudRequestId: meta.cloudRequestId } : {};
|
|
124
|
+
const workingDirField = meta?.workingDir ? { workingDir: meta.workingDir } : {};
|
|
125
|
+
const shell = process.env.SHELL ? path.basename(process.env.SHELL) : void 0;
|
|
126
|
+
const shellTypeField = shell ? { shellType: shell } : {};
|
|
124
127
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
125
128
|
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
126
129
|
// makes the JSONL easy to eyeball.
|
|
@@ -134,11 +137,14 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
134
137
|
...ruleNameField,
|
|
135
138
|
...dlpFields,
|
|
136
139
|
...cloudLinkField,
|
|
140
|
+
...workingDirField,
|
|
141
|
+
...shellTypeField,
|
|
137
142
|
...testRun,
|
|
138
143
|
agent: meta?.agent,
|
|
139
144
|
mcpServer: meta?.mcpServer,
|
|
140
145
|
sessionId: meta?.sessionId,
|
|
141
|
-
hostname: os.hostname()
|
|
146
|
+
hostname: os.hostname(),
|
|
147
|
+
platform: os.platform()
|
|
142
148
|
});
|
|
143
149
|
}
|
|
144
150
|
function appendConfigAudit(entry) {
|
|
@@ -1003,6 +1009,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1003
1009
|
if (f === PARSE_FAIL) return command;
|
|
1004
1010
|
try {
|
|
1005
1011
|
const strips = [];
|
|
1012
|
+
const rewrites = [];
|
|
1013
|
+
const msgSpans = /* @__PURE__ */ new Set();
|
|
1006
1014
|
syntax.Walk(f, (node) => {
|
|
1007
1015
|
if (!node) return false;
|
|
1008
1016
|
const n = node;
|
|
@@ -1018,25 +1026,46 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1018
1026
|
if (nextParts.length !== 1) continue;
|
|
1019
1027
|
const quotedNode = nextParts[0];
|
|
1020
1028
|
const nt = syntax.NodeType(quotedNode);
|
|
1029
|
+
const markStrip = () => {
|
|
1030
|
+
const s = next.Pos().Offset();
|
|
1031
|
+
const e = next.End().Offset();
|
|
1032
|
+
strips.push([s, e]);
|
|
1033
|
+
msgSpans.add(`${s}:${e}`);
|
|
1034
|
+
};
|
|
1021
1035
|
if (nt === "SglQuoted") {
|
|
1022
|
-
|
|
1036
|
+
markStrip();
|
|
1023
1037
|
} else if (nt === "DblQuoted") {
|
|
1024
1038
|
const innerParts = quotedNode.Parts || [];
|
|
1025
1039
|
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
1026
1040
|
if (allLit) {
|
|
1027
|
-
|
|
1041
|
+
markStrip();
|
|
1028
1042
|
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
1029
|
-
|
|
1043
|
+
markStrip();
|
|
1030
1044
|
}
|
|
1031
1045
|
}
|
|
1032
1046
|
}
|
|
1047
|
+
for (const arg of args) {
|
|
1048
|
+
const s = arg.Pos().Offset();
|
|
1049
|
+
const e = arg.End().Offset();
|
|
1050
|
+
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
1051
|
+
const resolved = resolveWordLiteral(arg);
|
|
1052
|
+
if (resolved === null) continue;
|
|
1053
|
+
const source = command.slice(s, e);
|
|
1054
|
+
if (resolved === source) continue;
|
|
1055
|
+
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
1056
|
+
rewrites.push([s, e, resolved]);
|
|
1057
|
+
}
|
|
1033
1058
|
return true;
|
|
1034
1059
|
});
|
|
1035
|
-
|
|
1036
|
-
|
|
1060
|
+
const edits = [
|
|
1061
|
+
...strips.map(([s, e]) => [s, e, '""']),
|
|
1062
|
+
...rewrites
|
|
1063
|
+
];
|
|
1064
|
+
if (edits.length === 0) return command;
|
|
1065
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
1037
1066
|
let result = command;
|
|
1038
|
-
for (const [
|
|
1039
|
-
result = result.slice(0,
|
|
1067
|
+
for (const [s, e, rep] of edits) {
|
|
1068
|
+
result = result.slice(0, s) + rep + result.slice(e);
|
|
1040
1069
|
}
|
|
1041
1070
|
return result;
|
|
1042
1071
|
} catch {
|
|
@@ -1201,8 +1230,35 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
1201
1230
|
"shield:project-jail:block-read-ssh",
|
|
1202
1231
|
"shield:project-jail:block-read-aws",
|
|
1203
1232
|
"shield:project-jail:block-read-env",
|
|
1204
|
-
"shield:project-jail:review-read-credentials"
|
|
1233
|
+
"shield:project-jail:review-read-credentials",
|
|
1234
|
+
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1235
|
+
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1236
|
+
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1237
|
+
"review-drop-truncate-shell"
|
|
1238
|
+
]);
|
|
1239
|
+
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
1240
|
+
"psql",
|
|
1241
|
+
"mysql",
|
|
1242
|
+
"mariadb",
|
|
1243
|
+
"sqlite3",
|
|
1244
|
+
"sqlplus",
|
|
1245
|
+
"cockroach",
|
|
1246
|
+
"clickhouse-client",
|
|
1247
|
+
"mongo",
|
|
1248
|
+
"mongosh"
|
|
1205
1249
|
]);
|
|
1250
|
+
var SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
|
|
1251
|
+
function analyzeSqlDestructive(command) {
|
|
1252
|
+
if (!SQL_DDL_RE.test(command)) return null;
|
|
1253
|
+
const { actions } = analyzeShellCommand(command);
|
|
1254
|
+
if (!actions.some((a) => SQL_DB_CLIS.has(a))) return null;
|
|
1255
|
+
return {
|
|
1256
|
+
ruleName: "review-drop-truncate-shell",
|
|
1257
|
+
verdict: "review",
|
|
1258
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
1259
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1206
1262
|
function isProtectedHomePath(rawPath) {
|
|
1207
1263
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
1208
1264
|
let underHome = false;
|
|
@@ -1451,19 +1507,20 @@ function extractShellDestinations(command) {
|
|
|
1451
1507
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1452
1508
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1453
1509
|
function analyzeFsOperation(command) {
|
|
1454
|
-
|
|
1455
|
-
if (
|
|
1456
|
-
|
|
1457
|
-
fsOpCache.
|
|
1458
|
-
fsOpCache.
|
|
1510
|
+
const normalized = normalizeCommandForPolicy(command);
|
|
1511
|
+
if (!FS_OP_PRESCREEN_RE.test(normalized)) return null;
|
|
1512
|
+
if (fsOpCache.has(normalized)) {
|
|
1513
|
+
const hit = fsOpCache.get(normalized) ?? null;
|
|
1514
|
+
fsOpCache.delete(normalized);
|
|
1515
|
+
fsOpCache.set(normalized, hit);
|
|
1459
1516
|
return hit;
|
|
1460
1517
|
}
|
|
1461
|
-
const computed = analyzeFsOperationImpl(
|
|
1518
|
+
const computed = analyzeFsOperationImpl(normalized);
|
|
1462
1519
|
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
1463
1520
|
const oldest = fsOpCache.keys().next().value;
|
|
1464
1521
|
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
1465
1522
|
}
|
|
1466
|
-
fsOpCache.set(
|
|
1523
|
+
fsOpCache.set(normalized, computed);
|
|
1467
1524
|
return computed;
|
|
1468
1525
|
}
|
|
1469
1526
|
function analyzeFsOperationImpl(command) {
|
|
@@ -2156,6 +2213,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2156
2213
|
ruleDescription: fsVerdict.reason
|
|
2157
2214
|
};
|
|
2158
2215
|
}
|
|
2216
|
+
const sqlVerdict = analyzeSqlDestructive(bashCommand);
|
|
2217
|
+
if (sqlVerdict) {
|
|
2218
|
+
return {
|
|
2219
|
+
decision: sqlVerdict.verdict,
|
|
2220
|
+
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2221
|
+
reason: sqlVerdict.reason,
|
|
2222
|
+
tier: 2,
|
|
2223
|
+
ruleName: sqlVerdict.ruleName,
|
|
2224
|
+
ruleDescription: sqlVerdict.description
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2159
2227
|
}
|
|
2160
2228
|
if (config.policy.smartRules.length > 0) {
|
|
2161
2229
|
const matchedRule = config.policy.smartRules.find(
|
|
@@ -4885,7 +4953,8 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4885
4953
|
}
|
|
4886
4954
|
return _authorizeHeadlessCore(toolName, args, meta, options);
|
|
4887
4955
|
}
|
|
4888
|
-
async function _authorizeHeadlessCore(toolName, args,
|
|
4956
|
+
async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
4957
|
+
const meta = options?.cwd && !metaArg?.workingDir ? { ...metaArg, workingDir: options.cwd } : metaArg;
|
|
4889
4958
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
4890
4959
|
const pauseState = checkPause();
|
|
4891
4960
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|