@node9/proxy 1.43.0 → 1.45.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.
@@ -1264,7 +1264,12 @@ var init_dist = __esm({
1264
1264
  // SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
1265
1265
  // raw-regex smart rule is suppressed for bash — its cond1 read a grep
1266
1266
  // alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
1267
- "review-drop-truncate-shell"
1267
+ "review-drop-truncate-shell",
1268
+ // chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
1269
+ // regex smart rule is suppressed for bash — it matched `chmod 777` inside a
1270
+ // `node -e` / `python -c` string literal (a detection pattern, not a run
1271
+ // command) → false positive.
1272
+ "shield:filesystem:review-chmod-777"
1268
1273
  ]);
1269
1274
  FS_OP_CACHE_MAX = 5e3;
1270
1275
  fsOpCache = /* @__PURE__ */ new Map();
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ __export(audit_exports, {
75
75
  appendLocalAudit: () => appendLocalAudit,
76
76
  appendToLog: () => appendToLog,
77
77
  buildArgsPreview: () => buildArgsPreview,
78
+ filePathFromArgs: () => filePathFromArgs,
78
79
  generateEventId: () => generateEventId,
79
80
  redactSecrets: () => redactSecrets
80
81
  });
@@ -131,6 +132,25 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
131
132
  cwd: process.cwd()
132
133
  });
133
134
  }
135
+ function filePathFromArgs(args) {
136
+ let obj = args;
137
+ if (typeof obj === "string") {
138
+ const t = obj.trim();
139
+ if (!t.startsWith("{") || !t.endsWith("}")) return void 0;
140
+ if (!t.includes("file_path") && !t.includes("notebook_path")) return void 0;
141
+ try {
142
+ obj = JSON.parse(t);
143
+ } catch {
144
+ return void 0;
145
+ }
146
+ }
147
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
148
+ const o = obj;
149
+ const v = o.file_path ?? o.notebook_path;
150
+ if (typeof v === "string" && v) return v.slice(0, 1024);
151
+ }
152
+ return void 0;
153
+ }
134
154
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
135
155
  const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
136
156
  const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
@@ -143,10 +163,19 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
143
163
  const workingDirField = meta?.workingDir ? { workingDir: meta.workingDir } : {};
144
164
  const shell = process.env.SHELL ? import_path.default.basename(process.env.SHELL) : void 0;
145
165
  const shellTypeField = shell ? { shellType: shell } : {};
166
+ const editFilePath = filePathFromArgs(args);
167
+ const editFilePathField = editFilePath ? { editFilePath } : {};
168
+ const loopCountField = typeof meta?.loopCount === "number" ? { loopCount: meta.loopCount } : {};
169
+ const transcriptPathField = meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {};
170
+ const taintFields = meta?.taintFromEid ? {
171
+ taintFromEid: meta.taintFromEid,
172
+ ...meta.taintSource && { taintSource: meta.taintSource }
173
+ } : {};
174
+ const eid = generateEventId();
146
175
  appendToLog(LOCAL_AUDIT_LOG, {
147
176
  // eid first: the outbox shipper dedups on it, and a fixed leading field
148
177
  // makes the JSONL easy to eyeball.
149
- eid: generateEventId(),
178
+ eid,
150
179
  ts: (/* @__PURE__ */ new Date()).toISOString(),
151
180
  tool: toolName,
152
181
  ...agentToolNameField,
@@ -158,6 +187,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
158
187
  ...cloudLinkField,
159
188
  ...workingDirField,
160
189
  ...shellTypeField,
190
+ ...editFilePathField,
191
+ ...loopCountField,
192
+ ...transcriptPathField,
193
+ ...taintFields,
161
194
  ...testRun,
162
195
  agent: meta?.agent,
163
196
  mcpServer: meta?.mcpServer,
@@ -165,6 +198,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
165
198
  hostname: import_os.default.hostname(),
166
199
  platform: import_os.default.platform()
167
200
  });
201
+ return eid;
168
202
  }
