@node9/proxy 2.5.0 → 2.6.1

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,6 +3431,7 @@ 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,
@@ -2780,7 +3619,68 @@ var init_config = __esm({
2780
3619
  },
2781
3620
  environments: {}
2782
3621
  };
3622
+ RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
3623
+ VERDICT_ORDER = ["allow", "review", "block"];
3624
+ ADVISORY_SMART_RULES = [
3625
+ // ── rm safety ─────────────────────────────────────────────────────────────
3626
+ // tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
3627
+ // Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
3628
+ // chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
3629
+ {
3630
+ name: "allow-rm-safe-paths",
3631
+ tool: "*",
3632
+ conditionMode: "all",
3633
+ conditions: [
3634
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
3635
+ { field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
3636
+ ],
3637
+ verdict: "allow",
3638
+ reason: "Deleting a known-safe build artifact path"
3639
+ },
3640
+ {
3641
+ name: "review-rm",
3642
+ tool: "*",
3643
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
3644
+ verdict: "review",
3645
+ reason: "rm can permanently delete files \u2014 confirm the target path",
3646
+ description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
3647
+ },
3648
+ // ── SQL safety (Safe by Default) ──────────────────────────────────────────
3649
+ // These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
3650
+ // mcp__postgres__query) with a destructive SQL statement in the 'sql' field.
3651
+ // The postgres shield upgrades these from 'review' → 'block' for stricter teams;
3652
+ // without a shield, users still get a human-approval gate on every destructive op.
3653
+ {
3654
+ name: "review-drop-table-sql",
3655
+ tool: "*",
3656
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
3657
+ verdict: "review",
3658
+ reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
3659
+ description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
3660
+ },
3661
+ {
3662
+ name: "review-truncate-sql",
3663
+ tool: "*",
3664
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
3665
+ verdict: "review",
3666
+ reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
3667
+ description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
3668
+ },
3669
+ {
3670
+ name: "review-drop-column-sql",
3671
+ tool: "*",
3672
+ conditions: [
3673
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
3674
+ ],
3675
+ verdict: "review",
3676
+ reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
3677
+ description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
3678
+ }
3679
+ ];
3680
+ cachedConfig = null;
3681
+ lastParsedRulesCache = null;
2783
3682
  CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
3683
+ cacheReadLastLoggedAt = 0;
2784
3684
  }
2785
3685
  });
2786
3686
 
@@ -2792,28 +3692,28 @@ var init_hasher = __esm({
2792
3692
  });
2793
3693
 
2794
3694
  // src/audit/index.ts
2795
- import path4 from "path";
2796
- import os4 from "os";
3695
+ import path5 from "path";
3696
+ import os5 from "os";
2797
3697
  var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
2798
3698
  var init_audit = __esm({
2799
3699
  "src/audit/index.ts"() {
2800
3700
  "use strict";
2801
3701
  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");
3702
+ LOCAL_AUDIT_LOG = path5.join(os5.homedir(), ".node9", "audit.log");
3703
+ HOOK_DEBUG_LOG = path5.join(os5.homedir(), ".node9", "hook-debug.log");
2804
3704
  }
2805
3705
  });
2806
3706
 
2807
3707
  // src/pricing/litellm.ts
2808
- import fs4 from "fs";
2809
- import path5 from "path";
2810
- import os5 from "os";
3708
+ import fs5 from "fs";
3709
+ import path6 from "path";
3710
+ import os6 from "os";
2811
3711
  function normalizeModel(raw) {
2812
3712
  return raw.replace(/-\d{8}$/, "").toLowerCase();
2813
3713
  }
