@node9/proxy 1.18.2 → 1.18.3

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.mjs CHANGED
@@ -428,9 +428,65 @@ function redactText(text) {
428
428
  }
429
429
  return { result, found };
430
430
  }
431
+ function isCatHeredocOrLit(part) {
432
+ if (!part) return false;
433
+ const t = syntax.NodeType(part);
434
+ if (t === "Lit") return true;
435
+ if (t !== "CmdSubst") return false;
436
+ const stmts = part.Stmts || [];
437
+ if (stmts.length !== 1) return false;
438
+ const stmt = stmts[0];
439
+ const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
440
+ const hasHeredoc = redirs.some((r) => r && r.Hdoc);
441
+ if (!hasHeredoc) return false;
442
+ const cmd = stmt.Cmd;
443
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
444
+ const firstArg2 = cmd.Args?.[0]?.Parts || [];
445
+ if (firstArg2.length !== 1 || syntax.NodeType(firstArg2[0]) !== "Lit") return false;
446
+ return (firstArg2[0].Value || "").toLowerCase() === "cat";
447
+ }
448
+ function parseShared(command) {
449
+ const cached = astCache.get(command);
450
+ if (cached !== void 0) {
451
+ astCache.delete(command);
452
+ astCache.set(command, cached);
453
+ return cached;
454
+ }
455
+ let parsed;
456
+ try {
457
+ parsed = sharedParser.Parse(command, "cmd");
458
+ } catch {
459
+ parsed = PARSE_FAIL;
460
+ }
461
+ if (astCache.size >= AST_CACHE_MAX) {
462
+ const oldest = astCache.keys().next().value;
463
+ if (oldest !== void 0) astCache.delete(oldest);
464
+ }
465
+ astCache.set(command, parsed);
466
+ return parsed;
467
+ }
468
+ function cachedNormalize(command, compute) {
469
+ const hit = normalizeCache.get(command);
470
+ if (hit !== void 0) {
471
+ normalizeCache.delete(command);
472
+ normalizeCache.set(command, hit);
473
+ return hit;
474
+ }
475
+ const result = compute();
476
+ if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
477
+ const oldest = normalizeCache.keys().next().value;
478
+ if (oldest !== void 0) normalizeCache.delete(oldest);
479
+ }
480
+ normalizeCache.set(command, result);
481
+ return result;
482
+ }
431
483
  function normalizeCommandForPolicy(command) {
484
+ return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
485
+ }
486
+ function normalizeCommandForPolicyImpl(command) {
487
+ const f = parseShared(command);
488
+ if (f === PARSE_FAIL) return command;
432
489
  try {
433
- const f = sharedParser.Parse(command, "cmd");
434
490
  const strips = [];
435
491
  syntax.Walk(f, (node) => {
436
492
  if (!node) return false;
@@ -452,7 +508,11 @@ function normalizeCommandForPolicy(command) {
452
508
  } else if (nt === "DblQuoted") {
453
509
  const innerParts = quotedNode.Parts || [];
454
510
  const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
455
- if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
511
+ if (allLit) {
512
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
513
+ } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
514
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
515
+ }
456
516
  }
457
517
  }
458
518
  return true;
@@ -520,6 +580,127 @@ function detectDangerousShellExec(command) {
520
580
  return null;
521
581
  }
522
582
  }
583
+ function isProtectedHomePath(rawPath) {
584
+ let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
585
+ let underHome = false;
586
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
587
+ p = p.replace(/^~[\\/]?/, "");
588
+ underHome = true;
589
+ } else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
590
+ p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
591
+ underHome = true;
592
+ }
593
+ if (!underHome) return false;
594
+ if (p === "" || p === "." || p === "./") return true;
595
+ for (const safe of HOME_CACHE_ALLOWLIST) {
596
+ if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
597
+ return false;
598
+ }
599
+ }
600
+ return true;
601
+ }
602
+ function extractLiteralArgs(callExpr) {
603
+ const args = callExpr.Args || [];
604
+ if (args.length === 0) return { name: "", flags: [], paths: [] };
605
+ const litFromWord = (w) => {
606
+ const parts = w?.Parts || [];
607
+ let s = "";
608
+ for (const p of parts) {
609
+ const t = syntax.NodeType(p);
610
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
611
+ else if (t === "SglQuoted") s += p.Value ?? "";
612
+ else if (t === "DblQuoted") {
613
+ const inner = p.Parts || [];
614
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
615
+ s += inner.map((ip) => ip.Value ?? "").join("");
616
+ } else {
617
+ return null;
618
+ }
619
+ }
620
+ return s;
621
+ };
622
+ const name = (litFromWord(args[0]) || "").toLowerCase();
623
+ const flags = [];
624
+ const paths = [];
625
+ for (let i = 1; i < args.length; i++) {
626
+ const v = litFromWord(args[i]);
627
+ if (v === null) continue;
628
+ if (v.startsWith("-")) flags.push(v);
629
+ else paths.push(v);
630
+ }
631
+ return { name, flags, paths };
632
+ }
633
+ function analyzeFsOperation(command) {
634
+ if (!FS_OP_PRESCREEN_RE.test(command)) return null;
635
+ if (fsOpCache.has(command)) {
636
+ const hit = fsOpCache.get(command) ?? null;
637
+ fsOpCache.delete(command);
638
+ fsOpCache.set(command, hit);
639
+ return hit;
640
+ }
641
+ const computed = analyzeFsOperationImpl(command);
642
+ if (fsOpCache.size >= FS_OP_CACHE_MAX) {
643
+ const oldest = fsOpCache.keys().next().value;
644
+ if (oldest !== void 0) fsOpCache.delete(oldest);
645
+ }
646
+ fsOpCache.set(command, computed);
647
+ return computed;
648
+ }
649
+ function analyzeFsOperationImpl(command) {
650
+ const f = parseShared(command);
651
+ if (f === PARSE_FAIL) return null;
652
+ let result = null;
653
+ try {
654
+ syntax.Walk(f, (node) => {
655
+ if (!node || result) return false;
656
+ const n = node;
657
+ if (syntax.NodeType(n) !== "CallExpr") return true;
658
+ const { name, flags, paths } = extractLiteralArgs(n);
659
+ if (!name) return true;
660
+ if (name === "rm") {
661
+ const flagStr = flags.join("").toLowerCase();
662
+ const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
663
+ const hasF = /[f]/.test(flagStr) || flags.includes("--force");
664
+ if (hasR && hasF) {
665
+ for (const p of paths) {
666
+ if (isProtectedHomePath(p)) {
667
+ result = {
668
+ ruleName: "block-rm-rf-home",
669
+ verdict: "block",
670
+ reason: "Recursive delete of home directory is irreversible",
671
+ path: p
672
+ };
673
+ return false;
674
+ }
675
+ if (p === "/" || /^\/+$/.test(p)) {
676
+ result = {
677
+ ruleName: "block-rm-rf-home",
678
+ verdict: "block",
679
+ reason: "Recursive delete of root is catastrophic",
680
+ path: p
681
+ };
682
+ return false;
683
+ }
684
+ }
685
+ }
686
+ }
687
+ if (FS_READ_TOOLS.has(name)) {
688
+ for (const p of paths) {
689
+ for (const sp of SENSITIVE_PATH_RULES) {
690
+ if (sp.match(p)) {
691
+ result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
692
+ return false;
693
+ }
694
+ }
695
+ }
696
+ }
697
+ return true;
698
+ });
699
+ return result;
700
+ } catch {
701
+ return null;
702
+ }
703
+ }
523
704
  function analyzeShellCommand(command) {
524
705
  const actions = [];
525
706
  const paths = [];
@@ -809,10 +990,18 @@ function getNestedValue(obj, path49) {
809
990
  function evaluateSmartConditions(args, rule) {
810
991
  if (!rule.conditions || rule.conditions.length === 0) return true;
811
992
  const mode = rule.conditionMode ?? "all";
993
+ const fieldCache = /* @__PURE__ */ new Map();
994
+ const resolveField = (field) => {
995
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
996
+ const rawVal = getNestedValue(args, field);
997
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
998
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
999
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1000
+ fieldCache.set(field, val);
1001
+ return val;
1002
+ };
812
1003
  const results = rule.conditions.map((cond) => {
813
- const rawVal = getNestedValue(args, cond.field);
814
- const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
815
- const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1004
+ const val = resolveField(cond.field);
816
1005
  switch (cond.op) {
817
1006
  case "exists":
818
1007
  return val !== null && val !== "";
@@ -1294,7 +1483,7 @@ function summarizeBlast(result, opts = {}) {
1294
1483
  }))
1295
1484
  };
1296
1485
  }
1297
- var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD;
1486
+ var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD;
1298
1487
  var init_dist = __esm({
1299
1488
  "packages/policy-engine/dist/index.mjs"() {
1300
1489
  "use strict";
@@ -1762,6 +1951,69 @@ var init_dist = __esm({
1762
1951
  ]);
1763
1952
  SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
1764
1953
  DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
1954
+ NORMALIZE_CACHE_MAX = 5e3;
1955
+ normalizeCache = /* @__PURE__ */ new Map();
1956
+ AST_CACHE_MAX = 5e3;
1957
+ astCache = /* @__PURE__ */ new Map();
1958
+ PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
1959
+ FS_READ_TOOLS = /* @__PURE__ */ new Set([
1960
+ "cat",
1961
+ "less",
1962
+ "head",
1963
+ "tail",
1964
+ "bat",
1965
+ "more",
1966
+ "open",
1967
+ "print",
1968
+ "nano",
1969
+ "vim",
1970
+ "vi",
1971
+ "emacs",
1972
+ "code",
1973
+ "type"
1974
+ ]);
1975
+ FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
1976
+ HOME_CACHE_ALLOWLIST = [
1977
+ ".cache",
1978
+ ".npm/_npx",
1979
+ ".npm/_cacache",
1980
+ ".cargo/registry",
1981
+ ".gradle/caches",
1982
+ ".gradle/.tmp",
1983
+ ".m2/repository",
1984
+ ".pnpm-store",
1985
+ ".yarn/cache",
1986
+ ".yarn/.cache",
1987
+ ".cache/pip",
1988
+ ".local/share/Trash",
1989
+ ".rustup/downloads"
1990
+ ];
1991
+ SENSITIVE_PATH_RULES = [
1992
+ {
1993
+ rule: "shield:project-jail:block-read-ssh",
1994
+ reason: "Reading SSH private keys is blocked by project-jail shield",
1995
+ match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
1996
+ },
1997
+ {
1998
+ rule: "shield:project-jail:block-read-aws",
1999
+ reason: "Reading AWS credentials is blocked by project-jail shield",
2000
+ match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
2001
+ },
2002
+ {
2003
+ rule: "shield:project-jail:block-read-env",
2004
+ reason: "Reading .env files is blocked by project-jail shield",
2005
+ match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
2006
+ },
2007
+ {
2008
+ rule: "shield:project-jail:block-read-credentials",
2009
+ reason: "Reading credential files is blocked by project-jail shield",
2010
+ match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
2011
+ p
2012
+ )
2013
+ }
2014
+ ];
2015
+ FS_OP_CACHE_MAX = 5e3;
2016
+ fsOpCache = /* @__PURE__ */ new Map();
1765
2017
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([
1766
2018
  "cat",
1767
2019
  "head",
@@ -5541,9 +5793,7 @@ function removeNode9McpServer(servers) {
5541
5793
  return true;
5542
5794
  }
5543
5795
  function printDaemonTip() {
5544
- console.log(
5545
- chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk.white("\n To view your history or manage persistent rules, run:") + chalk.green("\n node9 daemon --openui")
5546
- );
5796
+ console.log(chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups."));
5547
5797
  }
5548
5798
  function fullPathCommand(subcommand) {
5549
5799
  if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
@@ -6627,7 +6877,8 @@ function buildScanSummary(agents) {
6627
6877
  timestamp: f.timestamp,
6628
6878
  project: f.project,
6629
6879
  sessionId: f.sessionId,
6630
- agent: f.agent
6880
+ agent: f.agent,
6881
+ kind: f.kind
6631
6882
  }))
6632
6883
  );
6633
6884
  const byVerdict = {
@@ -6645,10 +6896,7 @@ function buildScanSummary(agents) {
6645
6896
  costUSD: a.scan.totalCostUSD
6646
6897
  })).filter((s) => s.sessions > 0 || s.findings > 0);
6647
6898
  const sections = buildSections(allFindings);
6648
- const wastedIters = allLoops.reduce(
6649
- (sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
6650
- 0
6651
- );
6899
+ const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
6652
6900
  const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
6653
6901
  return {
6654
6902
  stats,
@@ -7812,6 +8060,20 @@ function geminiModelPrice(model) {
7812
8060
  if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
7813
8061
  return null;
7814
8062
  }
8063
+ function isNode9SelfOutput(text) {
8064
+ let hits = 0;
8065
+ for (const re of SELF_OUTPUT_MARKERS) {
8066
+ if (re.test(text)) hits++;
8067
+ if (hits >= 2) return true;
8068
+ }
8069
+ return false;
8070
+ }
8071
+ function looksLikeFixtureToken(sample) {
8072
+ for (const re of FIXTURE_TOKEN_PATTERNS) {
8073
+ if (re.test(sample)) return true;
8074
+ }
8075
+ return false;
8076
+ }
7815
8077
  function num(n) {
7816
8078
  return n.toLocaleString();
7817
8079
  }
@@ -7875,6 +8137,45 @@ function buildRecurringPatternSet(findings) {
7875
8137
  }
7876
8138
  return recurring;
7877
8139
  }
8140
+ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
8141
+ const fsVerdict = analyzeFsOperation(command);
8142
+ if (!fsVerdict) return false;
8143
+ const synthRule = {
8144
+ name: fsVerdict.ruleName,
8145
+ tool: "bash",
8146
+ conditions: [],
8147
+ verdict: fsVerdict.verdict,
8148
+ reason: fsVerdict.reason
8149
+ };
8150
+ const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
8151
+ const synthSource = isShieldRule ? {
8152
+ shieldName: "project-jail",
8153
+ shieldLabel: "project-jail (AST)",
8154
+ sourceType: "shield",
8155
+ rule: synthRule
8156
+ } : {
8157
+ shieldName: "",
8158
+ shieldLabel: "default (AST)",
8159
+ sourceType: "default",
8160
+ rule: synthRule
8161
+ };
8162
+ const inputPreview = preview(input, 120);
8163
+ const isDupe = result.findings.some(
8164
+ (f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
8165
+ );
8166
+ if (!isDupe) {
8167
+ result.findings.push({
8168
+ source: synthSource,
8169
+ toolName,
8170
+ input,
8171
+ timestamp,
8172
+ project: projLabel,
8173
+ sessionId,
8174
+ agent
8175
+ });
8176
+ }
8177
+ return true;
8178
+ }
7878
8179
  function isStaleFinding(timestamp, now = Date.now()) {
7879
8180
  if (!timestamp) return false;
7880
8181
  const t = Date.parse(timestamp);
@@ -7908,15 +8209,24 @@ function detectLoops(calls, project, sessionId, agent) {
7908
8209
  const entry = counts.get(key) ?? {
7909
8210
  count: 0,
7910
8211
  timestamp: call.timestamp,
8212
+ firstTs: null,
8213
+ lastTs: null,
7911
8214
  input: call.input,
7912
8215
  toolName: call.toolName
7913
8216
  };
7914
8217
  entry.count++;
8218
+ const t = call.timestamp ? Date.parse(call.timestamp) : NaN;
8219
+ if (!Number.isNaN(t)) {
8220
+ if (entry.firstTs === null || t < entry.firstTs) entry.firstTs = t;
8221
+ if (entry.lastTs === null || t > entry.lastTs) entry.lastTs = t;
8222
+ }
7915
8223
  counts.set(key, entry);
7916
8224
  }
7917
8225
  const findings = [];
7918
8226
  for (const [, entry] of counts) {
7919
8227
  if (entry.count >= LOOP_THRESHOLD) {
8228
+ const span = entry.firstTs !== null && entry.lastTs !== null ? entry.lastTs - entry.firstTs : 0;
8229
+ const kind = span >= LOOP_TIMESPAN_THRESHOLD_MS ? "long-iteration" : "loop";
7920
8230
  findings.push({
7921
8231
  toolName: entry.toolName,
7922
8232
  commandPreview: preview(entry.input, 80),
@@ -7924,7 +8234,8 @@ function detectLoops(calls, project, sessionId, agent) {
7924
8234
  timestamp: entry.timestamp,
7925
8235
  project,
7926
8236
  sessionId,
7927
- agent
8237
+ agent,
8238
+ kind
7928
8239
  });
7929
8240
  }
7930
8241
  }
@@ -8143,8 +8454,10 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
8143
8454
  }
8144
8455
  const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
8145
8456
  if (!resultText) continue;
8457
+ if (isNode9SelfOutput(resultText)) continue;
8146
8458
  const dlpMatch = scanArgs({ text: resultText });
8147
8459
  if (dlpMatch) {
8460
+ if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
8148
8461
  if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
8149
8462
  const isDupe = result.dlpFindings.some(
8150
8463
  (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
@@ -8215,11 +8528,26 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
8215
8528
  });
8216
8529
  }
8217
8530
  }
8218
- let ruleMatched = false;
8531
+ let astFsMatched = false;
8532
+ const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
8533
+ if (astRanForBash) {
8534
+ astFsMatched = pushFsOpAstFinding(
8535
+ String(input.command ?? ""),
8536
+ toolName,
8537
+ input,
8538
+ entry.timestamp ?? "",
8539
+ projLabel,
8540
+ sessionId,
8541
+ "claude",
8542
+ result
8543
+ );
8544
+ }
8545
+ let ruleMatched = astFsMatched;
8219
8546
  for (const source of ruleSources) {
8220
8547
  const { rule } = source;
8221
8548
  if (rule.verdict === "allow") continue;
8222
8549
  if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
8550
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
8223
8551
  if (!evaluateSmartConditions(input, rule)) continue;
8224
8552
  const inputPreview = preview(input, 120);
8225
8553
  const isDupe = result.findings.some(
@@ -8415,11 +8743,26 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
8415
8743
  });
8416
8744
  }
8417
8745
  }
8418
- let ruleMatched = false;
8746
+ let astFsMatched = false;
8747
+ const astRanForBash = toolNameLower === "run_shell_command" || toolNameLower === "shell";
8748
+ if (astRanForBash) {
8749
+ astFsMatched = pushFsOpAstFinding(
8750
+ String(input.command ?? ""),
8751
+ toolName,
8752
+ input,
8753
+ msg.timestamp ?? "",
8754
+ projLabel,
8755
+ sessionId,
8756
+ "gemini",
8757
+ result
8758
+ );
8759
+ }
8760
+ let ruleMatched = astFsMatched;
8419
8761
  for (const source of ruleSources) {
8420
8762
  const { rule } = source;
8421
8763
  if (rule.verdict === "allow") continue;
8422
8764
  if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
8765
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
8423
8766
  if (!evaluateSmartConditions(input, rule)) continue;
8424
8767
  const inputPreview = preview(input, 120);
8425
8768
  const isDupe = result.findings.some(
@@ -8637,12 +8980,27 @@ function scanCodexHistory(startDate, onProgress, onLine) {
8637
8980
  });
8638
8981
  }
8639
8982
  }
8640
- let ruleMatched = false;
8983
+ let astFsMatched = false;
8984
+ const astRanForBash = toolNameLower === "exec_command" || toolNameLower === "bash";
8985
+ if (astRanForBash) {
8986
+ astFsMatched = pushFsOpAstFinding(
8987
+ String(input["command"] ?? ""),
8988
+ toolName,
8989
+ input,
8990
+ ts,
8991
+ projLabel,
8992
+ sessionId,
8993
+ "codex",
8994
+ result
8995
+ );
8996
+ }
8997
+ let ruleMatched = astFsMatched;
8641
8998
  for (const source of ruleSources) {
8642
8999
  const { rule } = source;
8643
9000
  if (rule.verdict === "allow") continue;
8644
9001
  if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
8645
9002
  continue;
9003
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
8646
9004
  if (!evaluateSmartConditions(input, rule)) continue;
8647
9005
  const inputPreview = preview(input, 120);
8648
9006
  const isDupe = result.findings.some(
@@ -8842,15 +9200,22 @@ function renderCompactScorecard(input) {
8842
9200
  chalk4.red("\u{1F6D1} ") + chalk4.red.bold(String(blockedCount).padEnd(4)) + chalk4.dim("would have blocked".padEnd(20)) + chalk4.dim(`(${topBlocked})`)
8843
9201
  );
8844
9202
  }
8845
- if (scan.loopFindings.length > 0) {
8846
- const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
9203
+ const realLoops = scan.loopFindings.filter((l) => l.kind !== "long-iteration");
9204
+ const longIterations = scan.loopFindings.filter((l) => l.kind === "long-iteration");
9205
+ if (realLoops.length > 0) {
9206
+ const wastedCalls = realLoops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
8847
9207
  const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
8848
9208
  const wasteParts = [];
8849
9209
  if (wastePct > 0) wasteParts.push(`${wastePct}% wasted`);
8850
9210
  if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
8851
9211
  const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
8852
9212
  console.log(
8853
- chalk4.yellow("\u{1F501} ") + chalk4.yellow.bold(String(scan.loopFindings.length).padEnd(4)) + chalk4.dim("agent loops".padEnd(20)) + chalk4.dim(wasteSummary)
9213
+ chalk4.yellow("\u{1F501} ") + chalk4.yellow.bold(String(realLoops.length).padEnd(4)) + chalk4.dim("agent loops".padEnd(20)) + chalk4.dim(wasteSummary)
9214
+ );
9215
+ }
9216
+ if (longIterations.length > 0) {
9217
+ console.log(
9218
+ chalk4.dim("\u{1F4C2} ") + chalk4.dim.bold(String(longIterations.length).padEnd(4)) + chalk4.dim("long iterations".padEnd(20)) + chalk4.dim("(deep work \u2014 not waste)")
8854
9219
  );
8855
9220
  }
8856
9221
  if (reviewCount > 0) {
@@ -9382,13 +9747,14 @@ function registerScanCommand(program2) {
9382
9747
  }
9383
9748
  );
9384
9749
  }
9385
- var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
9750
+ var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, AST_FS_REGEX_RULES, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
9386
9751
  var init_scan = __esm({
9387
9752
  "src/cli/commands/scan.ts"() {
9388
9753
  "use strict";
9389
9754
  init_shields();
9390
9755
  init_config();
9391
9756
  init_policy();
9757
+ init_dist();
9392
9758
  init_dlp();
9393
9759
  init_dist();
9394
9760
  init_scan_summary();
@@ -9441,6 +9807,22 @@ var init_scan = __esm({
9441
9807
  ".vue",
9442
9808
  ".svelte"
9443
9809
  ]);
9810
+ SELF_OUTPUT_MARKERS = [
9811
+ /redactedSample:\s*['"]/,
9812
+ /patternName:\s*['"]/,
9813
+ /\bseverity:\s*['"](?:block|review|allow)['"]/,
9814
+ /NODE9 SECURITY ALERT/
9815
+ ];
9816
+ FIXTURE_TOKEN_PATTERNS = [
9817
+ /(.)\1{5,}/,
9818
+ // 6+ repeated characters (aaaaaa, 000000)
9819
+ /(?:EXAMPLE|FAKE|DUMMY|PLACEHOLDER|XXXXX)/i,
9820
+ /abcdefghijklmn/i,
9821
+ // long alpha sequence — fixture, not entropy
9822
+ /1234567890/,
9823
+ // long digit sequence — fixture, not entropy
9824
+ /qwerty/i
9825
+ ];
9444
9826
  TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
9445
9827
  LOOP_TOOLS = /* @__PURE__ */ new Set([
9446
9828
  "bash",
@@ -9453,10 +9835,18 @@ var init_scan = __esm({
9453
9835
  "multiedit"
9454
9836
  ]);
9455
9837
  LOOP_THRESHOLD = 3;
9838
+ LOOP_TIMESPAN_THRESHOLD_MS = 10 * 60 * 1e3;
9456
9839
  STUCK_TOOLS_MIN_WASTE = 5;
9457
9840
  STUCK_TOOLS_LIMIT = 3;
9458
9841
  RECURRING_SESSION_THRESHOLD = 3;
9459
9842
  STALE_AGE_DAYS = 30;
9843
+ AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
9844
+ "block-rm-rf-home",
9845
+ "shield:project-jail:block-read-ssh",
9846
+ "shield:project-jail:block-read-aws",
9847
+ "shield:project-jail:block-read-env",
9848
+ "shield:project-jail:block-read-credentials"
9849
+ ]);
9460
9850
  DEFAULT_RULE_NAMES = new Set(
9461
9851
  DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
9462
9852
  );
@@ -9984,7 +10374,86 @@ function abandonPending() {
9984
10374
  }, 200);
9985
10375
  }
9986
10376
  }
10377
+ function logActivitySocket(msg) {
10378
+ try {
10379
+ fs20.appendFileSync(
10380
+ path22.join(homeDir, ".node9", "hook-debug.log"),
10381
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] [activity-socket] ${msg}
10382
+ `
10383
+ );
10384
+ } catch {
10385
+ }
10386
+ }
10387
+ function shouldRebind(now = Date.now()) {
10388
+ if (activityCircuitTripped) return false;
10389
+ activityRebindAttempts = activityRebindAttempts.filter(
10390
+ (t) => now - t < ACTIVITY_REBIND_WINDOW_MS
10391
+ );
10392
+ activityRebindAttempts.push(now);
10393
+ if (activityRebindAttempts.length > ACTIVITY_REBIND_MAX_ATTEMPTS) {
10394
+ activityCircuitTripped = true;
10395
+ return false;
10396
+ }
10397
+ return true;
10398
+ }
9987
10399
  function startActivitySocket() {
10400
+ bindActivitySocket();
10401
+ if (process.platform !== "win32") {
10402
+ try {
10403
+ activitySocketWatcher = fs20.watch(os18.tmpdir(), (eventType, filename) => {
10404
+ if (filename !== path22.basename(ACTIVITY_SOCKET_PATH2)) return;
10405
+ if (eventType !== "rename") return;
10406
+ if (fs20.existsSync(ACTIVITY_SOCKET_PATH2)) return;
10407
+ attemptRebind("watch-unlink");
10408
+ });
10409
+ activitySocketWatcher.on("error", (err2) => {
10410
+ logActivitySocket(`watcher error: ${err2.message}`);
10411
+ });
10412
+ activitySocketWatcher.unref();
10413
+ } catch (err2) {
10414
+ logActivitySocket(`failed to start watcher: ${err2.message}`);
10415
+ }
10416
+ }
10417
+ activityHealthInterval = setInterval(() => {
10418
+ if (!fs20.existsSync(ACTIVITY_SOCKET_PATH2)) attemptRebind("health-probe");
10419
+ }, ACTIVITY_HEALTH_PROBE_MS);
10420
+ activityHealthInterval.unref();
10421
+ process.on("exit", () => {
10422
+ if (activitySocketWatcher) {
10423
+ try {
10424
+ activitySocketWatcher.close();
10425
+ } catch {
10426
+ }
10427
+ }
10428
+ if (activityHealthInterval) clearInterval(activityHealthInterval);
10429
+ try {
10430
+ fs20.unlinkSync(ACTIVITY_SOCKET_PATH2);
10431
+ } catch {
10432
+ }
10433
+ });
10434
+ }
10435
+ function attemptRebind(reason) {
10436
+ if (!shouldRebind()) {
10437
+ logActivitySocket(
10438
+ `circuit breaker tripped after ${ACTIVITY_REBIND_MAX_ATTEMPTS} attempts in ${ACTIVITY_REBIND_WINDOW_MS}ms \u2014 flight recorder down`
10439
+ );
10440
+ broadcast("flight-recorder-down", {
10441
+ reason: "rebind-loop",
10442
+ message: "Activity socket repeatedly disappearing \u2014 run: node9 daemon restart"
10443
+ });
10444
+ return;
10445
+ }
10446
+ logActivitySocket(`rebinding (reason: ${reason}, attempt ${activityRebindAttempts.length})`);
10447
+ if (activitySocketServer) {
10448
+ try {
10449
+ activitySocketServer.close();
10450
+ } catch {
10451
+ }
10452
+ activitySocketServer = null;
10453
+ }
10454
+ bindActivitySocket();
10455
+ }
10456
+ function bindActivitySocket() {
9988
10457
  try {
9989
10458
  fs20.unlinkSync(ACTIVITY_SOCKET_PATH2);
9990
10459
  } catch {
@@ -10082,15 +10551,15 @@ function startActivitySocket() {
10082
10551
  socket.on("error", () => {
10083
10552
  });
10084
10553
  });
10085
- unixServer.listen(ACTIVITY_SOCKET_PATH2);
10086
- process.on("exit", () => {
10087
- try {
10088
- fs20.unlinkSync(ACTIVITY_SOCKET_PATH2);
10089
- } catch {
10090
- }
10554
+ unixServer.on("error", (err2) => {
10555
+ logActivitySocket(`server error: ${err2.message}`);
10091
10556
  });
10557
+ unixServer.listen(ACTIVITY_SOCKET_PATH2, () => {
10558
+ logActivitySocket(`bound to ${ACTIVITY_SOCKET_PATH2}`);
10559
+ });
10560
+ activitySocketServer = unixServer;
10092
10561
  }
10093
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
10562
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activitySocketWatcher, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
10094
10563
  var init_state2 = __esm({
10095
10564
  "src/daemon/state.ts"() {
10096
10565
  "use strict";
@@ -10146,6 +10615,14 @@ var init_state2 = __esm({
10146
10615
  "notebook_edit",
10147
10616
  "notebookedit"
10148
10617
  ]);
10618
+ ACTIVITY_REBIND_MAX_ATTEMPTS = 5;
10619
+ ACTIVITY_REBIND_WINDOW_MS = 6e4;
10620
+ ACTIVITY_HEALTH_PROBE_MS = 3e4;
10621
+ activitySocketServer = null;
10622
+ activitySocketWatcher = null;
10623
+ activityHealthInterval = null;
10624
+ activityRebindAttempts = [];
10625
+ activityCircuitTripped = false;
10149
10626
  }
10150
10627
  });
10151
10628
 
@@ -12292,6 +12769,8 @@ async function startTail(options = {}) {
12292
12769
  let initialReplayDone = false;
12293
12770
  const activityPending = /* @__PURE__ */ new Map();
12294
12771
  const orphanedResults = /* @__PURE__ */ new Map();
12772
+ let lastActivityFromDaemon = Date.now();
12773
+ let stallWarned = false;
12295
12774
  const authToken = getInternalToken() ?? "";
12296
12775
  const approvalQueue = [];
12297
12776
  let cardActive = false;
@@ -12518,6 +12997,24 @@ async function startTail(options = {}) {
12518
12997
  console.log(chalk29.dim("\n\u{1F6F0}\uFE0F Disconnected."));
12519
12998
  process.exit(0);
12520
12999
  });
13000
+ const STALL_THRESHOLD_MS = 6e4;
13001
+ const stallWatchdog = setInterval(() => {
13002
+ if (stallWarned) return;
13003
+ if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
13004
+ try {
13005
+ const auditMtime = fs44.statSync(auditLog).mtimeMs;
13006
+ if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
13007
+ console.log("");
13008
+ console.log(
13009
+ chalk29.yellow(
13010
+ "\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
13011
+ )
13012
+ );
13013
+ stallWarned = true;
13014
+ } catch {
13015
+ }
13016
+ }, STALL_THRESHOLD_MS / 2);
13017
+ stallWatchdog.unref();
12521
13018
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
12522
13019
  const req = http2.get(
12523
13020
  sseUrl,
@@ -12563,7 +13060,18 @@ async function startTail(options = {}) {
12563
13060
  }
12564
13061
  );
12565
13062
  function handleMessage(event, rawData) {
13063
+ lastActivityFromDaemon = Date.now();
12566
13064
  if (event === "csrf") return;
13065
+ if (event === "flight-recorder-down") {
13066
+ try {
13067
+ const parsed = JSON.parse(rawData);
13068
+ const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
13069
+ console.log("");
13070
+ console.log(chalk29.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
13071
+ } catch {
13072
+ }
13073
+ return;
13074
+ }
12567
13075
  if (event === "init") {
12568
13076
  try {
12569
13077
  const parsed = JSON.parse(rawData);
@@ -16139,7 +16647,6 @@ function registerInitCommand(program2) {
16139
16647
  console.log(chalk15.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
16140
16648
  console.log("");
16141
16649
  console.log(chalk15.white(" Watch live: ") + chalk15.cyan("node9 tail"));
16142
- console.log(chalk15.white(" Local UI: ") + chalk15.cyan("node9 daemon --openui"));
16143
16650
  console.log("");
16144
16651
  console.log(chalk15.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
16145
16652
  console.log(