@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.
- package/README.md +14 -1
- package/dist/cli.js +1373 -1146
- package/dist/cli.mjs +1366 -1139
- package/dist/dashboard.mjs +1031 -127
- package/dist/index.js +111 -48
- package/dist/index.mjs +111 -48
- 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,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
|
|
2796
|
-
import
|
|
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 =
|
|
2803
|
-
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
|
|
2809
|
-
import
|
|
2810
|
-
import
|
|
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(
|
|
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 =
|
|
2831
|
-
if (!
|
|
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
|
-
|
|
2838
|
-
|
|
3737
|
+
fs5.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
|
|
3738
|
+
fs5.renameSync(tmp, target);
|
|
2839
3739
|
} catch (err) {
|
|
2840
3740
|
try {
|
|
2841
|
-
|
|
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 = () =>
|
|
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
|
|
3128
|
-
import
|
|
3129
|
-
import
|
|
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 (!
|
|
3211
|
-
const raw =
|
|
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 =
|
|
4165
|
+
const projPath = path7.join(projectsDir, proj);
|
|
3266
4166
|
let files;
|
|
3267
4167
|
try {
|
|
3268
|
-
const stat =
|
|
4168
|
+
const stat = fs6.statSync(projPath);
|
|
3269
4169
|
if (!stat.isDirectory()) return;
|
|
3270
|
-
files =
|
|
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 =
|
|
4176
|
+
const filePath = path7.join(projPath, file);
|
|
3277
4177
|
try {
|
|
3278
|
-
if (
|
|
4178
|
+
if (fs6.statSync(filePath).mtimeMs < startMs) continue;
|
|
3279
4179
|
} catch {
|
|
3280
4180
|
continue;
|
|
3281
4181
|
}
|
|
3282
4182
|
try {
|
|
3283
|
-
const raw =
|
|
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 (!
|
|
4233
|
+
if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
|
|
3334
4234
|
let dirs;
|
|
3335
4235
|
try {
|
|
3336
|
-
dirs =
|
|
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 (!
|
|
4247
|
+
if (!fs6.existsSync(projectsDir)) return freezeClaudeCost(acc);
|
|
3348
4248
|
let dirs;
|
|
3349
4249
|
try {
|
|
3350
|
-
dirs =
|
|
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 =
|
|
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 (!
|
|
4318
|
+
if (!fs6.existsSync(sessionsBase)) return jsonlFiles;
|
|
3419
4319
|
try {
|
|
3420
|
-
for (const year of
|
|
3421
|
-
const yearPath =
|
|
4320
|
+
for (const year of fs6.readdirSync(sessionsBase)) {
|
|
4321
|
+
const yearPath = path7.join(sessionsBase, year);
|
|
3422
4322
|
try {
|
|
3423
|
-
if (!
|
|
4323
|
+
if (!fs6.statSync(yearPath).isDirectory()) continue;
|
|
3424
4324
|
} catch {
|
|
3425
4325
|
continue;
|
|
3426
4326
|
}
|
|
3427
|
-
for (const month of
|
|
3428
|
-
const monthPath =
|
|
4327
|
+
for (const month of fs6.readdirSync(yearPath)) {
|
|
4328
|
+
const monthPath = path7.join(yearPath, month);
|
|
3429
4329
|
try {
|
|
3430
|
-
if (!
|
|
4330
|
+
if (!fs6.statSync(monthPath).isDirectory()) continue;
|
|
3431
4331
|
} catch {
|
|
3432
4332
|
continue;
|
|
3433
4333
|
}
|
|
3434
|
-
for (const day of
|
|
3435
|
-
const dayPath =
|
|
4334
|
+
for (const day of fs6.readdirSync(monthPath)) {
|
|
4335
|
+
const dayPath = path7.join(monthPath, day);
|
|
3436
4336
|
try {
|
|
3437
|
-
if (!
|
|
4337
|
+
if (!fs6.statSync(dayPath).isDirectory()) continue;
|
|
3438
4338
|
} catch {
|
|
3439
4339
|
continue;
|
|
3440
4340
|
}
|
|
3441
|
-
for (const file of
|
|
3442
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
3579
|
-
dirs =
|
|
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 =
|
|
4484
|
+
const chatsDir = path7.join(geminiTmpDir, proj, "chats");
|
|
3585
4485
|
let files;
|
|
3586
4486
|
try {
|
|
3587
|
-
if (!
|
|
3588
|
-
files =
|
|
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:
|
|
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 (!
|
|
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 (!
|
|
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 ??
|
|
3633
|
-
const claudeProjectsDir = opts.claudeProjectsDir ??
|
|
3634
|
-
const codexSessionsDir = opts.codexSessionsDir ??
|
|
3635
|
-
const geminiTmpDir = opts.geminiTmpDir ??
|
|
3636
|
-
const hasAuditFile =
|
|
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
|
|
3874
|
-
import
|
|
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
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
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
|
|
3929
|
-
import
|
|
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 =
|
|
3935
|
-
NOTIFIED_FILE =
|
|
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
|
|
3995
|
-
import
|
|
3996
|
-
import
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
5251
|
+
const projPath = path10.join(projectsDir, proj);
|
|
4352
5252
|
try {
|
|
4353
|
-
if (!
|
|
5253
|
+
if (!fs7.statSync(projPath).isDirectory()) return;
|
|
4354
5254
|
} catch {
|
|
4355
5255
|
return;
|
|
4356
5256
|
}
|
|
4357
|
-
const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(
|
|
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 =
|
|
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 =
|
|
5301
|
+
const projectsDir = path10.join(os10.homedir(), ".claude", "projects");
|
|
4402
5302
|
const result = emptyClaudeScan();
|
|
4403
|
-
if (!
|
|
5303
|
+
if (!fs7.existsSync(projectsDir)) return result;
|
|
4404
5304
|
let projDirs;
|
|
4405
5305
|
try {
|
|
4406
|
-
projDirs =
|
|
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 =
|
|
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 (!
|
|
5342
|
+
if (!fs7.existsSync(tmpDir)) return result;
|
|
4443
5343
|
let slugDirs;
|
|
4444
5344
|
try {
|
|
4445
|
-
slugDirs =
|
|
5345
|
+
slugDirs = fs7.readdirSync(tmpDir);
|
|
4446
5346
|
} catch {
|
|
4447
5347
|
return result;
|
|
4448
5348
|
}
|
|
4449
5349
|
const ruleSources = buildRuleSources();
|
|
4450
|
-
for (const
|
|
4451
|
-
const slugPath =
|
|
5350
|
+
for (const slug2 of slugDirs) {
|
|
5351
|
+
const slugPath = path10.join(tmpDir, slug2);
|
|
4452
5352
|
try {
|
|
4453
|
-
if (!
|
|
5353
|
+
if (!fs7.statSync(slugPath).isDirectory()) continue;
|
|
4454
5354
|
} catch {
|
|
4455
5355
|
continue;
|
|
4456
5356
|
}
|
|
4457
|
-
let projLabel = stripTerminalEscapes(
|
|
5357
|
+
let projLabel = stripTerminalEscapes(slug2).slice(0, 40);
|
|
4458
5358
|
try {
|
|
4459
5359
|
projLabel = stripTerminalEscapes(
|
|
4460
|
-
|
|
4461
|
-
).replace(
|
|
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 =
|
|
4465
|
-
if (!
|
|
5364
|
+
const chatsDir = path10.join(slugPath, "chats");
|
|
5365
|
+
if (!fs7.existsSync(chatsDir)) continue;
|
|
4466
5366
|
let chatFiles;
|
|
4467
5367
|
try {
|
|
4468
|
-
chatFiles =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
5569
|
+
if (!fs7.existsSync(sessionsBase)) return result;
|
|
4670
5570
|
const jsonlFiles = [];
|
|
4671
5571
|
try {
|
|
4672
|
-
for (const year of
|
|
4673
|
-
const yearPath =
|
|
5572
|
+
for (const year of fs7.readdirSync(sessionsBase)) {
|
|
5573
|
+
const yearPath = path10.join(sessionsBase, year);
|
|
4674
5574
|
try {
|
|
4675
|
-
if (!
|
|
5575
|
+
if (!fs7.statSync(yearPath).isDirectory()) continue;
|
|
4676
5576
|
} catch {
|
|
4677
5577
|
continue;
|
|
4678
5578
|
}
|
|
4679
|
-
for (const month of
|
|
4680
|
-
const monthPath =
|
|
5579
|
+
for (const month of fs7.readdirSync(yearPath)) {
|
|
5580
|
+
const monthPath = path10.join(yearPath, month);
|
|
4681
5581
|
try {
|
|
4682
|
-
if (!
|
|
5582
|
+
if (!fs7.statSync(monthPath).isDirectory()) continue;
|
|
4683
5583
|
} catch {
|
|
4684
5584
|
continue;
|
|
4685
5585
|
}
|
|
4686
|
-
for (const day of
|
|
4687
|
-
const dayPath =
|
|
5586
|
+
for (const day of fs7.readdirSync(monthPath)) {
|
|
5587
|
+
const dayPath = path10.join(monthPath, day);
|
|
4688
5588
|
try {
|
|
4689
|
-
if (!
|
|
5589
|
+
if (!fs7.statSync(dayPath).isDirectory()) continue;
|
|
4690
5590
|
} catch {
|
|
4691
5591
|
continue;
|
|
4692
5592
|
}
|
|
4693
|
-
for (const file of
|
|
4694
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
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 =
|
|
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(
|
|
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
|
|
4978
|
-
import
|
|
4979
|
-
import
|
|
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
|
|
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 (!
|
|
5887
|
+
if (!fs8.existsSync(p)) {
|
|
4988
5888
|
resolve([]);
|
|
4989
5889
|
return;
|
|
4990
5890
|
}
|
|
4991
5891
|
let raw;
|
|
4992
5892
|
try {
|
|
4993
|
-
raw =
|
|
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
|
|
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 =
|
|
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 =
|
|
5127
|
-
const codexSessionsDir =
|
|
5128
|
-
const geminiTmpDir =
|
|
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(([
|
|
6209
|
-
name: basenameOf(
|
|
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 [
|
|
6647
|
-
return { path:
|
|
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({
|