2814
3714
  function readCache() {
2815
3715
  try {
2816
- const raw = JSON.parse(fs4.readFileSync(CACHE_FILE(), "utf-8"));
3716
+ const raw = JSON.parse(fs5.readFileSync(CACHE_FILE(), "utf-8"));
2817
3717
  if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
2818
3718
  return null;
2819
3719
  }
@@ -2827,18 +3727,18 @@ function readCache() {
2827
3727
  function writeCache(prices) {
2828
3728
  try {
2829
3729
  const target = CACHE_FILE();
2830
- const dir = path5.dirname(target);
2831
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3730
+ const dir = path6.dirname(target);
3731
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
2832
3732
  const tmp = target + ".tmp";
2833
3733
  const body = {
2834
3734
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2835
3735
  prices
2836
3736
  };
2837
- fs4.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
2838
- fs4.renameSync(tmp, target);
3737
+ fs5.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
3738
+ fs5.renameSync(tmp, target);
2839
3739
  } catch (err) {
2840
3740
  try {
2841
- fs4.appendFileSync(
3741
+ fs5.appendFileSync(
2842
3742
  HOOK_DEBUG_LOG,
2843
3743
  `[pricing] cache write failed: ${err.message}
2844
3744
  `
@@ -2982,7 +3882,7 @@ var init_litellm = __esm({
2982
3882
  "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
2983
3883
  "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
2984
3884
  };
2985
- CACHE_FILE = () => path5.join(os5.homedir(), ".node9", "model-pricing.json");
3885
+ CACHE_FILE = () => path6.join(os6.homedir(), ".node9", "model-pricing.json");
2986
3886
  TTL_MS = 24 * 60 * 60 * 1e3;
2987
3887
  memCache = null;
2988
3888
  memCacheAt = 0;
@@ -3124,9 +4024,9 @@ var init_scan_watermark = __esm({
3124
4024
  });
3125
4025
 
3126
4026
  // src/cli/aggregate/report-audit.ts
3127
- import fs5 from "fs";
3128
- import os6 from "os";
3129
- import path6 from "path";
4027
+ import fs6 from "fs";
4028
+ import os7 from "os";
4029
+ import path7 from "path";
3130
4030
  function buildTestTimestamps(allEntries) {
3131
4031
  const testTs = /* @__PURE__ */ new Set();
3132
4032
  for (const e of allEntries) {
@@ -3207,8 +4107,8 @@ function getDateRange(period, now) {
3207
4107
  }
3208
4108
  }
3209
4109
  function parseAuditLog(logPath) {
3210
- if (!fs5.existsSync(logPath)) return [];
3211
- const raw = fs5.readFileSync(logPath, "utf-8");
4110
+ if (!fs6.existsSync(logPath)) return [];
4111
+ const raw = fs6.readFileSync(logPath, "utf-8");
3212
4112
  return raw.split("\n").flatMap((line) => {
3213
4113
  if (!line.trim()) return [];
3214
4114
  try {
@@ -3262,25 +4162,25 @@ function freezeClaudeCost(acc) {
3262
4162
  };
3263
4163
  }
3264
4164
  function processClaudeCostProject(proj, projectsDir, start, end, acc) {
3265
- const projPath = path6.join(projectsDir, proj);
4165
+ const projPath = path7.join(projectsDir, proj);
3266
4166
  let files;
3267
4167
  try {
3268
- const stat = fs5.statSync(projPath);
4168
+ const stat = fs6.statSync(projPath);
3269
4169
  if (!stat.isDirectory()) return;
3270
- files = fs5.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
4170
+ files = fs6.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
3271
4171
  } catch {
3272
4172
  return;
3273
4173
  }
3274
4174
  const startMs = start.getTime();
3275
4175
  for (const file of files) {
3276
- const filePath = path6.join(projPath, file);
4176
+ const filePath = path7.join(projPath, file);
3277
4177
  try {
3278
- if (fs5.statSync(filePath).mtimeMs < startMs) continue;
4178
+ if (fs6.statSync(filePath).mtimeMs < startMs) continue;
3279
4179
  } catch {
3280
4180
  continue;
3281
4181
  }
3282
4182
  try {
3283
- const raw = fs5.readFileSync(filePath, "utf-8");
4183
+ const raw = fs6.readFileSync(filePath, "utf-8");
3284
4184
  for (const line of raw.split("\n")) {
3285
4185
  if (!line.trim()) continue;
3286
4186
  let entry;
@@ -3330,10 +4230,10 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
3330
4230
  }
3331
4231
  function loadClaudeCost(start, end, projectsDir) {
3332
4232
  const acc = emptyClaudeCostAccumulator();
3333
- if (!fs5.existsSync(projectsDir)) return freezeClaudeCost(acc);
4233
+ if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
3334
4234
  let dirs;
3335
4235
  try {
3336
- dirs = fs5.readdirSync(projectsDir);
4236
+ dirs = fs6.readdirSync(projectsDir);
3337
4237
  } catch {
3338
4238
  return freezeClaudeCost(acc);
3339
4239
  }
@@ -3344,10 +4244,10 @@ function loadClaudeCost(start, end, projectsDir) {
3344
4244
  }
3345
4245
  async function loadClaudeCostAsync(start, end, projectsDir) {
3346
4246
  const acc = emptyClaudeCostAccumulator();
3347
- if (!fs5.existsSync(projectsDir)) return freezeClaudeCost(acc);
4247
+ if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
3348
4248
  let dirs;
3349
4249
  try {
3350
- dirs = fs5.readdirSync(projectsDir);
4250
+ dirs = fs6.readdirSync(projectsDir);
3351
4251
  } catch {
3352
4252
  return freezeClaudeCost(acc);
3353
4253
  }
@@ -3360,7 +4260,7 @@ async function loadClaudeCostAsync(start, end, projectsDir) {
3360
4260
  function processCodexCostFile(filePath, start, end, acc) {
3361
4261
  let lines;
3362
4262
  try {
3363
- lines = fs5.readFileSync(filePath, "utf-8").split("\n");
4263
+ lines = fs6.readFileSync(filePath, "utf-8").split("\n");
3364
4264
  } catch {
3365
4265
  return;
3366
4266
  }
@@ -3415,31 +4315,31 @@ function processCodexCostFile(filePath, start, end, acc) {
3415
4315
  }
3416
4316
  function listCodexSessionFiles(sessionsBase) {
3417
4317
  const jsonlFiles = [];
3418
- if (!fs5.existsSync(sessionsBase)) return jsonlFiles;
4318
+ if (!fs6.existsSync(sessionsBase)) return jsonlFiles;
3419
4319
  try {
3420
- for (const year of fs5.readdirSync(sessionsBase)) {
3421
- const yearPath = path6.join(sessionsBase, year);
4320
+ for (const year of fs6.readdirSync(sessionsBase)) {
4321
+ const yearPath = path7.join(sessionsBase, year);
3422
4322
  try {
3423
- if (!fs5.statSync(yearPath).isDirectory()) continue;
4323
+ if (!fs6.statSync(yearPath).isDirectory()) continue;
3424
4324
  } catch {
3425
4325
  continue;
3426
4326
  }
3427
- for (const month of fs5.readdirSync(yearPath)) {
3428
- const monthPath = path6.join(yearPath, month);
4327
+ for (const month of fs6.readdirSync(yearPath)) {
4328
+ const monthPath = path7.join(yearPath, month);
3429
4329
  try {
3430
- if (!fs5.statSync(monthPath).isDirectory()) continue;
4330
+ if (!fs6.statSync(monthPath).isDirectory()) continue;
3431
4331
  } catch {
3432
4332
  continue;
3433
4333
  }
3434
- for (const day of fs5.readdirSync(monthPath)) {
3435
- const dayPath = path6.join(monthPath, day);
4334
+ for (const day of fs6.readdirSync(monthPath)) {
4335
+ const dayPath = path7.join(monthPath, day);
3436
4336
  try {
3437
- if (!fs5.statSync(dayPath).isDirectory()) continue;
4337
+ if (!fs6.statSync(dayPath).isDirectory()) continue;
3438
4338
  } catch {
3439
4339
  continue;
3440
4340
  }
3441
- for (const file of fs5.readdirSync(dayPath)) {
3442
- if (file.endsWith(".jsonl")) jsonlFiles.push(path6.join(dayPath, file));
4341
+ for (const file of fs6.readdirSync(dayPath)) {
4342
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path7.join(dayPath, file));
3443
4343
  }
3444
4344
  }
3445
4345
  }
@@ -3520,13 +4420,13 @@ function freezeGeminiCost(acc) {
3520
4420
  function processGeminiCostFile(filePath, projectKey, start, end, acc) {
3521
4421
  const startMs = start.getTime();
3522
4422
  try {
3523
- if (fs5.statSync(filePath).mtimeMs < startMs) return;
4423
+ if (fs6.statSync(filePath).mtimeMs < startMs) return;
3524
4424
  } catch {
3525
4425
  return;
3526
4426
  }
3527
4427
  let raw;
3528
4428
  try {
3529
- raw = fs5.readFileSync(filePath, "utf-8");
4429
+ raw = fs6.readFileSync(filePath, "utf-8");
3530
4430
  } catch {
3531
4431
  return;
3532
4432
  }
@@ -3575,30 +4475,30 @@ function listGeminiSessionFiles(geminiTmpDir) {
3575
4475
  const out = [];
3576
4476
  let dirs;
3577
4477
  try {
3578
- if (!fs5.statSync(geminiTmpDir).isDirectory()) return out;
3579
- dirs = fs5.readdirSync(geminiTmpDir);
4478
+ if (!fs6.statSync(geminiTmpDir).isDirectory()) return out;
4479
+ dirs = fs6.readdirSync(geminiTmpDir);
3580
4480
  } catch {
3581
4481
  return out;
3582
4482
  }
3583
4483
  for (const proj of dirs) {
3584
- const chatsDir = path6.join(geminiTmpDir, proj, "chats");
4484
+ const chatsDir = path7.join(geminiTmpDir, proj, "chats");
3585
4485
  let files;
3586
4486
  try {
3587
- if (!fs5.statSync(chatsDir).isDirectory()) continue;
3588
- files = fs5.readdirSync(chatsDir);
4487
+ if (!fs6.statSync(chatsDir).isDirectory()) continue;
4488
+ files = fs6.readdirSync(chatsDir);
3589
4489
  } catch {
3590
4490
  continue;
3591
4491
  }
3592
4492
  for (const f of files) {
3593
4493
  if (!f.endsWith(".jsonl")) continue;
3594
- out.push({ projectKey: proj, file: path6.join(chatsDir, f) });
4494
+ out.push({ projectKey: proj, file: path7.join(chatsDir, f) });
3595
4495
  }
3596
4496
  }
3597
4497
  return out;
3598
4498
  }
3599
4499
  function loadGeminiCost(start, end, geminiTmpDir) {
3600
4500
  const acc = emptyGeminiAccumulator();
3601
- if (!fs5.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
4501
+ if (!fs6.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3602
4502
  for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
3603
4503
  processGeminiCostFile(file, projectKey, start, end, acc);
3604
4504
  }
@@ -3606,7 +4506,7 @@ function loadGeminiCost(start, end, geminiTmpDir) {
3606
4506
  }
3607
4507
  async function loadGeminiCostAsync(start, end, geminiTmpDir) {
3608
4508
  const acc = emptyGeminiAccumulator();
3609
- if (!fs5.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
4509
+ if (!fs6.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3610
4510
  const files = listGeminiSessionFiles(geminiTmpDir);
3611
4511
  const CHUNK_SIZE = 5;
3612
4512
  for (let i = 0; i < files.length; i++) {
@@ -3629,11 +4529,11 @@ function dimensionOfBlock(checkedBy, ruleName) {
3629
4529
  }
3630
4530
  function aggregateReportFromAudit(period, opts = {}) {
3631
4531
  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);
4532
+ const auditLogPath2 = opts.auditLogPath ?? path7.join(os7.homedir(), ".node9", "audit.log");
4533
+ const claudeProjectsDir = opts.claudeProjectsDir ?? path7.join(os7.homedir(), ".claude", "projects");
4534
+ const codexSessionsDir = opts.codexSessionsDir ?? path7.join(os7.homedir(), ".codex", "sessions");
4535
+ const geminiTmpDir = opts.geminiTmpDir ?? path7.join(os7.homedir(), ".gemini", "tmp");
4536
+ const hasAuditFile = fs6.existsSync(auditLogPath2);
3637
4537
  const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath2);
3638
4538
  const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
3639
4539
  const { start, end } = getDateRange(period, now);
@@ -3870,18 +4770,18 @@ var init_report_audit = __esm({
3870
4770
  });
3871
4771
 
3872
4772
  // src/utils/provenance.ts
3873
- import path7 from "path";
3874
- import os7 from "os";
4773
+ import path8 from "path";
4774
+ import os8 from "os";
3875
4775
  var USER_PREFIXES;
3876
4776
  var init_provenance = __esm({
3877
4777
  "src/utils/provenance.ts"() {
3878
4778
  "use strict";
3879
4779
  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")
4780
+ path8.join(os8.homedir(), "bin"),
4781
+ path8.join(os8.homedir(), ".local", "bin"),
4782
+ path8.join(os8.homedir(), ".cargo", "bin"),
4783
+ path8.join(os8.homedir(), ".npm-global", "bin"),
4784
+ path8.join(os8.homedir(), ".volta", "bin")
3885
4785
  ];
3886
4786
  }
3887
4787
  });
@@ -3925,14 +4825,14 @@ var init_mcp_pin = __esm({
3925
4825
  });
3926
4826
 
3927
4827
  // src/daemon/hook-baseline.ts
3928
- import path8 from "path";
3929
- import os8 from "os";
4828
+ import path9 from "path";
4829
+ import os9 from "os";
3930
4830
  var BASELINE_FILE, NOTIFIED_FILE;
3931
4831
  var init_hook_baseline = __esm({
3932
4832
  "src/daemon/hook-baseline.ts"() {
3933
4833
  "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");
4834
+ BASELINE_FILE = path9.join(os9.homedir(), ".node9", "hooks-baseline.json");
4835
+ NOTIFIED_FILE = path9.join(os9.homedir(), ".node9", "hook-heal-notified.json");
3936
4836
  }
3937
4837
  });
3938
4838
 
@@ -3991,9 +4891,9 @@ var init_scan_history = __esm({
3991
4891
 
3992
4892
  // src/cli/commands/scan.ts
3993
4893
  import chalk4 from "chalk";
3994
- import fs6 from "fs";
3995
- import path9 from "path";
3996
- import os9 from "os";
4894
+ import fs7 from "fs";
4895
+ import path10 from "path";
4896
+ import os10 from "os";
3997
4897
  import stringWidth2 from "string-width";
3998
4898
  function claudeModelPrice2(model) {
3999
4899
  const t = pricingFor(model);
@@ -4142,7 +5042,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4142
5042
  const sessionId = file.replace(/\.jsonl$/, "");
4143
5043
  let raw;
4144
5044
  try {
4145
- raw = fs6.readFileSync(path9.join(projPath, file), "utf-8");
5045
+ raw = fs7.readFileSync(path10.join(projPath, file), "utf-8");
4146
5046
  } catch {
4147
5047
  return;
4148
5048
  }
@@ -4194,7 +5094,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4194
5094
  if (block.type !== "tool_result") continue;
4195
5095
  const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
4196
5096
  if (filePath) {
4197
- const ext = path9.extname(filePath).toLowerCase();
5097
+ const ext = path10.extname(filePath).toLowerCase();
4198
5098
  if (CODE_EXTENSIONS.has(ext)) continue;
4199
5099
  }
4200
5100
  const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
@@ -4251,7 +5151,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4251
5151
  const rawCmd = String(input.command ?? "").trimStart();
4252
5152
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
4253
5153
  const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
4254
- const inputFileExt = inputFilePath ? path9.extname(inputFilePath).toLowerCase() : "";
5154
+ const inputFileExt = inputFilePath ? path10.extname(inputFilePath).toLowerCase() : "";
4255
5155
  if (CODE_EXTENSIONS.has(inputFileExt)) continue;
4256
5156
  const dlpMatch = scanArgs(input);
4257
5157
  if (dlpMatch) {
@@ -4348,19 +5248,19 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4348
5248
  }
4349
5249
  }
4350
5250
  async function processClaudeProjectAsync(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
4351
- const projPath = path9.join(projectsDir, proj);
5251
+ const projPath = path10.join(projectsDir, proj);
4352
5252
  try {
4353
- if (!fs6.statSync(projPath).isDirectory()) return;
5253
+ if (!fs7.statSync(projPath).isDirectory()) return;
4354
5254
  } catch {
4355
5255
  return;
4356
5256
  }
4357
- const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os9.homedir(), "~")).slice(
5257
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os10.homedir(), "~")).slice(
4358
5258
  0,
4359
5259
  40
4360
5260
  );
4361
5261
  let files;
4362
5262
  try {
4363
- files = fs6.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
5263
+ files = fs7.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
4364
5264
  } catch {
4365
5265
  return;
4366
5266
  }
@@ -4398,12 +5298,12 @@ function emptyClaudeScan() {
4398
5298
  };
4399
5299
  }
4400
5300
  async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
4401
- const projectsDir = path9.join(os9.homedir(), ".claude", "projects");
5301
+ const projectsDir = path10.join(os10.homedir(), ".claude", "projects");
4402
5302
  const result = emptyClaudeScan();
4403
- if (!fs6.existsSync(projectsDir)) return result;
5303
+ if (!fs7.existsSync(projectsDir)) return result;
4404
5304
  let projDirs;
4405
5305
  try {
4406
- projDirs = fs6.readdirSync(projectsDir);
5306
+ projDirs = fs7.readdirSync(projectsDir);
4407
5307
  } catch {
4408
5308
  return result;
4409
5309
  }
@@ -4424,7 +5324,7 @@ async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
4424
5324
  return result;
4425
5325
  }
4426
5326
  function scanGeminiHistory(startDate, onProgress, onLine) {
4427
- const tmpDir = path9.join(os9.homedir(), ".gemini", "tmp");
5327
+ const tmpDir = path10.join(os10.homedir(), ".gemini", "tmp");
4428
5328
  const result = {
4429
5329
  filesScanned: 0,
4430
5330
  sessions: 0,
@@ -4439,33 +5339,33 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4439
5339
  sessionsWithEarlySecrets: 0
4440
5340
  };
4441
5341
  const dedup = emptyScanDedup();
4442
- if (!fs6.existsSync(tmpDir)) return result;
5342
+ if (!fs7.existsSync(tmpDir)) return result;
4443
5343
  let slugDirs;
4444
5344
  try {
4445
- slugDirs = fs6.readdirSync(tmpDir);
5345
+ slugDirs = fs7.readdirSync(tmpDir);
4446
5346
  } catch {
4447
5347
  return result;
4448
5348
  }
4449
5349
  const ruleSources = buildRuleSources();
4450
- for (const slug of slugDirs) {
4451
- const slugPath = path9.join(tmpDir, slug);
5350
+ for (const slug2 of slugDirs) {
5351
+ const slugPath = path10.join(tmpDir, slug2);
4452
5352
  try {
4453
- if (!fs6.statSync(slugPath).isDirectory()) continue;
5353
+ if (!fs7.statSync(slugPath).isDirectory()) continue;
4454
5354
  } catch {
4455
5355
  continue;
4456
5356
  }
4457
- let projLabel = stripTerminalEscapes(slug).slice(0, 40);
5357
+ let projLabel = stripTerminalEscapes(slug2).slice(0, 40);
4458
5358
  try {
4459
5359
  projLabel = stripTerminalEscapes(
4460
- fs6.readFileSync(path9.join(slugPath, ".project_root"), "utf-8").trim()
4461
- ).replace(os9.homedir(), "~").slice(0, 40);
5360
+ fs7.readFileSync(path10.join(slugPath, ".project_root"), "utf-8").trim()
5361
+ ).replace(os10.homedir(), "~").slice(0, 40);
4462
5362
  } catch {
4463
5363
  }
4464
- const chatsDir = path9.join(slugPath, "chats");
4465
- if (!fs6.existsSync(chatsDir)) continue;
5364
+ const chatsDir = path10.join(slugPath, "chats");
5365
+ if (!fs7.existsSync(chatsDir)) continue;
4466
5366
  let chatFiles;
4467
5367
  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")));
5368
+ chatFiles = fs7.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
4469
5369
  } catch {
4470
5370
  continue;
4471
5371
  }
@@ -4478,7 +5378,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4478
5378
  onProgress?.(result.filesScanned);
4479
5379
  let raw;
4480
5380
  try {
4481
- raw = fs6.readFileSync(path9.join(chatsDir, chatFile), "utf-8");
5381
+ raw = fs7.readFileSync(path10.join(chatsDir, chatFile), "utf-8");
4482
5382
  } catch {
4483
5383
  continue;
4484
5384
  }
@@ -4651,7 +5551,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4651
5551
  return result;
4652
5552
  }
4653
5553
  function scanCodexHistory(startDate, onProgress, onLine) {
4654
- const sessionsBase = path9.join(os9.homedir(), ".codex", "sessions");
5554
+ const sessionsBase = path10.join(os10.homedir(), ".codex", "sessions");
4655
5555
  const result = {
4656
5556
  filesScanned: 0,
4657
5557
  sessions: 0,
@@ -4666,32 +5566,32 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4666
5566
  sessionsWithEarlySecrets: 0
4667
5567
  };
4668
5568
  const dedup = emptyScanDedup();
4669
- if (!fs6.existsSync(sessionsBase)) return result;
5569
+ if (!fs7.existsSync(sessionsBase)) return result;
4670
5570
  const jsonlFiles = [];
4671
5571
  try {
4672
- for (const year of fs6.readdirSync(sessionsBase)) {
4673
- const yearPath = path9.join(sessionsBase, year);
5572
+ for (const year of fs7.readdirSync(sessionsBase)) {
5573
+ const yearPath = path10.join(sessionsBase, year);
4674
5574
  try {
4675
- if (!fs6.statSync(yearPath).isDirectory()) continue;
5575
+ if (!fs7.statSync(yearPath).isDirectory()) continue;
4676
5576
  } catch {
4677
5577
  continue;
4678
5578
  }
4679
- for (const month of fs6.readdirSync(yearPath)) {
4680
- const monthPath = path9.join(yearPath, month);
5579
+ for (const month of fs7.readdirSync(yearPath)) {
5580
+ const monthPath = path10.join(yearPath, month);
4681
5581
  try {
4682
- if (!fs6.statSync(monthPath).isDirectory()) continue;
5582
+ if (!fs7.statSync(monthPath).isDirectory()) continue;
4683
5583
  } catch {
4684
5584
  continue;
4685
5585
  }
4686
- for (const day of fs6.readdirSync(monthPath)) {
4687
- const dayPath = path9.join(monthPath, day);
5586
+ for (const day of fs7.readdirSync(monthPath)) {
5587
+ const dayPath = path10.join(monthPath, day);
4688
5588
  try {
4689
- if (!fs6.statSync(dayPath).isDirectory()) continue;
5589
+ if (!fs7.statSync(dayPath).isDirectory()) continue;
4690
5590
  } catch {
4691
5591
  continue;
4692
5592
  }
4693
- for (const file of fs6.readdirSync(dayPath)) {
4694
- if (file.endsWith(".jsonl")) jsonlFiles.push(path9.join(dayPath, file));
5593
+ for (const file of fs7.readdirSync(dayPath)) {
5594
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path10.join(dayPath, file));
4695
5595
  }
4696
5596
  }
4697
5597
  }
@@ -4705,7 +5605,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4705
5605
  onProgress?.(result.filesScanned);
4706
5606
  let lines;
4707
5607
  try {
4708
- lines = fs6.readFileSync(filePath, "utf-8").split("\n");
5608
+ lines = fs7.readFileSync(filePath, "utf-8").split("\n");
4709
5609
  } catch {
4710
5610
  continue;
4711
5611
  }
@@ -4732,7 +5632,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4732
5632
  sessionId = String(payload["id"] ?? filePath);
4733
5633
  startTime = String(payload["timestamp"] ?? "");
4734
5634
  const cwd = String(payload["cwd"] ?? "");
4735
- projLabel = stripTerminalEscapes(cwd.replace(os9.homedir(), "~")).slice(0, 40);
5635
+ projLabel = stripTerminalEscapes(cwd.replace(os10.homedir(), "~")).slice(0, 40);
4736
5636
  continue;
4737
5637
  }
4738
5638
  if (entry.type === "turn_context" && typeof payload["model"] === "string") {
@@ -4974,23 +5874,23 @@ var init_scan = __esm({
4974
5874
  });
4975
5875
 
4976
5876
  // src/tui/dashboard/data.ts
4977
- import fs7 from "fs";
4978
- import os10 from "os";
4979
- import path10 from "path";
5877
+ import fs8 from "fs";
5878
+ import os11 from "os";
5879
+ import path11 from "path";
4980
5880
  import http from "http";
4981
5881
  function auditLogPath() {
4982
- return path10.join(os10.homedir(), ".node9", "audit.log");
5882
+ return path11.join(os11.homedir(), ".node9", "audit.log");
4983
5883
  }
4984
5884
  function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
4985
5885
  return new Promise((resolve) => {
4986
5886
  const p = customPath ?? auditLogPath();
4987
- if (!fs7.existsSync(p)) {
5887
+ if (!fs8.existsSync(p)) {
4988
5888
  resolve([]);
4989
5889
  return;
4990
5890
  }
4991
5891
  let raw;
4992
5892
  try {
4993
- raw = fs7.readFileSync(p, "utf8");
5893
+ raw = fs8.readFileSync(p, "utf8");
4994
5894
  } catch {
4995
5895
  resolve([]);
4996
5896
  return;
@@ -5094,7 +5994,10 @@ function compactPathsInCommand(cmd) {
5094
5994
  function loadShieldStatus() {
5095
5995
  try {
5096
5996
  const all = Object.keys(SHIELDS).sort();
5097
- const activeSet = new Set(readActiveShields());
5997
+ const cfg = getConfig();
5998
+ const activeSet = new Set(
5999
+ cfg.policySource === "workspace" ? cfg.policy.appliedShields ?? [] : readActiveShields()
6000
+ );
5098
6001
  const active = all.filter((n) => activeSet.has(n));
5099
6002
  const inactive = all.filter((n) => !activeSet.has(n));
5100
6003
  return { active, inactive };
@@ -5119,13 +6022,13 @@ function loadBlast() {
5119
6022
  }
5120
6023
  }
5121
6024
  function shortenPath(p) {
5122
- const home = os10.homedir();
6025
+ const home = os11.homedir();
5123
6026
  return p.startsWith(home) ? p.replace(home, "~") : p;
5124
6027
  }
5125
6028
  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");
6029
+ const claudeProjectsDir = path11.join(os11.homedir(), ".claude", "projects");
6030
+ const codexSessionsDir = path11.join(os11.homedir(), ".codex", "sessions");
6031
+ const geminiTmpDir = path11.join(os11.homedir(), ".gemini", "tmp");
5129
6032
  const { start, end } = getDateRange(period, /* @__PURE__ */ new Date());
5130
6033
  const entries = await readAuditEntriesAsync();
5131
6034
  void ensurePricingLoaded();
@@ -5456,6 +6359,7 @@ var init_data = __esm({
5456
6359
  init_daemon();
5457
6360
  init_costSync();
5458
6361
  init_shields();
6362
+ init_config();
5459
6363
  init_decision();
5460
6364
  init_scan_watermark();
5461
6365
  init_report_audit();
@@ -6205,8 +7109,8 @@ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-run
6205
7109
  function TopToolsProjects({ audit }) {
6206
7110
  const data = audit?.data;
6207
7111
  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),
7112
+ const projects = data ? [...data.cost.byProject.entries()].map(([path12, r]) => ({
7113
+ name: basenameOf(path12),
6210
7114
  cost: r.cost,
6211
7115
  tokens: r.inputTokens + r.outputTokens
6212
7116
  })).sort((a, b) => b.cost - a.cost).slice(0, ROW_LIMIT) : [];
@@ -6643,8 +7547,8 @@ function pickTopLoopFile(loops) {
6643
7547
  map.set(k, (map.get(k) ?? 0) + (l.count ?? 0));
6644
7548
  }
6645
7549
  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 };
7550
+ const [path12, count] = [...map.entries()].sort((a, b) => b[1] - a[1])[0];
7551
+ return { path: path12, count };
6648
7552
  }
6649
7553
  var EMPTY_FILTERED_SCAN;
6650
7554
  var init_derive = __esm({