@node9/proxy 2.5.0 → 2.6.2

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.
@@ -228,9 +228,9 @@ function matchesPattern(text, patterns) {
228
228
  const withoutDotSlash = text.replace(/^\.\//, "");
229
229
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
230
230
  }
231
- function getNestedValue(obj, path11) {
231
+ function getNestedValue(obj, path12) {
232
232
  if (!obj || typeof obj !== "object") return null;
233
- const segments = path11.split(".");
233
+ const segments = path12.split(".");
234
234
  for (const seg of segments) {
235
235
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
236
236
  }
@@ -2316,6 +2316,32 @@ var init_daemon = __esm({
2316
2316
 
2317
2317
  // src/config-schema.ts
2318
2318
  import { z } from "zod";
2319
+ function sanitizeConfig(raw) {
2320
+ const result = ConfigFileSchema.safeParse(raw);
2321
+ if (result.success) {
2322
+ return { sanitized: result.data, error: null };
2323
+ }
2324
+ const invalidTopLevelKeys = new Set(
2325
+ result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
2326
+ );
2327
+ const sanitized = {};
2328
+ if (typeof raw === "object" && raw !== null) {
2329
+ for (const [key, value] of Object.entries(raw)) {
2330
+ if (!invalidTopLevelKeys.has(key)) {
2331
+ sanitized[key] = value;
2332
+ }
2333
+ }
2334
+ }
2335
+ const lines = result.error.issues.map((issue) => {
2336
+ const path12 = issue.path.length > 0 ? issue.path.join(".") : "root";
2337
+ return ` \u2022 ${path12}: ${issue.message}`;
2338
+ });
2339
+ return {
2340
+ sanitized,
2341
+ error: `Invalid config:
2342
+ ${lines.join("\n")}`
2343
+ };
2344
+ }
2319
2345
  var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
2320
2346
  var init_config_schema = __esm({
2321
2347
  "src/config-schema.ts"() {
@@ -2515,6 +2541,18 @@ function loadUserShields() {
2515
2541
  function buildSHIELDS() {
2516
2542
  return { ...BUILTIN_SHIELDS, ...loadUserShields() };
2517
2543
  }
2544
+ function resolveShieldName(input) {
2545
+ const lower = input.toLowerCase();
2546
+ if (SHIELDS[lower]) return lower;
2547
+ for (const [name, def] of Object.entries(SHIELDS)) {
2548
+ if (def.aliases.includes(lower)) return name;
2549
+ }
2550
+ return null;
2551
+ }
2552
+ function getShield(name) {
2553
+ const resolved = resolveShieldName(name);
2554
+ return resolved ? SHIELDS[resolved] : null;
2555
+ }
2518
2556
  function readShieldsFile() {
2519
2557
  try {
2520
2558
  const raw = fs3.readFileSync(SHIELDS_STATE_FILE, "utf-8");
@@ -2540,6 +2578,9 @@ function readShieldsFile() {
2540
2578
  function readActiveShields() {
2541
2579
  return readShieldsFile().active;
2542
2580
  }
2581
+ function readShieldOverrides() {
2582
+ return readShieldsFile().overrides ?? {};
2583
+ }
2543
2584
  var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
2544
2585
  var init_shields = __esm({
2545
2586
  "src/shields.ts"() {
@@ -2553,21 +2594,192 @@ var init_shields = __esm({
2553
2594
  });
2554
2595
 
2555
2596
  // src/config/managed.ts
2597
+ function rankIn(order, value) {
2598
+ return value === void 0 ? -1 : order.indexOf(value);
2599
+ }
2600
+ function floorValue(order, local, cloud, opts = {}) {
2601
+ if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
2602
+ if (opts.locked) return cloud;
2603
+ const localSet = opts.localWasSet ?? local !== void 0;
2604
+ if (!localSet || rankIn(order, local) === -1) return cloud;
2605
+ return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
2606
+ }
2607
+ function strictestOf(order, ...values) {
2608
+ let best;
2609
+ for (const v of values) {
2610
+ if (rankIn(order, v) === -1) continue;
2611
+ if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
2612
+ }
2613
+ return best;
2614
+ }
2615
+ function resolveByOrder(order, local, cloud, locked) {
2616
+ return floorValue(order, local, cloud, { locked }) ?? local;
2617
+ }
2618
+ function resolveManagedMode(local, cloud, locked) {
2619
+ return resolveByOrder(MODE_ORDER, local, cloud, locked);
2620
+ }
2621
+ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
2622
+ const next = { ...local };
2623
+ if (typeof managed.enabled === "boolean") {
2624
+ next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
2625
+ }
2626
+ if (typeof managed.mode === "string") {
2627
+ next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
2628
+ locked: locked.includes("egressMode"),
2629
+ // The default 'review' is seeded into egress before any merge, so absence
2630
+ // is invisible from `local.mode` alone — the caller tracks it for us.
2631
+ localWasSet: localModeUserSet
2632
+ }) ?? local.mode;
2633
+ }
2634
+ if (Array.isArray(managed.allow) && managed.allow.length > 0) {
2635
+ next.allow = [...managed.allow];
2636
+ }
2637
+ if (Array.isArray(managed.deny) && managed.deny.length > 0) {
2638
+ next.deny = [.../* @__PURE__ */ new Set([...local.deny ?? [], ...managed.deny])];
2639
+ }
2640
+ if (typeof managed.allowPrivate === "boolean") {
2641
+ next.allowPrivate = locked.includes("egressAllowPrivate") ? managed.allowPrivate : (local.allowPrivate ?? true) && managed.allowPrivate;
2642
+ }
2643
+ return next;
2644
+ }
2645
+ function applyManagedDlp(local, managed, locked) {
2646
+ const next = { ...local };
2647
+ if (typeof managed.enabled === "boolean") {
2648
+ next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
2649
+ }
2650
+ if (managed.enabled === true) {
2651
+ next.scanIgnoredTools = true;
2652
+ }
2653
+ if (typeof managed.pii === "string") {
2654
+ next.pii = resolveByOrder(
2655
+ DLP_PII_ORDER,
2656
+ local.pii ?? "off",
2657
+ managed.pii,
2658
+ locked.includes("dlpPii")
2659
+ );
2660
+ }
2661
+ if (typeof managed.reviewAction === "string") {
2662
+ next.reviewAction = resolveByOrder(
2663
+ DLP_REVIEW_ACTION_ORDER,
2664
+ local.reviewAction ?? "review",
2665
+ managed.reviewAction,
2666
+ locked.includes("dlpReviewAction")
2667
+ );
2668
+ }
2669
+ return next;
2670
+ }
2671
+ function applyManagedCommandChecks(local, managed, locked) {
2672
+ const next = { ...local };
2673
+ for (const key of COMMAND_CHECK_KEYS) {
2674
+ const m = managed[key];
2675
+ if (typeof m !== "string") continue;
2676
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
2677
+ const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
2678
+ locked: locked.includes(lockKey)
2679
+ });
2680
+ if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
2681
+ next[key] = resolved;
2682
+ }
2683
+ return next;
2684
+ }
2685
+ function applyManagedApprovers(local, managed) {
2686
+ return {
2687
+ ...local,
2688
+ native: typeof managed.native === "boolean" ? managed.native : local.native,
2689
+ browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
2690
+ cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
2691
+ terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
2692
+ };
2693
+ }
2694
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS, CLASS_B_COMMAND_CHECKS;
2556
2695
  var init_managed = __esm({
2557
2696
  "src/config/managed.ts"() {
2558
2697
  "use strict";
2698
+ MODE_ORDER = ["observe", "audit", "standard", "strict"];
2699
+ EGRESS_MODE_ORDER = ["off", "review", "block"];
2700
+ DLP_PII_ORDER = ["off", "block"];
2701
+ DLP_REVIEW_ACTION_ORDER = ["review", "block"];
2702
+ COMMAND_CHECK_ORDER = ["off", "review", "block"];
2703
+ COMMAND_CHECK_KEYS = [
2704
+ "inlineExec",
2705
+ "rmAdvisory",
2706
+ "chmod",
2707
+ "sqlDdl",
2708
+ "evalDynamic",
2709
+ "pipeChainHigh"
2710
+ ];
2711
+ CLASS_B_COMMAND_CHECKS = /* @__PURE__ */ new Set([
2712
+ "evalDynamic",
2713
+ "pipeChainHigh"
2714
+ ]);
2559
2715
  }
2560
2716
  });
2561
2717
 
2562
2718
  // src/shields/build.ts
2719
+ function escapeRegex(s) {
2720
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2721
+ }
2722
+ function slug(s) {
2723
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
2724
+ }
2725
+ function pathToRegexFragment(rawPath) {
2726
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
2727
+ const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
2728
+ if (segments.length === 0) return "";
2729
+ return `(^|${B})${segments.join(SEP)}(${B}|$)`;
2730
+ }
2731
+ function pathRules(rawPath, verdict, reason) {
2732
+ const value = pathToRegexFragment(rawPath);
2733
+ if (!value) return [];
2734
+ const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
2735
+ const s = slug(rawPath);
2736
+ return [
2737
+ {
2738
+ name: `${verdict}-path-${s}-bash`,
2739
+ tool: "bash",
2740
+ conditions: [{ field: "command", op: "matches", value }],
2741
+ verdict,
2742
+ reason: why
2743
+ },
2744
+ // Keep the historical `-anytool` name for the file_path rule: the
2745
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
2746
+ {
2747
+ name: `${verdict}-path-${s}-anytool`,
2748
+ tool: "*",
2749
+ conditions: [{ field: "file_path", op: "matches", value }],
2750
+ verdict,
2751
+ reason: why
2752
+ },
2753
+ {
2754
+ name: `${verdict}-path-${s}-anytool-path`,
2755
+ tool: "*",
2756
+ conditions: [{ field: "path", op: "matches", value }],
2757
+ verdict,
2758
+ reason: why
2759
+ },
2760
+ {
2761
+ name: `${verdict}-path-${s}-anytool-pattern`,
2762
+ tool: "*",
2763
+ conditions: [{ field: "pattern", op: "matches", value }],
2764
+ verdict,
2765
+ reason: why
2766
+ }
2767
+ ];
2768
+ }
2769
+ var B, SEP;
2563
2770
  var init_build = __esm({
2564
2771
  "src/shields/build.ts"() {
2565
2772
  "use strict";
2566
2773
  init_dist();
2774
+ B = "[\\s/\\\\]";
2775
+ SEP = "[/\\\\]";
2567
2776
  }
2568
2777
  });
2569
2778
 
2570
2779
  // src/auth/trusted-hosts.ts
2780
+ function normalizeHost(raw) {
2781
+ return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
2782
+ }
2571
2783
  var init_trusted_hosts = __esm({
2572
2784
  "src/auth/trusted-hosts.ts"() {
2573
2785
  "use strict";
@@ -2575,7 +2787,633 @@ var init_trusted_hosts = __esm({
2575
2787
  });
2576
2788
 
2577
2789
  // src/config/index.ts
2578
- var DANGEROUS_WORDS, DEFAULT_CONFIG, CACHE_LOG_REARM_MS;
2790
+ import fs4 from "fs";
2791
+ import path4 from "path";
2792
+ import os4 from "os";
2793
+ function getCredentials() {
2794
+ const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
2795
+ if (process.env.NODE9_API_KEY) {
2796
+ return {
2797
+ apiKey: process.env.NODE9_API_KEY,
2798
+ apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
2799
+ };
2800
+ }
2801
+ try {
2802
+ const credPath = path4.join(os4.homedir(), ".node9", "credentials.json");
2803
+ if (fs4.existsSync(credPath)) {
2804
+ const creds = JSON.parse(fs4.readFileSync(credPath, "utf-8"));
2805
+ const profileName = process.env.NODE9_PROFILE || "default";
2806
+ const profile = creds[profileName];
2807
+ if (profile?.apiKey) {
2808
+ return {
2809
+ apiKey: profile.apiKey,
2810
+ apiUrl: profile.apiUrl || DEFAULT_API_URL,
2811
+ localOnly: profile.localOnly === true || profileName !== "default"
2812
+ };
2813
+ }
2814
+ if (creds.apiKey) {
2815
+ return {
2816
+ apiKey: creds.apiKey,
2817
+ apiUrl: creds.apiUrl || DEFAULT_API_URL,
2818
+ localOnly: creds.localOnly === true
2819
+ };
2820
+ }
2821
+ }
2822
+ } catch {
2823
+ }
2824
+ return null;
2825
+ }
2826
+ function readRulesCacheResilient(cacheFile) {
2827
+ let existed = false;
2828
+ let sawReadError = false;
2829
+ for (let attempt = 0; attempt < 3; attempt++) {
2830
+ let content;
2831
+ try {
2832
+ content = fs4.readFileSync(cacheFile, "utf-8");
2833
+ existed = true;
2834
+ } catch (err) {
2835
+ if (err.code === "ENOENT") return {};
2836
+ sawReadError = true;
2837
+ continue;
2838
+ }
2839
+ try {
2840
+ const parsed = JSON.parse(content);
2841
+ lastParsedRulesCache = parsed;
2842
+ return parsed;
2843
+ } catch {
2844
+ }
2845
+ }
2846
+ if (existed || sawReadError) {
2847
+ const backup = path4.join(path4.dirname(cacheFile), "rules-cache.last-good.json");
2848
+ if (backup !== cacheFile) {
2849
+ try {
2850
+ const raw = JSON.parse(fs4.readFileSync(backup, "utf-8"));
2851
+ logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
2852
+ lastParsedRulesCache = raw;
2853
+ return raw;
2854
+ } catch {
2855
+ }
2856
+ }
2857
+ if (lastParsedRulesCache) {
2858
+ logCacheReadIssue(cacheFile, "RULES_CACHE_USED_MEMORY");
2859
+ return lastParsedRulesCache;
2860
+ }
2861
+ logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
2862
+ }
2863
+ return {};
2864
+ }
2865
+ function logCacheReadIssue(cacheFile, kind) {
2866
+ const now = Date.now();
2867
+ if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
2868
+ cacheReadLastLoggedAt = now;
2869
+ try {
2870
+ fs4.appendFileSync(
2871
+ path4.join(os4.homedir(), ".node9", "hook-debug.log"),
2872
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
2873
+ `
2874
+ );
2875
+ } catch {
2876
+ }
2877
+ }
2878
+ function getConfig(cwd) {
2879
+ if (!cwd && cachedConfig) return cachedConfig;
2880
+ const globalPath = path4.join(os4.homedir(), ".node9", "config.json");
2881
+ const projectPath = path4.join(cwd ?? process.cwd(), "node9.config.json");
2882
+ const globalConfig = tryLoadConfig(globalPath);
2883
+ const projectConfig = tryLoadConfig(projectPath);
2884
+ const mergedSettings = {
2885
+ ...DEFAULT_CONFIG.settings,
2886
+ approvers: { ...DEFAULT_CONFIG.settings.approvers },
2887
+ shipper: { ...DEFAULT_CONFIG.settings.shipper }
2888
+ };
2889
+ const mergedPolicy = {
2890
+ sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
2891
+ dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
2892
+ ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
2893
+ toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
2894
+ smartRules: [...DEFAULT_CONFIG.policy.smartRules],
2895
+ dlp: { ...DEFAULT_CONFIG.policy.dlp },
2896
+ egress: {
2897
+ ...DEFAULT_CONFIG.policy.egress,
2898
+ allow: [...DEFAULT_CONFIG.policy.egress.allow],
2899
+ deny: [...DEFAULT_CONFIG.policy.egress.deny]
2900
+ },
2901
+ loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
2902
+ injectionScan: {
2903
+ ...DEFAULT_CONFIG.policy.injectionScan,
2904
+ allow: [...DEFAULT_CONFIG.policy.injectionScan.allow]
2905
+ },
2906
+ skillPinning: {
2907
+ ...DEFAULT_CONFIG.policy.skillPinning,
2908
+ roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
2909
+ },
2910
+ // Left empty on purpose: the local file is read fresh at policy-eval time
2911
+ // (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
2912
+ // here. A managed list fills this below and flips trustedHostsManaged.
2913
+ trustedHosts: [],
2914
+ trustedHostsManaged: false,
2915
+ appPermissions: {},
2916
+ managedJailPaths: []
2917
+ };
2918
+ const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
2919
+ const rank = (v) => {
2920
+ const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
2921
+ return i === -1 ? 1 : i;
2922
+ };
2923
+ const pr2Creds = getCredentials();
2924
+ const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
2925
+ const applyLayer = (source, isProject = false, isCloud = false) => {
2926
+ if (!source) return;
2927
+ const s = source.settings || {};
2928
+ const p = source.policy || {};
2929
+ if (s.autoStartDaemon !== void 0) mergedSettings.autoStartDaemon = s.autoStartDaemon;
2930
+ if (s.enableHookLogDebug !== void 0)
2931
+ mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
2932
+ if (s.shipper) mergedSettings.shipper = { ...mergedSettings.shipper, ...s.shipper };
2933
+ if (s.cloudSyncIntervalHours !== void 0)
2934
+ mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
2935
+ if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
2936
+ if (s.mcpReconcileIntervalMinutes !== void 0)
2937
+ mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
2938
+ if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
2939
+ if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
2940
+ if (keyed && !isCloud) return;
2941
+ if (s.mode !== void 0) mergedSettings.mode = s.mode;
2942
+ if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
2943
+ if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
2944
+ if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
2945
+ mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
2946
+ if (s.environment !== void 0) mergedSettings.environment = s.environment;
2947
+ if (s.reviewChannel !== void 0) mergedSettings.reviewChannel = s.reviewChannel;
2948
+ if (s.mcpAllowWeakening !== void 0) mergedSettings.mcpAllowWeakening = s.mcpAllowWeakening;
2949
+ if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
2950
+ if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
2951
+ if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
2952
+ if (p.toolInspection)
2953
+ mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
2954
+ if (p.smartRules) {
2955
+ const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
2956
+ const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
2957
+ const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
2958
+ const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
2959
+ const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
2960
+ const filteredNonBlocks = defaultNonBlocks.filter(
2961
+ (r) => !r.name || !userRuleNames.has(r.name)
2962
+ );
2963
+ mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
2964
+ }
2965
+ if (p.dlp) {
2966
+ const d = p.dlp;
2967
+ if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
2968
+ if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
2969
+ if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
2970
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
2971
+ }
2972
+ if (p.commandChecks && typeof p.commandChecks === "object") {
2973
+ const src = p.commandChecks;
2974
+ const cc2 = {
2975
+ ...mergedPolicy.commandChecks
2976
+ };
2977
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
2978
+ const v = src[k];
2979
+ if (v !== "off" && v !== "review" && v !== "block") continue;
2980
+ if (isProject && rank(v) < rank(cc2[k])) continue;
2981
+ cc2[k] = v;
2982
+ }
2983
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
2984
+ const v = src[k];
2985
+ if (v === "review" || v === "block") cc2[k] = v;
2986
+ }
2987
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
2988
+ }
2989
+ if (p.egress) {
2990
+ const e = p.egress;
2991
+ if (e.enabled !== void 0 && !(isProject && e.enabled === false))
2992
+ mergedPolicy.egress.enabled = e.enabled;
2993
+ if (e.mode !== void 0) {
2994
+ const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
2995
+ if (!weaker) {
2996
+ mergedPolicy.egress.mode = e.mode;
2997
+ egressModeUserSet = true;
2998
+ }
2999
+ }
3000
+ if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
3001
+ mergedPolicy.egress.allow.push(...e.allow);
3002
+ }
3003
+ if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
3004
+ if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
3005
+ if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
3006
+ mergedPolicy.egress.allowPrivate = e.allowPrivate;
3007
+ }
3008
+ if (p.loopDetection) {
3009
+ const ld = p.loopDetection;
3010
+ if (ld.enabled !== void 0) mergedPolicy.loopDetection.enabled = ld.enabled;
3011
+ if (ld.threshold !== void 0) mergedPolicy.loopDetection.threshold = ld.threshold;
3012
+ if (ld.windowSeconds !== void 0)
3013
+ mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
3014
+ }
3015
+ if (p.injectionScan && typeof p.injectionScan === "object") {
3016
+ const is = p.injectionScan;
3017
+ if (is.enabled !== void 0) mergedPolicy.injectionScan.enabled = is.enabled;
3018
+ if (is.minConfidence !== void 0)
3019
+ mergedPolicy.injectionScan.minConfidence = is.minConfidence;
3020
+ if (Array.isArray(is.allow)) {
3021
+ for (const t of is.allow) {
3022
+ if (typeof t === "string" && t.length > 0) mergedPolicy.injectionScan.allow.push(t);
3023
+ }
3024
+ }
3025
+ }
3026
+ if (p.skillPinning && typeof p.skillPinning === "object") {
3027
+ const sp = p.skillPinning;
3028
+ if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
3029
+ if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
3030
+ if (Array.isArray(sp.roots)) {
3031
+ for (const r of sp.roots) {
3032
+ if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
3033
+ }
3034
+ }
3035
+ }
3036
+ const envs = source.environments || {};
3037
+ for (const [envName, envConfig] of Object.entries(envs)) {
3038
+ if (envConfig && typeof envConfig === "object") {
3039
+ const ec = envConfig;
3040
+ mergedEnvironments[envName] = {
3041
+ ...mergedEnvironments[envName],
3042
+ // Validate field types before merging — do not blindly spread user input
3043
+ ...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
3044
+ };
3045
+ }
3046
+ }
3047
+ };
3048
+ let egressModeUserSet = false;
3049
+ let egressAllowUserSet = false;
3050
+ applyLayer(globalConfig);
3051
+ applyLayer(
3052
+ projectConfig,
3053
+ /* isProject */
3054
+ true
3055
+ );
3056
+ if (keyed) {
3057
+ mergedSettings.approvers = { ...mergedSettings.approvers, cloud: true };
3058
+ }
3059
+ let cloudManagedShields = [];
3060
+ const managedCommandCheckKeys = /* @__PURE__ */ new Set();
3061
+ const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
3062
+ let modeCloudControlled = false;
3063
+ let modeCloudStaged = false;
3064
+ let cloudMandatesEnforcement = false;
3065
+ let cloudMandatesAppPerm = false;
3066
+ {
3067
+ const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
3068
+ try {
3069
+ const raw = readRulesCacheResilient(cacheFile);
3070
+ if (Array.isArray(raw.rules) && raw.rules.length > 0) {
3071
+ applyLayer(
3072
+ { policy: { smartRules: raw.rules } },
3073
+ false,
3074
+ /* isCloud */
3075
+ true
3076
+ );
3077
+ }
3078
+ if (Array.isArray(raw.shields)) {
3079
+ cloudManagedShields = raw.shields.filter((s) => typeof s === "string");
3080
+ }
3081
+ if (raw.managedConfig && typeof raw.managedConfig === "object") {
3082
+ const mc = raw.managedConfig;
3083
+ const locked = Array.isArray(mc.locked) ? mc.locked.filter((f) => typeof f === "string") : [];
3084
+ if (typeof mc.mode === "string") {
3085
+ if (keyed) {
3086
+ if (["observe", "audit", "standard", "strict"].includes(mc.mode)) {
3087
+ mergedSettings.mode = mc.mode;
3088
+ }
3089
+ } else {
3090
+ mergedSettings.mode = resolveManagedMode(
3091
+ mergedSettings.mode,
3092
+ mc.mode,
3093
+ locked.includes("mode")
3094
+ );
3095
+ }
3096
+ }
3097
+ if (typeof mc.mode === "string" || locked.includes("mode")) {
3098
+ modeCloudControlled = true;
3099
+ }
3100
+ if (mc.egress && typeof mc.egress === "object") {
3101
+ const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
3102
+ if (keyed) {
3103
+ const e = mc.egress;
3104
+ if (typeof e.enabled === "boolean") mergedPolicy.egress.enabled = e.enabled;
3105
+ if (e.mode === "off" || e.mode === "review" || e.mode === "block")
3106
+ mergedPolicy.egress.mode = e.mode;
3107
+ const allow = hosts(e.allow);
3108
+ if (allow) mergedPolicy.egress.allow = allow;
3109
+ const deny = hosts(e.deny);
3110
+ if (deny) mergedPolicy.egress.deny = deny;
3111
+ if (typeof e.allowPrivate === "boolean")
3112
+ mergedPolicy.egress.allowPrivate = e.allowPrivate;
3113
+ } else {
3114
+ mergedPolicy.egress = applyManagedEgress(
3115
+ mergedPolicy.egress,
3116
+ {
3117
+ enabled: typeof mc.egress.enabled === "boolean" ? mc.egress.enabled : void 0,
3118
+ mode: typeof mc.egress.mode === "string" ? mc.egress.mode : void 0,
3119
+ allow: hosts(mc.egress.allow),
3120
+ deny: hosts(mc.egress.deny),
3121
+ allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
3122
+ },
3123
+ locked,
3124
+ egressModeUserSet
3125
+ );
3126
+ }
3127
+ }
3128
+ if (mc.dlp && typeof mc.dlp === "object") {
3129
+ if (keyed) {
3130
+ if (typeof mc.dlp.enabled === "boolean") mergedPolicy.dlp.enabled = mc.dlp.enabled;
3131
+ if (mc.dlp.pii === "off" || mc.dlp.pii === "block") mergedPolicy.dlp.pii = mc.dlp.pii;
3132
+ if (mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block")
3133
+ mergedPolicy.dlp.reviewAction = mc.dlp.reviewAction;
3134
+ } else {
3135
+ mergedPolicy.dlp = applyManagedDlp(
3136
+ mergedPolicy.dlp,
3137
+ {
3138
+ enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
3139
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
3140
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
3141
+ },
3142
+ locked
3143
+ );
3144
+ }
3145
+ }
3146
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
3147
+ if (keyed) {
3148
+ const src = mc.commandChecks;
3149
+ const next = { ...mergedPolicy.commandChecks ?? {} };
3150
+ for (const key of COMMAND_CHECK_KEYS) {
3151
+ const val = src[key];
3152
+ if (val !== "off" && val !== "review" && val !== "block") continue;
3153
+ if (val === "off" && CLASS_B_COMMAND_CHECKS.has(key)) continue;
3154
+ next[key] = val;
3155
+ }
3156
+ mergedPolicy.commandChecks = next;
3157
+ } else {
3158
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
3159
+ mergedPolicy.commandChecks ?? {},
3160
+ mc.commandChecks,
3161
+ locked
3162
+ );
3163
+ }
3164
+ for (const [key, val] of Object.entries(mc.commandChecks)) {
3165
+ if (typeof val !== "string") continue;
3166
+ managedCommandCheckKeys.add(key);
3167
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3168
+ if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
3169
+ }
3170
+ }
3171
+ if (mc.approvers && typeof mc.approvers === "object") {
3172
+ const bool = (v) => typeof v === "boolean" ? v : void 0;
3173
+ mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
3174
+ native: bool(mc.approvers.native),
3175
+ browser: bool(mc.approvers.browser),
3176
+ cloud: bool(mc.approvers.cloud),
3177
+ terminal: bool(mc.approvers.terminal)
3178
+ });
3179
+ }
3180
+ if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
3181
+ mergedSettings.reviewChannel = mc.reviewChannel;
3182
+ mergedSettings.reviewChannelManaged = true;
3183
+ }
3184
+ if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
3185
+ mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
3186
+ }
3187
+ if (mc.injectionScan && typeof mc.injectionScan === "object") {
3188
+ const i = mc.injectionScan;
3189
+ const cur = mergedPolicy.injectionScan;
3190
+ mergedPolicy.injectionScan = {
3191
+ enabled: typeof i.enabled === "boolean" ? i.enabled : cur.enabled,
3192
+ minConfidence: i.minConfidence === "high" || i.minConfidence === "medium" ? i.minConfidence : cur.minConfidence,
3193
+ allow: Array.isArray(i.allow) ? i.allow.filter((x) => typeof x === "string") : cur.allow
3194
+ };
3195
+ }
3196
+ if (mc.loopDetection && typeof mc.loopDetection === "object") {
3197
+ const l = mc.loopDetection;
3198
+ const cur = mergedPolicy.loopDetection;
3199
+ mergedPolicy.loopDetection = {
3200
+ enabled: typeof l.enabled === "boolean" ? l.enabled : cur.enabled,
3201
+ threshold: typeof l.threshold === "number" && Number.isFinite(l.threshold) ? l.threshold : cur.threshold,
3202
+ windowSeconds: typeof l.windowSeconds === "number" && Number.isFinite(l.windowSeconds) ? l.windowSeconds : cur.windowSeconds
3203
+ };
3204
+ }
3205
+ if (mc.skillPinning && typeof mc.skillPinning === "object") {
3206
+ const sk = mc.skillPinning;
3207
+ const cur = mergedPolicy.skillPinning;
3208
+ mergedPolicy.skillPinning = {
3209
+ enabled: typeof sk.enabled === "boolean" ? sk.enabled : cur.enabled,
3210
+ mode: sk.mode === "block" || sk.mode === "warn" ? sk.mode : cur.mode,
3211
+ roots: Array.isArray(sk.roots) ? sk.roots.filter((x) => typeof x === "string") : cur.roots
3212
+ };
3213
+ }
3214
+ if (Array.isArray(mc.jailPaths)) {
3215
+ for (const jp of mc.jailPaths) {
3216
+ const path12 = typeof jp?.path === "string" ? jp.path.trim() : "";
3217
+ if (!path12) continue;
3218
+ const verdict = jp?.verdict === "review" ? "review" : "block";
3219
+ for (const r of pathRules(path12, verdict, "org-managed jail")) {
3220
+ mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
3221
+ }
3222
+ mergedPolicy.managedJailPaths.push({ path: path12, verdict });
3223
+ }
3224
+ }
3225
+ if (Array.isArray(mc.trustedHosts)) {
3226
+ mergedPolicy.trustedHostsManaged = true;
3227
+ mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost(h));
3228
+ }
3229
+ if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
3230
+ const coerced = {};
3231
+ for (const [srv, tools] of Object.entries(mc.appPermissions)) {
3232
+ if (!tools || typeof tools !== "object" || Array.isArray(tools)) continue;
3233
+ const m = {};
3234
+ for (const [tool, d] of Object.entries(tools)) {
3235
+ if (d === "allow" || d === "review" || d === "block") m[tool] = d;
3236
+ }
3237
+ if (Object.keys(m).length) coerced[srv] = m;
3238
+ }
3239
+ mergedPolicy.appPermissions = coerced;
3240
+ cloudMandatesAppPerm = Object.values(coerced).some(
3241
+ (tools) => Object.values(tools).some((d) => d === "block" || d === "review")
3242
+ );
3243
+ }
3244
+ const on = (v) => !!v && typeof v === "object" && v.enabled === true;
3245
+ cloudMandatesEnforcement = cloudMandatesAppPerm || Array.isArray(mc.jailPaths) && mc.jailPaths.some((jp) => typeof jp?.path === "string" && jp.path.trim() !== "") || on(mc.egress) || on(mc.dlp) || on(mc.injectionScan) || on(mc.skillPinning) || on(mc.loopDetection) || !!mc.commandChecks && typeof mc.commandChecks === "object" && Object.values(mc.commandChecks).some(
3246
+ (v) => typeof v === "string" && v !== "off"
3247
+ );
3248
+ }
3249
+ if (raw.panicMode === true) {
3250
+ mergedSettings.panicMode = true;
3251
+ }
3252
+ if (raw.shadowMode === true) {
3253
+ mergedSettings.mode = "observe";
3254
+ modeCloudStaged = true;
3255
+ }
3256
+ } catch {
3257
+ }
3258
+ }
3259
+ const shieldOverrides = readShieldOverrides();
3260
+ if (keyed) mergedPolicy.trustedHostsManaged = true;
3261
+ const activeShieldNames = keyed ? [...new Set(cloudManagedShields)] : [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
3262
+ const appliedShields = [];
3263
+ const cloudManagedSet = new Set(cloudManagedShields);
3264
+ for (const shieldName of activeShieldNames) {
3265
+ const isCloudMandated = cloudManagedSet.has(shieldName);
3266
+ const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
3267
+ if (!shield) continue;
3268
+ appliedShields.push(shieldName);
3269
+ const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
3270
+ const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
3271
+ for (const rule of shield.smartRules) {
3272
+ const collides = rule.name ? existingRuleNames.has(rule.name) : false;
3273
+ if (isCloudMandated) {
3274
+ if (collides) {
3275
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
3276
+ }
3277
+ mergedPolicy.smartRules.push({ ...rule, pinned: true });
3278
+ } else if (!collides) {
3279
+ const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
3280
+ mergedPolicy.smartRules.push(
3281
+ overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
3282
+ );
3283
+ }
3284
+ }
3285
+ const existingWords = new Set(mergedPolicy.dangerousWords);
3286
+ for (const word of shield.dangerousWords) {
3287
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
3288
+ }
3289
+ }
3290
+ mergedPolicy.appliedShields = appliedShields.sort();
3291
+ const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
3292
+ const cc = mergedPolicy.commandChecks ?? {};
3293
+ const advisoryKnobKey = (name) => {
3294
+ if (name === "review-rm") return "rmAdvisory";
3295
+ if (name?.endsWith("-sql")) return "sqlDdl";
3296
+ return void 0;
3297
+ };
3298
+ for (const rule of ADVISORY_SMART_RULES) {
3299
+ const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
3300
+ const knob = knobKey ? cc[knobKey] : void 0;
3301
+ if (knob === "off") continue;
3302
+ const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
3303
+ const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
3304
+ const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
3305
+ const knobVerdict = knob === "block" ? "block" : rule.verdict;
3306
+ if (!managed) {
3307
+ if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
3308
+ continue;
3309
+ }
3310
+ const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
3311
+ if (twin) {
3312
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
3313
+ }
3314
+ const injected = { ...rule, verdict: effective, pinned: true };
3315
+ if (rule.name === "review-rm" && effective !== "block") {
3316
+ injected.conditions = [
3317
+ ...rule.conditions ?? [],
3318
+ { field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
3319
+ ];
3320
+ injected.conditionMode = "all";
3321
+ }
3322
+ mergedPolicy.smartRules.push(injected);
3323
+ }
3324
+ const envMode = process.env.NODE9_MODE;
3325
+ if (envMode && !modeCloudControlled && // PR-2: one listening point includes the env var — a keyed machine's
3326
+ // mode comes from the workspace (or the shipped default), never the shell.
3327
+ !keyed && ["observe", "audit", "standard", "strict"].includes(envMode)) {
3328
+ mergedSettings.mode = envMode;
3329
+ }
3330
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
3331
+ mergedSettings.mode = "standard";
3332
+ }
3333
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
3334
+ if (modeCloudControlled && mergedSettings.mode === "strict") {
3335
+ for (const name of Object.keys(mergedEnvironments)) {
3336
+ if (mergedEnvironments[name]?.requireApproval === false) {
3337
+ const cleaned = { ...mergedEnvironments[name] };
3338
+ delete cleaned.requireApproval;
3339
+ mergedEnvironments[name] = cleaned;
3340
+ }
3341
+ }
3342
+ }
3343
+ if (managedFloorActive) {
3344
+ mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
3345
+ mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
3346
+ }
3347
+ mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
3348
+ mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
3349
+ mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
3350
+ mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
3351
+ const result = {
3352
+ settings: mergedSettings,
3353
+ policy: mergedPolicy,
3354
+ environments: mergedEnvironments,
3355
+ // PR-2 — the one truth introspection reads: which of the two working
3356
+ // modes this machine is in. 'workspace' = keyed, policy from the cloud;
3357
+ // 'local' = the local stack (incl. --local / named-profile keys).
3358
+ policySource: keyed ? "workspace" : "local"
3359
+ };
3360
+ if (!cwd) cachedConfig = result;
3361
+ return result;
3362
+ }
3363
+ function tryLoadConfig(filePath) {
3364
+ if (!fs4.existsSync(filePath)) return null;
3365
+ let raw;
3366
+ try {
3367
+ raw = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
3368
+ } catch (err) {
3369
+ const msg = err instanceof Error ? err.message : String(err);
3370
+ process.stderr.write(
3371
+ `
3372
+ \u26A0\uFE0F Node9: Failed to parse ${filePath}
3373
+ ${msg}
3374
+ \u2192 Using default config
3375
+
3376
+ `
3377
+ );
3378
+ return null;
3379
+ }
3380
+ const SUPPORTED_VERSION = "1.0";
3381
+ const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
3382
+ const fileVersion = raw?.version;
3383
+ if (fileVersion !== void 0) {
3384
+ const vStr = String(fileVersion);
3385
+ const fileMajor = vStr.split(".")[0];
3386
+ if (fileMajor !== SUPPORTED_MAJOR) {
3387
+ process.stderr.write(
3388
+ `
3389
+ \u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
3390
+
3391
+ `
3392
+ );
3393
+ return null;
3394
+ } else if (vStr !== SUPPORTED_VERSION) {
3395
+ process.stderr.write(
3396
+ `
3397
+ \u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
3398
+
3399
+ `
3400
+ );
3401
+ }
3402
+ }
3403
+ const { sanitized, error } = sanitizeConfig(raw);
3404
+ if (error) {
3405
+ process.stderr.write(
3406
+ `
3407
+ \u26A0\uFE0F Node9: Invalid config at ${filePath}:
3408
+ ${error.replace("Invalid config:\n", "")}
3409
+ \u2192 Invalid fields ignored, using defaults for those keys
3410
+
3411
+ `
3412
+ );
3413
+ }
3414
+ return sanitized;
3415
+ }
3416
+ var DANGEROUS_WORDS, DEFAULT_CONFIG, RM_SAFE_PATH_PATTERN, VERDICT_ORDER, ADVISORY_SMART_RULES, cachedConfig, lastParsedRulesCache, CACHE_LOG_REARM_MS, cacheReadLastLoggedAt;
2579
3417
  var init_config = __esm({
2580
3418
  "src/config/index.ts"() {
2581
3419
  "use strict";
@@ -2593,10 +3431,18 @@ var init_config = __esm({
2593
3431
  ];
2594
3432
  DEFAULT_CONFIG = {
2595
3433
  version: "1.0",
3434
+ policySource: "local",
2596
3435
  settings: {
2597
3436
  mode: "standard",
2598
3437
  autoStartDaemon: true,
2599
- enableHookLogDebug: true,
3438
+ // Off by default: this is a DEBUG facility, not part of the product's job.
3439
+ // When on, every tool call appends a row to ~/.node9/hook-debug.log, which
3440
+ // has no production reader and no size ceiling. Turn it on deliberately
3441
+ // (this setting, or NODE9_DEBUG=1) while investigating something.
3442
+ // Error breadcrumbs written from catch blocks are NOT gated by this and
3443
+ // keep working when it is off — see CLAUDE.md "Always write to
3444
+ // hook-debug.log in catch blocks that guard audit trail".
3445
+ enableHookLogDebug: false,
2600
3446
  approvalTimeoutMs: 12e4,
2601
3447
  // 120-second auto-deny timeout
2602
3448
  flightRecorder: true,
@@ -2780,7 +3626,68 @@ var init_config = __esm({
2780
3626
  },
2781
3627
  environments: {}
2782
3628
  };
3629
+ RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
3630
+ VERDICT_ORDER = ["allow", "review", "block"];
3631
+ ADVISORY_SMART_RULES = [
3632
+ // ── rm safety ─────────────────────────────────────────────────────────────
3633
+ // tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
3634
+ // Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
3635
+ // chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
3636
+ {
3637
+ name: "allow-rm-safe-paths",
3638
+ tool: "*",
3639
+ conditionMode: "all",
3640
+ conditions: [
3641
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
3642
+ { field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
3643
+ ],
3644
+ verdict: "allow",
3645
+ reason: "Deleting a known-safe build artifact path"
3646
+ },
3647
+ {
3648
+ name: "review-rm",
3649
+ tool: "*",
3650
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
3651
+ verdict: "review",
3652
+ reason: "rm can permanently delete files \u2014 confirm the target path",
3653
+ description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
3654
+ },
3655
+ // ── SQL safety (Safe by Default) ──────────────────────────────────────────
3656
+ // These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
3657
+ // mcp__postgres__query) with a destructive SQL statement in the 'sql' field.
3658
+ // The postgres shield upgrades these from 'review' → 'block' for stricter teams;
3659
+ // without a shield, users still get a human-approval gate on every destructive op.
3660
+ {
3661
+ name: "review-drop-table-sql",
3662
+ tool: "*",
3663
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
3664
+ verdict: "review",
3665
+ reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
3666
+ description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
3667
+ },
3668
+ {
3669
+ name: "review-truncate-sql",
3670
+ tool: "*",
3671
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
3672
+ verdict: "review",
3673
+ reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
3674
+ description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
3675
+ },
3676
+ {
3677
+ name: "review-drop-column-sql",
3678
+ tool: "*",
3679
+ conditions: [
3680
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
3681
+ ],
3682
+ verdict: "review",
3683
+ reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
3684
+ description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
3685
+ }
3686
+ ];
3687
+ cachedConfig = null;
3688
+ lastParsedRulesCache = null;
2783
3689
  CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
3690
+ cacheReadLastLoggedAt = 0;
2784
3691
  }
2785
3692
  });
2786
3693
 
@@ -2792,28 +3699,28 @@ var init_hasher = __esm({
2792
3699
  });
2793
3700
 
2794
3701
  // src/audit/index.ts
2795
- import path4 from "path";
2796
- import os4 from "os";
3702
+ import path5 from "path";
3703
+ import os5 from "os";
2797
3704
  var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
2798
3705
  var init_audit = __esm({
2799
3706
  "src/audit/index.ts"() {
2800
3707
  "use strict";
2801
3708
  init_hasher();
2802
- LOCAL_AUDIT_LOG = path4.join(os4.homedir(), ".node9", "audit.log");
2803
- HOOK_DEBUG_LOG = path4.join(os4.homedir(), ".node9", "hook-debug.log");
3709
+ LOCAL_AUDIT_LOG = path5.join(os5.homedir(), ".node9", "audit.log");
3710
+ HOOK_DEBUG_LOG = path5.join(os5.homedir(), ".node9", "hook-debug.log");
2804
3711
  }
2805
3712
  });
2806
3713
 
2807
3714
  // src/pricing/litellm.ts
2808
- import fs4 from "fs";
2809
- import path5 from "path";
2810
- import os5 from "os";
3715
+ import fs5 from "fs";
3716
+ import path6 from "path";
3717
+ import os6 from "os";
2811
3718
  function normalizeModel(raw) {
2812
3719
  return raw.replace(/-\d{8}$/, "").toLowerCase();
2813
3720
  }
2814
3721
  function readCache() {
2815
3722
  try {
2816
- const raw = JSON.parse(fs4.readFileSync(CACHE_FILE(), "utf-8"));
3723
+ const raw = JSON.parse(fs5.readFileSync(CACHE_FILE(), "utf-8"));
2817
3724
  if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
2818
3725
  return null;
2819
3726
  }
@@ -2827,18 +3734,18 @@ function readCache() {
2827
3734
  function writeCache(prices) {
2828
3735
  try {
2829
3736
  const target = CACHE_FILE();
2830
- const dir = path5.dirname(target);
2831
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3737
+ const dir = path6.dirname(target);
3738
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
2832
3739
  const tmp = target + ".tmp";
2833
3740
  const body = {
2834
3741
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2835
3742
  prices
2836
3743
  };
2837
- fs4.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
2838
- fs4.renameSync(tmp, target);
3744
+ fs5.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
3745
+ fs5.renameSync(tmp, target);
2839
3746
  } catch (err) {
2840
3747
  try {
2841
- fs4.appendFileSync(
3748
+ fs5.appendFileSync(
2842
3749
  HOOK_DEBUG_LOG,
2843
3750
  `[pricing] cache write failed: ${err.message}
2844
3751
  `
@@ -2982,7 +3889,7 @@ var init_litellm = __esm({
2982
3889
  "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
2983
3890
  "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
2984
3891
  };
2985
- CACHE_FILE = () => path5.join(os5.homedir(), ".node9", "model-pricing.json");
3892
+ CACHE_FILE = () => path6.join(os6.homedir(), ".node9", "model-pricing.json");
2986
3893
  TTL_MS = 24 * 60 * 60 * 1e3;
2987
3894
  memCache = null;
2988
3895
  memCacheAt = 0;
@@ -3124,9 +4031,9 @@ var init_scan_watermark = __esm({
3124
4031
  });
3125
4032
 
3126
4033
  // src/cli/aggregate/report-audit.ts
3127
- import fs5 from "fs";
3128
- import os6 from "os";
3129
- import path6 from "path";
4034
+ import fs6 from "fs";
4035
+ import os7 from "os";
4036
+ import path7 from "path";
3130
4037
  function buildTestTimestamps(allEntries) {
3131
4038
  const testTs = /* @__PURE__ */ new Set();
3132
4039
  for (const e of allEntries) {
@@ -3207,8 +4114,8 @@ function getDateRange(period, now) {
3207
4114
  }
3208
4115
  }
3209
4116
  function parseAuditLog(logPath) {
3210
- if (!fs5.existsSync(logPath)) return [];
3211
- const raw = fs5.readFileSync(logPath, "utf-8");
4117
+ if (!fs6.existsSync(logPath)) return [];
4118
+ const raw = fs6.readFileSync(logPath, "utf-8");
3212
4119
  return raw.split("\n").flatMap((line) => {
3213
4120
  if (!line.trim()) return [];
3214
4121
  try {
@@ -3262,25 +4169,25 @@ function freezeClaudeCost(acc) {
3262
4169
  };
3263
4170
  }
3264
4171
  function processClaudeCostProject(proj, projectsDir, start, end, acc) {
3265
- const projPath = path6.join(projectsDir, proj);
4172
+ const projPath = path7.join(projectsDir, proj);
3266
4173
  let files;
3267
4174
  try {
3268
- const stat = fs5.statSync(projPath);
4175
+ const stat = fs6.statSync(projPath);
3269
4176
  if (!stat.isDirectory()) return;
3270
- files = fs5.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
4177
+ files = fs6.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
3271
4178
  } catch {
3272
4179
  return;
3273
4180
  }
3274
4181
  const startMs = start.getTime();
3275
4182
  for (const file of files) {
3276
- const filePath = path6.join(projPath, file);
4183
+ const filePath = path7.join(projPath, file);
3277
4184
  try {
3278
- if (fs5.statSync(filePath).mtimeMs < startMs) continue;
4185
+ if (fs6.statSync(filePath).mtimeMs < startMs) continue;
3279
4186
  } catch {
3280
4187
  continue;
3281
4188
  }
3282
4189
  try {
3283
- const raw = fs5.readFileSync(filePath, "utf-8");
4190
+ const raw = fs6.readFileSync(filePath, "utf-8");
3284
4191
  for (const line of raw.split("\n")) {
3285
4192
  if (!line.trim()) continue;
3286
4193
  let entry;
@@ -3330,10 +4237,10 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
3330
4237
  }
3331
4238
  function loadClaudeCost(start, end, projectsDir) {
3332
4239
  const acc = emptyClaudeCostAccumulator();
3333
- if (!fs5.existsSync(projectsDir)) return freezeClaudeCost(acc);
4240
+ if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
3334
4241
  let dirs;
3335
4242
  try {
3336
- dirs = fs5.readdirSync(projectsDir);
4243
+ dirs = fs6.readdirSync(projectsDir);
3337
4244
  } catch {
3338
4245
  return freezeClaudeCost(acc);
3339
4246
  }
@@ -3344,10 +4251,10 @@ function loadClaudeCost(start, end, projectsDir) {
3344
4251
  }
3345
4252
  async function loadClaudeCostAsync(start, end, projectsDir) {
3346
4253
  const acc = emptyClaudeCostAccumulator();
3347
- if (!fs5.existsSync(projectsDir)) return freezeClaudeCost(acc);
4254
+ if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
3348
4255
  let dirs;
3349
4256
  try {
3350
- dirs = fs5.readdirSync(projectsDir);
4257
+ dirs = fs6.readdirSync(projectsDir);
3351
4258
  } catch {
3352
4259
  return freezeClaudeCost(acc);
3353
4260
  }
@@ -3360,7 +4267,7 @@ async function loadClaudeCostAsync(start, end, projectsDir) {
3360
4267
  function processCodexCostFile(filePath, start, end, acc) {
3361
4268
  let lines;
3362
4269
  try {
3363
- lines = fs5.readFileSync(filePath, "utf-8").split("\n");
4270
+ lines = fs6.readFileSync(filePath, "utf-8").split("\n");
3364
4271
  } catch {
3365
4272
  return;
3366
4273
  }
@@ -3415,31 +4322,31 @@ function processCodexCostFile(filePath, start, end, acc) {
3415
4322
  }
3416
4323
  function listCodexSessionFiles(sessionsBase) {
3417
4324
  const jsonlFiles = [];
3418
- if (!fs5.existsSync(sessionsBase)) return jsonlFiles;
4325
+ if (!fs6.existsSync(sessionsBase)) return jsonlFiles;
3419
4326
  try {
3420
- for (const year of fs5.readdirSync(sessionsBase)) {
3421
- const yearPath = path6.join(sessionsBase, year);
4327
+ for (const year of fs6.readdirSync(sessionsBase)) {
4328
+ const yearPath = path7.join(sessionsBase, year);
3422
4329
  try {
3423
- if (!fs5.statSync(yearPath).isDirectory()) continue;
4330
+ if (!fs6.statSync(yearPath).isDirectory()) continue;
3424
4331
  } catch {
3425
4332
  continue;
3426
4333
  }
3427
- for (const month of fs5.readdirSync(yearPath)) {
3428
- const monthPath = path6.join(yearPath, month);
4334
+ for (const month of fs6.readdirSync(yearPath)) {
4335
+ const monthPath = path7.join(yearPath, month);
3429
4336
  try {
3430
- if (!fs5.statSync(monthPath).isDirectory()) continue;
4337
+ if (!fs6.statSync(monthPath).isDirectory()) continue;
3431
4338
  } catch {
3432
4339
  continue;
3433
4340
  }
3434
- for (const day of fs5.readdirSync(monthPath)) {
3435
- const dayPath = path6.join(monthPath, day);
4341
+ for (const day of fs6.readdirSync(monthPath)) {
4342
+ const dayPath = path7.join(monthPath, day);
3436
4343
  try {
3437
- if (!fs5.statSync(dayPath).isDirectory()) continue;
4344
+ if (!fs6.statSync(dayPath).isDirectory()) continue;
3438
4345
  } catch {
3439
4346
  continue;
3440
4347
  }
3441
- for (const file of fs5.readdirSync(dayPath)) {
3442
- if (file.endsWith(".jsonl")) jsonlFiles.push(path6.join(dayPath, file));
4348
+ for (const file of fs6.readdirSync(dayPath)) {
4349
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path7.join(dayPath, file));
3443
4350
  }
3444
4351
  }
3445
4352
  }
@@ -3520,13 +4427,13 @@ function freezeGeminiCost(acc) {
3520
4427
  function processGeminiCostFile(filePath, projectKey, start, end, acc) {
3521
4428
  const startMs = start.getTime();
3522
4429
  try {
3523
- if (fs5.statSync(filePath).mtimeMs < startMs) return;
4430
+ if (fs6.statSync(filePath).mtimeMs < startMs) return;
3524
4431
  } catch {
3525
4432
  return;
3526
4433
  }
3527
4434
  let raw;
3528
4435
  try {
3529
- raw = fs5.readFileSync(filePath, "utf-8");
4436
+ raw = fs6.readFileSync(filePath, "utf-8");
3530
4437
  } catch {
3531
4438
  return;
3532
4439
  }
@@ -3575,30 +4482,30 @@ function listGeminiSessionFiles(geminiTmpDir) {
3575
4482
  const out = [];
3576
4483
  let dirs;
3577
4484
  try {
3578
- if (!fs5.statSync(geminiTmpDir).isDirectory()) return out;
3579
- dirs = fs5.readdirSync(geminiTmpDir);
4485
+ if (!fs6.statSync(geminiTmpDir).isDirectory()) return out;
4486
+ dirs = fs6.readdirSync(geminiTmpDir);
3580
4487
  } catch {
3581
4488
  return out;
3582
4489
  }
3583
4490
  for (const proj of dirs) {
3584
- const chatsDir = path6.join(geminiTmpDir, proj, "chats");
4491
+ const chatsDir = path7.join(geminiTmpDir, proj, "chats");
3585
4492
  let files;
3586
4493
  try {
3587
- if (!fs5.statSync(chatsDir).isDirectory()) continue;
3588
- files = fs5.readdirSync(chatsDir);
4494
+ if (!fs6.statSync(chatsDir).isDirectory()) continue;
4495
+ files = fs6.readdirSync(chatsDir);
3589
4496
  } catch {
3590
4497
  continue;
3591
4498
  }
3592
4499
  for (const f of files) {
3593
4500
  if (!f.endsWith(".jsonl")) continue;
3594
- out.push({ projectKey: proj, file: path6.join(chatsDir, f) });
4501
+ out.push({ projectKey: proj, file: path7.join(chatsDir, f) });
3595
4502
  }
3596
4503
  }
3597
4504
  return out;
3598
4505
  }
3599
4506
  function loadGeminiCost(start, end, geminiTmpDir) {
3600
4507
  const acc = emptyGeminiAccumulator();
3601
- if (!fs5.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
4508
+ if (!fs6.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3602
4509
  for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
3603
4510
  processGeminiCostFile(file, projectKey, start, end, acc);
3604
4511
  }
@@ -3606,7 +4513,7 @@ function loadGeminiCost(start, end, geminiTmpDir) {
3606
4513
  }
3607
4514
  async function loadGeminiCostAsync(start, end, geminiTmpDir) {
3608
4515
  const acc = emptyGeminiAccumulator();
3609
- if (!fs5.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
4516
+ if (!fs6.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3610
4517
  const files = listGeminiSessionFiles(geminiTmpDir);
3611
4518
  const CHUNK_SIZE = 5;
3612
4519
  for (let i = 0; i < files.length; i++) {
@@ -3629,11 +4536,11 @@ function dimensionOfBlock(checkedBy, ruleName) {
3629
4536
  }
3630
4537
  function aggregateReportFromAudit(period, opts = {}) {
3631
4538
  const now = opts.now ?? /* @__PURE__ */ new Date();
3632
- const auditLogPath2 = opts.auditLogPath ?? path6.join(os6.homedir(), ".node9", "audit.log");
3633
- const claudeProjectsDir = opts.claudeProjectsDir ?? path6.join(os6.homedir(), ".claude", "projects");
3634
- const codexSessionsDir = opts.codexSessionsDir ?? path6.join(os6.homedir(), ".codex", "sessions");
3635
- const geminiTmpDir = opts.geminiTmpDir ?? path6.join(os6.homedir(), ".gemini", "tmp");
3636
- const hasAuditFile = fs5.existsSync(auditLogPath2);
4539
+ const auditLogPath2 = opts.auditLogPath ?? path7.join(os7.homedir(), ".node9", "audit.log");
4540
+ const claudeProjectsDir = opts.claudeProjectsDir ?? path7.join(os7.homedir(), ".claude", "projects");
4541
+ const codexSessionsDir = opts.codexSessionsDir ?? path7.join(os7.homedir(), ".codex", "sessions");
4542
+ const geminiTmpDir = opts.geminiTmpDir ?? path7.join(os7.homedir(), ".gemini", "tmp");
4543
+ const hasAuditFile = fs6.existsSync(auditLogPath2);
3637
4544
  const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath2);
3638
4545
  const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
3639
4546
  const { start, end } = getDateRange(period, now);
@@ -3870,18 +4777,18 @@ var init_report_audit = __esm({
3870
4777
  });
3871
4778
 
3872
4779
  // src/utils/provenance.ts
3873
- import path7 from "path";
3874
- import os7 from "os";
4780
+ import path8 from "path";
4781
+ import os8 from "os";
3875
4782
  var USER_PREFIXES;
3876
4783
  var init_provenance = __esm({
3877
4784
  "src/utils/provenance.ts"() {
3878
4785
  "use strict";
3879
4786
  USER_PREFIXES = [
3880
- path7.join(os7.homedir(), "bin"),
3881
- path7.join(os7.homedir(), ".local", "bin"),
3882
- path7.join(os7.homedir(), ".cargo", "bin"),
3883
- path7.join(os7.homedir(), ".npm-global", "bin"),
3884
- path7.join(os7.homedir(), ".volta", "bin")
4787
+ path8.join(os8.homedir(), "bin"),
4788
+ path8.join(os8.homedir(), ".local", "bin"),
4789
+ path8.join(os8.homedir(), ".cargo", "bin"),
4790
+ path8.join(os8.homedir(), ".npm-global", "bin"),
4791
+ path8.join(os8.homedir(), ".volta", "bin")
3885
4792
  ];
3886
4793
  }
3887
4794
  });
@@ -3925,14 +4832,14 @@ var init_mcp_pin = __esm({
3925
4832
  });
3926
4833
 
3927
4834
  // src/daemon/hook-baseline.ts
3928
- import path8 from "path";
3929
- import os8 from "os";
4835
+ import path9 from "path";
4836
+ import os9 from "os";
3930
4837
  var BASELINE_FILE, NOTIFIED_FILE;
3931
4838
  var init_hook_baseline = __esm({
3932
4839
  "src/daemon/hook-baseline.ts"() {
3933
4840
  "use strict";
3934
- BASELINE_FILE = path8.join(os8.homedir(), ".node9", "hooks-baseline.json");
3935
- NOTIFIED_FILE = path8.join(os8.homedir(), ".node9", "hook-heal-notified.json");
4841
+ BASELINE_FILE = path9.join(os9.homedir(), ".node9", "hooks-baseline.json");
4842
+ NOTIFIED_FILE = path9.join(os9.homedir(), ".node9", "hook-heal-notified.json");
3936
4843
  }
3937
4844
  });
3938
4845
 
@@ -3991,9 +4898,9 @@ var init_scan_history = __esm({
3991
4898
 
3992
4899
  // src/cli/commands/scan.ts
3993
4900
  import chalk4 from "chalk";
3994
- import fs6 from "fs";
3995
- import path9 from "path";
3996
- import os9 from "os";
4901
+ import fs7 from "fs";
4902
+ import path10 from "path";
4903
+ import os10 from "os";
3997
4904
  import stringWidth2 from "string-width";
3998
4905
  function claudeModelPrice2(model) {
3999
4906
  const t = pricingFor(model);
@@ -4142,7 +5049,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4142
5049
  const sessionId = file.replace(/\.jsonl$/, "");
4143
5050
  let raw;
4144
5051
  try {
4145
- raw = fs6.readFileSync(path9.join(projPath, file), "utf-8");
5052
+ raw = fs7.readFileSync(path10.join(projPath, file), "utf-8");
4146
5053
  } catch {
4147
5054
  return;
4148
5055
  }
@@ -4194,7 +5101,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4194
5101
  if (block.type !== "tool_result") continue;
4195
5102
  const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
4196
5103
  if (filePath) {
4197
- const ext = path9.extname(filePath).toLowerCase();
5104
+ const ext = path10.extname(filePath).toLowerCase();
4198
5105
  if (CODE_EXTENSIONS.has(ext)) continue;
4199
5106
  }
4200
5107
  const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
@@ -4251,7 +5158,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4251
5158
  const rawCmd = String(input.command ?? "").trimStart();
4252
5159
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
4253
5160
  const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
4254
- const inputFileExt = inputFilePath ? path9.extname(inputFilePath).toLowerCase() : "";
5161
+ const inputFileExt = inputFilePath ? path10.extname(inputFilePath).toLowerCase() : "";
4255
5162
  if (CODE_EXTENSIONS.has(inputFileExt)) continue;
4256
5163
  const dlpMatch = scanArgs(input);
4257
5164
  if (dlpMatch) {
@@ -4348,19 +5255,19 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4348
5255
  }
4349
5256
  }
4350
5257
  async function processClaudeProjectAsync(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
4351
- const projPath = path9.join(projectsDir, proj);
5258
+ const projPath = path10.join(projectsDir, proj);
4352
5259
  try {
4353
- if (!fs6.statSync(projPath).isDirectory()) return;
5260
+ if (!fs7.statSync(projPath).isDirectory()) return;
4354
5261
  } catch {
4355
5262
  return;
4356
5263
  }
4357
- const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os9.homedir(), "~")).slice(
5264
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os10.homedir(), "~")).slice(
4358
5265
  0,
4359
5266
  40
4360
5267
  );
4361
5268
  let files;
4362
5269
  try {
4363
- files = fs6.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
5270
+ files = fs7.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
4364
5271
  } catch {
4365
5272
  return;
4366
5273
  }
@@ -4398,12 +5305,12 @@ function emptyClaudeScan() {
4398
5305
  };
4399
5306
  }
4400
5307
  async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
4401
- const projectsDir = path9.join(os9.homedir(), ".claude", "projects");
5308
+ const projectsDir = path10.join(os10.homedir(), ".claude", "projects");
4402
5309
  const result = emptyClaudeScan();
4403
- if (!fs6.existsSync(projectsDir)) return result;
5310
+ if (!fs7.existsSync(projectsDir)) return result;
4404
5311
  let projDirs;
4405
5312
  try {
4406
- projDirs = fs6.readdirSync(projectsDir);
5313
+ projDirs = fs7.readdirSync(projectsDir);
4407
5314
  } catch {
4408
5315
  return result;
4409
5316
  }
@@ -4424,7 +5331,7 @@ async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
4424
5331
  return result;
4425
5332
  }
4426
5333
  function scanGeminiHistory(startDate, onProgress, onLine) {
4427
- const tmpDir = path9.join(os9.homedir(), ".gemini", "tmp");
5334
+ const tmpDir = path10.join(os10.homedir(), ".gemini", "tmp");
4428
5335
  const result = {
4429
5336
  filesScanned: 0,
4430
5337
  sessions: 0,
@@ -4439,33 +5346,33 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4439
5346
  sessionsWithEarlySecrets: 0
4440
5347
  };
4441
5348
  const dedup = emptyScanDedup();
4442
- if (!fs6.existsSync(tmpDir)) return result;
5349
+ if (!fs7.existsSync(tmpDir)) return result;
4443
5350
  let slugDirs;
4444
5351
  try {
4445
- slugDirs = fs6.readdirSync(tmpDir);
5352
+ slugDirs = fs7.readdirSync(tmpDir);
4446
5353
  } catch {
4447
5354
  return result;
4448
5355
  }
4449
5356
  const ruleSources = buildRuleSources();
4450
- for (const slug of slugDirs) {
4451
- const slugPath = path9.join(tmpDir, slug);
5357
+ for (const slug2 of slugDirs) {
5358
+ const slugPath = path10.join(tmpDir, slug2);
4452
5359
  try {
4453
- if (!fs6.statSync(slugPath).isDirectory()) continue;
5360
+ if (!fs7.statSync(slugPath).isDirectory()) continue;
4454
5361
  } catch {
4455
5362
  continue;
4456
5363
  }
4457
- let projLabel = stripTerminalEscapes(slug).slice(0, 40);
5364
+ let projLabel = stripTerminalEscapes(slug2).slice(0, 40);
4458
5365
  try {
4459
5366
  projLabel = stripTerminalEscapes(
4460
- fs6.readFileSync(path9.join(slugPath, ".project_root"), "utf-8").trim()
4461
- ).replace(os9.homedir(), "~").slice(0, 40);
5367
+ fs7.readFileSync(path10.join(slugPath, ".project_root"), "utf-8").trim()
5368
+ ).replace(os10.homedir(), "~").slice(0, 40);
4462
5369
  } catch {
4463
5370
  }
4464
- const chatsDir = path9.join(slugPath, "chats");
4465
- if (!fs6.existsSync(chatsDir)) continue;
5371
+ const chatsDir = path10.join(slugPath, "chats");
5372
+ if (!fs7.existsSync(chatsDir)) continue;
4466
5373
  let chatFiles;
4467
5374
  try {
4468
- chatFiles = fs6.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
5375
+ chatFiles = fs7.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
4469
5376
  } catch {
4470
5377
  continue;
4471
5378
  }
@@ -4478,7 +5385,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4478
5385
  onProgress?.(result.filesScanned);
4479
5386
  let raw;
4480
5387
  try {
4481
- raw = fs6.readFileSync(path9.join(chatsDir, chatFile), "utf-8");
5388
+ raw = fs7.readFileSync(path10.join(chatsDir, chatFile), "utf-8");
4482
5389
  } catch {
4483
5390
  continue;
4484
5391
  }
@@ -4651,7 +5558,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4651
5558
  return result;
4652
5559
  }
4653
5560
  function scanCodexHistory(startDate, onProgress, onLine) {
4654
- const sessionsBase = path9.join(os9.homedir(), ".codex", "sessions");
5561
+ const sessionsBase = path10.join(os10.homedir(), ".codex", "sessions");
4655
5562
  const result = {
4656
5563
  filesScanned: 0,
4657
5564
  sessions: 0,
@@ -4666,32 +5573,32 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4666
5573
  sessionsWithEarlySecrets: 0
4667
5574
  };
4668
5575
  const dedup = emptyScanDedup();
4669
- if (!fs6.existsSync(sessionsBase)) return result;
5576
+ if (!fs7.existsSync(sessionsBase)) return result;
4670
5577
  const jsonlFiles = [];
4671
5578
  try {
4672
- for (const year of fs6.readdirSync(sessionsBase)) {
4673
- const yearPath = path9.join(sessionsBase, year);
5579
+ for (const year of fs7.readdirSync(sessionsBase)) {
5580
+ const yearPath = path10.join(sessionsBase, year);
4674
5581
  try {
4675
- if (!fs6.statSync(yearPath).isDirectory()) continue;
5582
+ if (!fs7.statSync(yearPath).isDirectory()) continue;
4676
5583
  } catch {
4677
5584
  continue;
4678
5585
  }
4679
- for (const month of fs6.readdirSync(yearPath)) {
4680
- const monthPath = path9.join(yearPath, month);
5586
+ for (const month of fs7.readdirSync(yearPath)) {
5587
+ const monthPath = path10.join(yearPath, month);
4681
5588
  try {
4682
- if (!fs6.statSync(monthPath).isDirectory()) continue;
5589
+ if (!fs7.statSync(monthPath).isDirectory()) continue;
4683
5590
  } catch {
4684
5591
  continue;
4685
5592
  }
4686
- for (const day of fs6.readdirSync(monthPath)) {
4687
- const dayPath = path9.join(monthPath, day);
5593
+ for (const day of fs7.readdirSync(monthPath)) {
5594
+ const dayPath = path10.join(monthPath, day);
4688
5595
  try {
4689
- if (!fs6.statSync(dayPath).isDirectory()) continue;
5596
+ if (!fs7.statSync(dayPath).isDirectory()) continue;
4690
5597
  } catch {
4691
5598
  continue;
4692
5599
  }
4693
- for (const file of fs6.readdirSync(dayPath)) {
4694
- if (file.endsWith(".jsonl")) jsonlFiles.push(path9.join(dayPath, file));
5600
+ for (const file of fs7.readdirSync(dayPath)) {
5601
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path10.join(dayPath, file));
4695
5602
  }
4696
5603
  }
4697
5604
  }
@@ -4705,7 +5612,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4705
5612
  onProgress?.(result.filesScanned);
4706
5613
  let lines;
4707
5614
  try {
4708
- lines = fs6.readFileSync(filePath, "utf-8").split("\n");
5615
+ lines = fs7.readFileSync(filePath, "utf-8").split("\n");
4709
5616
  } catch {
4710
5617
  continue;
4711
5618
  }
@@ -4732,7 +5639,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4732
5639
  sessionId = String(payload["id"] ?? filePath);
4733
5640
  startTime = String(payload["timestamp"] ?? "");
4734
5641
  const cwd = String(payload["cwd"] ?? "");
4735
- projLabel = stripTerminalEscapes(cwd.replace(os9.homedir(), "~")).slice(0, 40);
5642
+ projLabel = stripTerminalEscapes(cwd.replace(os10.homedir(), "~")).slice(0, 40);
4736
5643
  continue;
4737
5644
  }
4738
5645
  if (entry.type === "turn_context" && typeof payload["model"] === "string") {
@@ -4974,23 +5881,23 @@ var init_scan = __esm({
4974
5881
  });
4975
5882
 
4976
5883
  // src/tui/dashboard/data.ts
4977
- import fs7 from "fs";
4978
- import os10 from "os";
4979
- import path10 from "path";
5884
+ import fs8 from "fs";
5885
+ import os11 from "os";
5886
+ import path11 from "path";
4980
5887
  import http from "http";
4981
5888
  function auditLogPath() {
4982
- return path10.join(os10.homedir(), ".node9", "audit.log");
5889
+ return path11.join(os11.homedir(), ".node9", "audit.log");
4983
5890
  }
4984
5891
  function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
4985
5892
  return new Promise((resolve) => {
4986
5893
  const p = customPath ?? auditLogPath();
4987
- if (!fs7.existsSync(p)) {
5894
+ if (!fs8.existsSync(p)) {
4988
5895
  resolve([]);
4989
5896
  return;
4990
5897
  }
4991
5898
  let raw;
4992
5899
  try {
4993
- raw = fs7.readFileSync(p, "utf8");
5900
+ raw = fs8.readFileSync(p, "utf8");
4994
5901
  } catch {
4995
5902
  resolve([]);
4996
5903
  return;
@@ -5094,7 +6001,10 @@ function compactPathsInCommand(cmd) {
5094
6001
  function loadShieldStatus() {
5095
6002
  try {
5096
6003
  const all = Object.keys(SHIELDS).sort();
5097
- const activeSet = new Set(readActiveShields());
6004
+ const cfg = getConfig();
6005
+ const activeSet = new Set(
6006
+ cfg.policySource === "workspace" ? cfg.policy.appliedShields ?? [] : readActiveShields()
6007
+ );
5098
6008
  const active = all.filter((n) => activeSet.has(n));
5099
6009
  const inactive = all.filter((n) => !activeSet.has(n));
5100
6010
  return { active, inactive };
@@ -5119,13 +6029,13 @@ function loadBlast() {
5119
6029
  }
5120
6030
  }
5121
6031
  function shortenPath(p) {
5122
- const home = os10.homedir();
6032
+ const home = os11.homedir();
5123
6033
  return p.startsWith(home) ? p.replace(home, "~") : p;
5124
6034
  }
5125
6035
  async function loadReportAuditAsync(period) {
5126
- const claudeProjectsDir = path10.join(os10.homedir(), ".claude", "projects");
5127
- const codexSessionsDir = path10.join(os10.homedir(), ".codex", "sessions");
5128
- const geminiTmpDir = path10.join(os10.homedir(), ".gemini", "tmp");
6036
+ const claudeProjectsDir = path11.join(os11.homedir(), ".claude", "projects");
6037
+ const codexSessionsDir = path11.join(os11.homedir(), ".codex", "sessions");
6038
+ const geminiTmpDir = path11.join(os11.homedir(), ".gemini", "tmp");
5129
6039
  const { start, end } = getDateRange(period, /* @__PURE__ */ new Date());
5130
6040
  const entries = await readAuditEntriesAsync();
5131
6041
  void ensurePricingLoaded();
@@ -5456,6 +6366,7 @@ var init_data = __esm({
5456
6366
  init_daemon();
5457
6367
  init_costSync();
5458
6368
  init_shields();
6369
+ init_config();
5459
6370
  init_decision();
5460
6371
  init_scan_watermark();
5461
6372
  init_report_audit();
@@ -6205,8 +7116,8 @@ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-run
6205
7116
  function TopToolsProjects({ audit }) {
6206
7117
  const data = audit?.data;
6207
7118
  const tools = data ? [...data.toolMap.entries()].sort(([, a], [, b]) => b.calls - a.calls).slice(0, ROW_LIMIT) : [];
6208
- const projects = data ? [...data.cost.byProject.entries()].map(([path11, r]) => ({
6209
- name: basenameOf(path11),
7119
+ const projects = data ? [...data.cost.byProject.entries()].map(([path12, r]) => ({
7120
+ name: basenameOf(path12),
6210
7121
  cost: r.cost,
6211
7122
  tokens: r.inputTokens + r.outputTokens
6212
7123
  })).sort((a, b) => b.cost - a.cost).slice(0, ROW_LIMIT) : [];
@@ -6643,8 +7554,8 @@ function pickTopLoopFile(loops) {
6643
7554
  map.set(k, (map.get(k) ?? 0) + (l.count ?? 0));
6644
7555
  }
6645
7556
  if (map.size === 0) return void 0;
6646
- const [path11, count] = [...map.entries()].sort((a, b) => b[1] - a[1])[0];
6647
- return { path: path11, count };
7557
+ const [path12, count] = [...map.entries()].sort((a, b) => b[1] - a[1])[0];
7558
+ return { path: path12, count };
6648
7559
  }
6649
7560
  var EMPTY_FILTERED_SCAN;
6650
7561
  var init_derive = __esm({