@node9/proxy 2.8.0 → 2.8.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/README.md +5 -3
- package/dist/cli.js +10 -4
- package/dist/cli.mjs +10 -4
- package/dist/dashboard.mjs +40 -5
- package/dist/scan-ink.mjs +575 -348
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<a href="https://www.npmjs.com/package/node9-ai"><img src="https://img.shields.io/npm/dm/node9-ai.svg" alt="monthly downloads" /></a>
|
|
6
6
|
<a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" /></a>
|
|
7
7
|
<a href="https://node9.ai/docs"><img src="https://img.shields.io/badge/docs-node9.ai-blue" alt="Documentation" /></a>
|
|
8
|
-
<a href="https://
|
|
8
|
+
<a href="https://github.com/node9-ai/node9-proxy/blob/main/.github/workflows/agent-security.yml"><img src="https://img.shields.io/badge/node9-self--scanned-a855f7?style=flat&labelColor=%231A1A2E&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNCAxNCI+PHBhdGggZmlsbD0iI0Y1RTlGRiIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03IDAuNCAxLjYgMi41djQuMmMwIDMuMSAyLjMgNS42IDUuNCA2LjkgMy4xLTEuMyA1LjQtMy44IDUuNC02LjlWMi41TDcgMC40Wm0wIDEuNSAzLjkgMS41djMuM2MwIDIuMy0xLjYgNC4yLTMuOSA1LjMtMi4zLTEuMS0zLjktMy0zLjktNS4zVjMuNEw3IDEuOVptMCAyLjJhMS45IDEuOSAwIDAgMC0xIDMuNXYxLjZoMlY3LjZhMS45IDEuOSAwIDAgMC0xLTMuNVoiLz48L3N2Zz4K" alt="node9 self-scanned" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
Node9 sits between your AI agent and the tools it can use — **discover** what it's already been doing, **protect** against risky actions in real time, and **review** what happened over any time window.
|
|
@@ -108,12 +108,14 @@ What it checks:
|
|
|
108
108
|
|
|
109
109
|
```yaml
|
|
110
110
|
# .github/workflows/agent-security.yml
|
|
111
|
-
- uses: node9-ai/
|
|
111
|
+
- uses: node9-ai/node9-proxy@v2
|
|
112
112
|
with:
|
|
113
113
|
fail-on: high # or 'never' to just comment
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
Marketplace: **[node9 Agent Security
|
|
116
|
+
Marketplace: **[node9 Agent Security](https://github.com/marketplace/actions/node9-agent-security)**
|
|
117
|
+
|
|
118
|
+
Running it? Add the **[`scanned by node9` badge](docs/badges.md)** to your README.
|
|
117
119
|
|
|
118
120
|
## Live monitoring
|
|
119
121
|
|
package/dist/cli.js
CHANGED
|
@@ -15757,6 +15757,7 @@ function registerScanCommand(program2) {
|
|
|
15757
15757
|
})();
|
|
15758
15758
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
15759
15759
|
const enabledShields = getConfig().policy.appliedShields ?? [];
|
|
15760
|
+
const keyed = isKeyedForPolicy();
|
|
15760
15761
|
const enabledShieldSet = new Set(enabledShields);
|
|
15761
15762
|
const screenshotMode = options.compact || options.narrative;
|
|
15762
15763
|
const useInk = !options.classic && !drillDown;
|
|
@@ -15909,7 +15910,8 @@ function registerScanCommand(program2) {
|
|
|
15909
15910
|
blastExposures,
|
|
15910
15911
|
blockedCount,
|
|
15911
15912
|
reviewCount,
|
|
15912
|
-
enabledShields
|
|
15913
|
+
enabledShields,
|
|
15914
|
+
keyed
|
|
15913
15915
|
});
|
|
15914
15916
|
return;
|
|
15915
15917
|
}
|
|
@@ -15921,7 +15923,8 @@ function registerScanCommand(program2) {
|
|
|
15921
15923
|
blastExposures,
|
|
15922
15924
|
blockedCount,
|
|
15923
15925
|
reviewCount,
|
|
15924
|
-
enabledShields
|
|
15926
|
+
enabledShields,
|
|
15927
|
+
keyed
|
|
15925
15928
|
});
|
|
15926
15929
|
return;
|
|
15927
15930
|
}
|
|
@@ -16012,7 +16015,8 @@ function registerScanCommand(program2) {
|
|
|
16012
16015
|
blastExposures,
|
|
16013
16016
|
blockedCount,
|
|
16014
16017
|
reviewCount,
|
|
16015
|
-
enabledShields
|
|
16018
|
+
enabledShields,
|
|
16019
|
+
keyed
|
|
16016
16020
|
},
|
|
16017
16021
|
rangeLabel2
|
|
16018
16022
|
);
|
|
@@ -16025,7 +16029,8 @@ function registerScanCommand(program2) {
|
|
|
16025
16029
|
blastExposures,
|
|
16026
16030
|
blockedCount,
|
|
16027
16031
|
reviewCount,
|
|
16028
|
-
enabledShields
|
|
16032
|
+
enabledShields,
|
|
16033
|
+
keyed
|
|
16029
16034
|
});
|
|
16030
16035
|
}
|
|
16031
16036
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
@@ -16248,6 +16253,7 @@ var import_chalk6, import_fs26, import_path28, import_os25, import_string_width2
|
|
|
16248
16253
|
var init_scan = __esm({
|
|
16249
16254
|
"src/cli/commands/scan.ts"() {
|
|
16250
16255
|
"use strict";
|
|
16256
|
+
init_keyed_guard();
|
|
16251
16257
|
import_chalk6 = __toESM(require("chalk"));
|
|
16252
16258
|
import_fs26 = __toESM(require("fs"));
|
|
16253
16259
|
import_path28 = __toESM(require("path"));
|
package/dist/cli.mjs
CHANGED
|
@@ -15761,6 +15761,7 @@ function registerScanCommand(program2) {
|
|
|
15761
15761
|
})();
|
|
15762
15762
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
15763
15763
|
const enabledShields = getConfig().policy.appliedShields ?? [];
|
|
15764
|
+
const keyed = isKeyedForPolicy();
|
|
15764
15765
|
const enabledShieldSet = new Set(enabledShields);
|
|
15765
15766
|
const screenshotMode = options.compact || options.narrative;
|
|
15766
15767
|
const useInk = !options.classic && !drillDown;
|
|
@@ -15913,7 +15914,8 @@ function registerScanCommand(program2) {
|
|
|
15913
15914
|
blastExposures,
|
|
15914
15915
|
blockedCount,
|
|
15915
15916
|
reviewCount,
|
|
15916
|
-
enabledShields
|
|
15917
|
+
enabledShields,
|
|
15918
|
+
keyed
|
|
15917
15919
|
});
|
|
15918
15920
|
return;
|
|
15919
15921
|
}
|
|
@@ -15925,7 +15927,8 @@ function registerScanCommand(program2) {
|
|
|
15925
15927
|
blastExposures,
|
|
15926
15928
|
blockedCount,
|
|
15927
15929
|
reviewCount,
|
|
15928
|
-
enabledShields
|
|
15930
|
+
enabledShields,
|
|
15931
|
+
keyed
|
|
15929
15932
|
});
|
|
15930
15933
|
return;
|
|
15931
15934
|
}
|
|
@@ -16016,7 +16019,8 @@ function registerScanCommand(program2) {
|
|
|
16016
16019
|
blastExposures,
|
|
16017
16020
|
blockedCount,
|
|
16018
16021
|
reviewCount,
|
|
16019
|
-
enabledShields
|
|
16022
|
+
enabledShields,
|
|
16023
|
+
keyed
|
|
16020
16024
|
},
|
|
16021
16025
|
rangeLabel2
|
|
16022
16026
|
);
|
|
@@ -16029,7 +16033,8 @@ function registerScanCommand(program2) {
|
|
|
16029
16033
|
blastExposures,
|
|
16030
16034
|
blockedCount,
|
|
16031
16035
|
reviewCount,
|
|
16032
|
-
enabledShields
|
|
16036
|
+
enabledShields,
|
|
16037
|
+
keyed
|
|
16033
16038
|
});
|
|
16034
16039
|
}
|
|
16035
16040
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
@@ -16252,6 +16257,7 @@ var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTE
|
|
|
16252
16257
|
var init_scan = __esm({
|
|
16253
16258
|
"src/cli/commands/scan.ts"() {
|
|
16254
16259
|
"use strict";
|
|
16260
|
+
init_keyed_guard();
|
|
16255
16261
|
init_shields();
|
|
16256
16262
|
init_config();
|
|
16257
16263
|
init_policy();
|
package/dist/dashboard.mjs
CHANGED
|
@@ -4809,6 +4809,22 @@ var init_report_audit = __esm({
|
|
|
4809
4809
|
}
|
|
4810
4810
|
});
|
|
4811
4811
|
|
|
4812
|
+
// src/config/keyed-guard.ts
|
|
4813
|
+
import chalk2 from "chalk";
|
|
4814
|
+
function isKeyedForPolicy() {
|
|
4815
|
+
try {
|
|
4816
|
+
return getConfig().policySource === "workspace";
|
|
4817
|
+
} catch {
|
|
4818
|
+
return false;
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
var init_keyed_guard = __esm({
|
|
4822
|
+
"src/config/keyed-guard.ts"() {
|
|
4823
|
+
"use strict";
|
|
4824
|
+
init_config();
|
|
4825
|
+
}
|
|
4826
|
+
});
|
|
4827
|
+
|
|
4812
4828
|
// src/utils/provenance.ts
|
|
4813
4829
|
import path9 from "path";
|
|
4814
4830
|
import os8 from "os";
|
|
@@ -4891,7 +4907,7 @@ var init_setup_pi_shim = __esm({
|
|
|
4891
4907
|
});
|
|
4892
4908
|
|
|
4893
4909
|
// src/setup.ts
|
|
4894
|
-
import
|
|
4910
|
+
import chalk3 from "chalk";
|
|
4895
4911
|
import { confirm as rawConfirm } from "@inquirer/prompts";
|
|
4896
4912
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4897
4913
|
import * as yaml from "yaml";
|
|
@@ -4906,7 +4922,7 @@ var init_setup = __esm({
|
|
|
4906
4922
|
});
|
|
4907
4923
|
|
|
4908
4924
|
// src/cli/render/scan-derive.ts
|
|
4909
|
-
import
|
|
4925
|
+
import chalk4 from "chalk";
|
|
4910
4926
|
import stringWidth from "string-width";
|
|
4911
4927
|
var init_scan_derive = __esm({
|
|
4912
4928
|
"src/cli/render/scan-derive.ts"() {
|
|
@@ -4930,7 +4946,7 @@ var init_scan_history = __esm({
|
|
|
4930
4946
|
});
|
|
4931
4947
|
|
|
4932
4948
|
// src/cli/commands/scan.ts
|
|
4933
|
-
import
|
|
4949
|
+
import chalk5 from "chalk";
|
|
4934
4950
|
import fs8 from "fs";
|
|
4935
4951
|
import path11 from "path";
|
|
4936
4952
|
import os10 from "os";
|
|
@@ -5845,6 +5861,7 @@ var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTE
|
|
|
5845
5861
|
var init_scan = __esm({
|
|
5846
5862
|
"src/cli/commands/scan.ts"() {
|
|
5847
5863
|
"use strict";
|
|
5864
|
+
init_keyed_guard();
|
|
5848
5865
|
init_shields();
|
|
5849
5866
|
init_config();
|
|
5850
5867
|
init_policy();
|
|
@@ -7093,6 +7110,22 @@ var init_Protection = __esm({
|
|
|
7093
7110
|
}
|
|
7094
7111
|
});
|
|
7095
7112
|
|
|
7113
|
+
// src/cli/render/ink/panels/cost-note.ts
|
|
7114
|
+
function costNoteLines(keyed) {
|
|
7115
|
+
return keyed ? ["Your plan and exact spend:", "dashboard \u2192 Report"] : ["To see your plan or exact spend, connect:", "node9 login (for free)"];
|
|
7116
|
+
}
|
|
7117
|
+
function costNoteLinesForThisMachine() {
|
|
7118
|
+
return costNoteLines(isKeyedForPolicy());
|
|
7119
|
+
}
|
|
7120
|
+
var COST_LABEL;
|
|
7121
|
+
var init_cost_note = __esm({
|
|
7122
|
+
"src/cli/render/ink/panels/cost-note.ts"() {
|
|
7123
|
+
"use strict";
|
|
7124
|
+
init_keyed_guard();
|
|
7125
|
+
COST_LABEL = "COST \xB7 based on API value";
|
|
7126
|
+
}
|
|
7127
|
+
});
|
|
7128
|
+
|
|
7096
7129
|
// src/tui/dashboard/views/report/panels/Cost.tsx
|
|
7097
7130
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
7098
7131
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
@@ -7116,7 +7149,7 @@ function Cost({ audit }) {
|
|
|
7116
7149
|
flexGrow: 1,
|
|
7117
7150
|
flexBasis: 0,
|
|
7118
7151
|
children: [
|
|
7119
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children:
|
|
7152
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: COST_LABEL }),
|
|
7120
7153
|
audit === null ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "loading\u2026" }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
7121
7154
|
/* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
|
|
7122
7155
|
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W2, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
|
|
@@ -7139,7 +7172,8 @@ function Cost({ audit }) {
|
|
|
7139
7172
|
/* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
|
|
7140
7173
|
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Trend" }) }),
|
|
7141
7174
|
/* @__PURE__ */ jsx3(Text3, { children: trend || /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "(no data)" }) })
|
|
7142
|
-
] })
|
|
7175
|
+
] }),
|
|
7176
|
+
costNoteLinesForThisMachine().map((line) => /* @__PURE__ */ jsx3(Box3, { height: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: line }) }, line))
|
|
7143
7177
|
] })
|
|
7144
7178
|
]
|
|
7145
7179
|
}
|
|
@@ -7151,6 +7185,7 @@ var init_Cost = __esm({
|
|
|
7151
7185
|
"use strict";
|
|
7152
7186
|
init_panels();
|
|
7153
7187
|
init_format();
|
|
7188
|
+
init_cost_note();
|
|
7154
7189
|
init_util();
|
|
7155
7190
|
LABEL_W2 = 10;
|
|
7156
7191
|
}
|
package/dist/scan-ink.mjs
CHANGED
|
@@ -39,327 +39,169 @@ function formatCost(usd) {
|
|
|
39
39
|
return `$${(usd / 1e3).toFixed(1)}K`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// src/
|
|
43
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
44
|
-
var LABEL_W = 16;
|
|
45
|
-
function CostPanel({ summary, width }) {
|
|
46
|
-
const total = summary.stats.totalCostUSD;
|
|
47
|
-
return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
48
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "COST \xB7 API value" }),
|
|
49
|
-
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
50
|
-
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
|
|
51
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: formatCost(total) })
|
|
52
|
-
] }),
|
|
53
|
-
summary.byAgent.map((agent) => /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
54
|
-
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: agent.label }) }),
|
|
55
|
-
/* @__PURE__ */ jsx3(Text3, { children: formatCost(agent.costUSD) })
|
|
56
|
-
] }, agent.id)),
|
|
57
|
-
summary.loopWastedUSD > 0 ? /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
58
|
-
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Wasted on loops" }) }),
|
|
59
|
-
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "~" + formatCost(summary.loopWastedUSD) })
|
|
60
|
-
] }) : null
|
|
61
|
-
] });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// src/cli/render/ink/panels/ActivityPanel.tsx
|
|
65
|
-
import { Box as Box4, Text as Text4 } from "ink";
|
|
66
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
67
|
-
var LABEL_W2 = 16;
|
|
68
|
-
function fmtNum(n) {
|
|
69
|
-
return n.toLocaleString();
|
|
70
|
-
}
|
|
71
|
-
function countMcpFindings(scan) {
|
|
72
|
-
let n = 0;
|
|
73
|
-
for (const f of scan.findings) {
|
|
74
|
-
if (f.toolName.startsWith("mcp__")) n++;
|
|
75
|
-
}
|
|
76
|
-
return n;
|
|
77
|
-
}
|
|
78
|
-
function ActivityPanel({ summary, scan, width }) {
|
|
79
|
-
const { sessions, totalToolCalls, bashCalls, totalCostUSD } = summary.stats;
|
|
80
|
-
const perSession = sessions > 0 ? totalCostUSD / sessions : 0;
|
|
81
|
-
const toolsPerSession = sessions > 0 ? Math.round(totalToolCalls / sessions) : 0;
|
|
82
|
-
const mcpCount = countMcpFindings(scan);
|
|
83
|
-
const topAgent = summary.byAgent.slice().sort((a, b) => b.sessions - a.sessions)[0];
|
|
84
|
-
return /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
85
|
-
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "ACTIVITY" }),
|
|
86
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
87
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Sessions" }) }),
|
|
88
|
-
/* @__PURE__ */ jsx4(Text4, { bold: true, children: fmtNum(sessions) })
|
|
89
|
-
] }),
|
|
90
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
91
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools" }) }),
|
|
92
|
-
/* @__PURE__ */ jsx4(Text4, { children: fmtNum(totalToolCalls) })
|
|
93
|
-
] }),
|
|
94
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
95
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Shell" }) }),
|
|
96
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(bashCalls) })
|
|
97
|
-
] }),
|
|
98
|
-
mcpCount > 0 ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
99
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " MCP" }) }),
|
|
100
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(mcpCount) })
|
|
101
|
-
] }) : null,
|
|
102
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
103
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools / session" }) }),
|
|
104
|
-
/* @__PURE__ */ jsx4(Text4, { children: "~" + fmtNum(toolsPerSession) })
|
|
105
|
-
] }),
|
|
106
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
107
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Cost / session" }) }),
|
|
108
|
-
/* @__PURE__ */ jsx4(Text4, { children: "~$" + Math.round(perSession).toLocaleString() })
|
|
109
|
-
] }),
|
|
110
|
-
topAgent ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
111
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Top agent" }) }),
|
|
112
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `${topAgent.label} (${fmtNum(topAgent.sessions)})` })
|
|
113
|
-
] }) : null
|
|
114
|
-
] });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// src/cli/render/ink/panels/LeaksPanel.tsx
|
|
118
|
-
import { Box as Box5, Text as Text5 } from "ink";
|
|
119
|
-
|
|
120
|
-
// src/cli/render/scan-derive.ts
|
|
42
|
+
// src/config/keyed-guard.ts
|
|
121
43
|
import chalk from "chalk";
|
|
122
|
-
import stringWidth from "string-width";
|
|
123
|
-
function topRulesByVerdict(sections, verdict, n) {
|
|
124
|
-
const matched = [];
|
|
125
|
-
for (const section of sections) {
|
|
126
|
-
for (const rule of section.rules) {
|
|
127
|
-
const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
|
|
128
|
-
if (matches) matched.push({ name: rule.name, count: rule.findings.length });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return matched.sort((a, b) => b.count - a.count).slice(0, n);
|
|
132
|
-
}
|
|
133
|
-
function computeLoopWaste(loops, totalToolCalls) {
|
|
134
|
-
const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
135
|
-
const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
|
|
136
|
-
return { wastedCalls, wastePct };
|
|
137
|
-
}
|
|
138
|
-
function rollupByShield(sections, topRulesPerShield = 3) {
|
|
139
|
-
const out = [];
|
|
140
|
-
for (const section of sections) {
|
|
141
|
-
if (section.sourceType !== "shield") continue;
|
|
142
|
-
if (!section.shieldKey) continue;
|
|
143
|
-
const totalCatches = section.blockedCount + section.reviewCount;
|
|
144
|
-
const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
|
|
145
|
-
out.push({
|
|
146
|
-
shieldName: section.shieldKey,
|
|
147
|
-
totalCatches,
|
|
148
|
-
blockCatches: section.blockedCount,
|
|
149
|
-
reviewCatches: section.reviewCount,
|
|
150
|
-
topRuleLabels
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return out.sort((a, b) => b.totalCatches - a.totalCatches);
|
|
154
|
-
}
|
|
155
|
-
function originForRule(ruleName, sections, enabled) {
|
|
156
|
-
for (const section of sections) {
|
|
157
|
-
if (section.rules.some((r) => r.name === ruleName)) {
|
|
158
|
-
if (section.sourceType === "default") return "default";
|
|
159
|
-
if (section.sourceType === "shield") {
|
|
160
|
-
const key = section.shieldKey ?? section.id;
|
|
161
|
-
return enabled.has(key) ? `\u2713 shield:${key}` : `needs shield:${key}`;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return "";
|
|
166
|
-
}
|
|
167
|
-
function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
|
|
168
|
-
const t = new Date(timestamp).getTime();
|
|
169
|
-
if (Number.isNaN(t)) return "?";
|
|
170
|
-
const days = Math.floor((now.getTime() - t) / 864e5);
|
|
171
|
-
if (days < 1) return "today";
|
|
172
|
-
if (days > 90) return "90d+";
|
|
173
|
-
return `${days}d`;
|
|
174
|
-
}
|
|
175
44
|
|
|
176
|
-
// src/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
] }, i)),
|
|
198
|
-
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
199
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2192 " }),
|
|
200
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "DLP" }),
|
|
201
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
|
|
202
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "node9 mask" }),
|
|
203
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: " (runtime + cleanup)" })
|
|
204
|
-
] })
|
|
205
|
-
] });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// src/cli/render/ink/panels/BlockedPanel.tsx
|
|
209
|
-
import { Box as Box6, Text as Text6 } from "ink";
|
|
210
|
-
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
211
|
-
var ROW_LIMIT2 = 12;
|
|
212
|
-
function BlockedPanel({
|
|
213
|
-
summary,
|
|
214
|
-
width,
|
|
215
|
-
enabledShields = []
|
|
216
|
-
}) {
|
|
217
|
-
const enabled = new Set(enabledShields);
|
|
218
|
-
const allRules = topRulesByVerdict(summary.sections, "block", Number.MAX_SAFE_INTEGER);
|
|
219
|
-
const rules = allRules.slice(0, ROW_LIMIT2);
|
|
220
|
-
if (rules.length === 0) return null;
|
|
221
|
-
const origins = rules.map((r) => originForRule(r.name, summary.sections, enabled));
|
|
222
|
-
const anyNeeds = origins.some((o) => o.startsWith("needs shield:"));
|
|
223
|
-
const footer = enabled.size === 0 ? "\u2192 install node9 + enable shields above" : anyNeeds ? "\u2192 \u2713 marks rules from enabled shields \xB7 enable the rest above" : "\u2192 \u2713 all these rules come from enabled shields";
|
|
224
|
-
return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
|
|
225
|
-
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "red", children: [
|
|
226
|
-
"WOULD HAVE BLOCKED",
|
|
227
|
-
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: topOf(rules.length, allRules.length) })
|
|
228
|
-
] }),
|
|
229
|
-
rules.map((rule, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
230
|
-
/* @__PURE__ */ jsx6(Box6, { width: 3, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717" }) }),
|
|
231
|
-
/* @__PURE__ */ jsx6(Box6, { width: 24, children: /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: rule.name }) }),
|
|
232
|
-
/* @__PURE__ */ jsx6(Box6, { width: 6, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
233
|
-
/* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
234
|
-
] }, i)),
|
|
235
|
-
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: footer }) })
|
|
236
|
-
] });
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// src/cli/render/ink/panels/BlastRadiusPanel.tsx
|
|
240
|
-
import { Box as Box7, Text as Text7 } from "ink";
|
|
241
|
-
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
242
|
-
var ROW_LIMIT3 = 4;
|
|
243
|
-
var ENV_ROW_LIMIT = 3;
|
|
244
|
-
function BlastRadiusPanel({
|
|
245
|
-
blast,
|
|
246
|
-
blastExposures,
|
|
247
|
-
width
|
|
248
|
-
}) {
|
|
249
|
-
if (blastExposures === 0) return null;
|
|
250
|
-
const shown = Math.min(blast.reachable.length, ROW_LIMIT3) + Math.min(blast.envFindings.length, ENV_ROW_LIMIT);
|
|
251
|
-
return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
|
|
252
|
-
/* @__PURE__ */ jsxs7(Text7, { bold: true, color: "yellow", children: [
|
|
253
|
-
"BLAST RADIUS",
|
|
254
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: topOf(shown, blastExposures) })
|
|
255
|
-
] }),
|
|
256
|
-
blast.reachable.slice(0, ROW_LIMIT3).map((path, i) => {
|
|
257
|
-
const desc = path.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
|
|
258
|
-
return /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
259
|
-
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }) }),
|
|
260
|
-
/* @__PURE__ */ jsx7(Box7, { width: 36, children: /* @__PURE__ */ jsx7(Text7, { children: path.label }) }),
|
|
261
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: desc })
|
|
262
|
-
] }, i);
|
|
263
|
-
}),
|
|
264
|
-
blast.envFindings.slice(0, 3).map((env, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
265
|
-
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0" }) }),
|
|
266
|
-
/* @__PURE__ */ jsx7(Text7, { children: env.key }),
|
|
267
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` (${env.patternName})` })
|
|
268
|
-
] }, `env-${i}`)),
|
|
269
|
-
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
270
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2192 " }),
|
|
271
|
-
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "project-jail" }),
|
|
272
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " shield blocks agent reads of these paths" })
|
|
273
|
-
] })
|
|
274
|
-
] });
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/cli/render/ink/panels/ReviewQueuePanel.tsx
|
|
278
|
-
import { Box as Box8, Text as Text8 } from "ink";
|
|
279
|
-
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
280
|
-
var ROW_LIMIT4 = 5;
|
|
281
|
-
function ReviewQueuePanel({
|
|
282
|
-
summary,
|
|
283
|
-
width,
|
|
284
|
-
enabledShields = []
|
|
285
|
-
}) {
|
|
286
|
-
const enabled = new Set(enabledShields);
|
|
287
|
-
const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT4);
|
|
288
|
-
if (rules.length === 0) return null;
|
|
289
|
-
return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
290
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "REVIEW QUEUE" }),
|
|
291
|
-
rules.map((rule, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
292
|
-
/* @__PURE__ */ jsx8(Box8, { width: 22, children: /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", children: rule.name }) }),
|
|
293
|
-
/* @__PURE__ */ jsx8(Box8, { width: 6, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
294
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
295
|
-
] }, i)),
|
|
296
|
-
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
297
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
|
|
298
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", wrap: "truncate-end", children: "runtime approval" })
|
|
299
|
-
] })
|
|
300
|
-
] });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// src/cli/render/ink/panels/AgentLoopsPanel.tsx
|
|
304
|
-
import { Box as Box9, Text as Text9 } from "ink";
|
|
305
|
-
import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
306
|
-
var TOOL_ROW_LIMIT = 5;
|
|
307
|
-
var STUCK_ROW_LIMIT = 1;
|
|
308
|
-
function fmtNum2(n) {
|
|
309
|
-
return n.toLocaleString();
|
|
310
|
-
}
|
|
311
|
-
function trimRight(s, width) {
|
|
312
|
-
if (s.length <= width) return s;
|
|
313
|
-
return "\u2026" + s.slice(s.length - (width - 1));
|
|
314
|
-
}
|
|
315
|
-
function AgentLoopsPanel({ loopFindings, width }) {
|
|
316
|
-
if (loopFindings.length === 0) return null;
|
|
317
|
-
const byTool = /* @__PURE__ */ new Map();
|
|
318
|
-
let totalRepeats = 0;
|
|
319
|
-
for (const f of loopFindings) {
|
|
320
|
-
const repeats = Math.max(0, f.count - 1);
|
|
321
|
-
byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
|
|
322
|
-
totalRepeats += repeats;
|
|
323
|
-
}
|
|
324
|
-
const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOOL_ROW_LIMIT);
|
|
325
|
-
const topStuck = [...loopFindings].sort((a, b) => b.count - a.count).slice(0, STUCK_ROW_LIMIT);
|
|
326
|
-
return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
327
|
-
/* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
|
|
328
|
-
"AGENT LOOPS",
|
|
329
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: topOf(toolEntries.length, byTool.size) })
|
|
330
|
-
] }),
|
|
331
|
-
toolEntries.map(([tool, repeats]) => {
|
|
332
|
-
const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
|
|
333
|
-
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
334
|
-
/* @__PURE__ */ jsx9(Box9, { width: 10, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: tool }) }),
|
|
335
|
-
/* @__PURE__ */ jsx9(Box9, { width: 14, children: /* @__PURE__ */ jsx9(Text9, { children: `\xD7${fmtNum2(repeats)}` }) }),
|
|
336
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${pct}%` })
|
|
337
|
-
] }, tool);
|
|
338
|
-
}),
|
|
339
|
-
topStuck.length > 0 ? /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
340
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Top stuck:" }),
|
|
341
|
-
topStuck.map((f, i) => {
|
|
342
|
-
const target = trimRight(f.commandPreview || f.toolName, 32);
|
|
343
|
-
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
344
|
-
/* @__PURE__ */ jsx9(Box9, { width: 8, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: `\xD7${fmtNum2(f.count)}` }) }),
|
|
345
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: target })
|
|
346
|
-
] }, `stuck-${i}`);
|
|
45
|
+
// src/config-schema.ts
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
48
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
49
|
+
});
|
|
50
|
+
var SmartConditionSchema = z.object({
|
|
51
|
+
field: z.string().min(1, "Condition field must not be empty"),
|
|
52
|
+
op: z.enum(
|
|
53
|
+
[
|
|
54
|
+
"matches",
|
|
55
|
+
"notMatches",
|
|
56
|
+
"contains",
|
|
57
|
+
"notContains",
|
|
58
|
+
"exists",
|
|
59
|
+
"notExists",
|
|
60
|
+
"matchesGlob",
|
|
61
|
+
"notMatchesGlob"
|
|
62
|
+
],
|
|
63
|
+
{
|
|
64
|
+
errorMap: () => ({
|
|
65
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
347
66
|
})
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
value: z.string().optional(),
|
|
70
|
+
flags: z.string().optional()
|
|
71
|
+
}).refine(
|
|
72
|
+
(c) => {
|
|
73
|
+
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
{ message: "matchesGlob and notMatchesGlob conditions require a value field" }
|
|
77
|
+
);
|
|
78
|
+
var SmartRuleSchema = z.object({
|
|
79
|
+
name: z.string().optional(),
|
|
80
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
81
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
82
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
83
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
84
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
85
|
+
}),
|
|
86
|
+
reason: z.string().optional(),
|
|
87
|
+
description: z.string().optional(),
|
|
88
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
89
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
90
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
91
|
+
dependsOnState: z.array(z.string()).transform(
|
|
92
|
+
(arr) => arr.filter(
|
|
93
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
94
|
+
)
|
|
95
|
+
).optional(),
|
|
96
|
+
recoveryCommand: z.string().optional()
|
|
97
|
+
});
|
|
98
|
+
var ConfigFileSchema = z.object({
|
|
99
|
+
version: z.string().optional(),
|
|
100
|
+
settings: z.object({
|
|
101
|
+
mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
102
|
+
autoStartDaemon: z.boolean().optional(),
|
|
103
|
+
// enableUndo / policy.snapshot: removed with the undo feature. The
|
|
104
|
+
// schema strips undeclared keys silently (plain zod object — no
|
|
105
|
+
// .strict(), no .passthrough()), so a config still carrying them stays
|
|
106
|
+
// valid and simply loses them. Keeping the fields would instead make
|
|
107
|
+
// `node9_config_get` advertise a removed feature as a live knob.
|
|
108
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
109
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
110
|
+
approvalTimeoutSeconds: z.number().nonnegative().optional(),
|
|
111
|
+
flightRecorder: z.boolean().optional(),
|
|
112
|
+
approvers: z.object({
|
|
113
|
+
native: z.boolean().optional(),
|
|
114
|
+
browser: z.boolean().optional(),
|
|
115
|
+
cloud: z.boolean().optional(),
|
|
116
|
+
terminal: z.boolean().optional()
|
|
117
|
+
}).optional(),
|
|
118
|
+
environment: z.string().optional(),
|
|
119
|
+
slackEnabled: z.boolean().optional(),
|
|
120
|
+
enableTrustSessions: z.boolean().optional(),
|
|
121
|
+
allowGlobalPause: z.boolean().optional(),
|
|
122
|
+
auditHashArgs: z.boolean().optional(),
|
|
123
|
+
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
124
|
+
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
125
|
+
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
126
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
127
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
128
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
129
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
130
|
+
reviewChannel: z.enum(["ask", "approver"]).optional(),
|
|
131
|
+
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
132
|
+
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
133
|
+
// must run them from the CLI. node9's threat model is the agent itself.
|
|
134
|
+
mcpAllowWeakening: z.boolean().optional(),
|
|
135
|
+
// Auto-wire reconciler (P3 2.6): auto-wrap new ungoverned MCP servers vs
|
|
136
|
+
// nudge-only (default), and the scan cadence in minutes.
|
|
137
|
+
mcpAutoWrap: z.boolean().optional(),
|
|
138
|
+
mcpReconcileIntervalMinutes: z.number().positive().optional(),
|
|
139
|
+
mcpStaleAfterDays: z.number().min(0).optional(),
|
|
140
|
+
cloudSyncIntervalHours: z.number().positive().optional(),
|
|
141
|
+
// Seconds-granular override for the cloud policy sync cadence. Wins over
|
|
142
|
+
// cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
|
|
143
|
+
// for an incident; clamped to a 15s floor so it can't hammer the API.
|
|
144
|
+
// Unset → falls back to hours, then the 5h default.
|
|
145
|
+
cloudSyncIntervalSeconds: z.number().positive().optional(),
|
|
146
|
+
// Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
|
|
147
|
+
// to true; set false to fall back to local-only auditing.
|
|
148
|
+
shipper: z.object({
|
|
149
|
+
enabled: z.boolean().optional(),
|
|
150
|
+
intervalSeconds: z.number().min(5).optional()
|
|
151
|
+
}).optional()
|
|
152
|
+
}).optional(),
|
|
153
|
+
policy: z.object({
|
|
154
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
155
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
156
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
157
|
+
toolInspection: z.record(z.string()).optional(),
|
|
158
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
159
|
+
dlp: z.object({
|
|
160
|
+
enabled: z.boolean().optional(),
|
|
161
|
+
scanIgnoredTools: z.boolean().optional(),
|
|
162
|
+
pii: z.enum(["off", "block"]).optional(),
|
|
163
|
+
reviewAction: z.enum(["review", "block"]).optional()
|
|
164
|
+
}).optional(),
|
|
165
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
166
|
+
// deliberately exclude 'off' — tighten-only.
|
|
167
|
+
commandChecks: z.object({
|
|
168
|
+
inlineExec: z.enum(["off", "review", "block"]).optional(),
|
|
169
|
+
rmAdvisory: z.enum(["off", "review", "block"]).optional(),
|
|
170
|
+
chmod: z.enum(["off", "review", "block"]).optional(),
|
|
171
|
+
sqlDdl: z.enum(["off", "review", "block"]).optional(),
|
|
172
|
+
evalDynamic: z.enum(["review", "block"]).optional(),
|
|
173
|
+
pipeChainHigh: z.enum(["review", "block"]).optional()
|
|
174
|
+
}).optional(),
|
|
175
|
+
egress: z.object({
|
|
176
|
+
enabled: z.boolean().optional(),
|
|
177
|
+
mode: z.enum(["off", "review", "block"]).optional(),
|
|
178
|
+
allow: z.array(z.string()).optional(),
|
|
179
|
+
deny: z.array(z.string()).optional(),
|
|
180
|
+
allowPrivate: z.boolean().optional()
|
|
181
|
+
}).optional(),
|
|
182
|
+
loopDetection: z.object({
|
|
183
|
+
enabled: z.boolean().optional(),
|
|
184
|
+
threshold: z.number().min(2).optional(),
|
|
185
|
+
windowSeconds: z.number().min(10).optional()
|
|
186
|
+
}).optional(),
|
|
187
|
+
injectionScan: z.object({
|
|
188
|
+
enabled: z.boolean().optional(),
|
|
189
|
+
minConfidence: z.enum(["medium", "high"]).optional(),
|
|
190
|
+
allow: z.array(z.string()).optional()
|
|
191
|
+
}).optional(),
|
|
192
|
+
skillPinning: z.object({
|
|
193
|
+
enabled: z.boolean().optional(),
|
|
194
|
+
mode: z.enum(["warn", "block"]).optional(),
|
|
195
|
+
roots: z.array(z.string()).optional()
|
|
196
|
+
}).optional()
|
|
197
|
+
}).optional(),
|
|
198
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
199
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
358
200
|
|
|
359
|
-
// src/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
201
|
+
// src/shields.ts
|
|
202
|
+
import fs from "fs";
|
|
203
|
+
import path from "path";
|
|
204
|
+
import os from "os";
|
|
363
205
|
|
|
364
206
|
// packages/policy-engine/dist/index.mjs
|
|
365
207
|
import safeRegex from "safe-regex2";
|
|
@@ -1637,40 +1479,425 @@ var redis_default = {
|
|
|
1637
1479
|
verdict: "review",
|
|
1638
1480
|
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
1639
1481
|
}
|
|
1640
|
-
],
|
|
1641
|
-
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
1642
|
-
};
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
};
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1482
|
+
],
|
|
1483
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
1484
|
+
};
|
|
1485
|
+
function validateShieldDefinition(raw) {
|
|
1486
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1487
|
+
return { error: "Shield file is not an object" };
|
|
1488
|
+
}
|
|
1489
|
+
const r = raw;
|
|
1490
|
+
if (typeof r.name !== "string" || !r.name) return { error: "Shield file missing 'name'" };
|
|
1491
|
+
if (typeof r.description !== "string") return { error: "Shield file missing 'description'" };
|
|
1492
|
+
if (!Array.isArray(r.aliases)) return { error: "Shield file missing 'aliases' array" };
|
|
1493
|
+
if (!Array.isArray(r.smartRules)) return { error: "Shield file missing 'smartRules' array" };
|
|
1494
|
+
if (!Array.isArray(r.dangerousWords))
|
|
1495
|
+
return { error: "Shield file missing 'dangerousWords' array" };
|
|
1496
|
+
return { ok: r };
|
|
1497
|
+
}
|
|
1498
|
+
var BUILTIN_SHIELDS = {
|
|
1499
|
+
[aws_default.name]: aws_default,
|
|
1500
|
+
[bash_safe_default.name]: bash_safe_default,
|
|
1501
|
+
[docker_default.name]: docker_default,
|
|
1502
|
+
[filesystem_default.name]: filesystem_default,
|
|
1503
|
+
[github_default.name]: github_default,
|
|
1504
|
+
[k8s_default.name]: k8s_default,
|
|
1505
|
+
[mongodb_default.name]: mongodb_default,
|
|
1506
|
+
[postgres_default.name]: postgres_default,
|
|
1507
|
+
[project_jail_default.name]: project_jail_default,
|
|
1508
|
+
[redis_default.name]: redis_default
|
|
1509
|
+
};
|
|
1510
|
+
function assertBuiltinShieldRegexesAreSafe() {
|
|
1511
|
+
for (const shield of Object.values(BUILTIN_SHIELDS)) {
|
|
1512
|
+
for (const rule of shield.smartRules) {
|
|
1513
|
+
const conditions = rule.conditions ?? [];
|
|
1514
|
+
for (const cond of conditions) {
|
|
1515
|
+
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
1516
|
+
const pattern = cond.value;
|
|
1517
|
+
if (!pattern) continue;
|
|
1518
|
+
if (!safeRegex3(pattern)) {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
assertBuiltinShieldRegexesAreSafe();
|
|
1528
|
+
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
1529
|
+
|
|
1530
|
+
// src/shields.ts
|
|
1531
|
+
var USER_SHIELDS_DIR = path.join(os.homedir(), ".node9", "shields");
|
|
1532
|
+
function loadUserShields() {
|
|
1533
|
+
const result = {};
|
|
1534
|
+
let entries;
|
|
1535
|
+
try {
|
|
1536
|
+
entries = fs.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
if (err.code !== "ENOENT") {
|
|
1539
|
+
process.stderr.write(
|
|
1540
|
+
`[node9] Could not read user shields dir ${USER_SHIELDS_DIR}: ${String(err)}
|
|
1541
|
+
`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
return result;
|
|
1545
|
+
}
|
|
1546
|
+
for (const file of entries) {
|
|
1547
|
+
const filePath = path.join(USER_SHIELDS_DIR, file);
|
|
1548
|
+
try {
|
|
1549
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1550
|
+
const v = validateShieldDefinition(raw);
|
|
1551
|
+
if ("error" in v) {
|
|
1552
|
+
process.stderr.write(`[node9] ${v.error}: ${filePath}
|
|
1553
|
+
`);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
result[v.ok.name] = v.ok;
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
|
|
1559
|
+
`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return result;
|
|
1563
|
+
}
|
|
1564
|
+
function buildSHIELDS() {
|
|
1565
|
+
return { ...BUILTIN_SHIELDS, ...loadUserShields() };
|
|
1566
|
+
}
|
|
1567
|
+
var SHIELDS = buildSHIELDS();
|
|
1568
|
+
var SHIELDS_STATE_FILE = path.join(os.homedir(), ".node9", "shields.json");
|
|
1569
|
+
|
|
1570
|
+
// src/config/index.ts
|
|
1571
|
+
var CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
|
|
1572
|
+
|
|
1573
|
+
// src/cli/render/ink/panels/cost-note.ts
|
|
1574
|
+
var COST_LABEL = "COST \xB7 based on API value";
|
|
1575
|
+
function costNoteLines(keyed) {
|
|
1576
|
+
return keyed ? ["Your plan and exact spend:", "dashboard \u2192 Report"] : ["To see your plan or exact spend, connect:", "node9 login (for free)"];
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/cli/render/ink/panels/CostPanel.tsx
|
|
1580
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1581
|
+
var LABEL_W = 16;
|
|
1582
|
+
function CostPanel({ summary, width, keyed }) {
|
|
1583
|
+
const total = summary.stats.totalCostUSD;
|
|
1584
|
+
return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1585
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: COST_LABEL }),
|
|
1586
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1587
|
+
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
|
|
1588
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: formatCost(total) })
|
|
1589
|
+
] }),
|
|
1590
|
+
summary.byAgent.map((agent) => /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1591
|
+
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: agent.label }) }),
|
|
1592
|
+
/* @__PURE__ */ jsx3(Text3, { children: formatCost(agent.costUSD) })
|
|
1593
|
+
] }, agent.id)),
|
|
1594
|
+
summary.loopWastedUSD > 0 ? /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1595
|
+
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Wasted on loops" }) }),
|
|
1596
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "~" + formatCost(summary.loopWastedUSD) })
|
|
1597
|
+
] }) : null,
|
|
1598
|
+
costNoteLines(keyed ?? false).map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: line }, line))
|
|
1599
|
+
] });
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/cli/render/ink/panels/ActivityPanel.tsx
|
|
1603
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
1604
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1605
|
+
var LABEL_W2 = 16;
|
|
1606
|
+
function fmtNum(n) {
|
|
1607
|
+
return n.toLocaleString();
|
|
1608
|
+
}
|
|
1609
|
+
function countMcpFindings(scan) {
|
|
1610
|
+
let n = 0;
|
|
1611
|
+
for (const f of scan.findings) {
|
|
1612
|
+
if (f.toolName.startsWith("mcp__")) n++;
|
|
1613
|
+
}
|
|
1614
|
+
return n;
|
|
1615
|
+
}
|
|
1616
|
+
function ActivityPanel({ summary, scan, width }) {
|
|
1617
|
+
const { sessions, totalToolCalls, bashCalls, totalCostUSD } = summary.stats;
|
|
1618
|
+
const perSession = sessions > 0 ? totalCostUSD / sessions : 0;
|
|
1619
|
+
const toolsPerSession = sessions > 0 ? Math.round(totalToolCalls / sessions) : 0;
|
|
1620
|
+
const mcpCount = countMcpFindings(scan);
|
|
1621
|
+
const topAgent = summary.byAgent.slice().sort((a, b) => b.sessions - a.sessions)[0];
|
|
1622
|
+
return /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1623
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "ACTIVITY" }),
|
|
1624
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1625
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Sessions" }) }),
|
|
1626
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: fmtNum(sessions) })
|
|
1627
|
+
] }),
|
|
1628
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1629
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools" }) }),
|
|
1630
|
+
/* @__PURE__ */ jsx4(Text4, { children: fmtNum(totalToolCalls) })
|
|
1631
|
+
] }),
|
|
1632
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1633
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Shell" }) }),
|
|
1634
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(bashCalls) })
|
|
1635
|
+
] }),
|
|
1636
|
+
mcpCount > 0 ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1637
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " MCP" }) }),
|
|
1638
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(mcpCount) })
|
|
1639
|
+
] }) : null,
|
|
1640
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1641
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools / session" }) }),
|
|
1642
|
+
/* @__PURE__ */ jsx4(Text4, { children: "~" + fmtNum(toolsPerSession) })
|
|
1643
|
+
] }),
|
|
1644
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1645
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Cost / session" }) }),
|
|
1646
|
+
/* @__PURE__ */ jsx4(Text4, { children: "~$" + Math.round(perSession).toLocaleString() })
|
|
1647
|
+
] }),
|
|
1648
|
+
topAgent ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1649
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Top agent" }) }),
|
|
1650
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `${topAgent.label} (${fmtNum(topAgent.sessions)})` })
|
|
1651
|
+
] }) : null
|
|
1652
|
+
] });
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/cli/render/ink/panels/LeaksPanel.tsx
|
|
1656
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
1657
|
+
|
|
1658
|
+
// src/cli/render/scan-derive.ts
|
|
1659
|
+
import chalk2 from "chalk";
|
|
1660
|
+
import stringWidth from "string-width";
|
|
1661
|
+
function topRulesByVerdict(sections, verdict, n) {
|
|
1662
|
+
const matched = [];
|
|
1663
|
+
for (const section of sections) {
|
|
1664
|
+
for (const rule of section.rules) {
|
|
1665
|
+
const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
|
|
1666
|
+
if (matches) matched.push({ name: rule.name, count: rule.findings.length });
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return matched.sort((a, b) => b.count - a.count).slice(0, n);
|
|
1670
|
+
}
|
|
1671
|
+
function computeLoopWaste(loops, totalToolCalls) {
|
|
1672
|
+
const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
1673
|
+
const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
|
|
1674
|
+
return { wastedCalls, wastePct };
|
|
1675
|
+
}
|
|
1676
|
+
function rollupByShield(sections, topRulesPerShield = 3) {
|
|
1677
|
+
const out = [];
|
|
1678
|
+
for (const section of sections) {
|
|
1679
|
+
if (section.sourceType !== "shield") continue;
|
|
1680
|
+
if (!section.shieldKey) continue;
|
|
1681
|
+
const totalCatches = section.blockedCount + section.reviewCount;
|
|
1682
|
+
const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
|
|
1683
|
+
out.push({
|
|
1684
|
+
shieldName: section.shieldKey,
|
|
1685
|
+
totalCatches,
|
|
1686
|
+
blockCatches: section.blockedCount,
|
|
1687
|
+
reviewCatches: section.reviewCount,
|
|
1688
|
+
topRuleLabels
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
return out.sort((a, b) => b.totalCatches - a.totalCatches);
|
|
1692
|
+
}
|
|
1693
|
+
function originForRule(ruleName, sections, enabled) {
|
|
1694
|
+
for (const section of sections) {
|
|
1695
|
+
if (section.rules.some((r) => r.name === ruleName)) {
|
|
1696
|
+
if (section.sourceType === "default") return "default";
|
|
1697
|
+
if (section.sourceType === "shield") {
|
|
1698
|
+
const key = section.shieldKey ?? section.id;
|
|
1699
|
+
return enabled.has(key) ? `\u2713 shield:${key}` : `needs shield:${key}`;
|
|
1668
1700
|
}
|
|
1669
1701
|
}
|
|
1670
1702
|
}
|
|
1703
|
+
return "";
|
|
1671
1704
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1705
|
+
function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
|
|
1706
|
+
const t = new Date(timestamp).getTime();
|
|
1707
|
+
if (Number.isNaN(t)) return "?";
|
|
1708
|
+
const days = Math.floor((now.getTime() - t) / 864e5);
|
|
1709
|
+
if (days < 1) return "today";
|
|
1710
|
+
if (days > 90) return "90d+";
|
|
1711
|
+
return `${days}d`;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/cli/render/ink/panels/title-count.ts
|
|
1715
|
+
function topOf(shown, total) {
|
|
1716
|
+
return total > shown ? ` \xB7 top ${shown} of ${total}` : "";
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/cli/render/ink/panels/LeaksPanel.tsx
|
|
1720
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1721
|
+
var ROW_LIMIT = 4;
|
|
1722
|
+
function LeaksPanel({ summary, width, now = /* @__PURE__ */ new Date() }) {
|
|
1723
|
+
const leaks = summary.leaks;
|
|
1724
|
+
if (leaks.length === 0) return null;
|
|
1725
|
+
return /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
|
|
1726
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, color: "red", children: [
|
|
1727
|
+
"CREDENTIAL LEAKS",
|
|
1728
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: topOf(Math.min(leaks.length, ROW_LIMIT), leaks.length) })
|
|
1729
|
+
] }),
|
|
1730
|
+
leaks.slice(0, ROW_LIMIT).map((leak, i) => /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1731
|
+
/* @__PURE__ */ jsx5(Box5, { width: 5, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: relativeDate(leak.timestamp, now).padStart(4) }) }),
|
|
1732
|
+
/* @__PURE__ */ jsx5(Box5, { width: 16, children: /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, wrap: "truncate-end", children: leak.patternName }) }),
|
|
1733
|
+
/* @__PURE__ */ jsx5(Box5, { width: 15, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: `[${leak.toolName}]` }) }),
|
|
1734
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: leak.agent })
|
|
1735
|
+
] }, i)),
|
|
1736
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1737
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2192 " }),
|
|
1738
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "DLP" }),
|
|
1739
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
|
|
1740
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "node9 mask" }),
|
|
1741
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: " (runtime + cleanup)" })
|
|
1742
|
+
] })
|
|
1743
|
+
] });
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/cli/render/ink/panels/BlockedPanel.tsx
|
|
1747
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
1748
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1749
|
+
var ROW_LIMIT2 = 12;
|
|
1750
|
+
function BlockedPanel({
|
|
1751
|
+
summary,
|
|
1752
|
+
width,
|
|
1753
|
+
enabledShields = []
|
|
1754
|
+
}) {
|
|
1755
|
+
const enabled = new Set(enabledShields);
|
|
1756
|
+
const allRules = topRulesByVerdict(summary.sections, "block", Number.MAX_SAFE_INTEGER);
|
|
1757
|
+
const rules = allRules.slice(0, ROW_LIMIT2);
|
|
1758
|
+
if (rules.length === 0) return null;
|
|
1759
|
+
const origins = rules.map((r) => originForRule(r.name, summary.sections, enabled));
|
|
1760
|
+
const anyNeeds = origins.some((o) => o.startsWith("needs shield:"));
|
|
1761
|
+
const footer = enabled.size === 0 ? "\u2192 install node9 + enable shields above" : anyNeeds ? "\u2192 \u2713 marks rules from enabled shields \xB7 enable the rest above" : "\u2192 \u2713 all these rules come from enabled shields";
|
|
1762
|
+
return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
|
|
1763
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "red", children: [
|
|
1764
|
+
"WOULD HAVE BLOCKED",
|
|
1765
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: topOf(rules.length, allRules.length) })
|
|
1766
|
+
] }),
|
|
1767
|
+
rules.map((rule, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1768
|
+
/* @__PURE__ */ jsx6(Box6, { width: 3, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717" }) }),
|
|
1769
|
+
/* @__PURE__ */ jsx6(Box6, { width: 24, children: /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: rule.name }) }),
|
|
1770
|
+
/* @__PURE__ */ jsx6(Box6, { width: 6, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
1771
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
1772
|
+
] }, i)),
|
|
1773
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: footer }) })
|
|
1774
|
+
] });
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/cli/render/ink/panels/BlastRadiusPanel.tsx
|
|
1778
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
1779
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1780
|
+
var ROW_LIMIT3 = 4;
|
|
1781
|
+
var ENV_ROW_LIMIT = 3;
|
|
1782
|
+
function BlastRadiusPanel({
|
|
1783
|
+
blast,
|
|
1784
|
+
blastExposures,
|
|
1785
|
+
width
|
|
1786
|
+
}) {
|
|
1787
|
+
if (blastExposures === 0) return null;
|
|
1788
|
+
const shown = Math.min(blast.reachable.length, ROW_LIMIT3) + Math.min(blast.envFindings.length, ENV_ROW_LIMIT);
|
|
1789
|
+
return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
|
|
1790
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, color: "yellow", children: [
|
|
1791
|
+
"BLAST RADIUS",
|
|
1792
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: topOf(shown, blastExposures) })
|
|
1793
|
+
] }),
|
|
1794
|
+
blast.reachable.slice(0, ROW_LIMIT3).map((path2, i) => {
|
|
1795
|
+
const desc = path2.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
|
|
1796
|
+
return /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1797
|
+
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }) }),
|
|
1798
|
+
/* @__PURE__ */ jsx7(Box7, { width: 36, children: /* @__PURE__ */ jsx7(Text7, { children: path2.label }) }),
|
|
1799
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: desc })
|
|
1800
|
+
] }, i);
|
|
1801
|
+
}),
|
|
1802
|
+
blast.envFindings.slice(0, 3).map((env, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1803
|
+
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0" }) }),
|
|
1804
|
+
/* @__PURE__ */ jsx7(Text7, { children: env.key }),
|
|
1805
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` (${env.patternName})` })
|
|
1806
|
+
] }, `env-${i}`)),
|
|
1807
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1808
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2192 " }),
|
|
1809
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "project-jail" }),
|
|
1810
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " shield blocks agent reads of these paths" })
|
|
1811
|
+
] })
|
|
1812
|
+
] });
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/cli/render/ink/panels/ReviewQueuePanel.tsx
|
|
1816
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
1817
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1818
|
+
var ROW_LIMIT4 = 5;
|
|
1819
|
+
function ReviewQueuePanel({
|
|
1820
|
+
summary,
|
|
1821
|
+
width,
|
|
1822
|
+
enabledShields = []
|
|
1823
|
+
}) {
|
|
1824
|
+
const enabled = new Set(enabledShields);
|
|
1825
|
+
const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT4);
|
|
1826
|
+
if (rules.length === 0) return null;
|
|
1827
|
+
return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1828
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "REVIEW QUEUE" }),
|
|
1829
|
+
rules.map((rule, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
1830
|
+
/* @__PURE__ */ jsx8(Box8, { width: 22, children: /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", children: rule.name }) }),
|
|
1831
|
+
/* @__PURE__ */ jsx8(Box8, { width: 6, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
1832
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
1833
|
+
] }, i)),
|
|
1834
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
1835
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
|
|
1836
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", wrap: "truncate-end", children: "runtime approval" })
|
|
1837
|
+
] })
|
|
1838
|
+
] });
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/cli/render/ink/panels/AgentLoopsPanel.tsx
|
|
1842
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
1843
|
+
import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1844
|
+
var TOOL_ROW_LIMIT = 5;
|
|
1845
|
+
var STUCK_ROW_LIMIT = 1;
|
|
1846
|
+
function fmtNum2(n) {
|
|
1847
|
+
return n.toLocaleString();
|
|
1848
|
+
}
|
|
1849
|
+
function trimRight(s, width) {
|
|
1850
|
+
if (s.length <= width) return s;
|
|
1851
|
+
return "\u2026" + s.slice(s.length - (width - 1));
|
|
1852
|
+
}
|
|
1853
|
+
function AgentLoopsPanel({ loopFindings, width }) {
|
|
1854
|
+
if (loopFindings.length === 0) return null;
|
|
1855
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
1856
|
+
let totalRepeats = 0;
|
|
1857
|
+
for (const f of loopFindings) {
|
|
1858
|
+
const repeats = Math.max(0, f.count - 1);
|
|
1859
|
+
byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
|
|
1860
|
+
totalRepeats += repeats;
|
|
1861
|
+
}
|
|
1862
|
+
const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOOL_ROW_LIMIT);
|
|
1863
|
+
const topStuck = [...loopFindings].sort((a, b) => b.count - a.count).slice(0, STUCK_ROW_LIMIT);
|
|
1864
|
+
return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1865
|
+
/* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
|
|
1866
|
+
"AGENT LOOPS",
|
|
1867
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: topOf(toolEntries.length, byTool.size) })
|
|
1868
|
+
] }),
|
|
1869
|
+
toolEntries.map(([tool, repeats]) => {
|
|
1870
|
+
const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
|
|
1871
|
+
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1872
|
+
/* @__PURE__ */ jsx9(Box9, { width: 10, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: tool }) }),
|
|
1873
|
+
/* @__PURE__ */ jsx9(Box9, { width: 14, children: /* @__PURE__ */ jsx9(Text9, { children: `\xD7${fmtNum2(repeats)}` }) }),
|
|
1874
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${pct}%` })
|
|
1875
|
+
] }, tool);
|
|
1876
|
+
}),
|
|
1877
|
+
topStuck.length > 0 ? /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
1878
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Top stuck:" }),
|
|
1879
|
+
topStuck.map((f, i) => {
|
|
1880
|
+
const target = trimRight(f.commandPreview || f.toolName, 32);
|
|
1881
|
+
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1882
|
+
/* @__PURE__ */ jsx9(Box9, { width: 8, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: `\xD7${fmtNum2(f.count)}` }) }),
|
|
1883
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: target })
|
|
1884
|
+
] }, `stuck-${i}`);
|
|
1885
|
+
})
|
|
1886
|
+
] }) : null,
|
|
1887
|
+
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1888
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
|
|
1889
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", wrap: "truncate-end", children: "live loop-detector" })
|
|
1890
|
+
] })
|
|
1891
|
+
] });
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/cli/render/ink/panels/ShieldsPanel.tsx
|
|
1895
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
1896
|
+
|
|
1897
|
+
// src/protection.ts
|
|
1898
|
+
var PROTECTIVE_SHIELD_DISCOUNTS = {
|
|
1899
|
+
"project-jail": 0.7
|
|
1900
|
+
};
|
|
1674
1901
|
|
|
1675
1902
|
// src/cli/render/ink/panels/ShieldsPanel.tsx
|
|
1676
1903
|
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
@@ -1739,7 +1966,7 @@ function StaticScorecard({ input, rangeLabel, now }) {
|
|
|
1739
1966
|
/* @__PURE__ */ jsx11(Header, { rangeLabel }),
|
|
1740
1967
|
/* @__PURE__ */ jsx11(SeverityBand, { label: "Spend & activity", width }),
|
|
1741
1968
|
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
|
|
1742
|
-
/* @__PURE__ */ jsx11(CostPanel, { summary, width: halfWidth }),
|
|
1969
|
+
/* @__PURE__ */ jsx11(CostPanel, { summary, width: halfWidth, keyed: input.keyed }),
|
|
1743
1970
|
/* @__PURE__ */ jsx11(ActivityPanel, { summary, scan: input.scan, width: halfWidth })
|
|
1744
1971
|
] }),
|
|
1745
1972
|
hasCritical ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|