@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.
- package/LICENSE +78 -59
- package/README.md +14 -1
- package/dist/cli.js +1384 -1149
- package/dist/cli.mjs +1377 -1142
- package/dist/dashboard.mjs +1039 -128
- package/dist/index.js +122 -51
- package/dist/index.mjs +122 -51
- package/package.json +3 -1
package/dist/dashboard.mjs
CHANGED
|
@@ -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,
|
|
231
|
+
function getNestedValue(obj, path12) {
|
|
232
232
|
if (!obj || typeof obj !== "object") return null;
|
|
233
|
-
const segments =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2796
|
-
import
|
|
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 =
|
|
2803
|
-
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
|
|
2809
|
-
import
|
|
2810
|
-
import
|
|
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(
|
|
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 =
|
|
2831
|
-
if (!
|
|
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
|
-
|
|
2838
|
-
|
|
3744
|
+
fs5.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
|
|
3745
|
+
fs5.renameSync(tmp, target);
|
|
2839
3746
|
} catch (err) {
|
|
2840
3747
|
try {
|
|
2841
|
-
|
|
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 = () =>
|
|
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
|
|
3128
|
-
import
|
|
3129
|
-
import
|
|
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 (!
|
|
3211
|
-
const raw =
|
|
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 =
|
|
4172
|
+
const projPath = path7.join(projectsDir, proj);
|
|
3266
4173
|
let files;
|
|
3267
4174
|
try {
|
|
3268
|
-
const stat =
|
|
4175
|
+
const stat = fs6.statSync(projPath);
|
|
3269
4176
|
if (!stat.isDirectory()) return;
|
|
3270
|
-
files =
|
|
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 =
|
|
4183
|
+
const filePath = path7.join(projPath, file);
|
|
3277
4184
|
try {
|
|
3278
|
-
if (
|
|
4185
|
+
if (fs6.statSync(filePath).mtimeMs < startMs) continue;
|
|
3279
4186
|
} catch {
|
|
3280
4187
|
continue;
|
|
3281
4188
|
}
|
|
3282
4189
|
try {
|
|
3283
|
-
const raw =
|
|
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 (!
|
|
4240
|
+
if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
|
|
3334
4241
|
let dirs;
|
|
3335
4242
|
try {
|
|
3336
|
-
dirs =
|
|
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 (!
|
|
4254
|
+
if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
|
|
3348
4255
|
let dirs;
|
|
3349
4256
|
try {
|
|
3350
|
-
dirs =
|
|
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 =
|
|
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 (!
|
|
4325
|
+
if (!fs6.existsSync(sessionsBase)) return jsonlFiles;
|
|
3419
4326
|
try {
|
|
3420
|
-
for (const year of
|
|
3421
|
-
const yearPath =
|
|
4327
|
+
for (const year of fs6.readdirSync(sessionsBase)) {
|
|
4328
|
+
const yearPath = path7.join(sessionsBase, year);
|
|
3422
4329
|
try {
|
|
3423
|
-
if (!
|
|
4330
|
+
if (!fs6.statSync(yearPath).isDirectory()) continue;
|
|
3424
4331
|
} catch {
|
|
3425
4332
|
continue;
|
|
3426
4333
|
}
|
|
3427
|
-
for (const month of
|
|
3428
|
-
const monthPath =
|
|
4334
|
+
for (const month of fs6.readdirSync(yearPath)) {
|
|
4335
|
+
const monthPath = path7.join(yearPath, month);
|
|
3429
4336
|
try {
|
|
3430
|
-
if (!
|
|
4337
|
+
if (!fs6.statSync(monthPath).isDirectory()) continue;
|
|
3431
4338
|
} catch {
|
|
3432
4339
|
continue;
|
|
3433
4340
|
}
|
|
3434
|
-
for (const day of
|
|
3435
|
-
const dayPath =
|
|
4341
|
+
for (const day of fs6.readdirSync(monthPath)) {
|
|
4342
|
+
const dayPath = path7.join(monthPath, day);
|
|
3436
4343
|
try {
|
|
3437
|
-
if (!
|
|
4344
|
+
if (!fs6.statSync(dayPath).isDirectory()) continue;
|
|
3438
4345
|
} catch {
|
|
3439
4346
|
continue;
|
|
3440
4347
|
}
|
|
3441
|
-
for (const file of
|
|
3442
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
3579
|
-
dirs =
|
|
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 =
|
|
4491
|
+
const chatsDir = path7.join(geminiTmpDir, proj, "chats");
|
|
3585
4492
|
let files;
|
|
3586
4493
|
try {
|
|
3587
|
-
if (!
|
|
3588
|
-
files =
|
|
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:
|
|
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 (!
|
|
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 (!
|
|
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 ??
|
|
3633
|
-
const claudeProjectsDir = opts.claudeProjectsDir ??
|
|
3634
|
-
const codexSessionsDir = opts.codexSessionsDir ??
|
|
3635
|
-
const geminiTmpDir = opts.geminiTmpDir ??
|
|
3636
|
-
const hasAuditFile =
|
|
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
|
|
3874
|
-
import
|
|
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
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
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
|
|
3929
|
-
import
|
|
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 =
|
|
3935
|
-
NOTIFIED_FILE =
|
|
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
|
|
3995
|
-
import
|
|
3996
|
-
import
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
5258
|
+
const projPath = path10.join(projectsDir, proj);
|
|
4352
5259
|
try {
|
|
4353
|
-
if (!
|
|
5260
|
+
if (!fs7.statSync(projPath).isDirectory()) return;
|
|
4354
5261
|
} catch {
|
|
4355
5262
|
return;
|
|
4356
5263
|
}
|
|
4357
|
-
const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(
|
|
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 =
|
|
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 =
|
|
5308
|
+
const projectsDir = path10.join(os10.homedir(), ".claude", "projects");
|
|
4402
5309
|
const result = emptyClaudeScan();
|
|
4403
|
-
if (!
|
|
5310
|
+
if (!fs7.existsSync(projectsDir)) return result;
|
|
4404
5311
|
let projDirs;
|
|
4405
5312
|
try {
|
|
4406
|
-
projDirs =
|
|
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 =
|
|
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 (!
|
|
5349
|
+
if (!fs7.existsSync(tmpDir)) return result;
|
|
4443
5350
|
let slugDirs;
|
|
4444
5351
|
try {
|
|
4445
|
-
slugDirs =
|
|
5352
|
+
slugDirs = fs7.readdirSync(tmpDir);
|
|
4446
5353
|
} catch {
|
|
4447
5354
|
return result;
|
|
4448
5355
|
}
|
|
4449
5356
|
const ruleSources = buildRuleSources();
|
|
4450
|
-
for (const
|
|
4451
|
-
const slugPath =
|
|
5357
|
+
for (const slug2 of slugDirs) {
|
|
5358
|
+
const slugPath = path10.join(tmpDir, slug2);
|
|
4452
5359
|
try {
|
|
4453
|
-
if (!
|
|
5360
|
+
if (!fs7.statSync(slugPath).isDirectory()) continue;
|
|
4454
5361
|
} catch {
|
|
4455
5362
|
continue;
|
|
4456
5363
|
}
|
|
4457
|
-
let projLabel = stripTerminalEscapes(
|
|
5364
|
+
let projLabel = stripTerminalEscapes(slug2).slice(0, 40);
|
|
4458
5365
|
try {
|
|
4459
5366
|
projLabel = stripTerminalEscapes(
|
|
4460
|
-
|
|
4461
|
-
).replace(
|
|
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 =
|
|
4465
|
-
if (!
|
|
5371
|
+
const chatsDir = path10.join(slugPath, "chats");
|
|
5372
|
+
if (!fs7.existsSync(chatsDir)) continue;
|
|
4466
5373
|
let chatFiles;
|
|
4467
5374
|
try {
|
|
4468
|
-
chatFiles =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
5576
|
+
if (!fs7.existsSync(sessionsBase)) return result;
|
|
4670
5577
|
const jsonlFiles = [];
|
|
4671
5578
|
try {
|
|
4672
|
-
for (const year of
|
|
4673
|
-
const yearPath =
|
|
5579
|
+
for (const year of fs7.readdirSync(sessionsBase)) {
|
|
5580
|
+
const yearPath = path10.join(sessionsBase, year);
|
|
4674
5581
|
try {
|
|
4675
|
-
if (!
|
|
5582
|
+
if (!fs7.statSync(yearPath).isDirectory()) continue;
|
|
4676
5583
|
} catch {
|
|
4677
5584
|
continue;
|
|
4678
5585
|
}
|
|
4679
|
-
for (const month of
|
|
4680
|
-
const monthPath =
|
|
5586
|
+
for (const month of fs7.readdirSync(yearPath)) {
|
|
5587
|
+
const monthPath = path10.join(yearPath, month);
|
|
4681
5588
|
try {
|
|
4682
|
-
if (!
|
|
5589
|
+
if (!fs7.statSync(monthPath).isDirectory()) continue;
|
|
4683
5590
|
} catch {
|
|
4684
5591
|
continue;
|
|
4685
5592
|
}
|
|
4686
|
-
for (const day of
|
|
4687
|
-
const dayPath =
|
|
5593
|
+
for (const day of fs7.readdirSync(monthPath)) {
|
|
5594
|
+
const dayPath = path10.join(monthPath, day);
|
|
4688
5595
|
try {
|
|
4689
|
-
if (!
|
|
5596
|
+
if (!fs7.statSync(dayPath).isDirectory()) continue;
|
|
4690
5597
|
} catch {
|
|
4691
5598
|
continue;
|
|
4692
5599
|
}
|
|
4693
|
-
for (const file of
|
|
4694
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
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 =
|
|
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(
|
|
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
|
|
4978
|
-
import
|
|
4979
|
-
import
|
|
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
|
|
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 (!
|
|
5894
|
+
if (!fs8.existsSync(p)) {
|
|
4988
5895
|
resolve([]);
|
|
4989
5896
|
return;
|
|
4990
5897
|
}
|
|
4991
5898
|
let raw;
|
|
4992
5899
|
try {
|
|
4993
|
-
raw =
|
|
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
|
|
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 =
|
|
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 =
|
|
5127
|
-
const codexSessionsDir =
|
|
5128
|
-
const geminiTmpDir =
|
|
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(([
|
|
6209
|
-
name: basenameOf(
|
|
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 [
|
|
6647
|
-
return { path:
|
|
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({
|