@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.
@@ -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
- strips.push([next.Pos().Offset(), next.End().Offset()]);
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
- strips.push([next.Pos().Offset(), next.End().Offset()]);
276
+ markStrip();
269
277
  } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
270
- strips.push([next.Pos().Offset(), next.End().Offset()]);
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
- if (strips.length === 0) return command;
277
- strips.sort((a, b) => b[0] - a[0]);
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 [start, end] of strips) {
280
- result = result.slice(0, start) + '""' + result.slice(end);
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
- if (!FS_OP_PRESCREEN_RE.test(command)) return null;
391
- if (fsOpCache.has(command)) {
392
- const hit = fsOpCache.get(command) ?? null;
393
- fsOpCache.delete(command);
394
- fsOpCache.set(command, hit);
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(command);
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(command, computed);
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
- strips.push([next.Pos().Offset(), next.End().Offset()]);
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
- strips.push([next.Pos().Offset(), next.End().Offset()]);
1071
+ markStrip();
1058
1072
  } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
1059
- strips.push([next.Pos().Offset(), next.End().Offset()]);
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
- if (strips.length === 0) return command;
1066
- strips.sort((a, b) => b[0] - a[0]);
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 [start, end] of strips) {
1069
- result = result.slice(0, start) + '""' + result.slice(end);
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
- if (!FS_OP_PRESCREEN_RE.test(command)) return null;
1485
- if (fsOpCache.has(command)) {
1486
- const hit = fsOpCache.get(command) ?? null;
1487
- fsOpCache.delete(command);
1488
- fsOpCache.set(command, hit);
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(command);
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(command, computed);
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, meta, options) {
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
- strips.push([next.Pos().Offset(), next.End().Offset()]);
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
- strips.push([next.Pos().Offset(), next.End().Offset()]);
1041
+ markStrip();
1028
1042
  } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
1029
- strips.push([next.Pos().Offset(), next.End().Offset()]);
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
- if (strips.length === 0) return command;
1036
- strips.sort((a, b) => b[0] - a[0]);
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 [start, end] of strips) {
1039
- result = result.slice(0, start) + '""' + result.slice(end);
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
- if (!FS_OP_PRESCREEN_RE.test(command)) return null;
1455
- if (fsOpCache.has(command)) {
1456
- const hit = fsOpCache.get(command) ?? null;
1457
- fsOpCache.delete(command);
1458
- fsOpCache.set(command, hit);
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(command);
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(command, computed);
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, meta, options) {
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.31.0",
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",