169
203
  function appendConfigAudit(entry) {
170
204
  appendToLog(LOCAL_AUDIT_LOG, {
@@ -1278,7 +1312,12 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
1278
1312
  // SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
1279
1313
  // raw-regex smart rule is suppressed for bash — its cond1 read a grep
1280
1314
  // alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
1281
- "review-drop-truncate-shell"
1315
+ "review-drop-truncate-shell",
1316
+ // chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
1317
+ // regex smart rule is suppressed for bash — it matched `chmod 777` inside a
1318
+ // `node -e` / `python -c` string literal (a detection pattern, not a run
1319
+ // command) → false positive.
1320
+ "shield:filesystem:review-chmod-777"
1282
1321
  ]);
1283
1322
  var SQL_DB_CLIS = /* @__PURE__ */ new Set([
1284
1323
  "psql",
@@ -1303,6 +1342,62 @@ function analyzeSqlDestructive(command) {
1303
1342
  description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
1304
1343
  };
1305
1344
  }
1345
+ var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
1346
+ var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
1347
+ "sudo",
1348
+ "doas",
1349
+ "env",
1350
+ "xargs",
1351
+ "time",
1352
+ "nice",
1353
+ "ionice",
1354
+ "nohup",
1355
+ "setsid",
1356
+ "stdbuf",
1357
+ "timeout",
1358
+ "command",
1359
+ "exec"
1360
+ ]);
1361
+ function chmodHasOpenPermMode(command) {
1362
+ const f = parseShared(command);
1363
+ if (f === PARSE_FAIL) return false;
1364
+ let found = false;
1365
+ try {
1366
+ syntax.Walk(f, (node) => {
1367
+ if (!node || found) return false;
1368
+ const n = node;
1369
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1370
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1371
+ if (words.length === 0) return true;
1372
+ const name = (words[0] ?? "").toLowerCase();
1373
+ let idx = -1;
1374
+ if (name === "chmod") idx = 0;
1375
+ else if (COMMAND_WRAPPERS.has(name))
1376
+ idx = words.findIndex((w, i) => i > 0 && w?.toLowerCase() === "chmod");
1377
+ if (idx < 0) return true;
1378
+ for (let i = idx + 1; i < words.length; i++) {
1379
+ const w = words[i];
1380
+ if (w !== null && w.startsWith("-")) continue;
1381
+ if (w !== null && CHMOD_OPEN_PERM_TOKENS.has(w.toLowerCase())) found = true;
1382
+ break;
1383
+ }
1384
+ return true;
1385
+ });
1386
+ } catch {
1387
+ return found;
1388
+ }
1389
+ return found;
1390
+ }
1391
+ function analyzeChmod777(command) {
1392
+ if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
1393
+ if (!chmodHasOpenPermMode(command)) return null;
1394
+ return {
1395
+ ruleName: "shield:filesystem:review-chmod-777",
1396
+ verdict: "review",
1397
+ reason: "chmod 777 requires human approval (filesystem shield)",
1398
+ description: "The AI wants to make a file world-writable/executable (chmod 777). This removes the permission protection on the file so any user or process can modify or run it."
1399
+ };
1400
+ }
1306
1401
  function isProtectedHomePath(rawPath) {
1307
1402
  let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
1308
1403
  let underHome = false;
@@ -2268,6 +2363,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2268
2363
  ruleDescription: sqlVerdict.description
2269
2364
  };
2270
2365
  }
2366
+ const chmodVerdict = analyzeChmod777(bashCommand);
2367
+ if (chmodVerdict) {
2368
+ return {
2369
+ decision: chmodVerdict.verdict,
2370
+ blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
2371
+ reason: chmodVerdict.reason,
2372
+ tier: 2,
2373
+ ruleName: chmodVerdict.ruleName,
2374
+ ruleDescription: chmodVerdict.description
2375
+ };
2376
+ }
2271
2377
  }
