@node9/proxy 1.18.1 → 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.js CHANGED
@@ -444,9 +444,65 @@ function redactText(text) {
444
444
  }
445
445
  return { result, found };
446
446
  }
447
+ function isCatHeredocOrLit(part) {
448
+ if (!part) return false;
449
+ const t = syntax.NodeType(part);
450
+ if (t === "Lit") return true;
451
+ if (t !== "CmdSubst") return false;
452
+ const stmts = part.Stmts || [];
453
+ if (stmts.length !== 1) return false;
454
+ const stmt = stmts[0];
455
+ const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
456
+ const hasHeredoc = redirs.some((r) => r && r.Hdoc);
457
+ if (!hasHeredoc) return false;
458
+ const cmd = stmt.Cmd;
459
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
460
+ const firstArg2 = cmd.Args?.[0]?.Parts || [];
461
+ if (firstArg2.length !== 1 || syntax.NodeType(firstArg2[0]) !== "Lit") return false;
462
+ return (firstArg2[0].Value || "").toLowerCase() === "cat";
463
+ }
464
+ function parseShared(command) {
465
+ const cached = astCache.get(command);
466
+ if (cached !== void 0) {
467
+ astCache.delete(command);
468
+ astCache.set(command, cached);
469
+ return cached;
470
+ }
471
+ let parsed;
472
+ try {
473
+ parsed = sharedParser.Parse(command, "cmd");
474
+ } catch {
475
+ parsed = PARSE_FAIL;
476
+ }
477
+ if (astCache.size >= AST_CACHE_MAX) {
478
+ const oldest = astCache.keys().next().value;
479
+ if (oldest !== void 0) astCache.delete(oldest);
480
+ }
481
+ astCache.set(command, parsed);
482
+ return parsed;
483
+ }
484
+ function cachedNormalize(command, compute) {
485
+ const hit = normalizeCache.get(command);
486
+ if (hit !== void 0) {
487
+ normalizeCache.delete(command);
488
+ normalizeCache.set(command, hit);
489
+ return hit;
490
+ }
491
+ const result = compute();
492
+ if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
493
+ const oldest = normalizeCache.keys().next().value;
494
+ if (oldest !== void 0) normalizeCache.delete(oldest);
495
+ }
496
+ normalizeCache.set(command, result);
497
+ return result;
498
+ }
447
499
  function normalizeCommandForPolicy(command) {
500
+ return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
501
+ }
502
+ function normalizeCommandForPolicyImpl(command) {
503
+ const f = parseShared(command);
504
+ if (f === PARSE_FAIL) return command;
448
505
  try {
449
- const f = sharedParser.Parse(command, "cmd");
450
506
  const strips = [];
451
507
  syntax.Walk(f, (node) => {
452
508
  if (!node) return false;
@@ -468,7 +524,11 @@ function normalizeCommandForPolicy(command) {
468
524
  } else if (nt === "DblQuoted") {
469
525
  const innerParts = quotedNode.Parts || [];
470
526
  const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
471
- if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
527
+ if (allLit) {
528
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
529
+ } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
530
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
531
+ }
472
532
  }
473
533
  }
474
534
  return true;
@@ -536,6 +596,127 @@ function detectDangerousShellExec(command) {
536
596
  return null;
537
597
  }
538
598
  }
599
+ function isProtectedHomePath(rawPath) {
600
+ let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
601
+ let underHome = false;
602
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
603
+ p = p.replace(/^~[\\/]?/, "");
604
+ underHome = true;
605
+ } else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
606
+ p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
607
+ underHome = true;
608
+ }
609
+ if (!underHome) return false;
610
+ if (p === "" || p === "." || p === "./") return true;
611
+ for (const safe of HOME_CACHE_ALLOWLIST) {
612
+ if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
613
+ return false;
614
+ }
615
+ }
616
+ return true;
617
+ }
618
+ function extractLiteralArgs(callExpr) {
619
+ const args = callExpr.Args || [];
620
+ if (args.length === 0) return { name: "", flags: [], paths: [] };
621
+ const litFromWord = (w) => {
622
+ const parts = w?.Parts || [];
623
+ let s = "";
624
+ for (const p of parts) {
625
+ const t = syntax.NodeType(p);
626
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
627
+ else if (t === "SglQuoted") s += p.Value ?? "";
628
+ else if (t === "DblQuoted") {
629
+ const inner = p.Parts || [];
630
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
631
+ s += inner.map((ip) => ip.Value ?? "").join("");
632
+ } else {
633
+ return null;
634
+ }
635
+ }
636
+ return s;
637
+ };
638
+ const name = (litFromWord(args[0]) || "").toLowerCase();
639
+ const flags = [];
640
+ const paths = [];
641
+ for (let i = 1; i < args.length; i++) {
642
+ const v = litFromWord(args[i]);
643
+ if (v === null) continue;
644
+ if (v.startsWith("-")) flags.push(v);
645
+ else paths.push(v);
646
+ }
647
+ return { name, flags, paths };
648
+ }
649
+ function analyzeFsOperation(command) {
650
+ if (!FS_OP_PRESCREEN_RE.test(command)) return null;
651
+ if (fsOpCache.has(command)) {
652
+ const hit = fsOpCache.get(command) ?? null;
653
+ fsOpCache.delete(command);
654
+ fsOpCache.set(command, hit);
655
+ return hit;
656
+ }
657
+ const computed = analyzeFsOperationImpl(command);
658
+ if (fsOpCache.size >= FS_OP_CACHE_MAX) {
659
+ const oldest = fsOpCache.keys().next().value;
660
+ if (oldest !== void 0) fsOpCache.delete(oldest);
661
+ }
662
+ fsOpCache.set(command, computed);
663
+ return computed;
664
+ }
665
+ function analyzeFsOperationImpl(command) {
666
+ const f = parseShared(command);
667
+ if (f === PARSE_FAIL) return null;
668
+ let result = null;
669
+ try {
670
+ syntax.Walk(f, (node) => {
671
+ if (!node || result) return false;
672
+ const n = node;
673
+ if (syntax.NodeType(n) !== "CallExpr") return true;
674
+ const { name, flags, paths } = extractLiteralArgs(n);
675
+ if (!name) return true;
676
+ if (name === "rm") {
677
+ const flagStr = flags.join("").toLowerCase();
678
+ const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
679
+ const hasF = /[f]/.test(flagStr) || flags.includes("--force");
680
+ if (hasR && hasF) {
681
+ for (const p of paths) {
682
+ if (isProtectedHomePath(p)) {
683
+ result = {
684
+ ruleName: "block-rm-rf-home",
685
+ verdict: "block",
686
+ reason: "Recursive delete of home directory is irreversible",
687
+ path: p
688
+ };
689
+ return false;
690
+ }
691
+ if (p === "/" || /^\/+$/.test(p)) {
692
+ result = {
693
+ ruleName: "block-rm-rf-home",
694
+ verdict: "block",
695
+ reason: "Recursive delete of root is catastrophic",
696
+ path: p
697
+ };
698
+ return false;
699
+ }
700
+ }
701
+ }
702
+ }
703
+ if (FS_READ_TOOLS.has(name)) {
704
+ for (const p of paths) {
705
+ for (const sp of SENSITIVE_PATH_RULES) {
706
+ if (sp.match(p)) {
707
+ result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
708
+ return false;
709
+ }
710
+ }
711
+ }
712
+ }
713
+ return true;
714
+ });
715
+ return result;
716
+ } catch {
717
+ return null;
718
+ }
719
+ }
539
720
  function analyzeShellCommand(command) {
540
721
  const actions = [];
541
722
  const paths = [];
@@ -825,10 +1006,18 @@ function getNestedValue(obj, path49) {
825
1006
  function evaluateSmartConditions(args, rule) {
826
1007
  if (!rule.conditions || rule.conditions.length === 0) return true;
827
1008
  const mode = rule.conditionMode ?? "all";
1009
+ const fieldCache = /* @__PURE__ */ new Map();
1010
+ const resolveField = (field) => {
1011
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
1012
+ const rawVal = getNestedValue(args, field);
1013
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
1014
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
1015
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1016
+ fieldCache.set(field, val);
1017
+ return val;
1018
+ };
828
1019
  const results = rule.conditions.map((cond) => {
829
- const rawVal = getNestedValue(args, cond.field);
830
- const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
831
- const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1020
+ const val = resolveField(cond.field);
832
1021
  switch (cond.op) {
833
1022
  case "exists":
834
1023
  return val !== null && val !== "";
@@ -1310,7 +1499,7 @@ function summarizeBlast(result, opts = {}) {
1310
1499
  }))
1311
1500
  };
1312
1501
  }
1313
- var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto2, 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;
1502
+ var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto2, 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;
1314
1503
  var init_dist = __esm({
1315
1504
  "packages/policy-engine/dist/index.mjs"() {
1316
1505
  "use strict";
@@ -1784,6 +1973,69 @@ var init_dist = __esm({
1784
1973
  ]);
1785
1974
  SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
1786
1975
  DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
1976
+ NORMALIZE_CACHE_MAX = 5e3;
1977
+ normalizeCache = /* @__PURE__ */ new Map();
1978
+ AST_CACHE_MAX = 5e3;
1979
+ astCache = /* @__PURE__ */ new Map();
1980
+ PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
1981
+ FS_READ_TOOLS = /* @__PURE__ */ new Set([
1982
+ "cat",
1983
+ "less",
1984
+ "head",
1985
+ "tail",
1986
+ "bat",
1987
+ "more",
1988
+ "open",
1989
+ "print",
1990
+ "nano",
1991
+ "vim",
1992
+ "vi",
1993
+ "emacs",
1994
+ "code",
1995
+ "type"
1996
+ ]);
1997
+ FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
1998
+ HOME_CACHE_ALLOWLIST = [
1999
+ ".cache",
2000
+ ".npm/_npx",
2001
+ ".npm/_cacache",
2002
+ ".cargo/registry",
2003
+ ".gradle/caches",
2004
+ ".gradle/.tmp",
2005
+ ".m2/repository",
2006
+ ".pnpm-store",
2007
+ ".yarn/cache",
2008
+ ".yarn/.cache",
2009
+ ".cache/pip",
2010
+ ".local/share/Trash",
2011
+ ".rustup/downloads"
2012
+ ];
2013
+ SENSITIVE_PATH_RULES = [
2014
+ {
2015
+ rule: "shield:project-jail:block-read-ssh",
2016
+ reason: "Reading SSH private keys is blocked by project-jail shield",
2017
+ match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
2018
+ },
2019
+ {
2020
+ rule: "shield:project-jail:block-read-aws",
2021
+ reason: "Reading AWS credentials is blocked by project-jail shield",
2022
+ match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
2023
+ },
2024
+ {
2025
+ rule: "shield:project-jail:block-read-env",
2026
+ reason: "Reading .env files is blocked by project-jail shield",
2027
+ match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
2028
+ },
2029
+ {
2030
+ rule: "shield:project-jail:block-read-credentials",
2031
+ reason: "Reading credential files is blocked by project-jail shield",
2032
+ match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
2033
+ p
2034
+ )
2035
+ }
2036
+ ];
2037
+ FS_OP_CACHE_MAX = 5e3;
2038
+ fsOpCache = /* @__PURE__ */ new Map();
1787
2039
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([
1788
2040
  "cat",
1789
2041
  "head",
@@ -5559,9 +5811,7 @@ function removeNode9McpServer(servers) {
5559
5811
  return true;
5560
5812
  }
5561
5813
  function printDaemonTip() {
5562
- console.log(
5563
- import_chalk.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk.default.white("\n To view your history or manage persistent rules, run:") + import_chalk.default.green("\n node9 daemon --openui")
5564
- );
5814
+ console.log(import_chalk.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups."));
5565
5815
  }
5566
5816
  function fullPathCommand(subcommand) {
5567
5817
  if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
@@ -6651,7 +6901,8 @@ function buildScanSummary(agents) {
6651
6901
  timestamp: f.timestamp,
6652
6902
  project: f.project,
6653
6903
  sessionId: f.sessionId,
6654
- agent: f.agent
6904
+ agent: f.agent,
6905
+ kind: f.kind
6655
6906
  }))
6656
6907
  );
6657
6908
  const byVerdict = {
@@ -6669,10 +6920,7 @@ function buildScanSummary(agents) {
6669
6920
  costUSD: a.scan.totalCostUSD
6670
6921
  })).filter((s) => s.sessions > 0 || s.findings > 0);
6671
6922
  const sections = buildSections(allFindings);
6672
- const wastedIters = allLoops.reduce(
6673
- (sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
6674
- 0
6675
- );
6923
+ const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
6676
6924
  const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
6677
6925
  return {
6678
6926
  stats,
@@ -7833,6 +8081,20 @@ function geminiModelPrice(model) {
7833
8081
  if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
7834
8082
  return null;
7835
8083
  }
8084
+ function isNode9SelfOutput(text) {
8085
+ let hits = 0;
8086
+ for (const re of SELF_OUTPUT_MARKERS) {
8087
+ if (re.test(text)) hits++;
8088
+ if (hits >= 2) return true;
8089
+ }
8090
+ return false;
8091
+ }
8092
+ function looksLikeFixtureToken(sample) {
8093
+ for (const re of FIXTURE_TOKEN_PATTERNS) {
8094
+ if (re.test(sample)) return true;
8095
+ }
8096
+ return false;
8097
+ }
7836
8098
  function num(n) {
7837
8099
  return n.toLocaleString();
7838
8100
  }
@@ -7896,6 +8158,45 @@ function buildRecurringPatternSet(findings) {
7896
8158
  }
7897
8159
  return recurring;
7898
8160
  }
8161
+ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
8162
+ const fsVerdict = analyzeFsOperation(command);
8163
+ if (!fsVerdict) return false;
8164
+ const synthRule = {
8165
+ name: fsVerdict.ruleName,
8166
+ tool: "bash",
8167
+ conditions: [],
8168
+ verdict: fsVerdict.verdict,
8169
+ reason: fsVerdict.reason
8170
+ };
8171
+ const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
8172
+ const synthSource = isShieldRule ? {
8173
+ shieldName: "project-jail",
8174
+ shieldLabel: "project-jail (AST)",
8175
+ sourceType: "shield",
8176
+ rule: synthRule
8177
+ } : {
8178
+ shieldName: "",
8179
+ shieldLabel: "default (AST)",
8180
+ sourceType: "default",
8181
+ rule: synthRule
8182
+ };
8183
+ const inputPreview = preview(input, 120);
8184
+ const isDupe = result.findings.some(
8185
+ (f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
8186
+ );
8187
+ if (!isDupe) {
8188
+ result.findings.push({
8189
+ source: synthSource,
8190
+ toolName,
8191
+ input,
8192
+ timestamp,
8193
+ project: projLabel,
8194
+ sessionId,
8195
+ agent
8196
+ });
8197
+ }
8198
+ return true;
8199
+ }
7899
8200
  function isStaleFinding(timestamp, now = Date.now()) {
7900
8201
  if (!timestamp) return false;
7901
8202
  const t = Date.parse(timestamp);
@@ -7929,15 +8230,24 @@ function detectLoops(calls, project, sessionId, agent) {
7929
8230
  const entry = counts.get(key) ?? {
7930
8231
  count: 0,
7931
8232
  timestamp: call.timestamp,
8233
+ firstTs: null,
8234
+ lastTs: null,
7932
8235
  input: call.input,
7933
8236
  toolName: call.toolName
7934
8237
  };
7935
8238
  entry.count++;
8239
+ const t = call.timestamp ? Date.parse(call.timestamp) : NaN;
8240
+ if (!Number.isNaN(t)) {
8241
+ if (entry.firstTs === null || t < entry.firstTs) entry.firstTs = t;
8242
+ if (entry.lastTs === null || t > entry.lastTs) entry.lastTs = t;
8243
+ }
7936
8244
  counts.set(key, entry);
7937
8245
  }
7938
8246
  const findings = [];
7939
8247
  for (const [, entry] of counts) {
7940
8248
  if (entry.count >= LOOP_THRESHOLD) {
8249
+ const span = entry.firstTs !== null && entry.lastTs !== null ? entry.lastTs - entry.firstTs : 0;
8250
+ const kind = span >= LOOP_TIMESPAN_THRESHOLD_MS ? "long-iteration" : "loop";
7941
8251
  findings.push({
7942
8252
  toolName: entry.toolName,
7943
8253
  commandPreview: preview(entry.input, 80),
@@ -7945,7 +8255,8 @@ function detectLoops(calls, project, sessionId, agent) {
7945
8255
  timestamp: entry.timestamp,
7946
8256
  project,
7947
8257
  sessionId,
7948
- agent
8258
+ agent,
8259
+ kind
7949
8260
  });
7950
8261
  }
7951
8262
  }
@@ -8164,8 +8475,10 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
8164
8475
  }
8165
8476
  const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
8166
8477
  if (!resultText) continue;
8478
+ if (isNode9SelfOutput(resultText)) continue;
8167
8479
  const dlpMatch = scanArgs({ text: resultText });
8168
8480
  if (dlpMatch) {
8481
+ if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
8169
8482
  if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
8170
8483
  const isDupe = result.dlpFindings.some(
8171
8484
  (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
@@ -8236,11 +8549,26 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
8236
8549
  });
8237
8550
  }
8238
8551
  }
8239
- let ruleMatched = false;
8552
+ let astFsMatched = false;
8553
+ const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
8554
+ if (astRanForBash) {
8555
+ astFsMatched = pushFsOpAstFinding(
8556
+ String(input.command ?? ""),
8557
+ toolName,
8558
+ input,
8559
+ entry.timestamp ?? "",
8560
+ projLabel,
8561
+ sessionId,
8562
+ "claude",
8563
+ result
8564
+ );
8565
+ }
8566
+ let ruleMatched = astFsMatched;
8240
8567
  for (const source of ruleSources) {
8241
8568
  const { rule } = source;
8242
8569
  if (rule.verdict === "allow") continue;
8243
8570
  if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
8571
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
8244
8572
  if (!evaluateSmartConditions(input, rule)) continue;
8245
8573
  const inputPreview = preview(input, 120);
8246
8574
  const isDupe = result.findings.some(
@@ -8436,11 +8764,26 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
8436
8764
  });
8437
8765
  }
8438
8766
  }
8439
- let ruleMatched = false;
8767
+ let astFsMatched = false;
8768
+ const astRanForBash = toolNameLower === "run_shell_command" || toolNameLower === "shell";
8769
+ if (astRanForBash) {
8770
+ astFsMatched = pushFsOpAstFinding(
8771
+ String(input.command ?? ""),
8772
+ toolName,
8773
+ input,
8774
+ msg.timestamp ?? "",
8775
+ projLabel,
8776
+ sessionId,
8777
+ "gemini",
8778
+ result
8779
+ );
8780
+ }
8781
+ let ruleMatched = astFsMatched;
8440
8782
  for (const source of ruleSources) {
8441
8783
  const { rule } = source;
8442
8784
  if (rule.verdict === "allow") continue;
8443
8785
  if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
8786
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
8444
8787
  if (!evaluateSmartConditions(input, rule)) continue;
8445
8788
  const inputPreview = preview(input, 120);
8446
8789
  const isDupe = result.findings.some(
@@ -8658,12 +9001,27 @@ function scanCodexHistory(startDate, onProgress, onLine) {
8658
9001
  });
8659
9002
  }
8660
9003
  }
8661
- let ruleMatched = false;
9004
+ let astFsMatched = false;
9005
+ const astRanForBash = toolNameLower === "exec_command" || toolNameLower === "bash";
9006
+ if (astRanForBash) {
9007
+ astFsMatched = pushFsOpAstFinding(
9008
+ String(input["command"] ?? ""),
9009
+ toolName,
9010
+ input,
9011
+ ts,
9012
+ projLabel,
9013
+ sessionId,
9014
+ "codex",
9015
+ result
9016
+ );
9017
+ }
9018
+ let ruleMatched = astFsMatched;
8662
9019
  for (const source of ruleSources) {
8663
9020
  const { rule } = source;
8664
9021
  if (rule.verdict === "allow") continue;
8665
9022
  if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
8666
9023
  continue;
9024
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
8667
9025
  if (!evaluateSmartConditions(input, rule)) continue;
8668
9026
  const inputPreview = preview(input, 120);
8669
9027
  const isDupe = result.findings.some(
@@ -8863,15 +9221,22 @@ function renderCompactScorecard(input) {
8863
9221
  import_chalk4.default.red("\u{1F6D1} ") + import_chalk4.default.red.bold(String(blockedCount).padEnd(4)) + import_chalk4.default.dim("would have blocked".padEnd(20)) + import_chalk4.default.dim(`(${topBlocked})`)
8864
9222
  );
8865
9223
  }
8866
- if (scan.loopFindings.length > 0) {
8867
- const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
9224
+ const realLoops = scan.loopFindings.filter((l) => l.kind !== "long-iteration");
9225
+ const longIterations = scan.loopFindings.filter((l) => l.kind === "long-iteration");
9226
+ if (realLoops.length > 0) {
9227
+ const wastedCalls = realLoops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
8868
9228
  const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
8869
9229
  const wasteParts = [];
8870
9230
  if (wastePct > 0) wasteParts.push(`${wastePct}% wasted`);
8871
9231
  if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
8872
9232
  const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
8873
9233
  console.log(
8874
- import_chalk4.default.yellow("\u{1F501} ") + import_chalk4.default.yellow.bold(String(scan.loopFindings.length).padEnd(4)) + import_chalk4.default.dim("agent loops".padEnd(20)) + import_chalk4.default.dim(wasteSummary)
9234
+ import_chalk4.default.yellow("\u{1F501} ") + import_chalk4.default.yellow.bold(String(realLoops.length).padEnd(4)) + import_chalk4.default.dim("agent loops".padEnd(20)) + import_chalk4.default.dim(wasteSummary)
9235
+ );
9236
+ }
9237
+ if (longIterations.length > 0) {
9238
+ console.log(
9239
+ import_chalk4.default.dim("\u{1F4C2} ") + import_chalk4.default.dim.bold(String(longIterations.length).padEnd(4)) + import_chalk4.default.dim("long iterations".padEnd(20)) + import_chalk4.default.dim("(deep work \u2014 not waste)")
8875
9240
  );
8876
9241
  }
8877
9242
  if (reviewCount > 0) {
@@ -9403,7 +9768,7 @@ function registerScanCommand(program2) {
9403
9768
  }
9404
9769
  );
9405
9770
  }
9406
- var import_chalk4, import_fs18, import_path20, import_os17, 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;
9771
+ var import_chalk4, import_fs18, import_path20, import_os17, 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;
9407
9772
  var init_scan = __esm({
9408
9773
  "src/cli/commands/scan.ts"() {
9409
9774
  "use strict";
@@ -9414,6 +9779,7 @@ var init_scan = __esm({
9414
9779
  init_shields();
9415
9780
  init_config();
9416
9781
  init_policy();
9782
+ init_dist();
9417
9783
  init_dlp();
9418
9784
  init_dist();
9419
9785
  init_scan_summary();
@@ -9466,6 +9832,22 @@ var init_scan = __esm({
9466
9832
  ".vue",
9467
9833
  ".svelte"
9468
9834
  ]);
9835
+ SELF_OUTPUT_MARKERS = [
9836
+ /redactedSample:\s*['"]/,
9837
+ /patternName:\s*['"]/,
9838
+ /\bseverity:\s*['"](?:block|review|allow)['"]/,
9839
+ /NODE9 SECURITY ALERT/
9840
+ ];
9841
+ FIXTURE_TOKEN_PATTERNS = [
9842
+ /(.)\1{5,}/,
9843
+ // 6+ repeated characters (aaaaaa, 000000)
9844
+ /(?:EXAMPLE|FAKE|DUMMY|PLACEHOLDER|XXXXX)/i,
9845
+ /abcdefghijklmn/i,
9846
+ // long alpha sequence — fixture, not entropy
9847
+ /1234567890/,
9848
+ // long digit sequence — fixture, not entropy
9849
+ /qwerty/i
9850
+ ];
9469
9851
  TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
9470
9852
  LOOP_TOOLS = /* @__PURE__ */ new Set([
9471
9853
  "bash",
@@ -9478,10 +9860,18 @@ var init_scan = __esm({
9478
9860
  "multiedit"
9479
9861
  ]);
9480
9862
  LOOP_THRESHOLD = 3;
9863
+ LOOP_TIMESPAN_THRESHOLD_MS = 10 * 60 * 1e3;
9481
9864
  STUCK_TOOLS_MIN_WASTE = 5;
9482
9865
  STUCK_TOOLS_LIMIT = 3;
9483
9866
  RECURRING_SESSION_THRESHOLD = 3;
9484
9867
  STALE_AGE_DAYS = 30;
9868
+ AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
9869
+ "block-rm-rf-home",
9870
+ "shield:project-jail:block-read-ssh",
9871
+ "shield:project-jail:block-read-aws",
9872
+ "shield:project-jail:block-read-env",
9873
+ "shield:project-jail:block-read-credentials"
9874
+ ]);
9485
9875
  DEFAULT_RULE_NAMES = new Set(
9486
9876
  DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
9487
9877
  );
@@ -10004,7 +10394,86 @@ function abandonPending() {
10004
10394
  }, 200);
10005
10395
  }
10006
10396
  }
10397
+ function logActivitySocket(msg) {
10398
+ try {
10399
+ import_fs20.default.appendFileSync(
10400
+ import_path22.default.join(homeDir, ".node9", "hook-debug.log"),
10401
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] [activity-socket] ${msg}
10402
+ `
10403
+ );
10404
+ } catch {
10405
+ }
10406
+ }
10407
+ function shouldRebind(now = Date.now()) {
10408
+ if (activityCircuitTripped) return false;
10409
+ activityRebindAttempts = activityRebindAttempts.filter(
10410
+ (t) => now - t < ACTIVITY_REBIND_WINDOW_MS
10411
+ );
10412
+ activityRebindAttempts.push(now);
10413
+ if (activityRebindAttempts.length > ACTIVITY_REBIND_MAX_ATTEMPTS) {
10414
+ activityCircuitTripped = true;
10415
+ return false;
10416
+ }
10417
+ return true;
10418
+ }
10007
10419
  function startActivitySocket() {
10420
+ bindActivitySocket();
10421
+ if (process.platform !== "win32") {
10422
+ try {
10423
+ activitySocketWatcher = import_fs20.default.watch(import_os18.default.tmpdir(), (eventType, filename) => {
10424
+ if (filename !== import_path22.default.basename(ACTIVITY_SOCKET_PATH2)) return;
10425
+ if (eventType !== "rename") return;
10426
+ if (import_fs20.default.existsSync(ACTIVITY_SOCKET_PATH2)) return;
10427
+ attemptRebind("watch-unlink");
10428
+ });
10429
+ activitySocketWatcher.on("error", (err2) => {
10430
+ logActivitySocket(`watcher error: ${err2.message}`);
10431
+ });
10432
+ activitySocketWatcher.unref();
10433
+ } catch (err2) {
10434
+ logActivitySocket(`failed to start watcher: ${err2.message}`);
10435
+ }
10436
+ }
10437
+ activityHealthInterval = setInterval(() => {
10438
+ if (!import_fs20.default.existsSync(ACTIVITY_SOCKET_PATH2)) attemptRebind("health-probe");
10439
+ }, ACTIVITY_HEALTH_PROBE_MS);
10440
+ activityHealthInterval.unref();
10441
+ process.on("exit", () => {
10442
+ if (activitySocketWatcher) {
10443
+ try {
10444
+ activitySocketWatcher.close();
10445
+ } catch {
10446
+ }
10447
+ }
10448
+ if (activityHealthInterval) clearInterval(activityHealthInterval);
10449
+ try {
10450
+ import_fs20.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
10451
+ } catch {
10452
+ }
10453
+ });
10454
+ }
10455
+ function attemptRebind(reason) {
10456
+ if (!shouldRebind()) {
10457
+ logActivitySocket(
10458
+ `circuit breaker tripped after ${ACTIVITY_REBIND_MAX_ATTEMPTS} attempts in ${ACTIVITY_REBIND_WINDOW_MS}ms \u2014 flight recorder down`
10459
+ );
10460
+ broadcast("flight-recorder-down", {
10461
+ reason: "rebind-loop",
10462
+ message: "Activity socket repeatedly disappearing \u2014 run: node9 daemon restart"
10463
+ });
10464
+ return;
10465
+ }
10466
+ logActivitySocket(`rebinding (reason: ${reason}, attempt ${activityRebindAttempts.length})`);
10467
+ if (activitySocketServer) {
10468
+ try {
10469
+ activitySocketServer.close();
10470
+ } catch {
10471
+ }
10472
+ activitySocketServer = null;
10473
+ }
10474
+ bindActivitySocket();
10475
+ }
10476
+ function bindActivitySocket() {
10008
10477
  try {
10009
10478
  import_fs20.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
10010
10479
  } catch {
@@ -10102,15 +10571,15 @@ function startActivitySocket() {
10102
10571
  socket.on("error", () => {
10103
10572
  });
10104
10573
  });
10105
- unixServer.listen(ACTIVITY_SOCKET_PATH2);
10106
- process.on("exit", () => {
10107
- try {
10108
- import_fs20.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
10109
- } catch {
10110
- }
10574
+ unixServer.on("error", (err2) => {
10575
+ logActivitySocket(`server error: ${err2.message}`);
10111
10576
  });
10577
+ unixServer.listen(ACTIVITY_SOCKET_PATH2, () => {
10578
+ logActivitySocket(`bound to ${ACTIVITY_SOCKET_PATH2}`);
10579
+ });
10580
+ activitySocketServer = unixServer;
10112
10581
  }
10113
- var import_net2, import_fs20, import_path22, import_os18, import_crypto6, 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;
10582
+ var import_net2, import_fs20, import_path22, import_os18, import_crypto6, 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;
10114
10583
  var init_state2 = __esm({
10115
10584
  "src/daemon/state.ts"() {
10116
10585
  "use strict";
@@ -10171,6 +10640,14 @@ var init_state2 = __esm({
10171
10640
  "notebook_edit",
10172
10641
  "notebookedit"
10173
10642
  ]);
10643
+ ACTIVITY_REBIND_MAX_ATTEMPTS = 5;
10644
+ ACTIVITY_REBIND_WINDOW_MS = 6e4;
10645
+ ACTIVITY_HEALTH_PROBE_MS = 3e4;
10646
+ activitySocketServer = null;
10647
+ activitySocketWatcher = null;
10648
+ activityHealthInterval = null;
10649
+ activityRebindAttempts = [];
10650
+ activityCircuitTripped = false;
10174
10651
  }
10175
10652
  });
10176
10653
 
@@ -12312,6 +12789,8 @@ async function startTail(options = {}) {
12312
12789
  let initialReplayDone = false;
12313
12790
  const activityPending = /* @__PURE__ */ new Map();
12314
12791
  const orphanedResults = /* @__PURE__ */ new Map();
12792
+ let lastActivityFromDaemon = Date.now();
12793
+ let stallWarned = false;
12315
12794
  const authToken = getInternalToken() ?? "";
12316
12795
  const approvalQueue = [];
12317
12796
  let cardActive = false;
@@ -12538,6 +13017,24 @@ async function startTail(options = {}) {
12538
13017
  console.log(import_chalk29.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
12539
13018
  process.exit(0);
12540
13019
  });
13020
+ const STALL_THRESHOLD_MS = 6e4;
13021
+ const stallWatchdog = setInterval(() => {
13022
+ if (stallWarned) return;
13023
+ if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
13024
+ try {
13025
+ const auditMtime = import_fs44.default.statSync(auditLog).mtimeMs;
13026
+ if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
13027
+ console.log("");
13028
+ console.log(
13029
+ import_chalk29.default.yellow(
13030
+ "\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
13031
+ )
13032
+ );
13033
+ stallWarned = true;
13034
+ } catch {
13035
+ }
13036
+ }, STALL_THRESHOLD_MS / 2);
13037
+ stallWatchdog.unref();
12541
13038
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
12542
13039
  const req = import_http2.default.get(
12543
13040
  sseUrl,
@@ -12583,7 +13080,18 @@ async function startTail(options = {}) {
12583
13080
  }
12584
13081
  );
12585
13082
  function handleMessage(event, rawData) {
13083
+ lastActivityFromDaemon = Date.now();
12586
13084
  if (event === "csrf") return;
13085
+ if (event === "flight-recorder-down") {
13086
+ try {
13087
+ const parsed = JSON.parse(rawData);
13088
+ const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
13089
+ console.log("");
13090
+ console.log(import_chalk29.default.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
13091
+ } catch {
13092
+ }
13093
+ return;
13094
+ }
12587
13095
  if (event === "init") {
12588
13096
  try {
12589
13097
  const parsed = JSON.parse(rawData);
@@ -16166,7 +16674,6 @@ function registerInitCommand(program2) {
16166
16674
  console.log(import_chalk15.default.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
16167
16675
  console.log("");
16168
16676
  console.log(import_chalk15.default.white(" Watch live: ") + import_chalk15.default.cyan("node9 tail"));
16169
- console.log(import_chalk15.default.white(" Local UI: ") + import_chalk15.default.cyan("node9 daemon --openui"));
16170
16677
  console.log("");
16171
16678
  console.log(import_chalk15.default.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"));
16172
16679
  console.log(