2272
2378
  if (config.policy.smartRules.length > 0) {
2273
2379
  const matchedRule = config.policy.smartRules.find(
@@ -4370,14 +4476,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
4370
4476
  const { id, allowCount } = await res.json();
4371
4477
  return { id, allowCount: allowCount ?? 1 };
4372
4478
  }
4373
- async function notifyTaint(filePath, source) {
4479
+ async function notifyTaint(filePath, source, fromEid) {
4374
4480
  if (!isDaemonRunning()) return;
4375
4481
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
4376
4482
  try {
4377
4483
  await fetch(`${base}/taint`, {
4378
4484
  method: "POST",
4379
4485
  headers: { "Content-Type": "application/json" },
4380
- body: JSON.stringify({ path: filePath, source }),
4486
+ // Phase D2 fromEid is the audit eid that created this taint (edge source).
4487
+ body: JSON.stringify({ path: filePath, source, ...fromEid && { fromEid } }),
4381
4488
  signal: AbortSignal.timeout(1e3)
4382
4489
  });
4383
4490
  } catch {
@@ -5065,7 +5172,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5065
5172
  if (filePaths.length > 0) {
5066
5173
  const taintResult = await checkTaint(filePaths);
5067
5174
  if (taintResult.tainted && taintResult.record) {
5068
- const { path: taintedPath, source: taintSource } = taintResult.record;
5175
+ const {
5176
+ path: taintedPath,
5177
+ source: taintSource,
5178
+ fromEid: taintFromEid
5179
+ } = taintResult.record;
5069
5180
  taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
5070
5181
  if (config.policy.egress?.enabled) {
5071
5182
  const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
@@ -5079,7 +5190,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5079
5190
  args,
5080
5191
  "deny",
5081
5192
  isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
5082
- { ...meta, ruleName: `taint-egress:${eg.host}` },
5193
+ // Phase D2 — the causal taint edge: from the row that created
5194
+ // the taint (taintFromEid) to this block row.
5195
+ { ...meta, ruleName: `taint-egress:${eg.host}`, taintFromEid, taintSource },
5083
5196
  hashAuditArgs
5084
5197
  );
5085
5198
  if (isObserveMode) {
@@ -5116,8 +5229,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5116
5229
  if (dlpMatch) {
5117
5230
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
5118
5231
  if (dlpMatch.severity === "block") {
5232
+ let dlpEid;
5119
5233
  if (!isManual)
5120
- appendLocalAudit(
5234
+ dlpEid = appendLocalAudit(
5121
5235
  toolName,
5122
5236
  args,
5123
5237
  "deny",
@@ -5130,7 +5244,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5130
5244
  true
5131
5245
  );
5132
5246
  if (isWriteTool(toolName) && filePath) {
5133
- await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
5247
+ await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`, dlpEid);
5134
5248
  }
5135
5249
  if (isObserveMode) {
5136
5250
  return {
@@ -5222,7 +5336,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5222
5336
  if (loopResult.looping) {
5223
5337
  const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
5224
5338
  if (!isManual)
5225
- appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
5339
+ appendLocalAudit(
5340
+ toolName,
5341
+ args,
5342
+ "deny",
5343
+ "loop-detected",
5344
+ // Phase B — carry the loop magnitude so the SaaS shows "looped Nx".
5345
+ { ...meta, loopCount: loopResult.count },
5346
+ hashAuditArgs
5347
+ );
5226
5348
  return {
5227
5349
  approved: false,
5228
5350
  reason,
package/dist/index.mjs CHANGED
@@ -52,6 +52,7 @@ __export(audit_exports, {
52
52
  appendLocalAudit: () => appendLocalAudit,
53
53
  appendToLog: () => appendToLog,
54
54
  buildArgsPreview: () => buildArgsPreview,
55
+ filePathFromArgs: () => filePathFromArgs,
55
56
  generateEventId: () => generateEventId,
56
57
  redactSecrets: () => redactSecrets
57
58
  });
@@ -112,6 +113,25 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
112
113
  cwd: process.cwd()
113
114
  });
114
115
  }
116
+ function filePathFromArgs(args) {
117
+ let obj = args;
118
+ if (typeof obj === "string") {
119
+ const t = obj.trim();
120
+ if (!t.startsWith("{") || !t.endsWith("}")) return void 0;
121
+ if (!t.includes("file_path") && !t.includes("notebook_path")) return void 0;
122
+ try {
123
+ obj = JSON.parse(t);
124
+ } catch {
125
+ return void 0;
126
+ }
127
+ }
128
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
129
+ const o = obj;
130
+ const v = o.file_path ?? o.notebook_path;
131
+ if (typeof v === "string" && v) return v.slice(0, 1024);
132
+ }
133
+ return void 0;
134
+ }
115
135
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
116
136
  const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
117
137
  const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
@@ -124,10 +144,19 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
124
144
  const workingDirField = meta?.workingDir ? { workingDir: meta.workingDir } : {};
125
145
  const shell = process.env.SHELL ? path.basename(process.env.SHELL) : void 0;
126
146
  const shellTypeField = shell ? { shellType: shell } : {};
147
+ const editFilePath = filePathFromArgs(args);
148
+ const editFilePathField = editFilePath ? { editFilePath } : {};
149
+ const loopCountField = typeof meta?.loopCount === "number" ? { loopCount: meta.loopCount } : {};
150
+ const transcriptPathField = meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {};
151
+ const taintFields = meta?.taintFromEid ? {
152
+ taintFromEid: meta.taintFromEid,
153
+ ...meta.taintSource && { taintSource: meta.taintSource }
154
+ } : {};
155
+ const eid = generateEventId();
127
156
  appendToLog(LOCAL_AUDIT_LOG, {
128
157
  // eid first: the outbox shipper dedups on it, and a fixed leading field
129
158
  // makes the JSONL easy to eyeball.
130
- eid: generateEventId(),
159
+ eid,
131
160
  ts: (/* @__PURE__ */ new Date()).toISOString(),
132
161
  tool: toolName,
133
162
  ...agentToolNameField,
@@ -139,6 +168,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
139
168
  ...cloudLinkField,
140
169
  ...workingDirField,
141
170
  ...shellTypeField,
171
+ ...editFilePathField,
172
+ ...loopCountField,
173
+ ...transcriptPathField,
174
+ ...taintFields,
142
175
  ...testRun,
143
176
  agent: meta?.agent,
144
177
  mcpServer: meta?.mcpServer,
@@ -146,6 +179,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
146
179
  hostname: os.hostname(),
147
180
  platform: os.platform()
148
181
  });
182
+ return eid;
149
183
  }
150
184
  function appendConfigAudit(entry) {
151
185
  appendToLog(LOCAL_AUDIT_LOG, {
@@ -1248,7 +1282,12 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
1248
1282
  // SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
1249
1283
  // raw-regex smart rule is suppressed for bash — its cond1 read a grep
1250
1284
  // alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
1251
- "review-drop-truncate-shell"
1285
+ "review-drop-truncate-shell",
1286
+ // chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
1287
+ // regex smart rule is suppressed for bash — it matched `chmod 777` inside a
1288
+ // `node -e` / `python -c` string literal (a detection pattern, not a run
1289
+ // command) → false positive.
1290
+ "shield:filesystem:review-chmod-777"
1252
1291
  ]);
1253
1292
  var SQL_DB_CLIS = /* @__PURE__ */ new Set([
1254
1293
  "psql",
@@ -1273,6 +1312,62 @@ function analyzeSqlDestructive(command) {
1273
1312
  description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
1274
1313
  };
1275
1314
  }
1315
+ var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
1316
+ var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
1317
+ "sudo",
1318
+ "doas",
1319
+ "env",
1320
+ "xargs",
1321
+ "time",
1322
+ "nice",
1323
+ "ionice",
1324
+ "nohup",
1325
+ "setsid",
1326
+ "stdbuf",
1327
+ "timeout",
1328
+ "command",
1329
+ "exec"
1330
+ ]);
1331
+ function chmodHasOpenPermMode(command) {
1332
+ const f = parseShared(command);
1333
+ if (f === PARSE_FAIL) return false;
1334
+ let found = false;
1335
+ try {
1336
+ syntax.Walk(f, (node) => {
1337
+ if (!node || found) return false;
1338
+ const n = node;
1339
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1340
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1341
+ if (words.length === 0) return true;
1342
+ const name = (words[0] ?? "").toLowerCase();
1343
+ let idx = -1;
1344
+ if (name === "chmod") idx = 0;
1345
+ else if (COMMAND_WRAPPERS.has(name))
1346
+ idx = words.findIndex((w, i) => i > 0 && w?.toLowerCase() === "chmod");
1347
+ if (idx < 0) return true;
1348
+ for (let i = idx + 1; i < words.length; i++) {
1349
+ const w = words[i];
1350
+ if (w !== null && w.startsWith("-")) continue;
1351
+ if (w !== null && CHMOD_OPEN_PERM_TOKENS.has(w.toLowerCase())) found = true;
1352
+ break;
1353
+ }
1354
+ return true;
1355
+ });
1356
+ } catch {
1357
+ return found;
1358
+ }
1359
+ return found;
1360
+ }
1361
+ function analyzeChmod777(command) {
1362
+ if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
1363
+ if (!chmodHasOpenPermMode(command)) return null;
1364
+ return {
1365
+ ruleName: "shield:filesystem:review-chmod-777",
1366
+ verdict: "review",
1367
+ reason: "chmod 777 requires human approval (filesystem shield)",
1368
+ description: "The AI wants to make a file world-writable/executable (chmod 777). This removes the permission protection on the file so any user or process can modify or run it."
1369
+ };
1370
+ }
1276
1371
  function isProtectedHomePath(rawPath) {
1277
1372
  let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
1278
1373
  let underHome = false;
@@ -2238,6 +2333,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2238
2333
  ruleDescription: sqlVerdict.description
2239
2334
  };
2240
2335
  }
2336
+ const chmodVerdict = analyzeChmod777(bashCommand);
2337
+ if (chmodVerdict) {
2338
+ return {
2339
+ decision: chmodVerdict.verdict,
2340
+ blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
2341
+ reason: chmodVerdict.reason,
2342
+ tier: 2,
2343
+ ruleName: chmodVerdict.ruleName,
2344
+ ruleDescription: chmodVerdict.description
2345
+ };
2346
+ }
2241
2347
  }
2242
2348
  if (config.policy.smartRules.length > 0) {
2243
2349
  const matchedRule = config.policy.smartRules.find(
@@ -4340,14 +4446,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
4340
4446
  const { id, allowCount } = await res.json();
4341
4447
  return { id, allowCount: allowCount ?? 1 };
4342
4448
  }
4343
- async function notifyTaint(filePath, source) {
4449
+ async function notifyTaint(filePath, source, fromEid) {
4344
4450
  if (!isDaemonRunning()) return;
4345
4451
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
4346
4452
  try {
4347
4453
  await fetch(`${base}/taint`, {
4348
4454
  method: "POST",
4349
4455
  headers: { "Content-Type": "application/json" },
4350
- body: JSON.stringify({ path: filePath, source }),
4456
+ // Phase D2 fromEid is the audit eid that created this taint (edge source).
4457
+ body: JSON.stringify({ path: filePath, source, ...fromEid && { fromEid } }),
4351
4458
  signal: AbortSignal.timeout(1e3)
4352
4459
  });
4353
4460
  } catch {
@@ -5035,7 +5142,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5035
5142
  if (filePaths.length > 0) {
5036
5143
  const taintResult = await checkTaint(filePaths);
5037
5144
  if (taintResult.tainted && taintResult.record) {
5038
- const { path: taintedPath, source: taintSource } = taintResult.record;
5145
+ const {
5146
+ path: taintedPath,
5147
+ source: taintSource,
5148
+ fromEid: taintFromEid
5149
+ } = taintResult.record;
5039
5150
  taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
5040
5151
  if (config.policy.egress?.enabled) {
5041
5152
  const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
@@ -5049,7 +5160,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5049
5160
  args,
5050
5161
  "deny",
5051
5162
  isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
5052
- { ...meta, ruleName: `taint-egress:${eg.host}` },
5163
+ // Phase D2 — the causal taint edge: from the row that created
5164
+ // the taint (taintFromEid) to this block row.
5165
+ { ...meta, ruleName: `taint-egress:${eg.host}`, taintFromEid, taintSource },
5053
5166
  hashAuditArgs
5054
5167
  );
5055
5168
  if (isObserveMode) {
@@ -5086,8 +5199,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5086
5199
  if (dlpMatch) {
5087
5200
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
5088
5201
  if (dlpMatch.severity === "block") {
5202
+ let dlpEid;
5089
5203
  if (!isManual)
5090
- appendLocalAudit(
5204
+ dlpEid = appendLocalAudit(
5091
5205
  toolName,
5092
5206
  args,
5093
5207
  "deny",
@@ -5100,7 +5214,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5100
5214
  true
5101
5215
  );
5102
5216
  if (isWriteTool(toolName) && filePath) {
5103
- await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
5217
+ await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`, dlpEid);
5104
5218
  }
5105
5219
  if (isObserveMode) {
5106
5220
  return {
@@ -5192,7 +5306,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5192
5306
  if (loopResult.looping) {
5193
5307
  const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
5194
5308
  if (!isManual)
5195
- appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
5309
+ appendLocalAudit(
5310
+ toolName,
5311
+ args,
5312
+ "deny",
5313
+ "loop-detected",
5314
+ // Phase B — carry the loop magnitude so the SaaS shows "looped Nx".
5315
+ { ...meta, loopCount: loopResult.count },
5316
+ hashAuditArgs
5317
+ );
5196
5318
  return {
5197
5319
  approved: false,
5198
5320
  reason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.43.0",
3
+ "version": "1.45.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",