@node9/proxy 1.35.2 → 1.37.0
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/dist/cli.js +1727 -547
- package/dist/cli.mjs +1727 -547
- package/package.json +3 -2
package/dist/cli.mjs
CHANGED
|
@@ -185,8 +185,8 @@ function sanitizeConfig(raw) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
const lines = result.error.issues.map((issue) => {
|
|
188
|
-
const
|
|
189
|
-
return ` \u2022 ${
|
|
188
|
+
const path58 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
189
|
+
return ` \u2022 ${path58}: ${issue.message}`;
|
|
190
190
|
});
|
|
191
191
|
return {
|
|
192
192
|
sanitized,
|
|
@@ -1240,9 +1240,9 @@ function matchesPattern(text, patterns) {
|
|
|
1240
1240
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1241
1241
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1242
1242
|
}
|
|
1243
|
-
function getNestedValue(obj,
|
|
1243
|
+
function getNestedValue(obj, path58) {
|
|
1244
1244
|
if (!obj || typeof obj !== "object") return null;
|
|
1245
|
-
const segments =
|
|
1245
|
+
const segments = path58.split(".");
|
|
1246
1246
|
for (const seg of segments) {
|
|
1247
1247
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1248
1248
|
}
|
|
@@ -1755,8 +1755,8 @@ function narrativeRuleLabel(name) {
|
|
|
1755
1755
|
"eval-dynamic": "dynamic eval",
|
|
1756
1756
|
"config-set": "Redis CONFIG SET"
|
|
1757
1757
|
};
|
|
1758
|
-
for (const [key,
|
|
1759
|
-
if (stripped.includes(key)) return
|
|
1758
|
+
for (const [key, label2] of Object.entries(map)) {
|
|
1759
|
+
if (stripped.includes(key)) return label2;
|
|
1760
1760
|
}
|
|
1761
1761
|
return stripped;
|
|
1762
1762
|
}
|
|
@@ -1768,6 +1768,17 @@ function stripRulePrefixes(name) {
|
|
|
1768
1768
|
n = n.replace(/^(block|review|allow)-/, "");
|
|
1769
1769
|
return n;
|
|
1770
1770
|
}
|
|
1771
|
+
function computeSecurityScore(opts) {
|
|
1772
|
+
const { critical, high, medium, total } = opts;
|
|
1773
|
+
if (total === 0) return { score: 100, tier: "good" };
|
|
1774
|
+
const criticalRate = critical / total;
|
|
1775
|
+
const highRate = high / total;
|
|
1776
|
+
const mediumRate = medium / total;
|
|
1777
|
+
const deduction = Math.min(criticalRate * 3e3, 60) + Math.min(highRate * 500, 30) + Math.min(mediumRate * 100, 15);
|
|
1778
|
+
const score = Math.max(0, Math.min(100, Math.round(100 - deduction)));
|
|
1779
|
+
const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
|
|
1780
|
+
return { score, tier };
|
|
1781
|
+
}
|
|
1771
1782
|
function truncateBlastPath(full) {
|
|
1772
1783
|
if (!full) return "";
|
|
1773
1784
|
const cleaned = full.replace(/[/\\]+$/, "");
|
|
@@ -4922,12 +4933,12 @@ async function explainPolicy(toolName, args) {
|
|
|
4922
4933
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
4923
4934
|
);
|
|
4924
4935
|
if (matchedRule) {
|
|
4925
|
-
const
|
|
4936
|
+
const label2 = `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`;
|
|
4926
4937
|
if (matchedRule.verdict === "allow") {
|
|
4927
4938
|
steps.push({
|
|
4928
4939
|
name: "Smart rules",
|
|
4929
4940
|
outcome: "allow",
|
|
4930
|
-
detail: `${
|
|
4941
|
+
detail: `${label2} \u2192 allow`,
|
|
4931
4942
|
isFinal: true
|
|
4932
4943
|
});
|
|
4933
4944
|
return { tool: toolName, args, waterfall, steps, decision: "allow" };
|
|
@@ -4935,7 +4946,7 @@ async function explainPolicy(toolName, args) {
|
|
|
4935
4946
|
steps.push({
|
|
4936
4947
|
name: "Smart rules",
|
|
4937
4948
|
outcome: matchedRule.verdict,
|
|
4938
|
-
detail: `${
|
|
4949
|
+
detail: `${label2} \u2192 ${matchedRule.verdict}${matchedRule.reason ? `: ${matchedRule.reason}` : ""}`,
|
|
4939
4950
|
isFinal: true
|
|
4940
4951
|
});
|
|
4941
4952
|
return {
|
|
@@ -4944,7 +4955,7 @@ async function explainPolicy(toolName, args) {
|
|
|
4944
4955
|
waterfall,
|
|
4945
4956
|
steps,
|
|
4946
4957
|
decision: matchedRule.verdict,
|
|
4947
|
-
blockedByLabel:
|
|
4958
|
+
blockedByLabel: label2
|
|
4948
4959
|
};
|
|
4949
4960
|
}
|
|
4950
4961
|
steps.push({
|
|
@@ -4996,7 +5007,7 @@ async function explainPolicy(toolName, args) {
|
|
|
4996
5007
|
});
|
|
4997
5008
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
4998
5009
|
if (evalVerdict) {
|
|
4999
|
-
const
|
|
5010
|
+
const label2 = evalVerdict === "block" ? "Node9: Eval Remote Execution" : "Node9: Eval Dynamic Content";
|
|
5000
5011
|
const detail = evalVerdict === "block" ? "eval of remote download (curl/wget) \u2014 near-certain supply-chain attack" : "eval of dynamic content (variable or subshell expansion) \u2014 requires approval";
|
|
5001
5012
|
steps.push({ name: "AST eval detection", outcome: evalVerdict, detail, isFinal: true });
|
|
5002
5013
|
return {
|
|
@@ -5005,7 +5016,7 @@ async function explainPolicy(toolName, args) {
|
|
|
5005
5016
|
waterfall,
|
|
5006
5017
|
steps,
|
|
5007
5018
|
decision: evalVerdict,
|
|
5008
|
-
blockedByLabel:
|
|
5019
|
+
blockedByLabel: label2
|
|
5009
5020
|
};
|
|
5010
5021
|
}
|
|
5011
5022
|
steps.push({
|
|
@@ -6941,10 +6952,10 @@ function checkPin(serverKey, currentHash, cwd) {
|
|
|
6941
6952
|
if (!homeEntry) return "new";
|
|
6942
6953
|
return homeEntry.toolsHash === currentHash ? "match" : "mismatch";
|
|
6943
6954
|
}
|
|
6944
|
-
function updatePin(serverKey,
|
|
6955
|
+
function updatePin(serverKey, label2, toolsHash, toolNames) {
|
|
6945
6956
|
const pins = readMcpPins();
|
|
6946
6957
|
pins.servers[serverKey] = {
|
|
6947
|
-
label,
|
|
6958
|
+
label: label2,
|
|
6948
6959
|
toolsHash,
|
|
6949
6960
|
toolNames,
|
|
6950
6961
|
toolCount: toolNames.length,
|
|
@@ -8275,9 +8286,9 @@ function writeToml(filePath, data) {
|
|
|
8275
8286
|
async function setupCodex() {
|
|
8276
8287
|
seedMcpPinsIfMissing();
|
|
8277
8288
|
const homeDir2 = os12.homedir();
|
|
8278
|
-
const
|
|
8289
|
+
const configPath2 = path15.join(homeDir2, ".codex", "config.toml");
|
|
8279
8290
|
const hooksPath = path15.join(homeDir2, ".codex", "hooks.json");
|
|
8280
|
-
const config = readToml(
|
|
8291
|
+
const config = readToml(configPath2) ?? {};
|
|
8281
8292
|
const servers = config.mcp_servers ?? {};
|
|
8282
8293
|
let anythingChanged = false;
|
|
8283
8294
|
const hooksFile = readJson(hooksPath) ?? {};
|
|
@@ -8352,7 +8363,7 @@ async function setupCodex() {
|
|
|
8352
8363
|
if (!hasNode9McpServer(servers)) {
|
|
8353
8364
|
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8354
8365
|
config.mcp_servers = servers;
|
|
8355
|
-
writeToml(
|
|
8366
|
+
writeToml(configPath2, config);
|
|
8356
8367
|
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8357
8368
|
anythingChanged = true;
|
|
8358
8369
|
}
|
|
@@ -8364,7 +8375,7 @@ async function setupCodex() {
|
|
|
8364
8375
|
}
|
|
8365
8376
|
if (serversToWrap.length > 0) {
|
|
8366
8377
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8367
|
-
console.log(chalk.white(` ${
|
|
8378
|
+
console.log(chalk.white(` ${configPath2}`));
|
|
8368
8379
|
for (const { name, upstream } of serversToWrap) {
|
|
8369
8380
|
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8370
8381
|
}
|
|
@@ -8379,7 +8390,7 @@ async function setupCodex() {
|
|
|
8379
8390
|
};
|
|
8380
8391
|
}
|
|
8381
8392
|
config.mcp_servers = servers;
|
|
8382
|
-
writeToml(
|
|
8393
|
+
writeToml(configPath2, config);
|
|
8383
8394
|
console.log(chalk.green(`
|
|
8384
8395
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8385
8396
|
anythingChanged = true;
|
|
@@ -8427,7 +8438,7 @@ async function setupCodex() {
|
|
|
8427
8438
|
}
|
|
8428
8439
|
function teardownCodex() {
|
|
8429
8440
|
const homeDir2 = os12.homedir();
|
|
8430
|
-
const
|
|
8441
|
+
const configPath2 = path15.join(homeDir2, ".codex", "config.toml");
|
|
8431
8442
|
const hooksPath = path15.join(homeDir2, ".codex", "hooks.json");
|
|
8432
8443
|
const hooksFile = readJson(hooksPath);
|
|
8433
8444
|
if (hooksFile?.hooks) {
|
|
@@ -8445,7 +8456,7 @@ function teardownCodex() {
|
|
|
8445
8456
|
console.log(chalk.green(" \u2705 Removed Node9 hooks from ~/.codex/hooks.json"));
|
|
8446
8457
|
}
|
|
8447
8458
|
}
|
|
8448
|
-
const config = readToml(
|
|
8459
|
+
const config = readToml(configPath2);
|
|
8449
8460
|
if (!config?.mcp_servers) {
|
|
8450
8461
|
console.log(chalk.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
|
|
8451
8462
|
return;
|
|
@@ -8468,7 +8479,7 @@ function teardownCodex() {
|
|
|
8468
8479
|
}
|
|
8469
8480
|
}
|
|
8470
8481
|
if (changed) {
|
|
8471
|
-
writeToml(
|
|
8482
|
+
writeToml(configPath2, config);
|
|
8472
8483
|
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codex/config.toml"));
|
|
8473
8484
|
} else {
|
|
8474
8485
|
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codex/config.toml"));
|
|
@@ -8723,18 +8734,18 @@ function teardownVSCode() {
|
|
|
8723
8734
|
}
|
|
8724
8735
|
async function setupClaudeDesktop() {
|
|
8725
8736
|
seedMcpPinsIfMissing();
|
|
8726
|
-
const
|
|
8727
|
-
if (!
|
|
8737
|
+
const configPath2 = claudeDesktopConfigPath();
|
|
8738
|
+
if (!configPath2) {
|
|
8728
8739
|
console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
8729
8740
|
return;
|
|
8730
8741
|
}
|
|
8731
|
-
const config = readJson(
|
|
8742
|
+
const config = readJson(configPath2) ?? {};
|
|
8732
8743
|
const servers = config.mcpServers ?? {};
|
|
8733
8744
|
let anythingChanged = false;
|
|
8734
8745
|
if (!hasNode9McpServer(servers)) {
|
|
8735
8746
|
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8736
8747
|
config.mcpServers = servers;
|
|
8737
|
-
writeJson(
|
|
8748
|
+
writeJson(configPath2, config);
|
|
8738
8749
|
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8739
8750
|
anythingChanged = true;
|
|
8740
8751
|
}
|
|
@@ -8745,7 +8756,7 @@ async function setupClaudeDesktop() {
|
|
|
8745
8756
|
}
|
|
8746
8757
|
if (serversToWrap.length > 0) {
|
|
8747
8758
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8748
|
-
console.log(chalk.white(` ${
|
|
8759
|
+
console.log(chalk.white(` ${configPath2}`));
|
|
8749
8760
|
for (const { name, upstream } of serversToWrap) {
|
|
8750
8761
|
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8751
8762
|
}
|
|
@@ -8760,7 +8771,7 @@ async function setupClaudeDesktop() {
|
|
|
8760
8771
|
};
|
|
8761
8772
|
}
|
|
8762
8773
|
config.mcpServers = servers;
|
|
8763
|
-
writeJson(
|
|
8774
|
+
writeJson(configPath2, config);
|
|
8764
8775
|
console.log(chalk.green(`
|
|
8765
8776
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8766
8777
|
anythingChanged = true;
|
|
@@ -8787,12 +8798,12 @@ async function setupClaudeDesktop() {
|
|
|
8787
8798
|
}
|
|
8788
8799
|
}
|
|
8789
8800
|
function teardownClaudeDesktop() {
|
|
8790
|
-
const
|
|
8791
|
-
if (!
|
|
8801
|
+
const configPath2 = claudeDesktopConfigPath();
|
|
8802
|
+
if (!configPath2) {
|
|
8792
8803
|
console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
8793
8804
|
return;
|
|
8794
8805
|
}
|
|
8795
|
-
const config = readJson(
|
|
8806
|
+
const config = readJson(configPath2);
|
|
8796
8807
|
if (!config?.mcpServers) {
|
|
8797
8808
|
console.log(chalk.blue(" \u2139\uFE0F Claude Desktop config not found \u2014 nothing to remove"));
|
|
8798
8809
|
return;
|
|
@@ -8800,7 +8811,7 @@ function teardownClaudeDesktop() {
|
|
|
8800
8811
|
let changed = false;
|
|
8801
8812
|
if (removeNode9McpServer(config.mcpServers)) {
|
|
8802
8813
|
changed = true;
|
|
8803
|
-
console.log(chalk.green(` \u2705 Removed node9 MCP server entry from ${
|
|
8814
|
+
console.log(chalk.green(` \u2705 Removed node9 MCP server entry from ${configPath2}`));
|
|
8804
8815
|
}
|
|
8805
8816
|
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
8806
8817
|
const args = server.args;
|
|
@@ -8815,7 +8826,7 @@ function teardownClaudeDesktop() {
|
|
|
8815
8826
|
}
|
|
8816
8827
|
}
|
|
8817
8828
|
if (changed) {
|
|
8818
|
-
writeJson(
|
|
8829
|
+
writeJson(configPath2, config);
|
|
8819
8830
|
console.log(chalk.green(" \u2705 Unwrapped MCP servers in Claude Desktop config"));
|
|
8820
8831
|
} else {
|
|
8821
8832
|
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
|
|
@@ -8843,7 +8854,7 @@ async function setupOpencode() {
|
|
|
8843
8854
|
const homeDir2 = os12.homedir();
|
|
8844
8855
|
const configDir = path15.join(homeDir2, ".config", "opencode");
|
|
8845
8856
|
const pluginsDir = path15.join(configDir, "plugins");
|
|
8846
|
-
const
|
|
8857
|
+
const configPath2 = path15.join(configDir, "opencode.json");
|
|
8847
8858
|
const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
|
|
8848
8859
|
try {
|
|
8849
8860
|
fs13.mkdirSync(pluginsDir, { recursive: true });
|
|
@@ -8877,7 +8888,7 @@ async function setupOpencode() {
|
|
|
8877
8888
|
);
|
|
8878
8889
|
}
|
|
8879
8890
|
}
|
|
8880
|
-
const config = readJson(
|
|
8891
|
+
const config = readJson(configPath2) ?? {};
|
|
8881
8892
|
const mcp = config.mcp ?? {};
|
|
8882
8893
|
let configChanged = false;
|
|
8883
8894
|
const desiredCommand = [...node9ArgvForShim(), "mcp-server"];
|
|
@@ -8898,7 +8909,7 @@ async function setupOpencode() {
|
|
|
8898
8909
|
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8899
8910
|
}
|
|
8900
8911
|
}
|
|
8901
|
-
if (configChanged) writeJson(
|
|
8912
|
+
if (configChanged) writeJson(configPath2, config);
|
|
8902
8913
|
if (pluginChanged || configChanged) {
|
|
8903
8914
|
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Opencode!"));
|
|
8904
8915
|
console.log(chalk.gray(" Restart Opencode for changes to take effect."));
|
|
@@ -8911,7 +8922,7 @@ function teardownOpencode() {
|
|
|
8911
8922
|
const homeDir2 = os12.homedir();
|
|
8912
8923
|
const configDir = path15.join(homeDir2, ".config", "opencode");
|
|
8913
8924
|
const pluginsDir = path15.join(configDir, "plugins");
|
|
8914
|
-
const
|
|
8925
|
+
const configPath2 = path15.join(configDir, "opencode.json");
|
|
8915
8926
|
const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
|
|
8916
8927
|
try {
|
|
8917
8928
|
if (fs13.existsSync(pluginPath)) {
|
|
@@ -8921,7 +8932,7 @@ function teardownOpencode() {
|
|
|
8921
8932
|
} catch (err2) {
|
|
8922
8933
|
console.log(chalk.yellow(` \u26A0\uFE0F Could not remove ${pluginPath}: ${String(err2)}`));
|
|
8923
8934
|
}
|
|
8924
|
-
const config = readJson(
|
|
8935
|
+
const config = readJson(configPath2);
|
|
8925
8936
|
if (!config) {
|
|
8926
8937
|
console.log(chalk.blue(" \u2139\uFE0F ~/.config/opencode/opencode.json not found \u2014 nothing to remove"));
|
|
8927
8938
|
return;
|
|
@@ -8937,7 +8948,7 @@ function teardownOpencode() {
|
|
|
8937
8948
|
}
|
|
8938
8949
|
if (changed) {
|
|
8939
8950
|
config.mcp = mcp;
|
|
8940
|
-
writeJson(
|
|
8951
|
+
writeJson(configPath2, config);
|
|
8941
8952
|
} else {
|
|
8942
8953
|
console.log(chalk.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
|
|
8943
8954
|
}
|
|
@@ -9008,15 +9019,15 @@ function hermesAllowlistPath(homeDir2 = os12.homedir()) {
|
|
|
9008
9019
|
}
|
|
9009
9020
|
function setupHermes() {
|
|
9010
9021
|
const homeDir2 = os12.homedir();
|
|
9011
|
-
const
|
|
9022
|
+
const configPath2 = hermesConfigPath(homeDir2);
|
|
9012
9023
|
const allowlistPath = hermesAllowlistPath(homeDir2);
|
|
9013
|
-
if (!fs13.existsSync(
|
|
9014
|
-
console.log(chalk.yellow(` \u26A0\uFE0F Hermes config not found at ${
|
|
9015
|
-
console.log(chalk.gray(" Run `hermes setup` first, then re-run node9
|
|
9024
|
+
if (!fs13.existsSync(configPath2)) {
|
|
9025
|
+
console.log(chalk.yellow(` \u26A0\uFE0F Hermes config not found at ${configPath2}`));
|
|
9026
|
+
console.log(chalk.gray(" Run `hermes setup` first, then re-run node9 agents add hermes."));
|
|
9016
9027
|
return;
|
|
9017
9028
|
}
|
|
9018
9029
|
let anythingChanged = false;
|
|
9019
|
-
const raw = fs13.readFileSync(
|
|
9030
|
+
const raw = fs13.readFileSync(configPath2, "utf-8");
|
|
9020
9031
|
const doc = yaml.parseDocument(raw);
|
|
9021
9032
|
if (doc.errors.length > 0) {
|
|
9022
9033
|
console.log(chalk.yellow(` \u26A0\uFE0F Hermes config.yaml has YAML parse errors:`));
|
|
@@ -9024,7 +9035,9 @@ function setupHermes() {
|
|
|
9024
9035
|
console.log(chalk.gray(` \u2022 ${err2.message}`));
|
|
9025
9036
|
}
|
|
9026
9037
|
console.log(
|
|
9027
|
-
chalk.gray(
|
|
9038
|
+
chalk.gray(
|
|
9039
|
+
" Fix the file (or run `hermes config edit`), then re-run node9 agents add hermes."
|
|
9040
|
+
)
|
|
9028
9041
|
);
|
|
9029
9042
|
return;
|
|
9030
9043
|
}
|
|
@@ -9054,7 +9067,7 @@ function setupHermes() {
|
|
|
9054
9067
|
anythingChanged = true;
|
|
9055
9068
|
}
|
|
9056
9069
|
if (anythingChanged) {
|
|
9057
|
-
fs13.writeFileSync(
|
|
9070
|
+
fs13.writeFileSync(configPath2, doc.toString());
|
|
9058
9071
|
}
|
|
9059
9072
|
let allowlist = {};
|
|
9060
9073
|
if (fs13.existsSync(allowlistPath)) {
|
|
@@ -9097,24 +9110,24 @@ function setupHermes() {
|
|
|
9097
9110
|
}
|
|
9098
9111
|
function teardownHermes() {
|
|
9099
9112
|
const homeDir2 = os12.homedir();
|
|
9100
|
-
const
|
|
9113
|
+
const configPath2 = hermesConfigPath(homeDir2);
|
|
9101
9114
|
const allowlistPath = hermesAllowlistPath(homeDir2);
|
|
9102
|
-
if (!fs13.existsSync(
|
|
9103
|
-
console.log(chalk.blue(` \u2139\uFE0F ${
|
|
9115
|
+
if (!fs13.existsSync(configPath2)) {
|
|
9116
|
+
console.log(chalk.blue(` \u2139\uFE0F ${configPath2} not found \u2014 nothing to remove`));
|
|
9104
9117
|
return;
|
|
9105
9118
|
}
|
|
9106
|
-
const raw = fs13.readFileSync(
|
|
9119
|
+
const raw = fs13.readFileSync(configPath2, "utf-8");
|
|
9107
9120
|
const doc = yaml.parseDocument(raw);
|
|
9108
9121
|
if (doc.errors.length > 0) {
|
|
9109
9122
|
console.log(
|
|
9110
|
-
chalk.yellow(` \u26A0\uFE0F Skipping ${
|
|
9123
|
+
chalk.yellow(` \u26A0\uFE0F Skipping ${configPath2} \u2014 file has YAML parse errors, fix it manually.`)
|
|
9111
9124
|
);
|
|
9112
9125
|
} else {
|
|
9113
|
-
teardownHermesConfigDoc(doc,
|
|
9126
|
+
teardownHermesConfigDoc(doc, configPath2);
|
|
9114
9127
|
}
|
|
9115
9128
|
teardownHermesAllowlist(allowlistPath);
|
|
9116
9129
|
}
|
|
9117
|
-
function teardownHermesConfigDoc(doc,
|
|
9130
|
+
function teardownHermesConfigDoc(doc, configPath2) {
|
|
9118
9131
|
let anythingChanged = false;
|
|
9119
9132
|
const current = doc.toJS() ?? {};
|
|
9120
9133
|
for (const { event } of HERMES_HOOK_PLAN) {
|
|
@@ -9136,10 +9149,10 @@ function teardownHermesConfigDoc(doc, configPath) {
|
|
|
9136
9149
|
anythingChanged = true;
|
|
9137
9150
|
}
|
|
9138
9151
|
if (anythingChanged) {
|
|
9139
|
-
fs13.writeFileSync(
|
|
9140
|
-
console.log(chalk.green(` \u2705 Removed Node9 hooks from ${
|
|
9152
|
+
fs13.writeFileSync(configPath2, doc.toString());
|
|
9153
|
+
console.log(chalk.green(` \u2705 Removed Node9 hooks from ${configPath2}`));
|
|
9141
9154
|
} else {
|
|
9142
|
-
console.log(chalk.blue(` \u2139\uFE0F No Node9 hooks found in ${
|
|
9155
|
+
console.log(chalk.blue(` \u2139\uFE0F No Node9 hooks found in ${configPath2}`));
|
|
9143
9156
|
}
|
|
9144
9157
|
}
|
|
9145
9158
|
function teardownHermesAllowlist(allowlistPath) {
|
|
@@ -9974,12 +9987,12 @@ function buildScanSummary(agents) {
|
|
|
9974
9987
|
}
|
|
9975
9988
|
function buildSections(findings) {
|
|
9976
9989
|
const sectionMap = /* @__PURE__ */ new Map();
|
|
9977
|
-
function ensureSection(id,
|
|
9990
|
+
function ensureSection(id, label2, subtitle, sourceType, shieldKey) {
|
|
9978
9991
|
let s = sectionMap.get(id);
|
|
9979
9992
|
if (!s) {
|
|
9980
9993
|
s = {
|
|
9981
9994
|
id,
|
|
9982
|
-
label,
|
|
9995
|
+
label: label2,
|
|
9983
9996
|
subtitle,
|
|
9984
9997
|
sourceType,
|
|
9985
9998
|
shieldKey,
|
|
@@ -13076,9 +13089,9 @@ function printRuleGroup(rule, topN, drillDown, previewWidth) {
|
|
|
13076
13089
|
}
|
|
13077
13090
|
}
|
|
13078
13091
|
function compactRuleLabel(name) {
|
|
13079
|
-
let
|
|
13080
|
-
|
|
13081
|
-
return
|
|
13092
|
+
let label2 = name.replace(/^shield:[^:]+:/, "");
|
|
13093
|
+
label2 = label2.replace(/^(block|review|allow)-/, "");
|
|
13094
|
+
return label2.replace(/-+/g, "-");
|
|
13082
13095
|
}
|
|
13083
13096
|
function renderCompactScorecard(input) {
|
|
13084
13097
|
const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
|
|
@@ -13179,9 +13192,9 @@ function renderNarrativeScorecard(input) {
|
|
|
13179
13192
|
for (const section of summary.sections) {
|
|
13180
13193
|
for (const rule of section.rules) {
|
|
13181
13194
|
const sev = classifyRuleSeverity2(rule.name, rule.verdict);
|
|
13182
|
-
const
|
|
13195
|
+
const label2 = narrativeRuleLabel2(rule.name);
|
|
13183
13196
|
const count = rule.findings.length;
|
|
13184
|
-
const display = count > 1 ? `${
|
|
13197
|
+
const display = count > 1 ? `${label2} \xD7${count}` : label2;
|
|
13185
13198
|
const entry = { label: display, count };
|
|
13186
13199
|
if (sev === "critical") critical.push(entry);
|
|
13187
13200
|
else if (sev === "high") high.push(entry);
|
|
@@ -14010,7 +14023,7 @@ function registerScanCommand(program2) {
|
|
|
14010
14023
|
console.log(chalk5.bold(" Enable real-time protection:"));
|
|
14011
14024
|
console.log("");
|
|
14012
14025
|
console.log(
|
|
14013
|
-
" " + chalk5.cyan("npm install -g
|
|
14026
|
+
" " + chalk5.cyan("npm install -g node9-ai") + chalk5.dim(" && ") + chalk5.cyan("node9 init --recommended")
|
|
14014
14027
|
);
|
|
14015
14028
|
console.log("");
|
|
14016
14029
|
console.log(
|
|
@@ -14311,8 +14324,8 @@ var init_session_counters = __esm({
|
|
|
14311
14324
|
if (!isFinite(amount) || amount < 0) return;
|
|
14312
14325
|
this._estimatedCost += amount;
|
|
14313
14326
|
}
|
|
14314
|
-
recordRuleHit(
|
|
14315
|
-
this._lastRuleHit =
|
|
14327
|
+
recordRuleHit(label2) {
|
|
14328
|
+
this._lastRuleHit = label2;
|
|
14316
14329
|
}
|
|
14317
14330
|
recordBlockedTool(toolName) {
|
|
14318
14331
|
this._lastBlockedTool = toolName;
|
|
@@ -14591,10 +14604,10 @@ function broadcast(event, data) {
|
|
|
14591
14604
|
activityRing.push({ event, data });
|
|
14592
14605
|
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
14593
14606
|
} else if (event === "activity-result") {
|
|
14594
|
-
const { id, status, label, costEstimate } = data;
|
|
14607
|
+
const { id, status, label: label2, costEstimate } = data;
|
|
14595
14608
|
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
14596
14609
|
if (activityRing[i].data.id === id) {
|
|
14597
|
-
Object.assign(activityRing[i].data, { status, label, costEstimate });
|
|
14610
|
+
Object.assign(activityRing[i].data, { status, label: label2, costEstimate });
|
|
14598
14611
|
break;
|
|
14599
14612
|
}
|
|
14600
14613
|
}
|
|
@@ -16902,11 +16915,11 @@ __export(tail_exports, {
|
|
|
16902
16915
|
shortenPathSummary: () => shortenPathSummary,
|
|
16903
16916
|
startTail: () => startTail
|
|
16904
16917
|
});
|
|
16905
|
-
import
|
|
16906
|
-
import
|
|
16907
|
-
import
|
|
16908
|
-
import
|
|
16909
|
-
import
|
|
16918
|
+
import http3 from "http";
|
|
16919
|
+
import chalk32 from "chalk";
|
|
16920
|
+
import fs56 from "fs";
|
|
16921
|
+
import os51 from "os";
|
|
16922
|
+
import path55 from "path";
|
|
16910
16923
|
import readline6 from "readline";
|
|
16911
16924
|
import { spawn as spawn8 } from "child_process";
|
|
16912
16925
|
function shortenPathSummary(s) {
|
|
@@ -16930,20 +16943,20 @@ function getModelContextLimit(model) {
|
|
|
16930
16943
|
return 2e5;
|
|
16931
16944
|
}
|
|
16932
16945
|
function readSessionUsage() {
|
|
16933
|
-
const projectsDir =
|
|
16934
|
-
if (!
|
|
16946
|
+
const projectsDir = path55.join(os51.homedir(), ".claude", "projects");
|
|
16947
|
+
if (!fs56.existsSync(projectsDir)) return null;
|
|
16935
16948
|
let latestFile = null;
|
|
16936
16949
|
let latestMtime = 0;
|
|
16937
16950
|
try {
|
|
16938
|
-
for (const dir of
|
|
16939
|
-
const dirPath =
|
|
16951
|
+
for (const dir of fs56.readdirSync(projectsDir)) {
|
|
16952
|
+
const dirPath = path55.join(projectsDir, dir);
|
|
16940
16953
|
try {
|
|
16941
|
-
if (!
|
|
16942
|
-
for (const file of
|
|
16954
|
+
if (!fs56.statSync(dirPath).isDirectory()) continue;
|
|
16955
|
+
for (const file of fs56.readdirSync(dirPath)) {
|
|
16943
16956
|
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
16944
|
-
const filePath =
|
|
16957
|
+
const filePath = path55.join(dirPath, file);
|
|
16945
16958
|
try {
|
|
16946
|
-
const mtime =
|
|
16959
|
+
const mtime = fs56.statSync(filePath).mtimeMs;
|
|
16947
16960
|
if (mtime > latestMtime) {
|
|
16948
16961
|
latestMtime = mtime;
|
|
16949
16962
|
latestFile = filePath;
|
|
@@ -16958,7 +16971,7 @@ function readSessionUsage() {
|
|
|
16958
16971
|
}
|
|
16959
16972
|
if (!latestFile) return null;
|
|
16960
16973
|
try {
|
|
16961
|
-
const lines =
|
|
16974
|
+
const lines = fs56.readFileSync(latestFile, "utf-8").split("\n");
|
|
16962
16975
|
let lastModel = "";
|
|
16963
16976
|
let lastInput = 0;
|
|
16964
16977
|
let lastOutput = 0;
|
|
@@ -16983,10 +16996,10 @@ function readSessionUsage() {
|
|
|
16983
16996
|
}
|
|
16984
16997
|
}
|
|
16985
16998
|
function formatContextStat(stat) {
|
|
16986
|
-
const pctColor = stat.fillPct >= 80 ?
|
|
16999
|
+
const pctColor = stat.fillPct >= 80 ? chalk32.red : stat.fillPct >= 50 ? chalk32.yellow : chalk32.cyan;
|
|
16987
17000
|
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
16988
17001
|
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
16989
|
-
return
|
|
17002
|
+
return chalk32.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk32.dim(
|
|
16990
17003
|
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
16991
17004
|
);
|
|
16992
17005
|
}
|
|
@@ -17009,32 +17022,32 @@ function agentLabel(agent, mcpServer, sessionId) {
|
|
|
17009
17022
|
const tag = sessionTag(sessionId);
|
|
17010
17023
|
const tagSuffix = tag ? `\xB7${tag}` : "";
|
|
17011
17024
|
if (!agent || agent === "Terminal") {
|
|
17012
|
-
return mcpServer ?
|
|
17025
|
+
return mcpServer ? chalk32.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17013
17026
|
}
|
|
17014
17027
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
17015
|
-
if (!short) return mcpServer ?
|
|
17016
|
-
return mcpServer ?
|
|
17028
|
+
if (!short) return mcpServer ? chalk32.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17029
|
+
return mcpServer ? chalk32.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk32.dim(`[${short}${tagSuffix}] `);
|
|
17017
17030
|
}
|
|
17018
17031
|
function formatBase(activity) {
|
|
17019
17032
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
17020
17033
|
const icon = getIcon(activity.tool);
|
|
17021
17034
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
17022
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
17035
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os51.homedir(), "~");
|
|
17023
17036
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
17024
|
-
return `${
|
|
17037
|
+
return `${chalk32.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk32.white.bold(toolName)} ${chalk32.dim(argsPreview)}`;
|
|
17025
17038
|
}
|
|
17026
17039
|
function renderResult(activity, result) {
|
|
17027
17040
|
const base = formatBase(activity);
|
|
17028
17041
|
let status;
|
|
17029
17042
|
if (result.status === "allow") {
|
|
17030
|
-
status =
|
|
17043
|
+
status = chalk32.green("\u2713 ALLOW");
|
|
17031
17044
|
} else if (result.status === "dlp") {
|
|
17032
|
-
status =
|
|
17045
|
+
status = chalk32.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
17033
17046
|
} else {
|
|
17034
|
-
status =
|
|
17047
|
+
status = chalk32.red("\u2717 BLOCK");
|
|
17035
17048
|
}
|
|
17036
17049
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
17037
|
-
const costSuffix = cost == null ? "" :
|
|
17050
|
+
const costSuffix = cost == null ? "" : chalk32.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
17038
17051
|
if (process.stdout.isTTY) {
|
|
17039
17052
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
17040
17053
|
readline6.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -17051,19 +17064,19 @@ function renderResult(activity, result) {
|
|
|
17051
17064
|
}
|
|
17052
17065
|
function renderPending(activity) {
|
|
17053
17066
|
if (!process.stdout.isTTY) return;
|
|
17054
|
-
const line = `${formatBase(activity)} ${
|
|
17067
|
+
const line = `${formatBase(activity)} ${chalk32.yellow("\u25CF \u2026")}`;
|
|
17055
17068
|
pendingShownForId = activity.id;
|
|
17056
17069
|
pendingWrappedLines = wrappedLineCount(line);
|
|
17057
17070
|
process.stdout.write(`${line}\r`);
|
|
17058
17071
|
}
|
|
17059
17072
|
async function ensureDaemon() {
|
|
17060
17073
|
let pidPort = null;
|
|
17061
|
-
if (
|
|
17074
|
+
if (fs56.existsSync(PID_FILE)) {
|
|
17062
17075
|
try {
|
|
17063
|
-
const { port } = JSON.parse(
|
|
17076
|
+
const { port } = JSON.parse(fs56.readFileSync(PID_FILE, "utf-8"));
|
|
17064
17077
|
pidPort = port;
|
|
17065
17078
|
} catch {
|
|
17066
|
-
console.error(
|
|
17079
|
+
console.error(chalk32.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
17067
17080
|
}
|
|
17068
17081
|
}
|
|
17069
17082
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -17074,7 +17087,7 @@ async function ensureDaemon() {
|
|
|
17074
17087
|
if (res.ok) return checkPort;
|
|
17075
17088
|
} catch {
|
|
17076
17089
|
}
|
|
17077
|
-
console.log(
|
|
17090
|
+
console.log(chalk32.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
17078
17091
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
17079
17092
|
detached: true,
|
|
17080
17093
|
stdio: "ignore",
|
|
@@ -17091,7 +17104,7 @@ async function ensureDaemon() {
|
|
|
17091
17104
|
} catch {
|
|
17092
17105
|
}
|
|
17093
17106
|
}
|
|
17094
|
-
console.error(
|
|
17107
|
+
console.error(chalk32.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
17095
17108
|
process.exit(1);
|
|
17096
17109
|
}
|
|
17097
17110
|
function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
@@ -17101,7 +17114,7 @@ function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
|
17101
17114
|
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
17102
17115
|
if (opts?.reason) bodyObj.reason = opts.reason;
|
|
17103
17116
|
const body = JSON.stringify(bodyObj);
|
|
17104
|
-
const req =
|
|
17117
|
+
const req = http3.request(
|
|
17105
17118
|
{
|
|
17106
17119
|
hostname: "127.0.0.1",
|
|
17107
17120
|
port,
|
|
@@ -17160,7 +17173,7 @@ function buildCardLines(req, localCount = 0) {
|
|
|
17160
17173
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
17161
17174
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
17162
17175
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
17163
|
-
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${
|
|
17176
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk32.dim(`(${req.agent})`)}` : "";
|
|
17164
17177
|
const lines = [
|
|
17165
17178
|
``,
|
|
17166
17179
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
@@ -17216,9 +17229,9 @@ function buildRecoveryCardLines(req) {
|
|
|
17216
17229
|
];
|
|
17217
17230
|
}
|
|
17218
17231
|
function readApproversFromDisk() {
|
|
17219
|
-
const
|
|
17232
|
+
const configPath2 = path55.join(os51.homedir(), ".node9", "config.json");
|
|
17220
17233
|
try {
|
|
17221
|
-
const raw = JSON.parse(
|
|
17234
|
+
const raw = JSON.parse(fs56.readFileSync(configPath2, "utf-8"));
|
|
17222
17235
|
const settings = raw.settings ?? {};
|
|
17223
17236
|
return settings.approvers ?? {};
|
|
17224
17237
|
} catch {
|
|
@@ -17227,22 +17240,22 @@ function readApproversFromDisk() {
|
|
|
17227
17240
|
}
|
|
17228
17241
|
function approverStatusLine() {
|
|
17229
17242
|
const a = readApproversFromDisk();
|
|
17230
|
-
const fmt = (
|
|
17243
|
+
const fmt = (label2, key) => {
|
|
17231
17244
|
const on = a[key] !== false;
|
|
17232
|
-
return `[${key[0]}]${
|
|
17245
|
+
return `[${key[0]}]${label2.slice(1)} ${on ? chalk32.green("\u2713") : chalk32.dim("\u2717")}`;
|
|
17233
17246
|
};
|
|
17234
17247
|
return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
17235
17248
|
}
|
|
17236
17249
|
function toggleApprover(channel) {
|
|
17237
|
-
const
|
|
17250
|
+
const configPath2 = path55.join(os51.homedir(), ".node9", "config.json");
|
|
17238
17251
|
try {
|
|
17239
|
-
const raw = JSON.parse(
|
|
17252
|
+
const raw = JSON.parse(fs56.readFileSync(configPath2, "utf-8"));
|
|
17240
17253
|
const settings = raw.settings ?? {};
|
|
17241
17254
|
const approvers = settings.approvers ?? {};
|
|
17242
17255
|
approvers[channel] = approvers[channel] === false;
|
|
17243
17256
|
settings.approvers = approvers;
|
|
17244
17257
|
raw.settings = settings;
|
|
17245
|
-
|
|
17258
|
+
fs56.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
|
|
17246
17259
|
} catch (err2) {
|
|
17247
17260
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
17248
17261
|
`);
|
|
@@ -17252,7 +17265,7 @@ async function startTail(options = {}) {
|
|
|
17252
17265
|
const port = await ensureDaemon();
|
|
17253
17266
|
if (options.clear) {
|
|
17254
17267
|
const result = await new Promise((resolve) => {
|
|
17255
|
-
const req2 =
|
|
17268
|
+
const req2 = http3.request(
|
|
17256
17269
|
{ method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
|
|
17257
17270
|
(res) => {
|
|
17258
17271
|
const status = res.statusCode ?? 0;
|
|
@@ -17274,7 +17287,7 @@ async function startTail(options = {}) {
|
|
|
17274
17287
|
req2.end();
|
|
17275
17288
|
});
|
|
17276
17289
|
if (result.ok) {
|
|
17277
|
-
console.log(
|
|
17290
|
+
console.log(chalk32.green("\u2713 Flight Recorder buffer cleared."));
|
|
17278
17291
|
} else if (result.code === "ECONNREFUSED") {
|
|
17279
17292
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
17280
17293
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -17320,7 +17333,7 @@ async function startTail(options = {}) {
|
|
|
17320
17333
|
const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
17321
17334
|
if (channel) {
|
|
17322
17335
|
toggleApprover(channel);
|
|
17323
|
-
console.log(
|
|
17336
|
+
console.log(chalk32.dim(` Approvers: ${approverStatusLine()}`));
|
|
17324
17337
|
}
|
|
17325
17338
|
};
|
|
17326
17339
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -17386,7 +17399,7 @@ async function startTail(options = {}) {
|
|
|
17386
17399
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
17387
17400
|
)
|
|
17388
17401
|
);
|
|
17389
|
-
const decisionStamp = action === "always-allow" ?
|
|
17402
|
+
const decisionStamp = action === "always-allow" ? chalk32.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk32.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk32.green("\u2713 ALLOWED") : action === "redirect" ? chalk32.yellow("\u21A9 REDIRECT AI") : chalk32.red("\u2717 DENIED");
|
|
17390
17403
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
17391
17404
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
17392
17405
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -17414,8 +17427,8 @@ async function startTail(options = {}) {
|
|
|
17414
17427
|
}
|
|
17415
17428
|
postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
|
|
17416
17429
|
try {
|
|
17417
|
-
|
|
17418
|
-
|
|
17430
|
+
fs56.appendFileSync(
|
|
17431
|
+
path55.join(os51.homedir(), ".node9", "hook-debug.log"),
|
|
17419
17432
|
`[tail] POST /decision failed: ${String(err2)}
|
|
17420
17433
|
`
|
|
17421
17434
|
);
|
|
@@ -17437,7 +17450,7 @@ async function startTail(options = {}) {
|
|
|
17437
17450
|
);
|
|
17438
17451
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
17439
17452
|
if (externalDecision) {
|
|
17440
|
-
const source = externalDecision === "allow" ?
|
|
17453
|
+
const source = externalDecision === "allow" ? chalk32.green("\u2713 ALLOWED") : chalk32.red("\u2717 DENIED");
|
|
17441
17454
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
17442
17455
|
}
|
|
17443
17456
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -17479,31 +17492,31 @@ async function startTail(options = {}) {
|
|
|
17479
17492
|
};
|
|
17480
17493
|
process.stdin.on("keypress", onKeypress);
|
|
17481
17494
|
}
|
|
17482
|
-
const auditLog =
|
|
17495
|
+
const auditLog = path55.join(os51.homedir(), ".node9", "audit.log");
|
|
17483
17496
|
try {
|
|
17484
|
-
const unackedDlp =
|
|
17497
|
+
const unackedDlp = fs56.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
17485
17498
|
if (unackedDlp > 0) {
|
|
17486
17499
|
console.log("");
|
|
17487
17500
|
console.log(
|
|
17488
|
-
|
|
17501
|
+
chalk32.bgRed.white.bold(
|
|
17489
17502
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
17490
17503
|
)
|
|
17491
17504
|
);
|
|
17492
17505
|
}
|
|
17493
17506
|
} catch {
|
|
17494
17507
|
}
|
|
17495
|
-
console.log(
|
|
17508
|
+
console.log(chalk32.cyan.bold(`
|
|
17496
17509
|
\u{1F6F0}\uFE0F Node9 tail`));
|
|
17497
17510
|
if (canApprove) {
|
|
17498
|
-
console.log(
|
|
17499
|
-
console.log(
|
|
17511
|
+
console.log(chalk32.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
17512
|
+
console.log(chalk32.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
17500
17513
|
}
|
|
17501
17514
|
const ctxStat = readSessionUsage();
|
|
17502
17515
|
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
17503
17516
|
if (options.history) {
|
|
17504
|
-
console.log(
|
|
17517
|
+
console.log(chalk32.dim("Showing history + live events.\n"));
|
|
17505
17518
|
} else {
|
|
17506
|
-
console.log(
|
|
17519
|
+
console.log(chalk32.dim("Showing live events only. Use --history to include past.\n"));
|
|
17507
17520
|
}
|
|
17508
17521
|
process.on("SIGINT", () => {
|
|
17509
17522
|
exitIdleMode();
|
|
@@ -17513,7 +17526,7 @@ async function startTail(options = {}) {
|
|
|
17513
17526
|
readline6.clearLine(process.stdout, 0);
|
|
17514
17527
|
readline6.cursorTo(process.stdout, 0);
|
|
17515
17528
|
}
|
|
17516
|
-
console.log(
|
|
17529
|
+
console.log(chalk32.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
17517
17530
|
process.exit(0);
|
|
17518
17531
|
});
|
|
17519
17532
|
const STALL_THRESHOLD_MS = 6e4;
|
|
@@ -17521,11 +17534,11 @@ async function startTail(options = {}) {
|
|
|
17521
17534
|
if (stallWarned) return;
|
|
17522
17535
|
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
17523
17536
|
try {
|
|
17524
|
-
const auditMtime =
|
|
17537
|
+
const auditMtime = fs56.statSync(auditLog).mtimeMs;
|
|
17525
17538
|
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
17526
17539
|
console.log("");
|
|
17527
17540
|
console.log(
|
|
17528
|
-
|
|
17541
|
+
chalk32.yellow(
|
|
17529
17542
|
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
17530
17543
|
)
|
|
17531
17544
|
);
|
|
@@ -17535,14 +17548,14 @@ async function startTail(options = {}) {
|
|
|
17535
17548
|
}, STALL_THRESHOLD_MS / 2);
|
|
17536
17549
|
stallWatchdog.unref();
|
|
17537
17550
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
17538
|
-
const req =
|
|
17551
|
+
const req = http3.get(
|
|
17539
17552
|
sseUrl,
|
|
17540
17553
|
{
|
|
17541
17554
|
headers: authToken ? { "X-Node9-Internal": authToken } : {}
|
|
17542
17555
|
},
|
|
17543
17556
|
(res) => {
|
|
17544
17557
|
if (res.statusCode !== 200) {
|
|
17545
|
-
console.error(
|
|
17558
|
+
console.error(chalk32.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
17546
17559
|
process.exit(1);
|
|
17547
17560
|
}
|
|
17548
17561
|
if (canApprove) enterIdleMode();
|
|
@@ -17573,7 +17586,7 @@ async function startTail(options = {}) {
|
|
|
17573
17586
|
readline6.clearLine(process.stdout, 0);
|
|
17574
17587
|
readline6.cursorTo(process.stdout, 0);
|
|
17575
17588
|
}
|
|
17576
|
-
console.log(
|
|
17589
|
+
console.log(chalk32.red("\n\u274C Daemon disconnected."));
|
|
17577
17590
|
process.exit(1);
|
|
17578
17591
|
});
|
|
17579
17592
|
}
|
|
@@ -17586,7 +17599,7 @@ async function startTail(options = {}) {
|
|
|
17586
17599
|
const parsed = JSON.parse(rawData);
|
|
17587
17600
|
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
17588
17601
|
console.log("");
|
|
17589
|
-
console.log(
|
|
17602
|
+
console.log(chalk32.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
17590
17603
|
} catch {
|
|
17591
17604
|
}
|
|
17592
17605
|
return;
|
|
@@ -17671,9 +17684,9 @@ async function startTail(options = {}) {
|
|
|
17671
17684
|
const rawSummary = data.argsSummary ?? data.tool;
|
|
17672
17685
|
const summary = shortenPathSummary(rawSummary);
|
|
17673
17686
|
const fileCount = data.fileCount ?? 0;
|
|
17674
|
-
const files = fileCount > 0 ?
|
|
17687
|
+
const files = fileCount > 0 ? chalk32.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
17675
17688
|
process.stdout.write(
|
|
17676
|
-
`${
|
|
17689
|
+
`${chalk32.dim(time)} ${chalk32.cyan("\u{1F4F8} snapshot")} ${chalk32.dim(hash)} ${summary}${files}
|
|
17677
17690
|
`
|
|
17678
17691
|
);
|
|
17679
17692
|
return;
|
|
@@ -17690,18 +17703,18 @@ async function startTail(options = {}) {
|
|
|
17690
17703
|
if (event === "execution-result") {
|
|
17691
17704
|
const exec = data;
|
|
17692
17705
|
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
17693
|
-
const arrow = exec.isError ?
|
|
17694
|
-
const
|
|
17706
|
+
const arrow = exec.isError ? chalk32.red(" \u21B3 \u2717") : chalk32.green(" \u21B3 \u2713");
|
|
17707
|
+
const label2 = agentLabel(exec.agent, exec.mcpServer);
|
|
17695
17708
|
const tool = (exec.tool ?? "").slice(0, 16);
|
|
17696
|
-
const duration = typeof exec.durationMs === "number" ?
|
|
17709
|
+
const duration = typeof exec.durationMs === "number" ? chalk32.dim(` (${exec.durationMs}ms)`) : "";
|
|
17697
17710
|
console.log(
|
|
17698
|
-
`${
|
|
17711
|
+
`${chalk32.gray(time)} ${arrow} ${label2}${chalk32.dim(tool)}${chalk32.dim(" completed")}${duration}`
|
|
17699
17712
|
);
|
|
17700
17713
|
}
|
|
17701
17714
|
}
|
|
17702
17715
|
req.on("error", (err2) => {
|
|
17703
17716
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
17704
|
-
console.error(
|
|
17717
|
+
console.error(chalk32.red(`
|
|
17705
17718
|
\u274C ${msg}`));
|
|
17706
17719
|
process.exit(1);
|
|
17707
17720
|
});
|
|
@@ -17712,7 +17725,7 @@ var init_tail = __esm({
|
|
|
17712
17725
|
"use strict";
|
|
17713
17726
|
init_daemon2();
|
|
17714
17727
|
init_daemon();
|
|
17715
|
-
PID_FILE =
|
|
17728
|
+
PID_FILE = path55.join(os51.homedir(), ".node9", "daemon.pid");
|
|
17716
17729
|
ICONS = {
|
|
17717
17730
|
bash: "\u{1F4BB}",
|
|
17718
17731
|
shell: "\u{1F4BB}",
|
|
@@ -17760,10 +17773,10 @@ __export(hud_exports, {
|
|
|
17760
17773
|
main: () => main,
|
|
17761
17774
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
17762
17775
|
});
|
|
17763
|
-
import
|
|
17764
|
-
import
|
|
17765
|
-
import
|
|
17766
|
-
import
|
|
17776
|
+
import fs57 from "fs";
|
|
17777
|
+
import path56 from "path";
|
|
17778
|
+
import os52 from "os";
|
|
17779
|
+
import http4 from "http";
|
|
17767
17780
|
async function readStdin() {
|
|
17768
17781
|
const chunks = [];
|
|
17769
17782
|
for await (const chunk2 of process.stdin) {
|
|
@@ -17781,7 +17794,7 @@ function queryDaemon() {
|
|
|
17781
17794
|
return new Promise((resolve) => {
|
|
17782
17795
|
const timeout = setTimeout(() => resolve(null), 50);
|
|
17783
17796
|
try {
|
|
17784
|
-
const req =
|
|
17797
|
+
const req = http4.get(
|
|
17785
17798
|
`http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
|
|
17786
17799
|
{ timeout: 50 },
|
|
17787
17800
|
(res) => {
|
|
@@ -17838,9 +17851,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
17838
17851
|
return ` (${m}m left)`;
|
|
17839
17852
|
}
|
|
17840
17853
|
function safeReadJson(filePath) {
|
|
17841
|
-
if (!
|
|
17854
|
+
if (!fs57.existsSync(filePath)) return null;
|
|
17842
17855
|
try {
|
|
17843
|
-
return JSON.parse(
|
|
17856
|
+
return JSON.parse(fs57.readFileSync(filePath, "utf-8"));
|
|
17844
17857
|
} catch {
|
|
17845
17858
|
return null;
|
|
17846
17859
|
}
|
|
@@ -17861,12 +17874,12 @@ function countHooksInFile(filePath) {
|
|
|
17861
17874
|
return Object.keys(cfg.hooks).length;
|
|
17862
17875
|
}
|
|
17863
17876
|
function countRulesInDir(rulesDir) {
|
|
17864
|
-
if (!
|
|
17877
|
+
if (!fs57.existsSync(rulesDir)) return 0;
|
|
17865
17878
|
let count = 0;
|
|
17866
17879
|
try {
|
|
17867
|
-
for (const entry of
|
|
17880
|
+
for (const entry of fs57.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
17868
17881
|
if (entry.isDirectory()) {
|
|
17869
|
-
count += countRulesInDir(
|
|
17882
|
+
count += countRulesInDir(path56.join(rulesDir, entry.name));
|
|
17870
17883
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17871
17884
|
count++;
|
|
17872
17885
|
}
|
|
@@ -17877,46 +17890,46 @@ function countRulesInDir(rulesDir) {
|
|
|
17877
17890
|
}
|
|
17878
17891
|
function isSamePath(a, b) {
|
|
17879
17892
|
try {
|
|
17880
|
-
return
|
|
17893
|
+
return path56.resolve(a) === path56.resolve(b);
|
|
17881
17894
|
} catch {
|
|
17882
17895
|
return false;
|
|
17883
17896
|
}
|
|
17884
17897
|
}
|
|
17885
17898
|
function countConfigs(cwd) {
|
|
17886
|
-
const homeDir2 =
|
|
17887
|
-
const claudeDir =
|
|
17899
|
+
const homeDir2 = os52.homedir();
|
|
17900
|
+
const claudeDir = path56.join(homeDir2, ".claude");
|
|
17888
17901
|
let claudeMdCount = 0;
|
|
17889
17902
|
let rulesCount = 0;
|
|
17890
17903
|
let hooksCount = 0;
|
|
17891
17904
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
17892
17905
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
17893
|
-
if (
|
|
17894
|
-
rulesCount += countRulesInDir(
|
|
17895
|
-
const userSettings =
|
|
17906
|
+
if (fs57.existsSync(path56.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
17907
|
+
rulesCount += countRulesInDir(path56.join(claudeDir, "rules"));
|
|
17908
|
+
const userSettings = path56.join(claudeDir, "settings.json");
|
|
17896
17909
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
17897
17910
|
hooksCount += countHooksInFile(userSettings);
|
|
17898
|
-
const userClaudeJson =
|
|
17911
|
+
const userClaudeJson = path56.join(homeDir2, ".claude.json");
|
|
17899
17912
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
17900
17913
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
17901
17914
|
userMcpServers.delete(name);
|
|
17902
17915
|
}
|
|
17903
17916
|
if (cwd) {
|
|
17904
|
-
if (
|
|
17905
|
-
if (
|
|
17906
|
-
const projectClaudeDir =
|
|
17917
|
+
if (fs57.existsSync(path56.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
17918
|
+
if (fs57.existsSync(path56.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
17919
|
+
const projectClaudeDir = path56.join(cwd, ".claude");
|
|
17907
17920
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
17908
17921
|
if (!overlapsUserScope) {
|
|
17909
|
-
if (
|
|
17910
|
-
rulesCount += countRulesInDir(
|
|
17911
|
-
const projSettings =
|
|
17922
|
+
if (fs57.existsSync(path56.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
17923
|
+
rulesCount += countRulesInDir(path56.join(projectClaudeDir, "rules"));
|
|
17924
|
+
const projSettings = path56.join(projectClaudeDir, "settings.json");
|
|
17912
17925
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
17913
17926
|
hooksCount += countHooksInFile(projSettings);
|
|
17914
17927
|
}
|
|
17915
|
-
if (
|
|
17916
|
-
const localSettings =
|
|
17928
|
+
if (fs57.existsSync(path56.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
17929
|
+
const localSettings = path56.join(projectClaudeDir, "settings.local.json");
|
|
17917
17930
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
17918
17931
|
hooksCount += countHooksInFile(localSettings);
|
|
17919
|
-
const mcpJsonServers = getMcpServerNames(
|
|
17932
|
+
const mcpJsonServers = getMcpServerNames(path56.join(cwd, ".mcp.json"));
|
|
17920
17933
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
17921
17934
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
17922
17935
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -17949,12 +17962,12 @@ function readActiveShieldsHud() {
|
|
|
17949
17962
|
return shieldsCache.value;
|
|
17950
17963
|
}
|
|
17951
17964
|
try {
|
|
17952
|
-
const shieldsPath =
|
|
17953
|
-
if (!
|
|
17965
|
+
const shieldsPath = path56.join(os52.homedir(), ".node9", "shields.json");
|
|
17966
|
+
if (!fs57.existsSync(shieldsPath)) {
|
|
17954
17967
|
shieldsCache = { value: [], ts: now };
|
|
17955
17968
|
return [];
|
|
17956
17969
|
}
|
|
17957
|
-
const parsed = JSON.parse(
|
|
17970
|
+
const parsed = JSON.parse(fs57.readFileSync(shieldsPath, "utf-8"));
|
|
17958
17971
|
if (!Array.isArray(parsed.active)) {
|
|
17959
17972
|
shieldsCache = { value: [], ts: now };
|
|
17960
17973
|
return [];
|
|
@@ -18056,17 +18069,17 @@ function renderContextLine(stdin) {
|
|
|
18056
18069
|
async function main() {
|
|
18057
18070
|
try {
|
|
18058
18071
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
18059
|
-
if (
|
|
18072
|
+
if (fs57.existsSync(path56.join(os52.homedir(), ".node9", "hud-debug"))) {
|
|
18060
18073
|
try {
|
|
18061
|
-
const logPath =
|
|
18074
|
+
const logPath = path56.join(os52.homedir(), ".node9", "hud-debug.log");
|
|
18062
18075
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
18063
18076
|
let size = 0;
|
|
18064
18077
|
try {
|
|
18065
|
-
size =
|
|
18078
|
+
size = fs57.statSync(logPath).size;
|
|
18066
18079
|
} catch {
|
|
18067
18080
|
}
|
|
18068
18081
|
if (size < MAX_LOG_SIZE) {
|
|
18069
|
-
|
|
18082
|
+
fs57.appendFileSync(
|
|
18070
18083
|
logPath,
|
|
18071
18084
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
18072
18085
|
);
|
|
@@ -18086,12 +18099,12 @@ async function main() {
|
|
|
18086
18099
|
const showEnvCounts = (() => {
|
|
18087
18100
|
try {
|
|
18088
18101
|
const cwd = stdin.cwd ?? process.cwd();
|
|
18089
|
-
for (const
|
|
18090
|
-
|
|
18091
|
-
|
|
18102
|
+
for (const configPath2 of [
|
|
18103
|
+
path56.join(cwd, "node9.config.json"),
|
|
18104
|
+
path56.join(os52.homedir(), ".node9", "config.json")
|
|
18092
18105
|
]) {
|
|
18093
|
-
if (!
|
|
18094
|
-
const cfg = JSON.parse(
|
|
18106
|
+
if (!fs57.existsSync(configPath2)) continue;
|
|
18107
|
+
const cfg = JSON.parse(fs57.readFileSync(configPath2, "utf-8"));
|
|
18095
18108
|
const hud = cfg.settings?.hud;
|
|
18096
18109
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
18097
18110
|
}
|
|
@@ -18137,10 +18150,10 @@ init_core();
|
|
|
18137
18150
|
init_setup();
|
|
18138
18151
|
init_daemon2();
|
|
18139
18152
|
import { Command } from "commander";
|
|
18140
|
-
import
|
|
18141
|
-
import
|
|
18142
|
-
import
|
|
18143
|
-
import
|
|
18153
|
+
import chalk33 from "chalk";
|
|
18154
|
+
import fs58 from "fs";
|
|
18155
|
+
import path57 from "path";
|
|
18156
|
+
import os53 from "os";
|
|
18144
18157
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
18145
18158
|
|
|
18146
18159
|
// src/utils/duration.ts
|
|
@@ -18180,8 +18193,8 @@ INSTRUCTIONS:
|
|
|
18180
18193
|
- Acknowledge the block to the user and ask if there is an alternative approach.
|
|
18181
18194
|
- If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
|
|
18182
18195
|
}
|
|
18183
|
-
const
|
|
18184
|
-
if (
|
|
18196
|
+
const label2 = blockedByLabel.toLowerCase();
|
|
18197
|
+
if (label2.includes("dlp") || label2.includes("secret detected") || label2.includes("credential review")) {
|
|
18185
18198
|
return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
|
|
18186
18199
|
CRITICAL INSTRUCTION: Do NOT retry this action.
|
|
18187
18200
|
REQUIRED ACTIONS:
|
|
@@ -18190,37 +18203,37 @@ REQUIRED ACTIONS:
|
|
|
18190
18203
|
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
18191
18204
|
Do NOT attempt to bypass this check or pass the credential through another tool.`;
|
|
18192
18205
|
}
|
|
18193
|
-
if (
|
|
18206
|
+
if (label2.includes("sql safety") && label2.includes("delete without where")) {
|
|
18194
18207
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
18195
18208
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
18196
18209
|
Do NOT retry without a WHERE clause.`;
|
|
18197
18210
|
}
|
|
18198
|
-
if (
|
|
18211
|
+
if (label2.includes("sql safety") && label2.includes("update without where")) {
|
|
18199
18212
|
return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
|
|
18200
18213
|
INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
|
|
18201
18214
|
Do NOT retry without a WHERE clause.`;
|
|
18202
18215
|
}
|
|
18203
|
-
if (
|
|
18216
|
+
if (label2.includes("dangerous word")) {
|
|
18204
18217
|
const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
|
|
18205
18218
|
const word = match?.[1] ?? "a dangerous keyword";
|
|
18206
18219
|
return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
|
|
18207
18220
|
INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
|
|
18208
18221
|
Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.`;
|
|
18209
18222
|
}
|
|
18210
|
-
if (
|
|
18223
|
+
if (label2.includes("path blocked") || label2.includes("sandbox")) {
|
|
18211
18224
|
return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
|
|
18212
18225
|
INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
|
|
18213
18226
|
Do NOT retry on the same path.`;
|
|
18214
18227
|
}
|
|
18215
|
-
if (
|
|
18228
|
+
if (label2.includes("inline execution")) {
|
|
18216
18229
|
return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
|
|
18217
18230
|
INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.`;
|
|
18218
18231
|
}
|
|
18219
|
-
if (
|
|
18232
|
+
if (label2.includes("strict mode")) {
|
|
18220
18233
|
return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
|
|
18221
18234
|
INSTRUCTION: Inform the user this action is pending approval. Wait for them to approve via the dashboard or run "node9 pause".`;
|
|
18222
18235
|
}
|
|
18223
|
-
if (
|
|
18236
|
+
if (label2.includes("rule") && label2.includes("default block")) {
|
|
18224
18237
|
const match = blockedByLabel.match(/rule "([^"]+)"/i);
|
|
18225
18238
|
const rule = match?.[1] ?? "a policy rule";
|
|
18226
18239
|
return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
|
|
@@ -19941,6 +19954,7 @@ import fs38 from "fs";
|
|
|
19941
19954
|
import path39 from "path";
|
|
19942
19955
|
import os34 from "os";
|
|
19943
19956
|
import * as yaml2 from "yaml";
|
|
19957
|
+
import { parse as parseToml2 } from "smol-toml";
|
|
19944
19958
|
function readJson2(filePath) {
|
|
19945
19959
|
if (!fs38.existsSync(filePath)) return null;
|
|
19946
19960
|
try {
|
|
@@ -19975,15 +19989,26 @@ function eventWired(root, ev, format) {
|
|
|
19975
19989
|
if (format === "matcher") return matchersHaveNode9Hook(arr);
|
|
19976
19990
|
return flatHaveNode9Hook(arr);
|
|
19977
19991
|
}
|
|
19978
|
-
function
|
|
19979
|
-
const
|
|
19980
|
-
if (parsed === null || parsed === "invalid") return { wrapped: [], present: false };
|
|
19981
|
-
const servers = parsed.mcpServers ?? {};
|
|
19982
|
-
const entries = Object.entries(servers);
|
|
19992
|
+
function detectMcp(servers) {
|
|
19993
|
+
const entries = Object.entries(servers ?? {});
|
|
19983
19994
|
const present = entries.some(([, s]) => s?.command === "node9");
|
|
19984
19995
|
const wrapped = entries.filter(([, s]) => s?.command === "node9" && Array.isArray(s.args) && s.args.length > 0).map(([name, s]) => `${name} \u2192 ${s.args.join(" ")}`);
|
|
19985
19996
|
return { wrapped, present };
|
|
19986
19997
|
}
|
|
19998
|
+
function readMcp(filePath, format) {
|
|
19999
|
+
if (!fs38.existsSync(filePath)) return { wrapped: [], present: false };
|
|
20000
|
+
try {
|
|
20001
|
+
if (format === "toml") {
|
|
20002
|
+
const parsed2 = parseToml2(fs38.readFileSync(filePath, "utf-8"));
|
|
20003
|
+
return detectMcp(parsed2?.mcp_servers);
|
|
20004
|
+
}
|
|
20005
|
+
const parsed = readJson2(filePath);
|
|
20006
|
+
if (parsed === null || parsed === "invalid") return { wrapped: [], present: false };
|
|
20007
|
+
return detectMcp(parsed.mcpServers);
|
|
20008
|
+
} catch {
|
|
20009
|
+
return { wrapped: [], present: false };
|
|
20010
|
+
}
|
|
20011
|
+
}
|
|
19987
20012
|
var exists = (p) => {
|
|
19988
20013
|
try {
|
|
19989
20014
|
return fs38.existsSync(p);
|
|
@@ -19999,7 +20024,7 @@ var AGENT_SPECS = [
|
|
|
19999
20024
|
{
|
|
20000
20025
|
id: "claude",
|
|
20001
20026
|
label: "Claude Code",
|
|
20002
|
-
setupCommand: "node9
|
|
20027
|
+
setupCommand: "node9 agents add claude",
|
|
20003
20028
|
hookFile: (h) => path39.join(h, ".claude", "settings.json"),
|
|
20004
20029
|
hookFormat: "matcher",
|
|
20005
20030
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
|
|
@@ -20009,7 +20034,7 @@ var AGENT_SPECS = [
|
|
|
20009
20034
|
{
|
|
20010
20035
|
id: "gemini",
|
|
20011
20036
|
label: "Gemini CLI",
|
|
20012
|
-
setupCommand: "node9
|
|
20037
|
+
setupCommand: "node9 agents add gemini",
|
|
20013
20038
|
hookFile: (h) => path39.join(h, ".gemini", "settings.json"),
|
|
20014
20039
|
hookFormat: "matcher",
|
|
20015
20040
|
hookEvents: [ck("BeforeTool"), lg("AfterTool")],
|
|
@@ -20019,16 +20044,18 @@ var AGENT_SPECS = [
|
|
|
20019
20044
|
{
|
|
20020
20045
|
id: "codex",
|
|
20021
20046
|
label: "Codex",
|
|
20022
|
-
setupCommand: "node9
|
|
20047
|
+
setupCommand: "node9 agents add codex",
|
|
20023
20048
|
hookFile: (h) => path39.join(h, ".codex", "hooks.json"),
|
|
20024
20049
|
hookFormat: "matcher",
|
|
20025
20050
|
hookEvents: [ck("PreToolUse"), ck("UserPromptSubmit")],
|
|
20051
|
+
mcpFile: (h) => path39.join(h, ".codex", "config.toml"),
|
|
20052
|
+
mcpFormat: "toml",
|
|
20026
20053
|
present: (h) => exists(path39.join(h, ".codex"))
|
|
20027
20054
|
},
|
|
20028
20055
|
{
|
|
20029
20056
|
id: "antigravity",
|
|
20030
20057
|
label: "Antigravity",
|
|
20031
|
-
setupCommand: "node9
|
|
20058
|
+
setupCommand: "node9 agents add antigravity",
|
|
20032
20059
|
hookFile: (h) => path39.join(h, ".gemini", "config", "hooks.json"),
|
|
20033
20060
|
hookFormat: "matcher",
|
|
20034
20061
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
|
|
@@ -20038,7 +20065,7 @@ var AGENT_SPECS = [
|
|
|
20038
20065
|
{
|
|
20039
20066
|
id: "copilot",
|
|
20040
20067
|
label: "GitHub Copilot",
|
|
20041
|
-
setupCommand: "node9
|
|
20068
|
+
setupCommand: "node9 agents add copilot",
|
|
20042
20069
|
hookFile: (h) => path39.join(h, ".copilot", "hooks", "node9.json"),
|
|
20043
20070
|
hookFormat: "flat",
|
|
20044
20071
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse"), ck("UserPromptSubmit")],
|
|
@@ -20048,7 +20075,7 @@ var AGENT_SPECS = [
|
|
|
20048
20075
|
{
|
|
20049
20076
|
id: "cursor",
|
|
20050
20077
|
label: "Cursor",
|
|
20051
|
-
setupCommand: "node9
|
|
20078
|
+
setupCommand: "node9 agents add cursor",
|
|
20052
20079
|
// MCP-only — no hook file (see note above).
|
|
20053
20080
|
hookFormat: "flat",
|
|
20054
20081
|
hookEvents: [],
|
|
@@ -20058,42 +20085,76 @@ var AGENT_SPECS = [
|
|
|
20058
20085
|
{
|
|
20059
20086
|
id: "hermes",
|
|
20060
20087
|
label: "Hermes Agent",
|
|
20061
|
-
setupCommand: "node9
|
|
20088
|
+
setupCommand: "node9 agents add hermes",
|
|
20062
20089
|
hookFile: (h) => hermesConfigPath(h),
|
|
20063
20090
|
hookFormat: "yaml",
|
|
20064
20091
|
hookEvents: [ck("pre_tool_call"), lg("post_tool_call")],
|
|
20065
20092
|
labelPad: 14,
|
|
20066
20093
|
// 'post_tool_call' is wider than the default
|
|
20067
20094
|
present: (h) => exists(hermesConfigPath(h))
|
|
20095
|
+
},
|
|
20096
|
+
{
|
|
20097
|
+
// Plugin-shim agents — protected by a node9-authored plugin/extension file
|
|
20098
|
+
// (no hooks, no MCP). hookFormat is unused for these (shimFile drives it).
|
|
20099
|
+
id: "opencode",
|
|
20100
|
+
label: "OpenCode",
|
|
20101
|
+
setupCommand: "node9 agents add opencode",
|
|
20102
|
+
hookFormat: "flat",
|
|
20103
|
+
hookEvents: [],
|
|
20104
|
+
shimFile: (h) => path39.join(h, ".config", "opencode", "plugins", "node9.js"),
|
|
20105
|
+
present: (h) => exists(path39.join(h, ".config", "opencode")) || exists(path39.join(h, ".config", "opencode", "plugins", "node9.js"))
|
|
20106
|
+
},
|
|
20107
|
+
{
|
|
20108
|
+
id: "pi",
|
|
20109
|
+
label: "Pi",
|
|
20110
|
+
setupCommand: "node9 agents add pi",
|
|
20111
|
+
hookFormat: "flat",
|
|
20112
|
+
hookEvents: [],
|
|
20113
|
+
shimFile: (h) => path39.join(h, ".pi", "agent", "extensions", "node9.js"),
|
|
20114
|
+
present: (h) => exists(path39.join(h, ".pi", "agent")) || exists(path39.join(h, ".pi", "agent", "extensions", "node9.js"))
|
|
20068
20115
|
}
|
|
20069
20116
|
];
|
|
20070
20117
|
function getAgentWiring(home = os34.homedir()) {
|
|
20071
20118
|
const detected = detectAgents(home);
|
|
20072
20119
|
return AGENT_SPECS.map((spec) => {
|
|
20073
|
-
const
|
|
20074
|
-
const primary = spec.hookEvents[0];
|
|
20075
|
-
const rootPresent = root !== "absent" && root !== "invalid";
|
|
20120
|
+
const present = spec.present(home);
|
|
20076
20121
|
const pad = spec.labelPad ?? DEFAULT_LABEL_PAD;
|
|
20077
|
-
|
|
20078
|
-
label: hookLabelOf(ev, pad),
|
|
20079
|
-
wired: rootPresent && eventWired(root, ev, spec.hookFormat)
|
|
20080
|
-
}));
|
|
20122
|
+
let hooks;
|
|
20081
20123
|
let wireState;
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
20124
|
+
let hookLabel;
|
|
20125
|
+
let settingsPath;
|
|
20126
|
+
if (spec.shimFile) {
|
|
20127
|
+
const shimWired = exists(spec.shimFile(home));
|
|
20128
|
+
hooks = [{ label: "node9 plugin (node9 check)", wired: shimWired }];
|
|
20129
|
+
wireState = shimWired ? "wired" : present ? "unwired" : "absent";
|
|
20130
|
+
hookLabel = "node9 plugin";
|
|
20131
|
+
settingsPath = spec.shimFile(home);
|
|
20132
|
+
} else {
|
|
20133
|
+
const root = spec.hookFile ? readHookRoot(spec.hookFile(home), spec.hookFormat) : "absent";
|
|
20134
|
+
const primary = spec.hookEvents[0];
|
|
20135
|
+
const rootPresent = root !== "absent" && root !== "invalid";
|
|
20136
|
+
hooks = spec.hookEvents.map((ev) => ({
|
|
20137
|
+
label: hookLabelOf(ev, pad),
|
|
20138
|
+
wired: rootPresent && eventWired(root, ev, spec.hookFormat)
|
|
20139
|
+
}));
|
|
20140
|
+
if (root === "absent") wireState = "absent";
|
|
20141
|
+
else if (root === "invalid") wireState = "invalid";
|
|
20142
|
+
else wireState = primary && eventWired(root, primary, spec.hookFormat) ? "wired" : "unwired";
|
|
20143
|
+
hookLabel = primary ? `${primary.key} hook` : "MCP proxy";
|
|
20144
|
+
settingsPath = spec.hookFile ? spec.hookFile(home) : spec.mcpFile ? spec.mcpFile(home) : "";
|
|
20145
|
+
}
|
|
20146
|
+
const mcp = spec.mcpFile ? readMcp(spec.mcpFile(home), spec.mcpFormat ?? "json") : null;
|
|
20086
20147
|
const anyHookWired = hooks.some((h) => h.wired);
|
|
20087
20148
|
return {
|
|
20088
20149
|
id: spec.id,
|
|
20089
20150
|
label: spec.label,
|
|
20090
20151
|
setupCommand: spec.setupCommand,
|
|
20091
20152
|
installed: detected[spec.id],
|
|
20092
|
-
present
|
|
20153
|
+
present,
|
|
20093
20154
|
hooks,
|
|
20094
20155
|
wireState,
|
|
20095
|
-
hookLabel
|
|
20096
|
-
settingsPath
|
|
20156
|
+
hookLabel,
|
|
20157
|
+
settingsPath,
|
|
20097
20158
|
configFormat: spec.hookFormat === "yaml" ? "YAML" : "JSON",
|
|
20098
20159
|
mcpServers: mcp ? mcp.wrapped : null,
|
|
20099
20160
|
mcpProtected: mcp ? mcp.present : false,
|
|
@@ -20130,10 +20191,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20130
20191
|
const which = execSync("which node9", { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
20131
20192
|
pass(`node9 found at ${which}`);
|
|
20132
20193
|
} catch {
|
|
20133
|
-
warn(
|
|
20134
|
-
"node9 not found in $PATH \u2014 hooks may not find it",
|
|
20135
|
-
"Run: npm install -g @node9/proxy"
|
|
20136
|
-
);
|
|
20194
|
+
warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g node9-ai");
|
|
20137
20195
|
}
|
|
20138
20196
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
20139
20197
|
if (nodeMajor >= 18) {
|
|
@@ -20203,7 +20261,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20203
20261
|
if (notConfigured.length > 0) {
|
|
20204
20262
|
console.log(
|
|
20205
20263
|
chalk11.gray(
|
|
20206
|
-
` \xB7 Not configured: ${notConfigured.join(", ")} \u2014 run \`node9
|
|
20264
|
+
` \xB7 Not configured: ${notConfigured.join(", ")} \u2014 run \`node9 agents add <agent>\` if you use one`
|
|
20207
20265
|
)
|
|
20208
20266
|
);
|
|
20209
20267
|
}
|
|
@@ -21184,10 +21242,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21184
21242
|
);
|
|
21185
21243
|
console.log("");
|
|
21186
21244
|
const COL1 = 18;
|
|
21187
|
-
const summaryRow = (icon,
|
|
21245
|
+
const summaryRow = (icon, label2, count, note, colorFn = (s) => s) => {
|
|
21188
21246
|
const countStr = colorFn(num2(count));
|
|
21189
21247
|
const noteStr = note ? chalk13.dim(" " + note) : "";
|
|
21190
|
-
console.log(" " + icon + " " + chalk13.white(
|
|
21248
|
+
console.log(" " + icon + " " + chalk13.white(label2.padEnd(COL1)) + countStr + noteStr);
|
|
21191
21249
|
};
|
|
21192
21250
|
summaryRow(
|
|
21193
21251
|
userApproved > 0 ? chalk13.green("\u2705") : chalk13.dim("\u2705"),
|
|
@@ -21254,21 +21312,21 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21254
21312
|
let leftStyled = " ".repeat(COL);
|
|
21255
21313
|
if (i < topTools.length) {
|
|
21256
21314
|
const [tool, { calls }] = topTools[i];
|
|
21257
|
-
const
|
|
21315
|
+
const label2 = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
|
|
21258
21316
|
const countStr = num2(calls).padStart(TOOL_COUNT_W);
|
|
21259
21317
|
const b = colorBar(calls, maxTool, BAR);
|
|
21260
21318
|
const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
|
|
21261
21319
|
const pad = Math.max(0, COL - rawLen);
|
|
21262
|
-
leftStyled = chalk13.white(
|
|
21320
|
+
leftStyled = chalk13.white(label2.padEnd(LABEL)) + b + " " + chalk13.white(countStr) + " ".repeat(pad);
|
|
21263
21321
|
}
|
|
21264
21322
|
let rightStyled = "";
|
|
21265
21323
|
if (i < topBlocks.length) {
|
|
21266
21324
|
const [reason, count] = topBlocks[i];
|
|
21267
21325
|
const readable = humanBlockReason(reason);
|
|
21268
|
-
const
|
|
21326
|
+
const label2 = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
|
|
21269
21327
|
const countStr = num2(count).padStart(BLOCK_COUNT_W);
|
|
21270
21328
|
const b = colorBar(count, maxBlock, BAR);
|
|
21271
|
-
rightStyled = chalk13.white(
|
|
21329
|
+
rightStyled = chalk13.white(label2.padEnd(LABEL)) + b + " " + chalk13.red(countStr);
|
|
21272
21330
|
}
|
|
21273
21331
|
console.log(" " + leftStyled + " " + rightStyled);
|
|
21274
21332
|
}
|
|
@@ -21281,9 +21339,9 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21281
21339
|
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
21282
21340
|
const maxAgent = Math.max(...agentMap.values(), 1);
|
|
21283
21341
|
for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
21284
|
-
const
|
|
21342
|
+
const label2 = agent.slice(0, LABEL - 1);
|
|
21285
21343
|
const b = colorBar(count, maxAgent, BAR);
|
|
21286
|
-
console.log(" " + chalk13.white(
|
|
21344
|
+
console.log(" " + chalk13.white(label2.padEnd(LABEL)) + b + " " + chalk13.white(num2(count)));
|
|
21287
21345
|
}
|
|
21288
21346
|
}
|
|
21289
21347
|
if (mcpMap.size > 0) {
|
|
@@ -21292,9 +21350,9 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21292
21350
|
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
21293
21351
|
const maxMcp = Math.max(...mcpMap.values(), 1);
|
|
21294
21352
|
for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
21295
|
-
const
|
|
21353
|
+
const label2 = server.slice(0, LABEL - 1).padEnd(LABEL);
|
|
21296
21354
|
const b = colorBar(count, maxMcp, BAR);
|
|
21297
|
-
console.log(" " + chalk13.white(
|
|
21355
|
+
console.log(" " + chalk13.white(label2) + b + " " + chalk13.white(num2(count)));
|
|
21298
21356
|
}
|
|
21299
21357
|
}
|
|
21300
21358
|
if (hourMap.size > 0) {
|
|
@@ -21315,13 +21373,13 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21315
21373
|
console.log(" " + chalk13.dim("\u2500".repeat(W - 2)));
|
|
21316
21374
|
const DAY_BAR = Math.max(8, Math.min(30, W - 36));
|
|
21317
21375
|
for (const [dateKey, { calls, blocked: db }] of dailyList) {
|
|
21318
|
-
const
|
|
21376
|
+
const label2 = fmtDate(dateKey).padEnd(10);
|
|
21319
21377
|
const b = colorBar(calls, maxDaily, DAY_BAR);
|
|
21320
21378
|
const dayCost = costByDay.get(dateKey);
|
|
21321
21379
|
const costNote = dayCost ? chalk13.magenta(` ${fmtCost2(dayCost)}`) : "";
|
|
21322
21380
|
const blockNote = db > 0 ? chalk13.red(` ${db} blocked`) : "";
|
|
21323
21381
|
console.log(
|
|
21324
|
-
" " + chalk13.dim(
|
|
21382
|
+
" " + chalk13.dim(label2) + " " + b + " " + chalk13.white(num2(calls)) + blockNote + costNote
|
|
21325
21383
|
);
|
|
21326
21384
|
}
|
|
21327
21385
|
}
|
|
@@ -21339,10 +21397,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21339
21397
|
["Output", costOutputTokens, chalk13.white(num2(costOutputTokens))],
|
|
21340
21398
|
["Cache write", costCacheWrite, chalk13.yellow(num2(costCacheWrite))]
|
|
21341
21399
|
];
|
|
21342
|
-
for (const [
|
|
21400
|
+
for (const [label2, count, colored] of nonCacheRows) {
|
|
21343
21401
|
if (count === 0) continue;
|
|
21344
21402
|
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
21345
|
-
console.log(" " + chalk13.white(
|
|
21403
|
+
console.log(" " + chalk13.white(label2.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
21346
21404
|
}
|
|
21347
21405
|
if (costCacheRead > 0) {
|
|
21348
21406
|
const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
|
|
@@ -21373,10 +21431,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21373
21431
|
const MODEL_LABEL = 22;
|
|
21374
21432
|
const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
|
|
21375
21433
|
for (const [model, cost] of modelList) {
|
|
21376
|
-
const
|
|
21434
|
+
const label2 = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
|
|
21377
21435
|
const b = colorBar(cost, maxModelCost, MODEL_BAR);
|
|
21378
21436
|
console.log(
|
|
21379
|
-
" " + chalk13.white(
|
|
21437
|
+
" " + chalk13.white(label2.padEnd(MODEL_LABEL)) + b + " " + chalk13.yellow(fmtCost2(cost))
|
|
21380
21438
|
);
|
|
21381
21439
|
}
|
|
21382
21440
|
}
|
|
@@ -21501,8 +21559,8 @@ import chalk15 from "chalk";
|
|
|
21501
21559
|
import fs42 from "fs";
|
|
21502
21560
|
import path43 from "path";
|
|
21503
21561
|
import os38 from "os";
|
|
21504
|
-
function printAgentSection(
|
|
21505
|
-
console.log(chalk15.bold(` ${
|
|
21562
|
+
function printAgentSection(label2, hookPairs, wrapped) {
|
|
21563
|
+
console.log(chalk15.bold(` ${label2}`));
|
|
21506
21564
|
for (const { name, present } of hookPairs) {
|
|
21507
21565
|
if (present) {
|
|
21508
21566
|
console.log(chalk15.green(` \u2713 ${name}`));
|
|
@@ -21694,32 +21752,32 @@ function registerInitCommand(program2) {
|
|
|
21694
21752
|
}
|
|
21695
21753
|
console.log("");
|
|
21696
21754
|
}
|
|
21697
|
-
const
|
|
21698
|
-
const isFirstInstall = !fs43.existsSync(
|
|
21699
|
-
if (fs43.existsSync(
|
|
21755
|
+
const configPath2 = path44.join(os39.homedir(), ".node9", "config.json");
|
|
21756
|
+
const isFirstInstall = !fs43.existsSync(configPath2);
|
|
21757
|
+
if (fs43.existsSync(configPath2) && !options.force) {
|
|
21700
21758
|
try {
|
|
21701
|
-
const existing = JSON.parse(fs43.readFileSync(
|
|
21759
|
+
const existing = JSON.parse(fs43.readFileSync(configPath2, "utf-8"));
|
|
21702
21760
|
const settings = existing.settings ?? {};
|
|
21703
21761
|
if (settings.mode !== chosenMode) {
|
|
21704
21762
|
settings.mode = chosenMode;
|
|
21705
21763
|
existing.settings = settings;
|
|
21706
|
-
fs43.writeFileSync(
|
|
21764
|
+
fs43.writeFileSync(configPath2, JSON.stringify(existing, null, 2) + "\n");
|
|
21707
21765
|
console.log(chalk16.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
21708
21766
|
} else {
|
|
21709
|
-
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${
|
|
21767
|
+
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath2}`));
|
|
21710
21768
|
}
|
|
21711
21769
|
} catch {
|
|
21712
|
-
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${
|
|
21770
|
+
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath2}`));
|
|
21713
21771
|
}
|
|
21714
21772
|
} else {
|
|
21715
21773
|
const configToSave = {
|
|
21716
21774
|
...DEFAULT_CONFIG,
|
|
21717
21775
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
21718
21776
|
};
|
|
21719
|
-
const dir = path44.dirname(
|
|
21777
|
+
const dir = path44.dirname(configPath2);
|
|
21720
21778
|
if (!fs43.existsSync(dir)) fs43.mkdirSync(dir, { recursive: true });
|
|
21721
|
-
fs43.writeFileSync(
|
|
21722
|
-
console.log(chalk16.green(`\u2705 Config created: ${
|
|
21779
|
+
fs43.writeFileSync(configPath2, JSON.stringify(configToSave, null, 2) + "\n");
|
|
21780
|
+
console.log(chalk16.green(`\u2705 Config created: ${configPath2}`));
|
|
21723
21781
|
console.log(chalk16.gray(` Mode: ${chosenMode}`));
|
|
21724
21782
|
}
|
|
21725
21783
|
if (options.skipSetup) return;
|
|
@@ -22030,13 +22088,13 @@ function registerUndoCommand(program2) {
|
|
|
22030
22088
|
const e = display[i];
|
|
22031
22089
|
const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
|
|
22032
22090
|
if (isGap) console.log(chalk18.gray(" \u2500\u2500 earlier \u2500\u2500"));
|
|
22033
|
-
const
|
|
22091
|
+
const label2 = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
|
|
22034
22092
|
const tool = e.tool.slice(0, 8).padEnd(8);
|
|
22035
22093
|
const when = formatAge2(e.timestamp).padEnd(10);
|
|
22036
22094
|
const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
|
|
22037
22095
|
console.log(
|
|
22038
22096
|
chalk18.white(
|
|
22039
|
-
` ${String(i + 1).padEnd(3)} ${
|
|
22097
|
+
` ${String(i + 1).padEnd(3)} ${label2} ${chalk18.cyan(tool)} ${chalk18.gray(when)} ${chalk18.gray(dir)}`
|
|
22040
22098
|
)
|
|
22041
22099
|
);
|
|
22042
22100
|
prevTs = e.timestamp;
|
|
@@ -23451,11 +23509,11 @@ function registerMcpPinCommand(program2) {
|
|
|
23451
23509
|
`);
|
|
23452
23510
|
process.exit(1);
|
|
23453
23511
|
}
|
|
23454
|
-
const
|
|
23512
|
+
const label2 = pins.servers[serverKey].label;
|
|
23455
23513
|
removePin(serverKey);
|
|
23456
23514
|
console.log(chalk21.green(`
|
|
23457
23515
|
\u{1F513} Pin removed for ${chalk21.cyan(serverKey)}`));
|
|
23458
|
-
console.log(chalk21.gray(` Server: ${
|
|
23516
|
+
console.log(chalk21.gray(` Server: ${label2}`));
|
|
23459
23517
|
console.log(chalk21.gray(" Next connection will re-pin with current tool definitions.\n"));
|
|
23460
23518
|
});
|
|
23461
23519
|
pinSubCmd.command("reset").description("Clear all MCP pins (next connection to each server will re-pin)").action(() => {
|
|
@@ -23667,15 +23725,1135 @@ function registerAgentsCommand(program2) {
|
|
|
23667
23725
|
// src/cli.ts
|
|
23668
23726
|
init_scan();
|
|
23669
23727
|
|
|
23728
|
+
// src/cli/commands/posture.ts
|
|
23729
|
+
import chalk25 from "chalk";
|
|
23730
|
+
|
|
23731
|
+
// src/posture/index.ts
|
|
23732
|
+
import os44 from "os";
|
|
23733
|
+
|
|
23734
|
+
// src/posture/secrets.ts
|
|
23735
|
+
init_dist();
|
|
23736
|
+
import fs46 from "fs";
|
|
23737
|
+
import path47 from "path";
|
|
23738
|
+
import os41 from "os";
|
|
23739
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
23740
|
+
function displayPath(p, home) {
|
|
23741
|
+
if (p === home) return "~";
|
|
23742
|
+
const prefix = home.endsWith(path47.sep) ? home : home + path47.sep;
|
|
23743
|
+
if (p.startsWith(prefix)) return "~" + path47.sep + p.slice(prefix.length);
|
|
23744
|
+
return p;
|
|
23745
|
+
}
|
|
23746
|
+
function safeRead(file) {
|
|
23747
|
+
try {
|
|
23748
|
+
const stat = fs46.statSync(file);
|
|
23749
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_FILE_BYTES) return null;
|
|
23750
|
+
return fs46.readFileSync(file, "utf8");
|
|
23751
|
+
} catch {
|
|
23752
|
+
return null;
|
|
23753
|
+
}
|
|
23754
|
+
}
|
|
23755
|
+
function candidateFiles(home, cwd) {
|
|
23756
|
+
const files = /* @__PURE__ */ new Set();
|
|
23757
|
+
try {
|
|
23758
|
+
for (const name of fs46.readdirSync(cwd)) {
|
|
23759
|
+
if (name === ".env" || name.startsWith(".env.")) files.add(path47.join(cwd, name));
|
|
23760
|
+
}
|
|
23761
|
+
} catch {
|
|
23762
|
+
}
|
|
23763
|
+
for (const spec of AGENT_SPECS) {
|
|
23764
|
+
if (spec.hookFile) files.add(spec.hookFile(home));
|
|
23765
|
+
if (spec.mcpFile) files.add(spec.mcpFile(home));
|
|
23766
|
+
}
|
|
23767
|
+
files.add(path47.join(home, ".env"));
|
|
23768
|
+
return [...files];
|
|
23769
|
+
}
|
|
23770
|
+
function credentialMaterial(home) {
|
|
23771
|
+
return [
|
|
23772
|
+
path47.join(home, ".ssh", "id_rsa"),
|
|
23773
|
+
path47.join(home, ".ssh", "id_dsa"),
|
|
23774
|
+
path47.join(home, ".ssh", "id_ecdsa"),
|
|
23775
|
+
path47.join(home, ".ssh", "id_ed25519"),
|
|
23776
|
+
path47.join(home, ".aws", "credentials"),
|
|
23777
|
+
path47.join(home, ".config", "gcloud", "application_default_credentials.json")
|
|
23778
|
+
];
|
|
23779
|
+
}
|
|
23780
|
+
function checkSecrets(ctx) {
|
|
23781
|
+
const home = ctx.home || os41.homedir();
|
|
23782
|
+
const findings = [];
|
|
23783
|
+
const plaintext = [];
|
|
23784
|
+
const plaintextPaths = [];
|
|
23785
|
+
for (const file of candidateFiles(home, ctx.cwd)) {
|
|
23786
|
+
const text = safeRead(file);
|
|
23787
|
+
if (!text) continue;
|
|
23788
|
+
const match = scanText(text);
|
|
23789
|
+
if (match) {
|
|
23790
|
+
plaintext.push(`${match.patternName} in ${displayPath(file, home)}`);
|
|
23791
|
+
plaintextPaths.push(file);
|
|
23792
|
+
}
|
|
23793
|
+
}
|
|
23794
|
+
if (plaintext.length > 0) {
|
|
23795
|
+
findings.push({
|
|
23796
|
+
category: "Secrets",
|
|
23797
|
+
severity: "critical",
|
|
23798
|
+
title: `${plaintext.length} plaintext secret${plaintext.length === 1 ? "" : "s"} on disk`,
|
|
23799
|
+
what: "API keys/tokens are sitting unencrypted in files on disk.",
|
|
23800
|
+
why: "They were saved in plaintext config / .env files.",
|
|
23801
|
+
who: "A tricked agent (or any program you run) could read and leak them.",
|
|
23802
|
+
detail: plaintext,
|
|
23803
|
+
fix: "Fix it now: run `node9 shield enable project-jail` (blocks credential-file reads in-path).",
|
|
23804
|
+
// Coverage is decided at the DLP layer — does node9 block the agent
|
|
23805
|
+
// reading these? (See enforcement.ts.)
|
|
23806
|
+
owner: "node9",
|
|
23807
|
+
coverageProbe: { kind: "fileRead", paths: plaintextPaths }
|
|
23808
|
+
});
|
|
23809
|
+
}
|
|
23810
|
+
const creds = [];
|
|
23811
|
+
const credPaths = [];
|
|
23812
|
+
for (const file of credentialMaterial(home)) {
|
|
23813
|
+
try {
|
|
23814
|
+
if (fs46.statSync(file).isFile()) {
|
|
23815
|
+
creds.push(displayPath(file, home));
|
|
23816
|
+
credPaths.push(file);
|
|
23817
|
+
}
|
|
23818
|
+
} catch {
|
|
23819
|
+
}
|
|
23820
|
+
}
|
|
23821
|
+
if (creds.length > 0) {
|
|
23822
|
+
findings.push({
|
|
23823
|
+
category: "Secrets",
|
|
23824
|
+
severity: "high",
|
|
23825
|
+
title: `${creds.length} credential file${creds.length === 1 ? "" : "s"} readable by the agent`,
|
|
23826
|
+
what: "Your SSH keys / cloud login files can be read by programs you run.",
|
|
23827
|
+
why: "They sit unlocked in your home folder.",
|
|
23828
|
+
who: "An unsandboxed agent could read them and use them to reach your servers / cloud.",
|
|
23829
|
+
detail: creds,
|
|
23830
|
+
fix: "Fix it now: run `node9 shield enable project-jail` (blocks ~/.ssh, ~/.aws, .env reads in-path).",
|
|
23831
|
+
owner: "node9",
|
|
23832
|
+
coverageProbe: { kind: "fileRead", paths: credPaths }
|
|
23833
|
+
});
|
|
23834
|
+
}
|
|
23835
|
+
return findings;
|
|
23836
|
+
}
|
|
23837
|
+
|
|
23838
|
+
// src/posture/egress.ts
|
|
23839
|
+
init_config();
|
|
23840
|
+
function evaluateEgressConfig(egress) {
|
|
23841
|
+
if (egress.enabled && egress.mode === "block") {
|
|
23842
|
+
return {
|
|
23843
|
+
category: "Egress",
|
|
23844
|
+
severity: "high",
|
|
23845
|
+
title: "Egress is locked, but node9 is not enforcing it",
|
|
23846
|
+
what: "Egress is set to block, but node9 is not applying the policy.",
|
|
23847
|
+
why: "node9 isn't wired in (or is in observe mode), so the lock has no effect.",
|
|
23848
|
+
who: "The lock protects nothing until node9 is enforcing in-path.",
|
|
23849
|
+
owner: "node9",
|
|
23850
|
+
detail: [],
|
|
23851
|
+
fix: "Run `node9 init` and ensure node9 is in enforcing mode.",
|
|
23852
|
+
coverageProbe: { kind: "egress" },
|
|
23853
|
+
// Open here means only "node9 isn't enforcing" — Coverage already says
|
|
23854
|
+
// that, so drop this row when open to avoid double-surfacing.
|
|
23855
|
+
redundantWhenOpen: true
|
|
23856
|
+
};
|
|
23857
|
+
}
|
|
23858
|
+
if (egress.enabled && egress.mode === "review") {
|
|
23859
|
+
return {
|
|
23860
|
+
category: "Egress",
|
|
23861
|
+
severity: "medium",
|
|
23862
|
+
title: "Egress is in review, but node9 is not enforcing it",
|
|
23863
|
+
what: "Egress is set to review (approval-gate), but node9 is not applying the policy.",
|
|
23864
|
+
why: "node9 isn't wired in (or is in observe mode), so the gate has no effect.",
|
|
23865
|
+
who: "Nothing gates outbound until node9 is enforcing in-path.",
|
|
23866
|
+
owner: "node9",
|
|
23867
|
+
detail: [],
|
|
23868
|
+
fix: "Run `node9 init` and ensure node9 is in enforcing mode.",
|
|
23869
|
+
coverageProbe: { kind: "egress" },
|
|
23870
|
+
// Open here means only "node9 isn't enforcing" — Coverage already says
|
|
23871
|
+
// that, so drop this row when open to avoid double-surfacing.
|
|
23872
|
+
redundantWhenOpen: true
|
|
23873
|
+
};
|
|
23874
|
+
}
|
|
23875
|
+
return {
|
|
23876
|
+
category: "Egress",
|
|
23877
|
+
severity: "high",
|
|
23878
|
+
title: "Egress is open",
|
|
23879
|
+
what: "Your agent can connect to any server on the internet.",
|
|
23880
|
+
why: "node9 isn't restricting where its network tools (curl, wget, ssh) can reach.",
|
|
23881
|
+
who: "If the agent is ever tricked, nothing stops it sending your data out.",
|
|
23882
|
+
owner: "node9",
|
|
23883
|
+
detail: [],
|
|
23884
|
+
fix: "Fix it now: run `node9 egress watch` (or `node9 egress lock` to hard-block).",
|
|
23885
|
+
coverageProbe: { kind: "egress" }
|
|
23886
|
+
};
|
|
23887
|
+
}
|
|
23888
|
+
function checkEgress(ctx) {
|
|
23889
|
+
const config = getConfig(ctx.cwd);
|
|
23890
|
+
const egress = config.policy.egress;
|
|
23891
|
+
return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
|
|
23892
|
+
}
|
|
23893
|
+
|
|
23894
|
+
// src/posture/gate.ts
|
|
23895
|
+
init_policy();
|
|
23896
|
+
var BASELINE = ["rm", "-rf", "/"].join(" ");
|
|
23897
|
+
async function checkGate(ctx) {
|
|
23898
|
+
const verdict = await evaluatePolicy2("Bash", { command: BASELINE }, ctx.agent, ctx.cwd);
|
|
23899
|
+
if (verdict.decision !== "block") {
|
|
23900
|
+
return [
|
|
23901
|
+
{
|
|
23902
|
+
category: "Approval gate",
|
|
23903
|
+
severity: "critical",
|
|
23904
|
+
title: "No approval gate is active \u2014 destructive commands run unchecked",
|
|
23905
|
+
what: "Dangerous shell commands aren't gated \u2014 even `rm -rf /` would run.",
|
|
23906
|
+
why: "No enforcing shield or smart rule is gating Bash.",
|
|
23907
|
+
who: "A confused or tricked agent could damage the machine with one command.",
|
|
23908
|
+
detail: [],
|
|
23909
|
+
owner: "node9",
|
|
23910
|
+
fix: "Turn on the gate: run `node9 shield enable bash-safe` (or add a smart rule). node9 then blocks dangerous commands and the negotiation loop tells the agent what's allowed."
|
|
23911
|
+
}
|
|
23912
|
+
];
|
|
23913
|
+
}
|
|
23914
|
+
return [
|
|
23915
|
+
{
|
|
23916
|
+
category: "Approval gate",
|
|
23917
|
+
severity: "advisory",
|
|
23918
|
+
title: "node9 is your approval gate \u2014 destructive commands are blocked",
|
|
23919
|
+
what: "Dangerous shell commands are blocked in-path by your shields and smart rules; when node9 blocks, the negotiation loop tells the agent what is allowed.",
|
|
23920
|
+
detail: [],
|
|
23921
|
+
owner: "node9",
|
|
23922
|
+
coverageProbe: { kind: "command", command: BASELINE },
|
|
23923
|
+
redundantWhenOpen: true
|
|
23924
|
+
}
|
|
23925
|
+
];
|
|
23926
|
+
}
|
|
23927
|
+
|
|
23928
|
+
// src/posture/supply-chain.ts
|
|
23929
|
+
init_provenance();
|
|
23930
|
+
import fs47 from "fs";
|
|
23931
|
+
import os42 from "os";
|
|
23932
|
+
import path48 from "path";
|
|
23933
|
+
import { parse as parseToml3 } from "smol-toml";
|
|
23934
|
+
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpx", "bunx", "dlx", "yarn", "pnpm", "bun"]);
|
|
23935
|
+
function isNode9Managed(command, args = []) {
|
|
23936
|
+
if (!command) return false;
|
|
23937
|
+
if (path48.basename(command).toLowerCase() === "node9") return true;
|
|
23938
|
+
if (PACKAGE_RUNNERS.has(path48.basename(command).toLowerCase())) {
|
|
23939
|
+
return args.some((a) => a === "node9" || path48.basename(a).toLowerCase() === "node9");
|
|
23940
|
+
}
|
|
23941
|
+
return false;
|
|
23942
|
+
}
|
|
23943
|
+
var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
|
|
23944
|
+
function readServers(file, format, agent) {
|
|
23945
|
+
try {
|
|
23946
|
+
const stat = fs47.statSync(file);
|
|
23947
|
+
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
|
|
23948
|
+
const text = fs47.readFileSync(file, "utf8");
|
|
23949
|
+
const map = format === "toml" ? parseToml3(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
|
|
23950
|
+
if (!map || typeof map !== "object") return [];
|
|
23951
|
+
return Object.entries(map).map(([name, v]) => ({
|
|
23952
|
+
name,
|
|
23953
|
+
command: v?.command,
|
|
23954
|
+
args: Array.isArray(v?.args) ? v.args : void 0,
|
|
23955
|
+
agent
|
|
23956
|
+
}));
|
|
23957
|
+
} catch {
|
|
23958
|
+
return [];
|
|
23959
|
+
}
|
|
23960
|
+
}
|
|
23961
|
+
function checkSupplyChain(ctx) {
|
|
23962
|
+
const home = ctx.home || os42.homedir();
|
|
23963
|
+
const servers = [];
|
|
23964
|
+
for (const spec of AGENT_SPECS) {
|
|
23965
|
+
if (!spec.mcpFile) continue;
|
|
23966
|
+
servers.push(...readServers(spec.mcpFile(home), spec.mcpFormat ?? "json", spec.label));
|
|
23967
|
+
}
|
|
23968
|
+
if (servers.length === 0) return [];
|
|
23969
|
+
const findings = [];
|
|
23970
|
+
const unmanaged = servers.filter((s) => s.command && !isNode9Managed(s.command, s.args));
|
|
23971
|
+
const suspect = unmanaged.filter(
|
|
23972
|
+
(s) => checkProvenance(s.command, ctx.cwd).trustLevel === "suspect"
|
|
23973
|
+
);
|
|
23974
|
+
if (suspect.length > 0) {
|
|
23975
|
+
findings.push({
|
|
23976
|
+
category: "Supply chain",
|
|
23977
|
+
severity: "high",
|
|
23978
|
+
title: `${suspect.length} MCP server${suspect.length === 1 ? "" : "s"} launched from an untrusted path`,
|
|
23979
|
+
what: "An MCP tool-server runs from an untrusted location.",
|
|
23980
|
+
why: "Its binary lives in /tmp or a world-writable directory.",
|
|
23981
|
+
who: "Anything on the machine could swap that binary for malware the agent then runs.",
|
|
23982
|
+
detail: suspect.map((s) => `${s.name} \u2192 ${s.command} (${s.agent})`),
|
|
23983
|
+
owner: "node9",
|
|
23984
|
+
fix: "node9 can pin + provenance-check MCP servers before they run."
|
|
23985
|
+
});
|
|
23986
|
+
}
|
|
23987
|
+
if (unmanaged.length > 0) {
|
|
23988
|
+
findings.push({
|
|
23989
|
+
category: "Supply chain",
|
|
23990
|
+
severity: "medium",
|
|
23991
|
+
title: `${unmanaged.length} of ${servers.length} MCP server${servers.length === 1 ? "" : "s"} run outside node9`,
|
|
23992
|
+
what: "Some MCP tool-servers run without node9 watching their tool calls.",
|
|
23993
|
+
why: "They're launched directly, not wrapped by node9.",
|
|
23994
|
+
who: "A poisoned or silently-updated server could act freely (tool-poisoning / rug-pull).",
|
|
23995
|
+
detail: unmanaged.slice(0, 5).map((s) => `${s.name} (${s.agent})`),
|
|
23996
|
+
owner: "node9",
|
|
23997
|
+
fix: "node9 can wrap MCP servers so every tool call is gated + pinned."
|
|
23998
|
+
});
|
|
23999
|
+
}
|
|
24000
|
+
return findings;
|
|
24001
|
+
}
|
|
24002
|
+
|
|
24003
|
+
// src/posture/privilege.ts
|
|
24004
|
+
init_policy();
|
|
24005
|
+
var SUDO_PROBE = "sudo chmod 777 /etc/passwd";
|
|
24006
|
+
async function checkPrivilege(ctx) {
|
|
24007
|
+
const findings = [];
|
|
24008
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
24009
|
+
const isRoot = uid === 0;
|
|
24010
|
+
if (isRoot) {
|
|
24011
|
+
findings.push({
|
|
24012
|
+
category: "Privilege",
|
|
24013
|
+
severity: "high",
|
|
24014
|
+
title: "Running as root",
|
|
24015
|
+
what: "The agent process is running as root (full system rights).",
|
|
24016
|
+
why: "It was started as uid 0.",
|
|
24017
|
+
who: "One bad command can change any file, user, or service on the machine.",
|
|
24018
|
+
detail: [],
|
|
24019
|
+
owner: "node9",
|
|
24020
|
+
fix: "node9 can block privileged commands (sudo, system-path writes) in-path."
|
|
24021
|
+
});
|
|
24022
|
+
}
|
|
24023
|
+
const verdict = await evaluatePolicy2("Bash", { command: SUDO_PROBE }, ctx.agent, ctx.cwd);
|
|
24024
|
+
if (verdict.decision !== "block") {
|
|
24025
|
+
findings.push({
|
|
24026
|
+
category: "Privilege",
|
|
24027
|
+
severity: isRoot ? "high" : "medium",
|
|
24028
|
+
title: "Privilege escalation is not gated",
|
|
24029
|
+
what: "node9 isn't gating `sudo`.",
|
|
24030
|
+
why: "No sudo rule is active in the current policy.",
|
|
24031
|
+
// Calibrated: don't claim the agent CAN become root — it depends on sudo config.
|
|
24032
|
+
who: "If `sudo` is passwordless (NOPASSWD), an agent could become root; with a password prompt the risk is lower.",
|
|
24033
|
+
detail: [],
|
|
24034
|
+
fix: "node9 can gate sudo / privilege-escalation in-path.",
|
|
24035
|
+
// Coverage probes the real policy: block OR review = gated (covered).
|
|
24036
|
+
owner: "node9",
|
|
24037
|
+
coverageProbe: { kind: "command", command: SUDO_PROBE }
|
|
24038
|
+
});
|
|
24039
|
+
}
|
|
24040
|
+
return findings;
|
|
24041
|
+
}
|
|
24042
|
+
|
|
24043
|
+
// src/posture/containment.ts
|
|
24044
|
+
import fs48 from "fs";
|
|
24045
|
+
function inContainer() {
|
|
24046
|
+
if (fs48.existsSync("/.dockerenv") || fs48.existsSync("/run/.containerenv")) return true;
|
|
24047
|
+
try {
|
|
24048
|
+
const cgroup = fs48.readFileSync("/proc/1/cgroup", "utf8");
|
|
24049
|
+
if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
|
|
24050
|
+
} catch {
|
|
24051
|
+
}
|
|
24052
|
+
return false;
|
|
24053
|
+
}
|
|
24054
|
+
function checkContainment(_ctx) {
|
|
24055
|
+
if (inContainer()) return [];
|
|
24056
|
+
return [
|
|
24057
|
+
{
|
|
24058
|
+
category: "Isolation",
|
|
24059
|
+
severity: "advisory",
|
|
24060
|
+
title: "Running directly on the host \u2014 no container",
|
|
24061
|
+
what: "The agent runs loose on your whole machine, not in a sandbox.",
|
|
24062
|
+
why: "It's started on the bare host, not inside a container or VM.",
|
|
24063
|
+
who: "If it gets tricked, the damage reaches every file and program \u2014 not one room.",
|
|
24064
|
+
detail: [],
|
|
24065
|
+
owner: "os",
|
|
24066
|
+
node9Reduces: true,
|
|
24067
|
+
fix: "node9 can shrink the blast radius without a container \u2014 you keep every tool:\n \u2022 node9 shield enable project-jail \u2014 block credential reads\n \u2022 node9 egress lock \u2014 block data exfil\nA container/VM adds full isolation, but you lose host access.",
|
|
24068
|
+
coverageProbe: { kind: "cantFix" }
|
|
24069
|
+
}
|
|
24070
|
+
];
|
|
24071
|
+
}
|
|
24072
|
+
|
|
24073
|
+
// src/posture/inbound.ts
|
|
24074
|
+
import fs49 from "fs";
|
|
24075
|
+
var KNOWN_SERVICE_PORTS = {
|
|
24076
|
+
5432: "PostgreSQL",
|
|
24077
|
+
6379: "Redis",
|
|
24078
|
+
3306: "MySQL/MariaDB",
|
|
24079
|
+
27017: "MongoDB",
|
|
24080
|
+
9200: "Elasticsearch",
|
|
24081
|
+
11211: "Memcached",
|
|
24082
|
+
5672: "RabbitMQ",
|
|
24083
|
+
9092: "Kafka",
|
|
24084
|
+
2379: "etcd",
|
|
24085
|
+
8086: "InfluxDB"
|
|
24086
|
+
};
|
|
24087
|
+
var KNOWN_SERVICE_COMMS = {
|
|
24088
|
+
postgres: "PostgreSQL",
|
|
24089
|
+
"redis-server": "Redis",
|
|
24090
|
+
mysqld: "MySQL",
|
|
24091
|
+
mariadbd: "MariaDB",
|
|
24092
|
+
mongod: "MongoDB"
|
|
24093
|
+
};
|
|
24094
|
+
var DB_LABEL = /PostgreSQL|Redis|MySQL|MariaDB|MongoDB/;
|
|
24095
|
+
var SHIELD_FOR_SERVICE = {
|
|
24096
|
+
PostgreSQL: {
|
|
24097
|
+
shield: "postgres",
|
|
24098
|
+
blocks: "DROP TABLE / TRUNCATE",
|
|
24099
|
+
rebind: "PostgreSQL \u2192 listen_addresses='localhost'"
|
|
24100
|
+
},
|
|
24101
|
+
Redis: { shield: "redis", blocks: "FLUSHALL / FLUSHDB", rebind: "Redis \u2192 bind 127.0.0.1" }
|
|
24102
|
+
};
|
|
24103
|
+
function buildNetworkFix(labels) {
|
|
24104
|
+
const shielded = [
|
|
24105
|
+
...new Map(
|
|
24106
|
+
labels.map((label2) => {
|
|
24107
|
+
const key = Object.keys(SHIELD_FOR_SERVICE).find((k) => label2.includes(k));
|
|
24108
|
+
return key ? SHIELD_FOR_SERVICE[key] : null;
|
|
24109
|
+
}).filter((s) => s !== null).map((s) => [s.shield, s])
|
|
24110
|
+
).values()
|
|
24111
|
+
];
|
|
24112
|
+
if (shielded.length === 0) {
|
|
24113
|
+
return {
|
|
24114
|
+
fix: "Bind to 127.0.0.1 or firewall the port; node9 gates the agent, not the socket.",
|
|
24115
|
+
reduces: false
|
|
24116
|
+
};
|
|
24117
|
+
}
|
|
24118
|
+
const protectLines = shielded.map((s) => ` \u2022 node9 shield enable ${s.shield} \u2014 blocks ${s.blocks}`).join("\n");
|
|
24119
|
+
const rebindLines = shielded.map((s) => ` \u2022 ${s.rebind}`).join("\n");
|
|
24120
|
+
return {
|
|
24121
|
+
fix: "Protect the agent now \u2014 node9 blocks destructive DB ops:\n" + protectLines + "\nClose the port to other machines (your part):\n" + rebindLines + "\n \u2022 or firewall the port",
|
|
24122
|
+
reduces: true
|
|
24123
|
+
};
|
|
24124
|
+
}
|
|
24125
|
+
function parseListeners(procText) {
|
|
24126
|
+
const out = [];
|
|
24127
|
+
for (const line of procText.split("\n").slice(1)) {
|
|
24128
|
+
const cols = line.trim().split(/\s+/);
|
|
24129
|
+
if (cols.length < 10) continue;
|
|
24130
|
+
if (cols[3] !== "0A") continue;
|
|
24131
|
+
const local = cols[1];
|
|
24132
|
+
const sep = local.lastIndexOf(":");
|
|
24133
|
+
if (sep < 0) continue;
|
|
24134
|
+
const addrHex = local.slice(0, sep);
|
|
24135
|
+
const port = parseInt(local.slice(sep + 1), 16);
|
|
24136
|
+
if (!/^0+$/.test(addrHex) || !Number.isFinite(port)) continue;
|
|
24137
|
+
out.push({ port, inode: cols[9] });
|
|
24138
|
+
}
|
|
24139
|
+
return out;
|
|
24140
|
+
}
|
|
24141
|
+
function tiesToAgent(proc, agentName) {
|
|
24142
|
+
const needle = agentName.trim().toLowerCase();
|
|
24143
|
+
if (needle.length < 4) return false;
|
|
24144
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24145
|
+
const boundary = new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`);
|
|
24146
|
+
return boundary.test(proc.comm.toLowerCase()) || boundary.test(proc.cmdline.toLowerCase());
|
|
24147
|
+
}
|
|
24148
|
+
function classifyListener(port, proc, agentName) {
|
|
24149
|
+
if (agentName && proc && tiesToAgent(proc, agentName)) {
|
|
24150
|
+
return { kind: "agent", label: `${proc.comm} on :${port}` };
|
|
24151
|
+
}
|
|
24152
|
+
const service = KNOWN_SERVICE_PORTS[port] ?? (proc ? KNOWN_SERVICE_COMMS[proc.comm] : void 0);
|
|
24153
|
+
if (service) return { kind: "service", label: `${service} on :${port}` };
|
|
24154
|
+
return { kind: "unknown", label: `${proc?.comm || "unknown process"} on :${port}` };
|
|
24155
|
+
}
|
|
24156
|
+
function collectListeners() {
|
|
24157
|
+
const byPort = /* @__PURE__ */ new Map();
|
|
24158
|
+
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
24159
|
+
try {
|
|
24160
|
+
for (const l of parseListeners(fs49.readFileSync(file, "utf8"))) {
|
|
24161
|
+
if (!byPort.has(l.port)) byPort.set(l.port, l);
|
|
24162
|
+
}
|
|
24163
|
+
} catch {
|
|
24164
|
+
}
|
|
24165
|
+
}
|
|
24166
|
+
return [...byPort.values()].sort((a, b) => a.port - b.port);
|
|
24167
|
+
}
|
|
24168
|
+
function readProc(pid) {
|
|
24169
|
+
let comm = "unknown";
|
|
24170
|
+
let cmdline = "";
|
|
24171
|
+
try {
|
|
24172
|
+
comm = fs49.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
|
|
24173
|
+
} catch {
|
|
24174
|
+
}
|
|
24175
|
+
try {
|
|
24176
|
+
cmdline = fs49.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
|
|
24177
|
+
} catch {
|
|
24178
|
+
}
|
|
24179
|
+
return { comm, cmdline };
|
|
24180
|
+
}
|
|
24181
|
+
function resolveProcesses(inodes) {
|
|
24182
|
+
const map = /* @__PURE__ */ new Map();
|
|
24183
|
+
if (inodes.size === 0) return map;
|
|
24184
|
+
let pids;
|
|
24185
|
+
try {
|
|
24186
|
+
pids = fs49.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
|
|
24187
|
+
} catch {
|
|
24188
|
+
return map;
|
|
24189
|
+
}
|
|
24190
|
+
for (const pid of pids) {
|
|
24191
|
+
let fds;
|
|
24192
|
+
try {
|
|
24193
|
+
fds = fs49.readdirSync(`/proc/${pid}/fd`);
|
|
24194
|
+
} catch {
|
|
24195
|
+
continue;
|
|
24196
|
+
}
|
|
24197
|
+
for (const fd of fds) {
|
|
24198
|
+
let link;
|
|
24199
|
+
try {
|
|
24200
|
+
link = fs49.readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
24201
|
+
} catch {
|
|
24202
|
+
continue;
|
|
24203
|
+
}
|
|
24204
|
+
const m = /^socket:\[(\d+)\]$/.exec(link);
|
|
24205
|
+
if (m && inodes.has(m[1]) && !map.has(m[1])) {
|
|
24206
|
+
map.set(m[1], readProc(pid));
|
|
24207
|
+
}
|
|
24208
|
+
}
|
|
24209
|
+
if (map.size === inodes.size) break;
|
|
24210
|
+
}
|
|
24211
|
+
return map;
|
|
24212
|
+
}
|
|
24213
|
+
function checkInbound(ctx) {
|
|
24214
|
+
const listeners = collectListeners();
|
|
24215
|
+
if (listeners.length === 0) return [];
|
|
24216
|
+
const procByInode = resolveProcesses(new Set(listeners.map((l) => l.inode)));
|
|
24217
|
+
const classified = listeners.map((l) => ({
|
|
24218
|
+
port: l.port,
|
|
24219
|
+
...classifyListener(l.port, procByInode.get(l.inode) ?? null, ctx.agent)
|
|
24220
|
+
}));
|
|
24221
|
+
const findings = [];
|
|
24222
|
+
const agentPorts = classified.filter((c) => c.kind === "agent");
|
|
24223
|
+
if (agentPorts.length > 0) {
|
|
24224
|
+
findings.push({
|
|
24225
|
+
category: "Agent inbound",
|
|
24226
|
+
severity: "advisory",
|
|
24227
|
+
title: `Your agent is reachable on 0.0.0.0 (port${agentPorts.length === 1 ? "" : "s"} ${agentPorts.map((a) => a.port).join(", ")})`,
|
|
24228
|
+
what: "Your agent itself is listening for incoming network connections.",
|
|
24229
|
+
why: "It's bound to 0.0.0.0, so other devices on the network can reach it.",
|
|
24230
|
+
who: "Anyone who can reach the port could send it instructions (pilot it). Confirm it requires an auth token.",
|
|
24231
|
+
detail: agentPorts.map((a) => a.label),
|
|
24232
|
+
owner: "os",
|
|
24233
|
+
fix: "Bind the agent port to 127.0.0.1, or require an auth token on inbound requests.",
|
|
24234
|
+
coverageProbe: { kind: "cantFix" }
|
|
24235
|
+
});
|
|
24236
|
+
}
|
|
24237
|
+
const exposed = classified.filter((c) => c.kind !== "agent");
|
|
24238
|
+
if (exposed.length > 0) {
|
|
24239
|
+
const hasDb = exposed.some((e) => DB_LABEL.test(e.label));
|
|
24240
|
+
const { fix, reduces } = buildNetworkFix(exposed.map((e) => e.label));
|
|
24241
|
+
findings.push({
|
|
24242
|
+
category: "Network exposure",
|
|
24243
|
+
severity: "advisory",
|
|
24244
|
+
title: `${exposed.length} service${exposed.length === 1 ? "" : "s"} reachable on 0.0.0.0`,
|
|
24245
|
+
what: "These services accept connections from your whole network, not just this laptop.",
|
|
24246
|
+
why: "They listen on 0.0.0.0 (all interfaces) instead of 127.0.0.1 (this machine only).",
|
|
24247
|
+
// Calibrated: 0.0.0.0 = your local network (WiFi), not the public internet
|
|
24248
|
+
// unless the box has a public IP.
|
|
24249
|
+
who: "Other devices on your network (e.g. your WiFi) can connect \u2014 usually not the whole internet unless this box has a public IP." + (hasDb ? " An open, unauthenticated database is a direct data-theft path." : ""),
|
|
24250
|
+
detail: exposed.map((e) => e.label),
|
|
24251
|
+
owner: "os",
|
|
24252
|
+
// Only when node9 actually has a shield for an exposed service — otherwise
|
|
24253
|
+
// (bare dev servers) it stays purely the user's to rebind.
|
|
24254
|
+
node9Reduces: reduces,
|
|
24255
|
+
fix,
|
|
24256
|
+
coverageProbe: { kind: "cantFix" }
|
|
24257
|
+
});
|
|
24258
|
+
}
|
|
24259
|
+
return findings;
|
|
24260
|
+
}
|
|
24261
|
+
|
|
24262
|
+
// src/posture/coverage.ts
|
|
24263
|
+
init_config();
|
|
24264
|
+
import os43 from "os";
|
|
24265
|
+
function checkCoverage(ctx) {
|
|
24266
|
+
const home = ctx.home || os43.homedir();
|
|
24267
|
+
const findings = [];
|
|
24268
|
+
const protectedAgents = getAgentWiring(home).filter((r) => r.isProtected);
|
|
24269
|
+
if (protectedAgents.length === 0) {
|
|
24270
|
+
findings.push({
|
|
24271
|
+
category: "Coverage",
|
|
24272
|
+
severity: "critical",
|
|
24273
|
+
title: "node9 is not in-path for any agent",
|
|
24274
|
+
what: "node9 isn't actually in the loop for any agent on this machine.",
|
|
24275
|
+
why: "No agent has node9 hooks or MCP wired in.",
|
|
24276
|
+
who: "Everything else here is unenforced \u2014 node9 can only report, not block.",
|
|
24277
|
+
detail: [],
|
|
24278
|
+
owner: "node9",
|
|
24279
|
+
fix: "Run `node9 init` to put node9 in-path for your agents."
|
|
24280
|
+
});
|
|
24281
|
+
return findings;
|
|
24282
|
+
}
|
|
24283
|
+
const mode = getConfig(ctx.cwd).settings.mode;
|
|
24284
|
+
if (mode === "observe" || mode === "audit") {
|
|
24285
|
+
findings.push({
|
|
24286
|
+
category: "Coverage",
|
|
24287
|
+
severity: "high",
|
|
24288
|
+
title: `node9 is in ${mode} mode \u2014 watching, not blocking`,
|
|
24289
|
+
what: "node9 is watching but not actually blocking anything.",
|
|
24290
|
+
why: `It's in ${mode} mode, which logs risky actions but lets them through.`,
|
|
24291
|
+
who: "The guardrails above are observed, not enforced.",
|
|
24292
|
+
detail: [],
|
|
24293
|
+
owner: "node9",
|
|
24294
|
+
fix: "Set mode to `standard` (or `strict`) to enforce in-path."
|
|
24295
|
+
});
|
|
24296
|
+
}
|
|
24297
|
+
return findings;
|
|
24298
|
+
}
|
|
24299
|
+
|
|
24300
|
+
// src/posture/score.ts
|
|
24301
|
+
init_dist();
|
|
24302
|
+
function scorePosture(findings, checksRun) {
|
|
24303
|
+
const open = findings.filter(
|
|
24304
|
+
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24305
|
+
);
|
|
24306
|
+
const count = (sev) => open.filter((f) => f.severity === sev).length;
|
|
24307
|
+
return computeSecurityScore({
|
|
24308
|
+
critical: count("critical"),
|
|
24309
|
+
high: count("high"),
|
|
24310
|
+
medium: count("medium"),
|
|
24311
|
+
// Denominator = number of checks evaluated. With computeSecurityScore's
|
|
24312
|
+
// caps this makes any critical → critical tier, any high → at-risk, and a
|
|
24313
|
+
// fully clean run (0 findings, checksRun > 0) → 100/good.
|
|
24314
|
+
total: Math.max(checksRun, 1)
|
|
24315
|
+
});
|
|
24316
|
+
}
|
|
24317
|
+
|
|
24318
|
+
// src/posture/headline.ts
|
|
24319
|
+
var SEVERITY_RANK = {
|
|
24320
|
+
critical: 0,
|
|
24321
|
+
high: 1,
|
|
24322
|
+
medium: 2,
|
|
24323
|
+
advisory: 3
|
|
24324
|
+
};
|
|
24325
|
+
function worstFinding(findings) {
|
|
24326
|
+
return [...findings].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
|
|
24327
|
+
}
|
|
24328
|
+
function deriveHeadline(allFindings) {
|
|
24329
|
+
const findings = allFindings.filter(
|
|
24330
|
+
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24331
|
+
);
|
|
24332
|
+
if (findings.length === 0 || findings.every((f) => f.severity === "advisory")) return null;
|
|
24333
|
+
const has = (category) => findings.some((f) => f.category === category);
|
|
24334
|
+
const secrets = has("Secrets");
|
|
24335
|
+
const egressOpen = has("Egress");
|
|
24336
|
+
const noIsolation = has("Isolation");
|
|
24337
|
+
const gateWeak = has("Approval gate");
|
|
24338
|
+
const notWired = findings.some((f) => f.category === "Coverage" && f.severity === "critical");
|
|
24339
|
+
const observeOnly = findings.some((f) => f.category === "Coverage" && f.severity === "high");
|
|
24340
|
+
let risk;
|
|
24341
|
+
if (secrets && egressOpen) {
|
|
24342
|
+
risk = "An agent on this host can read the credentials on this box and send them to any host" + (noIsolation ? ", and there is no container around it" : "") + ". One poisoned input \u2014 a malicious file, or a prompt-injection in a page it reads \u2014 is all it takes.";
|
|
24343
|
+
} else if (secrets) {
|
|
24344
|
+
risk = "An agent on this host can read the credentials on this box" + (noIsolation ? " with no sandbox around it" : "") + ". A single poisoned instruction would expose those keys.";
|
|
24345
|
+
} else if (egressOpen && gateWeak) {
|
|
24346
|
+
risk = "An agent here can run unrestricted commands and reach any host \u2014 an open path for a poisoned instruction to exfiltrate data or damage the box.";
|
|
24347
|
+
} else if (egressOpen) {
|
|
24348
|
+
risk = "An agent here can reach any host on the internet \u2014 an open exfiltration path the moment it is compromised.";
|
|
24349
|
+
} else if (gateWeak) {
|
|
24350
|
+
risk = "Destructive commands are not reliably blocked here \u2014 an agent given a bad instruction could damage the box.";
|
|
24351
|
+
} else {
|
|
24352
|
+
risk = worstFinding(findings)?.title ?? "Review the findings below.";
|
|
24353
|
+
}
|
|
24354
|
+
let action;
|
|
24355
|
+
if (notWired) {
|
|
24356
|
+
action = "Run `node9 init` \u2014 node9 is not in-path yet, so nothing here is enforced.";
|
|
24357
|
+
} else if (observeOnly) {
|
|
24358
|
+
action = "Switch node9 to enforcing mode \u2014 right now it is only watching, not blocking.";
|
|
24359
|
+
} else if (egressOpen) {
|
|
24360
|
+
action = "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
|
|
24361
|
+
} else if (secrets) {
|
|
24362
|
+
action = "node9 can block reads of sensitive paths (~/.ssh, ~/.aws) in-path.";
|
|
24363
|
+
} else if (gateWeak) {
|
|
24364
|
+
action = "node9 can enforce destructive-command blocking in-path.";
|
|
24365
|
+
} else {
|
|
24366
|
+
action = worstFinding(findings)?.fix ?? "Review the findings below.";
|
|
24367
|
+
}
|
|
24368
|
+
return { risk, action };
|
|
24369
|
+
}
|
|
24370
|
+
|
|
24371
|
+
// src/posture/enforcement.ts
|
|
24372
|
+
init_dlp();
|
|
24373
|
+
init_policy();
|
|
24374
|
+
init_config();
|
|
24375
|
+
function egressCoverage(env) {
|
|
24376
|
+
if (env.enforcing && env.egressBlocking) {
|
|
24377
|
+
return { state: "covered", level: "block", via: "node9 egress" };
|
|
24378
|
+
}
|
|
24379
|
+
if (env.enforcing && env.egressReviewing) {
|
|
24380
|
+
return { state: "covered", level: "review", via: "node9 egress" };
|
|
24381
|
+
}
|
|
24382
|
+
return { state: "open" };
|
|
24383
|
+
}
|
|
24384
|
+
function coverageFromVerdict(verdict, env, via) {
|
|
24385
|
+
if (!env.enforcing) return { state: "open" };
|
|
24386
|
+
if (verdict === "block") return { state: "covered", level: "block", via };
|
|
24387
|
+
if (verdict === "review") return { state: "covered", level: "review", via };
|
|
24388
|
+
return { state: "open" };
|
|
24389
|
+
}
|
|
24390
|
+
function viaFromRule(ruleName) {
|
|
24391
|
+
if (!ruleName) return void 0;
|
|
24392
|
+
const m = /^shield:([^:]+):/.exec(ruleName);
|
|
24393
|
+
return m ? `${m[1]} shield` : void 0;
|
|
24394
|
+
}
|
|
24395
|
+
async function annotateCoverage(findings, ctx) {
|
|
24396
|
+
const config = getConfig(ctx.cwd);
|
|
24397
|
+
const mode = config.settings.mode;
|
|
24398
|
+
const wired = getAgentWiring(ctx.home).some((r) => r.isProtected);
|
|
24399
|
+
const env = {
|
|
24400
|
+
enforcing: wired && mode !== "observe" && mode !== "audit",
|
|
24401
|
+
egressBlocking: config.policy.egress.enabled && config.policy.egress.mode === "block",
|
|
24402
|
+
egressReviewing: config.policy.egress.enabled && config.policy.egress.mode === "review"
|
|
24403
|
+
};
|
|
24404
|
+
for (const f of findings) {
|
|
24405
|
+
const probe = f.coverageProbe;
|
|
24406
|
+
if (!probe) continue;
|
|
24407
|
+
if (probe.kind === "cantFix") {
|
|
24408
|
+
f.coverage = { state: "cant-fix" };
|
|
24409
|
+
continue;
|
|
24410
|
+
}
|
|
24411
|
+
if (probe.kind === "egress") {
|
|
24412
|
+
f.coverage = egressCoverage(env);
|
|
24413
|
+
continue;
|
|
24414
|
+
}
|
|
24415
|
+
if (probe.kind === "fileRead") {
|
|
24416
|
+
const verdicts = probe.paths.map((p) => scanFilePath(p)?.severity ?? null);
|
|
24417
|
+
if (verdicts.length === 0 || verdicts.some((v) => v === null)) {
|
|
24418
|
+
f.coverage = coverageFromVerdict("allow", env);
|
|
24419
|
+
} else {
|
|
24420
|
+
const worst = verdicts.some((v) => v === "review") ? "review" : "block";
|
|
24421
|
+
f.coverage = coverageFromVerdict(worst, env, "node9 DLP");
|
|
24422
|
+
}
|
|
24423
|
+
continue;
|
|
24424
|
+
}
|
|
24425
|
+
const verdict = await evaluatePolicy2("Bash", { command: probe.command }, ctx.agent, ctx.cwd);
|
|
24426
|
+
f.coverage = coverageFromVerdict(
|
|
24427
|
+
verdict.decision,
|
|
24428
|
+
env,
|
|
24429
|
+
viaFromRule(verdict.ruleName)
|
|
24430
|
+
);
|
|
24431
|
+
}
|
|
24432
|
+
}
|
|
24433
|
+
|
|
24434
|
+
// src/posture/index.ts
|
|
24435
|
+
var POSTURE_CHECKS = [
|
|
24436
|
+
{ category: "Secrets", run: checkSecrets },
|
|
24437
|
+
{ category: "Egress", run: checkEgress },
|
|
24438
|
+
{ category: "Approval gate", run: checkGate },
|
|
24439
|
+
{ category: "Supply chain", run: checkSupplyChain },
|
|
24440
|
+
{ category: "Privilege", run: checkPrivilege },
|
|
24441
|
+
{ category: "Isolation", run: checkContainment },
|
|
24442
|
+
{ category: "Inbound", run: checkInbound },
|
|
24443
|
+
{ category: "Coverage", run: checkCoverage }
|
|
24444
|
+
];
|
|
24445
|
+
function dropEnforcementRedundant(findings) {
|
|
24446
|
+
const coveragePresent = findings.some((f) => f.category === "Coverage");
|
|
24447
|
+
if (!coveragePresent) return findings;
|
|
24448
|
+
return findings.filter((f) => !(f.redundantWhenOpen && f.coverage?.state === "open"));
|
|
24449
|
+
}
|
|
24450
|
+
async function runChecks(checks, ctx) {
|
|
24451
|
+
const findings = [];
|
|
24452
|
+
const passedCategories = [];
|
|
24453
|
+
const erroredCategories = [];
|
|
24454
|
+
for (const check of checks) {
|
|
24455
|
+
try {
|
|
24456
|
+
const result = await check.run(ctx);
|
|
24457
|
+
if (result.length === 0) passedCategories.push(check.category);
|
|
24458
|
+
else findings.push(...result);
|
|
24459
|
+
} catch (err2) {
|
|
24460
|
+
erroredCategories.push(check.category);
|
|
24461
|
+
if (process.env.NODE9_DEBUG) {
|
|
24462
|
+
console.error(`[posture] check "${check.category}" failed:`, err2?.message);
|
|
24463
|
+
}
|
|
24464
|
+
}
|
|
24465
|
+
}
|
|
24466
|
+
return { findings, passedCategories, erroredCategories };
|
|
24467
|
+
}
|
|
24468
|
+
async function runPosture(opts = {}) {
|
|
24469
|
+
const ctx = {
|
|
24470
|
+
home: opts.home ?? os44.homedir(),
|
|
24471
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
24472
|
+
agent: opts.agent
|
|
24473
|
+
};
|
|
24474
|
+
const {
|
|
24475
|
+
findings: rawFindings,
|
|
24476
|
+
passedCategories,
|
|
24477
|
+
erroredCategories
|
|
24478
|
+
} = await runChecks(POSTURE_CHECKS, ctx);
|
|
24479
|
+
await annotateCoverage(rawFindings, ctx);
|
|
24480
|
+
const findings = dropEnforcementRedundant(rawFindings);
|
|
24481
|
+
const { score, tier } = scorePosture(findings, POSTURE_CHECKS.length);
|
|
24482
|
+
return {
|
|
24483
|
+
agent: opts.agent ? `${opts.agent} on this host` : "agent on this host",
|
|
24484
|
+
findings,
|
|
24485
|
+
passedCategories,
|
|
24486
|
+
erroredCategories,
|
|
24487
|
+
headline: deriveHeadline(findings),
|
|
24488
|
+
score,
|
|
24489
|
+
tier,
|
|
24490
|
+
checksRun: POSTURE_CHECKS.length
|
|
24491
|
+
};
|
|
24492
|
+
}
|
|
24493
|
+
|
|
24494
|
+
// src/posture/render.ts
|
|
24495
|
+
import chalk24 from "chalk";
|
|
24496
|
+
var ICON = {
|
|
24497
|
+
critical: chalk24.red("\u274C"),
|
|
24498
|
+
high: chalk24.red("\u274C"),
|
|
24499
|
+
medium: chalk24.yellow("\u26A0\uFE0F "),
|
|
24500
|
+
advisory: chalk24.gray("\u26A0\uFE0F ")
|
|
24501
|
+
};
|
|
24502
|
+
var TIER_LABEL = {
|
|
24503
|
+
good: chalk24.green("Good"),
|
|
24504
|
+
"at-risk": chalk24.yellow("At risk"),
|
|
24505
|
+
critical: chalk24.red("Critical")
|
|
24506
|
+
};
|
|
24507
|
+
function wrap(text, width) {
|
|
24508
|
+
const out = [];
|
|
24509
|
+
let cur = "";
|
|
24510
|
+
for (const word of text.split(" ")) {
|
|
24511
|
+
if (cur && (cur + " " + word).length > width) {
|
|
24512
|
+
out.push(cur);
|
|
24513
|
+
cur = word;
|
|
24514
|
+
} else {
|
|
24515
|
+
cur = cur ? cur + " " + word : word;
|
|
24516
|
+
}
|
|
24517
|
+
}
|
|
24518
|
+
if (cur) out.push(cur);
|
|
24519
|
+
return out;
|
|
24520
|
+
}
|
|
24521
|
+
var LABEL_WIDTH = 14;
|
|
24522
|
+
function label(category) {
|
|
24523
|
+
return chalk24.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
|
|
24524
|
+
}
|
|
24525
|
+
function renderFinding(f) {
|
|
24526
|
+
const lines = [];
|
|
24527
|
+
lines.push(` ${ICON[f.severity]} ${label(f.category)}${f.title}`);
|
|
24528
|
+
const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
|
|
24529
|
+
const width = 80 - indent.length;
|
|
24530
|
+
for (const s of [f.what, f.why, f.who]) {
|
|
24531
|
+
if (s) for (const l of wrap(s, width)) lines.push(indent + chalk24.gray(l));
|
|
24532
|
+
}
|
|
24533
|
+
for (const d of f.detail) lines.push(indent + chalk24.gray(d));
|
|
24534
|
+
if (f.fix) {
|
|
24535
|
+
let first = true;
|
|
24536
|
+
for (const seg of f.fix.split("\n")) {
|
|
24537
|
+
for (const l of wrap(seg, width - 2)) {
|
|
24538
|
+
lines.push(indent + chalk24.cyan(first ? "\u2192 " + l : " " + l));
|
|
24539
|
+
first = false;
|
|
24540
|
+
}
|
|
24541
|
+
}
|
|
24542
|
+
}
|
|
24543
|
+
return lines;
|
|
24544
|
+
}
|
|
24545
|
+
function renderPosture(result) {
|
|
24546
|
+
const lines = [];
|
|
24547
|
+
const tier = TIER_LABEL[result.tier];
|
|
24548
|
+
lines.push("");
|
|
24549
|
+
lines.push(
|
|
24550
|
+
chalk24.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + chalk24.gray(` \u2014 ${result.agent}`) + ` ${chalk24.bold(`Score: ${result.score}/100`)} (${tier})`
|
|
24551
|
+
);
|
|
24552
|
+
const advisories = result.findings.filter(
|
|
24553
|
+
(f) => f.severity === "advisory" && f.coverage?.state !== "covered"
|
|
24554
|
+
).length;
|
|
24555
|
+
if (advisories > 0) {
|
|
24556
|
+
const word = advisories === 1 ? "advisory" : "advisories";
|
|
24557
|
+
const verb = advisories === 1 ? "doesn't" : "don't";
|
|
24558
|
+
lines.push(
|
|
24559
|
+
" " + chalk24.gray(
|
|
24560
|
+
`${advisories} ${word} below ${verb} affect the score \u2014 OS-level exposure node9 can't enforce, yours to weigh.`
|
|
24561
|
+
)
|
|
24562
|
+
);
|
|
24563
|
+
}
|
|
24564
|
+
lines.push("");
|
|
24565
|
+
if (result.headline) {
|
|
24566
|
+
const indent = " ";
|
|
24567
|
+
lines.push(` ${chalk24.red.bold("\u{1F525} Biggest risk")}`);
|
|
24568
|
+
for (const l of wrap(result.headline.risk, 74)) lines.push(indent + chalk24.white(l));
|
|
24569
|
+
const action = wrap(`Do this first: ${result.headline.action}`, 72);
|
|
24570
|
+
action.forEach((l, i) => lines.push(indent + chalk24.cyan(i === 0 ? "\u2192 " + l : " " + l)));
|
|
24571
|
+
lines.push("");
|
|
24572
|
+
}
|
|
24573
|
+
const covered = result.findings.filter((f) => f.coverage?.state === "covered");
|
|
24574
|
+
const open = result.findings.filter((f) => f.coverage?.state !== "covered");
|
|
24575
|
+
if (covered.length > 0) {
|
|
24576
|
+
lines.push(" " + chalk24.green("\u{1F7E2} node9 is already protecting you"));
|
|
24577
|
+
for (const f of covered) {
|
|
24578
|
+
const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
|
|
24579
|
+
const via = f.coverage?.via ?? "node9";
|
|
24580
|
+
lines.push(
|
|
24581
|
+
` ${chalk24.green("\u2705")} ${label(f.category)}${chalk24.gray(`${via} is ${gated} this`)}`
|
|
24582
|
+
);
|
|
24583
|
+
}
|
|
24584
|
+
lines.push("");
|
|
24585
|
+
}
|
|
24586
|
+
const node9Open = open.filter((f) => f.owner === "node9");
|
|
24587
|
+
const reduceOpen = open.filter((f) => f.owner !== "node9" && f.node9Reduces);
|
|
24588
|
+
const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
|
|
24589
|
+
if (node9Open.length > 0) {
|
|
24590
|
+
lines.push(" " + chalk24.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
|
|
24591
|
+
for (const f of node9Open) lines.push(...renderFinding(f));
|
|
24592
|
+
}
|
|
24593
|
+
if (reduceOpen.length > 0) {
|
|
24594
|
+
if (node9Open.length > 0) lines.push("");
|
|
24595
|
+
lines.push(
|
|
24596
|
+
" " + chalk24.yellow.bold("\u{1F512} node9 reduces these \u2014 run the command, the rest is yours")
|
|
24597
|
+
);
|
|
24598
|
+
for (const f of reduceOpen) lines.push(...renderFinding(f));
|
|
24599
|
+
}
|
|
24600
|
+
if (osOpen.length > 0) {
|
|
24601
|
+
if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
|
|
24602
|
+
lines.push(" " + chalk24.bold("\u{1F9F1} Only you can fix these \u2014 node9 can't"));
|
|
24603
|
+
for (const f of osOpen) lines.push(...renderFinding(f));
|
|
24604
|
+
}
|
|
24605
|
+
for (const cat of result.passedCategories) {
|
|
24606
|
+
lines.push(` ${chalk24.green("\u2705")} ${label(cat)}${chalk24.gray("no issues found")}`);
|
|
24607
|
+
}
|
|
24608
|
+
for (const cat of result.erroredCategories) {
|
|
24609
|
+
lines.push(` ${chalk24.gray("\u2022")} ${label(cat)}${chalk24.gray("could not be checked")}`);
|
|
24610
|
+
}
|
|
24611
|
+
lines.push("");
|
|
24612
|
+
const crit = open.filter((f) => f.severity === "critical").length;
|
|
24613
|
+
const high = open.filter((f) => f.severity === "high").length;
|
|
24614
|
+
const med = open.filter((f) => f.severity === "medium").length;
|
|
24615
|
+
const adv = open.filter((f) => f.severity === "advisory").length;
|
|
24616
|
+
const parts = [];
|
|
24617
|
+
if (crit) parts.push(chalk24.red(`${crit} critical`));
|
|
24618
|
+
if (high) parts.push(chalk24.red(`${high} high`));
|
|
24619
|
+
if (med) parts.push(chalk24.yellow(`${med} medium`));
|
|
24620
|
+
if (adv) parts.push(chalk24.gray(`${adv} advisory`));
|
|
24621
|
+
const summary = parts.length ? parts.join(" \xB7 ") : chalk24.green("no findings");
|
|
24622
|
+
lines.push(` ${summary} \xB7 ${chalk24.gray("track your fleet at app.node9.ai/posture")}`);
|
|
24623
|
+
lines.push("");
|
|
24624
|
+
return lines.join("\n");
|
|
24625
|
+
}
|
|
24626
|
+
|
|
24627
|
+
// src/posture/ship.ts
|
|
24628
|
+
import http2 from "http";
|
|
24629
|
+
import https5 from "https";
|
|
24630
|
+
import { URL as URL2 } from "url";
|
|
24631
|
+
function buildShipBody(result) {
|
|
24632
|
+
return {
|
|
24633
|
+
score: result.score,
|
|
24634
|
+
tier: result.tier,
|
|
24635
|
+
agent: result.agent,
|
|
24636
|
+
headline: result.headline,
|
|
24637
|
+
// { risk, action } | null — both safe strings
|
|
24638
|
+
findings: result.findings.map((f) => ({
|
|
24639
|
+
category: f.category,
|
|
24640
|
+
severity: f.severity,
|
|
24641
|
+
title: f.title,
|
|
24642
|
+
// Coverage state so the SaaS counts OPEN-only (matching the local score).
|
|
24643
|
+
// A non-sensitive enum — no values or paths. Default 'open' if unannotated.
|
|
24644
|
+
coverage: f.coverage?.state ?? "open",
|
|
24645
|
+
// Plain-language parity with the CLI report. Prose only, no paths.
|
|
24646
|
+
what: f.what,
|
|
24647
|
+
why: f.why,
|
|
24648
|
+
who: f.who,
|
|
24649
|
+
// The runnable fix / OS action — commands + advice, never a path.
|
|
24650
|
+
fix: f.fix,
|
|
24651
|
+
// Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
|
|
24652
|
+
owner: f.owner ?? "os"
|
|
24653
|
+
}))
|
|
24654
|
+
};
|
|
24655
|
+
}
|
|
24656
|
+
function postureUrlFrom(apiUrl) {
|
|
24657
|
+
return apiUrl.endsWith("/policies/sync") ? apiUrl.replace(/\/policies\/sync$/, "/posture/report") : null;
|
|
24658
|
+
}
|
|
24659
|
+
async function shipPosture(result, creds) {
|
|
24660
|
+
const url = postureUrlFrom(creds.apiUrl);
|
|
24661
|
+
if (!url) return false;
|
|
24662
|
+
const body = JSON.stringify(buildShipBody(result));
|
|
24663
|
+
const parsed = new URL2(url);
|
|
24664
|
+
const transport = parsed.protocol === "http:" ? http2 : https5;
|
|
24665
|
+
return new Promise((resolve) => {
|
|
24666
|
+
const req = transport.request(
|
|
24667
|
+
{
|
|
24668
|
+
hostname: parsed.hostname,
|
|
24669
|
+
port: parsed.port ? parseInt(parsed.port, 10) : void 0,
|
|
24670
|
+
path: parsed.pathname + parsed.search,
|
|
24671
|
+
method: "POST",
|
|
24672
|
+
headers: {
|
|
24673
|
+
"Content-Type": "application/json",
|
|
24674
|
+
"Content-Length": Buffer.byteLength(body),
|
|
24675
|
+
Authorization: `Bearer ${creds.apiKey}`
|
|
24676
|
+
},
|
|
24677
|
+
timeout: 1e4
|
|
24678
|
+
},
|
|
24679
|
+
(res) => {
|
|
24680
|
+
const ok2 = !!res.statusCode && res.statusCode >= 200 && res.statusCode < 300;
|
|
24681
|
+
res.resume();
|
|
24682
|
+
res.on("end", () => resolve(ok2));
|
|
24683
|
+
res.on("error", () => resolve(false));
|
|
24684
|
+
}
|
|
24685
|
+
);
|
|
24686
|
+
req.on("error", () => resolve(false));
|
|
24687
|
+
req.on("timeout", () => {
|
|
24688
|
+
req.destroy();
|
|
24689
|
+
resolve(false);
|
|
24690
|
+
});
|
|
24691
|
+
req.write(body);
|
|
24692
|
+
req.end();
|
|
24693
|
+
});
|
|
24694
|
+
}
|
|
24695
|
+
|
|
24696
|
+
// src/cli/commands/posture.ts
|
|
24697
|
+
init_sync();
|
|
24698
|
+
function registerPostureCommand(program2) {
|
|
24699
|
+
program2.command("posture").description("Security scorecard for the agent on this host (secrets, egress, gate)").option("--agent <name>", "label / policy scope for the agent being graded").option("--json", "emit the raw result as JSON instead of the scorecard").option("--ship", "send a redacted snapshot to your node9 dashboard").action(async (opts) => {
|
|
24700
|
+
const result = await runPosture({ agent: opts.agent });
|
|
24701
|
+
if (opts.json) {
|
|
24702
|
+
console.log(JSON.stringify(result, null, 2));
|
|
24703
|
+
} else {
|
|
24704
|
+
console.log(renderPosture(result));
|
|
24705
|
+
}
|
|
24706
|
+
if (opts.ship) {
|
|
24707
|
+
const creds = readCredentials();
|
|
24708
|
+
if (!creds) {
|
|
24709
|
+
console.error(chalk25.gray(" Run `node9 login` to ship this to your dashboard."));
|
|
24710
|
+
} else {
|
|
24711
|
+
const ok2 = await shipPosture(result, creds);
|
|
24712
|
+
console.error(
|
|
24713
|
+
ok2 ? chalk25.gray(" \u2713 Shipped to your node9 dashboard.") : chalk25.gray(" Could not reach the dashboard \u2014 saved locally only.")
|
|
24714
|
+
);
|
|
24715
|
+
}
|
|
24716
|
+
}
|
|
24717
|
+
if (result.tier === "critical") process.exitCode = 2;
|
|
24718
|
+
});
|
|
24719
|
+
}
|
|
24720
|
+
|
|
24721
|
+
// src/cli/commands/egress.ts
|
|
24722
|
+
init_config();
|
|
24723
|
+
init_dist();
|
|
24724
|
+
import chalk26 from "chalk";
|
|
24725
|
+
import fs50 from "fs";
|
|
24726
|
+
import os45 from "os";
|
|
24727
|
+
import path49 from "path";
|
|
24728
|
+
var DEFAULT_EGRESS = {
|
|
24729
|
+
enabled: false,
|
|
24730
|
+
mode: "review",
|
|
24731
|
+
allow: [],
|
|
24732
|
+
deny: [],
|
|
24733
|
+
allowPrivate: true
|
|
24734
|
+
};
|
|
24735
|
+
function configPath() {
|
|
24736
|
+
return path49.join(os45.homedir(), ".node9", "config.json");
|
|
24737
|
+
}
|
|
24738
|
+
function readRawConfig() {
|
|
24739
|
+
let text;
|
|
24740
|
+
try {
|
|
24741
|
+
text = fs50.readFileSync(configPath(), "utf8");
|
|
24742
|
+
} catch (err2) {
|
|
24743
|
+
if (err2.code === "ENOENT") return {};
|
|
24744
|
+
throw err2;
|
|
24745
|
+
}
|
|
24746
|
+
try {
|
|
24747
|
+
return JSON.parse(text);
|
|
24748
|
+
} catch {
|
|
24749
|
+
throw new Error(
|
|
24750
|
+
`${configPath()} is not valid JSON \u2014 fix it before changing egress (refusing to overwrite).`
|
|
24751
|
+
);
|
|
24752
|
+
}
|
|
24753
|
+
}
|
|
24754
|
+
function writeRawConfig(config) {
|
|
24755
|
+
const p = configPath();
|
|
24756
|
+
fs50.mkdirSync(path49.dirname(p), { recursive: true });
|
|
24757
|
+
fs50.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
24758
|
+
}
|
|
24759
|
+
function applyEgress(config, change) {
|
|
24760
|
+
const policy = config.policy = config.policy ?? {};
|
|
24761
|
+
const existing = policy.egress ?? {};
|
|
24762
|
+
policy.egress = { ...DEFAULT_EGRESS, ...existing, ...change };
|
|
24763
|
+
return config;
|
|
24764
|
+
}
|
|
24765
|
+
function withConfig(fn) {
|
|
24766
|
+
let config;
|
|
24767
|
+
try {
|
|
24768
|
+
config = readRawConfig();
|
|
24769
|
+
} catch (err2) {
|
|
24770
|
+
console.error(chalk26.red(`
|
|
24771
|
+
\u2717 ${err2.message}
|
|
24772
|
+
`));
|
|
24773
|
+
process.exitCode = 1;
|
|
24774
|
+
return false;
|
|
24775
|
+
}
|
|
24776
|
+
fn(config);
|
|
24777
|
+
writeRawConfig(config);
|
|
24778
|
+
return true;
|
|
24779
|
+
}
|
|
24780
|
+
function mutate(change) {
|
|
24781
|
+
return withConfig((config) => applyEgress(config, change));
|
|
24782
|
+
}
|
|
24783
|
+
function addHost(list, host) {
|
|
24784
|
+
return withConfig((config) => {
|
|
24785
|
+
const existing = config.policy?.egress ?? {};
|
|
24786
|
+
const current = { ...DEFAULT_EGRESS, ...existing };
|
|
24787
|
+
const updated = current[list].includes(host) ? current[list] : [...current[list], host];
|
|
24788
|
+
applyEgress(config, { [list]: updated });
|
|
24789
|
+
});
|
|
24790
|
+
}
|
|
24791
|
+
function showStatus() {
|
|
24792
|
+
const e = getConfig().policy.egress;
|
|
24793
|
+
const state = !e.enabled ? chalk26.red("OFF \u2014 your agent can reach any host") : e.mode === "block" ? chalk26.green("LOCKED (block) \u2014 unknown hosts are denied") : chalk26.yellow("WATCHING (review) \u2014 unknown hosts prompt you");
|
|
24794
|
+
console.log(chalk26.cyan.bold("\n\u{1F310} Egress control"));
|
|
24795
|
+
console.log(" State: " + state);
|
|
24796
|
+
console.log(
|
|
24797
|
+
chalk26.gray(
|
|
24798
|
+
` ${DEFAULT_EGRESS_ALLOWLIST.length} common dev/LLM hosts are always allowed (github, npm, pypi, anthropic, \u2026).`
|
|
24799
|
+
)
|
|
24800
|
+
);
|
|
24801
|
+
if (e.allow.length) console.log(" Your allow: " + e.allow.join(", "));
|
|
24802
|
+
if (e.deny.length) console.log(" Your deny: " + e.deny.join(", "));
|
|
24803
|
+
if (!e.enabled) {
|
|
24804
|
+
console.log(chalk26.gray("\n Turn it on: node9 egress watch (prompt on unknown hosts)"));
|
|
24805
|
+
console.log(chalk26.gray(" node9 egress lock (hard-block unknown hosts)"));
|
|
24806
|
+
}
|
|
24807
|
+
console.log("");
|
|
24808
|
+
}
|
|
24809
|
+
function registerEgressCommand(program2) {
|
|
24810
|
+
const egress = program2.command("egress").description("Control where your agent can send data (egress allowlist)");
|
|
24811
|
+
egress.command("watch").description("Prompt before the agent reaches an unknown host (review mode)").action(() => {
|
|
24812
|
+
if (!mutate({ enabled: true, mode: "review" })) return;
|
|
24813
|
+
console.log(chalk26.green("\n\u2713 Egress is now watched (review mode)."));
|
|
24814
|
+
console.log(
|
|
24815
|
+
chalk26.gray(" Routine hosts (LLM APIs, package registries, localhost) are allowed.")
|
|
24816
|
+
);
|
|
24817
|
+
console.log(
|
|
24818
|
+
chalk26.gray(" An unknown host will prompt you \u2014 run `node9 egress lock` to hard-block.\n")
|
|
24819
|
+
);
|
|
24820
|
+
});
|
|
24821
|
+
egress.command("lock").description("Block the agent from reaching unknown hosts (block mode)").action(() => {
|
|
24822
|
+
if (!mutate({ enabled: true, mode: "block" })) return;
|
|
24823
|
+
console.log(chalk26.green("\n\u2713 Egress is now locked (block mode)."));
|
|
24824
|
+
console.log(chalk26.gray(" Routine hosts are still allowed; unknown hosts are denied."));
|
|
24825
|
+
console.log(chalk26.gray(" Allow a specific host with `node9 egress allow <host>`.\n"));
|
|
24826
|
+
});
|
|
24827
|
+
egress.command("allow <host>").description("Allow an extra host (glob, e.g. *.mycorp.com)").action((host) => {
|
|
24828
|
+
if (!addHost("allow", host)) return;
|
|
24829
|
+
console.log(chalk26.green(`
|
|
24830
|
+
\u2713 Allowed egress to ${host}.
|
|
24831
|
+
`));
|
|
24832
|
+
});
|
|
24833
|
+
egress.command("deny <host>").description("Block an extra host (deny always wins)").action((host) => {
|
|
24834
|
+
if (!addHost("deny", host)) return;
|
|
24835
|
+
console.log(chalk26.green(`
|
|
24836
|
+
\u2713 Denied egress to ${host}.
|
|
24837
|
+
`));
|
|
24838
|
+
});
|
|
24839
|
+
egress.command("off").description("Turn egress control off").action(() => {
|
|
24840
|
+
if (!mutate({ enabled: false })) return;
|
|
24841
|
+
console.log(
|
|
24842
|
+
chalk26.yellow("\n\u2713 Egress control is off \u2014 the agent can reach any host again.\n")
|
|
24843
|
+
);
|
|
24844
|
+
});
|
|
24845
|
+
egress.action(showStatus);
|
|
24846
|
+
}
|
|
24847
|
+
|
|
23670
24848
|
// src/cli/commands/sessions.ts
|
|
23671
24849
|
init_scan_summary();
|
|
23672
24850
|
init_litellm();
|
|
23673
24851
|
init_cost_gemini();
|
|
23674
24852
|
init_cost_codex();
|
|
23675
|
-
import
|
|
23676
|
-
import
|
|
23677
|
-
import
|
|
23678
|
-
import
|
|
24853
|
+
import chalk27 from "chalk";
|
|
24854
|
+
import fs51 from "fs";
|
|
24855
|
+
import path50 from "path";
|
|
24856
|
+
import os46 from "os";
|
|
23679
24857
|
function modelPrice(model) {
|
|
23680
24858
|
const t = pricingFor(model);
|
|
23681
24859
|
if (!t) return null;
|
|
@@ -23692,10 +24870,10 @@ function encodeProjectPath(projectPath) {
|
|
|
23692
24870
|
}
|
|
23693
24871
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
23694
24872
|
const encoded = encodeProjectPath(projectPath);
|
|
23695
|
-
return
|
|
24873
|
+
return path50.join(os46.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
23696
24874
|
}
|
|
23697
24875
|
function projectLabel(projectPath) {
|
|
23698
|
-
return projectPath.replace(
|
|
24876
|
+
return projectPath.replace(os46.homedir(), "~");
|
|
23699
24877
|
}
|
|
23700
24878
|
function parseHistoryLines(lines) {
|
|
23701
24879
|
const entries = [];
|
|
@@ -23764,10 +24942,10 @@ function parseSessionLines(lines) {
|
|
|
23764
24942
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
23765
24943
|
}
|
|
23766
24944
|
function loadAuditEntries(auditPath) {
|
|
23767
|
-
const aPath = auditPath ??
|
|
24945
|
+
const aPath = auditPath ?? path50.join(os46.homedir(), ".node9", "audit.log");
|
|
23768
24946
|
let raw;
|
|
23769
24947
|
try {
|
|
23770
|
-
raw =
|
|
24948
|
+
raw = fs51.readFileSync(aPath, "utf-8");
|
|
23771
24949
|
} catch {
|
|
23772
24950
|
return [];
|
|
23773
24951
|
}
|
|
@@ -23803,8 +24981,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
23803
24981
|
return result;
|
|
23804
24982
|
}
|
|
23805
24983
|
function buildGeminiSessions(days, allAuditEntries) {
|
|
23806
|
-
const tmpDir =
|
|
23807
|
-
if (!
|
|
24984
|
+
const tmpDir = path50.join(os46.homedir(), ".gemini", "tmp");
|
|
24985
|
+
if (!fs51.existsSync(tmpDir)) return [];
|
|
23808
24986
|
const cutoff = days !== null ? (() => {
|
|
23809
24987
|
const d = /* @__PURE__ */ new Date();
|
|
23810
24988
|
d.setDate(d.getDate() - days);
|
|
@@ -23813,35 +24991,35 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
23813
24991
|
})() : null;
|
|
23814
24992
|
let slugDirs;
|
|
23815
24993
|
try {
|
|
23816
|
-
slugDirs =
|
|
24994
|
+
slugDirs = fs51.readdirSync(tmpDir);
|
|
23817
24995
|
} catch {
|
|
23818
24996
|
return [];
|
|
23819
24997
|
}
|
|
23820
24998
|
const summaries = [];
|
|
23821
24999
|
for (const slug of slugDirs) {
|
|
23822
|
-
const slugPath =
|
|
25000
|
+
const slugPath = path50.join(tmpDir, slug);
|
|
23823
25001
|
try {
|
|
23824
|
-
if (!
|
|
25002
|
+
if (!fs51.statSync(slugPath).isDirectory()) continue;
|
|
23825
25003
|
} catch {
|
|
23826
25004
|
continue;
|
|
23827
25005
|
}
|
|
23828
|
-
let projectRoot =
|
|
25006
|
+
let projectRoot = path50.join(os46.homedir(), slug);
|
|
23829
25007
|
try {
|
|
23830
|
-
projectRoot =
|
|
25008
|
+
projectRoot = fs51.readFileSync(path50.join(slugPath, ".project_root"), "utf-8").trim();
|
|
23831
25009
|
} catch {
|
|
23832
25010
|
}
|
|
23833
|
-
const chatsDir =
|
|
23834
|
-
if (!
|
|
25011
|
+
const chatsDir = path50.join(slugPath, "chats");
|
|
25012
|
+
if (!fs51.existsSync(chatsDir)) continue;
|
|
23835
25013
|
let chatFiles;
|
|
23836
25014
|
try {
|
|
23837
|
-
chatFiles =
|
|
25015
|
+
chatFiles = fs51.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
23838
25016
|
} catch {
|
|
23839
25017
|
continue;
|
|
23840
25018
|
}
|
|
23841
25019
|
for (const chatFile of chatFiles) {
|
|
23842
25020
|
let raw;
|
|
23843
25021
|
try {
|
|
23844
|
-
raw =
|
|
25022
|
+
raw = fs51.readFileSync(path50.join(chatsDir, chatFile), "utf-8");
|
|
23845
25023
|
} catch {
|
|
23846
25024
|
continue;
|
|
23847
25025
|
}
|
|
@@ -23921,8 +25099,8 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
23921
25099
|
return summaries;
|
|
23922
25100
|
}
|
|
23923
25101
|
function buildCodexSessions(days, allAuditEntries) {
|
|
23924
|
-
const sessionsBase =
|
|
23925
|
-
if (!
|
|
25102
|
+
const sessionsBase = path50.join(os46.homedir(), ".codex", "sessions");
|
|
25103
|
+
if (!fs51.existsSync(sessionsBase)) return [];
|
|
23926
25104
|
const cutoff = days !== null ? (() => {
|
|
23927
25105
|
const d = /* @__PURE__ */ new Date();
|
|
23928
25106
|
d.setDate(d.getDate() - days);
|
|
@@ -23931,29 +25109,29 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
23931
25109
|
})() : null;
|
|
23932
25110
|
const jsonlFiles = [];
|
|
23933
25111
|
try {
|
|
23934
|
-
for (const year of
|
|
23935
|
-
const yearPath =
|
|
25112
|
+
for (const year of fs51.readdirSync(sessionsBase)) {
|
|
25113
|
+
const yearPath = path50.join(sessionsBase, year);
|
|
23936
25114
|
try {
|
|
23937
|
-
if (!
|
|
25115
|
+
if (!fs51.statSync(yearPath).isDirectory()) continue;
|
|
23938
25116
|
} catch {
|
|
23939
25117
|
continue;
|
|
23940
25118
|
}
|
|
23941
|
-
for (const month of
|
|
23942
|
-
const monthPath =
|
|
25119
|
+
for (const month of fs51.readdirSync(yearPath)) {
|
|
25120
|
+
const monthPath = path50.join(yearPath, month);
|
|
23943
25121
|
try {
|
|
23944
|
-
if (!
|
|
25122
|
+
if (!fs51.statSync(monthPath).isDirectory()) continue;
|
|
23945
25123
|
} catch {
|
|
23946
25124
|
continue;
|
|
23947
25125
|
}
|
|
23948
|
-
for (const day of
|
|
23949
|
-
const dayPath =
|
|
25126
|
+
for (const day of fs51.readdirSync(monthPath)) {
|
|
25127
|
+
const dayPath = path50.join(monthPath, day);
|
|
23950
25128
|
try {
|
|
23951
|
-
if (!
|
|
25129
|
+
if (!fs51.statSync(dayPath).isDirectory()) continue;
|
|
23952
25130
|
} catch {
|
|
23953
25131
|
continue;
|
|
23954
25132
|
}
|
|
23955
|
-
for (const file of
|
|
23956
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
25133
|
+
for (const file of fs51.readdirSync(dayPath)) {
|
|
25134
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path50.join(dayPath, file));
|
|
23957
25135
|
}
|
|
23958
25136
|
}
|
|
23959
25137
|
}
|
|
@@ -23965,7 +25143,7 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
23965
25143
|
for (const filePath of jsonlFiles) {
|
|
23966
25144
|
let lines;
|
|
23967
25145
|
try {
|
|
23968
|
-
lines =
|
|
25146
|
+
lines = fs51.readFileSync(filePath, "utf-8").split("\n");
|
|
23969
25147
|
} catch {
|
|
23970
25148
|
continue;
|
|
23971
25149
|
}
|
|
@@ -24051,10 +25229,10 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
24051
25229
|
return summaries;
|
|
24052
25230
|
}
|
|
24053
25231
|
function buildSessions(days, historyPath) {
|
|
24054
|
-
const hPath = historyPath ??
|
|
25232
|
+
const hPath = historyPath ?? path50.join(os46.homedir(), ".claude", "history.jsonl");
|
|
24055
25233
|
let historyRaw = "";
|
|
24056
25234
|
try {
|
|
24057
|
-
historyRaw =
|
|
25235
|
+
historyRaw = fs51.readFileSync(hPath, "utf-8");
|
|
24058
25236
|
} catch {
|
|
24059
25237
|
}
|
|
24060
25238
|
const cutoff = days !== null ? (() => {
|
|
@@ -24078,7 +25256,7 @@ function buildSessions(days, historyPath) {
|
|
|
24078
25256
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
24079
25257
|
let sessionLines = [];
|
|
24080
25258
|
try {
|
|
24081
|
-
sessionLines =
|
|
25259
|
+
sessionLines = fs51.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
24082
25260
|
} catch {
|
|
24083
25261
|
}
|
|
24084
25262
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -24164,11 +25342,11 @@ function toolInputSummary(tool, input) {
|
|
|
24164
25342
|
}
|
|
24165
25343
|
function toolColor(tool) {
|
|
24166
25344
|
const t = tool.toLowerCase();
|
|
24167
|
-
if (t === "bash" || t === "execute_bash") return
|
|
24168
|
-
if (t === "write") return
|
|
24169
|
-
if (t === "edit" || t === "notebookedit") return
|
|
24170
|
-
if (t === "read") return
|
|
24171
|
-
return
|
|
25345
|
+
if (t === "bash" || t === "execute_bash") return chalk27.red;
|
|
25346
|
+
if (t === "write") return chalk27.green;
|
|
25347
|
+
if (t === "edit" || t === "notebookedit") return chalk27.yellow;
|
|
25348
|
+
if (t === "read") return chalk27.cyan;
|
|
25349
|
+
return chalk27.gray;
|
|
24172
25350
|
}
|
|
24173
25351
|
function barStr2(value, max, width) {
|
|
24174
25352
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
@@ -24178,7 +25356,7 @@ function barStr2(value, max, width) {
|
|
|
24178
25356
|
function colorBar2(value, max, width) {
|
|
24179
25357
|
const s = barStr2(value, max, width);
|
|
24180
25358
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
24181
|
-
return
|
|
25359
|
+
return chalk27.cyan(s.slice(0, filled)) + chalk27.dim(s.slice(filled));
|
|
24182
25360
|
}
|
|
24183
25361
|
function renderSummary(summaries) {
|
|
24184
25362
|
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
@@ -24208,45 +25386,45 @@ function renderSummary(summaries) {
|
|
|
24208
25386
|
}
|
|
24209
25387
|
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
24210
25388
|
const W = 20;
|
|
24211
|
-
console.log(
|
|
25389
|
+
console.log(chalk27.dim(" " + "\u2500".repeat(70)));
|
|
24212
25390
|
console.log(
|
|
24213
|
-
" " +
|
|
25391
|
+
" " + chalk27.bold.white(String(summaries.length).padEnd(4)) + chalk27.dim("sessions ") + chalk27.bold.yellow(fmtCost3(totalCost).padEnd(10)) + chalk27.dim("total ") + chalk27.bold.white(String(totalTools).padEnd(6)) + chalk27.dim("tool calls ") + chalk27.bold.white(String(totalFiles)) + chalk27.dim(" files modified") + (totalBlocked > 0 ? chalk27.dim(" ") + chalk27.red.bold(String(totalBlocked)) + chalk27.dim(" blocked by node9") : "")
|
|
24214
25392
|
);
|
|
24215
25393
|
console.log(
|
|
24216
|
-
" " +
|
|
25394
|
+
" " + chalk27.dim("avg ") + chalk27.white(fmtCost3(avgCost).padEnd(10)) + chalk27.dim("/session ") + chalk27.green(String(snapshots)) + chalk27.dim(` of ${summaries.length} sessions had snapshots`)
|
|
24217
25395
|
);
|
|
24218
25396
|
console.log("");
|
|
24219
|
-
console.log(" " +
|
|
25397
|
+
console.log(" " + chalk27.dim("Tool breakdown:"));
|
|
24220
25398
|
const maxGroup = Math.max(...Object.values(groups));
|
|
24221
|
-
for (const [
|
|
25399
|
+
for (const [label2, count] of Object.entries(groups)) {
|
|
24222
25400
|
if (count === 0) continue;
|
|
24223
25401
|
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
24224
25402
|
console.log(
|
|
24225
|
-
" " +
|
|
25403
|
+
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk27.white(String(count).padStart(4)) + chalk27.dim(` (${String(pct)}%)`)
|
|
24226
25404
|
);
|
|
24227
25405
|
}
|
|
24228
25406
|
console.log("");
|
|
24229
25407
|
if (topProjects.length > 1) {
|
|
24230
|
-
console.log(" " +
|
|
25408
|
+
console.log(" " + chalk27.dim("Cost by project:"));
|
|
24231
25409
|
const maxProjCost = topProjects[0][1];
|
|
24232
25410
|
for (const [proj, cost] of topProjects) {
|
|
24233
25411
|
console.log(
|
|
24234
|
-
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " +
|
|
25412
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk27.yellow(fmtCost3(cost))
|
|
24235
25413
|
);
|
|
24236
25414
|
}
|
|
24237
25415
|
console.log("");
|
|
24238
25416
|
}
|
|
24239
|
-
console.log(
|
|
25417
|
+
console.log(chalk27.dim(" " + "\u2500".repeat(70)));
|
|
24240
25418
|
console.log("");
|
|
24241
25419
|
}
|
|
24242
25420
|
function renderList(summaries, totalCost) {
|
|
24243
25421
|
if (summaries.length === 0) {
|
|
24244
|
-
console.log(
|
|
25422
|
+
console.log(chalk27.yellow(" No sessions found in the requested range.\n"));
|
|
24245
25423
|
return;
|
|
24246
25424
|
}
|
|
24247
|
-
const totalLabel = totalCost > 0 ?
|
|
25425
|
+
const totalLabel = totalCost > 0 ? chalk27.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
24248
25426
|
console.log(
|
|
24249
|
-
" " +
|
|
25427
|
+
" " + chalk27.white(String(summaries.length)) + chalk27.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
24250
25428
|
);
|
|
24251
25429
|
console.log("");
|
|
24252
25430
|
let lastGroup = "";
|
|
@@ -24254,51 +25432,51 @@ function renderList(summaries, totalCost) {
|
|
|
24254
25432
|
const activeDate = fmtDate2(s.lastActiveTime);
|
|
24255
25433
|
const group = activeDate + " " + s.projectLabel;
|
|
24256
25434
|
if (group !== lastGroup) {
|
|
24257
|
-
console.log(
|
|
25435
|
+
console.log(chalk27.dim(" \u2500\u2500\u2500 ") + chalk27.bold(activeDate) + chalk27.dim(" " + s.projectLabel));
|
|
24258
25436
|
lastGroup = group;
|
|
24259
25437
|
}
|
|
24260
25438
|
const startDate = fmtDate2(s.startTime);
|
|
24261
|
-
const dateRange = startDate !== activeDate ?
|
|
24262
|
-
const timeStr =
|
|
24263
|
-
const prompt =
|
|
24264
|
-
const tools = s.toolCalls.length > 0 ?
|
|
24265
|
-
const cost = s.costUSD > 0 ?
|
|
24266
|
-
const blocked = s.blockedCalls.length > 0 ?
|
|
24267
|
-
const snap = s.hasSnapshot ?
|
|
24268
|
-
const agentBadge =
|
|
25439
|
+
const dateRange = startDate !== activeDate ? chalk27.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
25440
|
+
const timeStr = chalk27.dim(fmtTime(s.startTime));
|
|
25441
|
+
const prompt = chalk27.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
25442
|
+
const tools = s.toolCalls.length > 0 ? chalk27.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk27.dim(" 0 tools");
|
|
25443
|
+
const cost = s.costUSD > 0 ? chalk27.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
25444
|
+
const blocked = s.blockedCalls.length > 0 ? chalk27.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
25445
|
+
const snap = s.hasSnapshot ? chalk27.green(" \u{1F4F8}") : "";
|
|
25446
|
+
const agentBadge = chalk27[agentColorName(s.agent ?? "claude")](
|
|
24269
25447
|
" " + agentBadgeText(s.agent ?? "claude", 0)
|
|
24270
25448
|
);
|
|
24271
|
-
const sid =
|
|
25449
|
+
const sid = chalk27.dim(" " + s.sessionId.slice(0, 8));
|
|
24272
25450
|
console.log(
|
|
24273
25451
|
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
24274
25452
|
);
|
|
24275
25453
|
}
|
|
24276
25454
|
console.log("");
|
|
24277
25455
|
console.log(
|
|
24278
|
-
|
|
25456
|
+
chalk27.dim(" Run") + " " + chalk27.cyan("node9 sessions --detail <session-id>") + chalk27.dim(" for full tool trace.")
|
|
24279
25457
|
);
|
|
24280
25458
|
console.log("");
|
|
24281
25459
|
}
|
|
24282
25460
|
function renderDetail(s) {
|
|
24283
25461
|
console.log("");
|
|
24284
|
-
console.log(
|
|
25462
|
+
console.log(chalk27.bold(" Session ") + chalk27.dim(s.sessionId));
|
|
24285
25463
|
console.log(
|
|
24286
|
-
|
|
25464
|
+
chalk27.bold(" Prompt ") + chalk27.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
24287
25465
|
);
|
|
24288
|
-
console.log(
|
|
25466
|
+
console.log(chalk27.bold(" Project ") + chalk27.white(s.projectLabel));
|
|
24289
25467
|
if (s.agent) {
|
|
24290
|
-
const agentLabel2 =
|
|
24291
|
-
console.log(
|
|
25468
|
+
const agentLabel2 = chalk27[agentColorName(s.agent)](agentDisplayName(s.agent));
|
|
25469
|
+
console.log(chalk27.bold(" Agent ") + agentLabel2);
|
|
24292
25470
|
}
|
|
24293
|
-
console.log(
|
|
25471
|
+
console.log(chalk27.bold(" When ") + chalk27.white(fmtDateTime(s.startTime)));
|
|
24294
25472
|
if (s.costUSD > 0)
|
|
24295
|
-
console.log(
|
|
25473
|
+
console.log(chalk27.bold(" Cost ") + chalk27.yellow("~" + fmtCost3(s.costUSD)));
|
|
24296
25474
|
console.log(
|
|
24297
|
-
|
|
25475
|
+
chalk27.bold(" Snapshot ") + (s.hasSnapshot ? chalk27.green("\u2713 taken") : chalk27.dim("none"))
|
|
24298
25476
|
);
|
|
24299
25477
|
console.log("");
|
|
24300
25478
|
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
24301
|
-
console.log(
|
|
25479
|
+
console.log(chalk27.dim(" No tool calls recorded.\n"));
|
|
24302
25480
|
return;
|
|
24303
25481
|
}
|
|
24304
25482
|
const timeline = [
|
|
@@ -24311,32 +25489,32 @@ function renderDetail(s) {
|
|
|
24311
25489
|
});
|
|
24312
25490
|
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
24313
25491
|
if (s.blockedCalls.length > 0)
|
|
24314
|
-
headerParts.push(
|
|
24315
|
-
console.log(
|
|
25492
|
+
headerParts.push(chalk27.red(`${s.blockedCalls.length} blocked by node9`));
|
|
25493
|
+
console.log(chalk27.bold(" " + headerParts.join(" \xB7 ")));
|
|
24316
25494
|
console.log("");
|
|
24317
25495
|
for (const entry of timeline) {
|
|
24318
25496
|
if (entry.kind === "tool") {
|
|
24319
25497
|
const tc = entry.tc;
|
|
24320
25498
|
const colorFn = toolColor(tc.tool);
|
|
24321
25499
|
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
24322
|
-
const detail =
|
|
24323
|
-
const ts = tc.timestamp ?
|
|
25500
|
+
const detail = chalk27.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
25501
|
+
const ts = tc.timestamp ? chalk27.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
24324
25502
|
console.log(` ${ts}${toolPad} ${detail}`);
|
|
24325
25503
|
} else {
|
|
24326
25504
|
const bc = entry.bc;
|
|
24327
|
-
const ts = bc.timestamp ?
|
|
24328
|
-
const
|
|
24329
|
-
const toolName =
|
|
24330
|
-
const argsSummary = bc.args ?
|
|
24331
|
-
const reason = bc.checkedBy ?
|
|
24332
|
-
console.log(` ${ts}${
|
|
25505
|
+
const ts = bc.timestamp ? chalk27.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
25506
|
+
const label2 = chalk27.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
25507
|
+
const toolName = chalk27.red(bc.tool.padEnd(10));
|
|
25508
|
+
const argsSummary = bc.args ? chalk27.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk27.dim("[args not logged]");
|
|
25509
|
+
const reason = bc.checkedBy ? chalk27.dim(" \u2190 " + bc.checkedBy) : "";
|
|
25510
|
+
console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
|
|
24333
25511
|
}
|
|
24334
25512
|
}
|
|
24335
25513
|
console.log("");
|
|
24336
25514
|
if (s.modifiedFiles.length > 0) {
|
|
24337
|
-
console.log(
|
|
25515
|
+
console.log(chalk27.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
24338
25516
|
for (const f of s.modifiedFiles) {
|
|
24339
|
-
console.log(" " +
|
|
25517
|
+
console.log(" " + chalk27.yellow(f));
|
|
24340
25518
|
}
|
|
24341
25519
|
console.log("");
|
|
24342
25520
|
}
|
|
@@ -24344,13 +25522,13 @@ function renderDetail(s) {
|
|
|
24344
25522
|
function registerSessionsCommand(program2) {
|
|
24345
25523
|
program2.command("sessions").description("Show what your AI agent did \u2014 sessions, tool calls, cost, and file changes").option("--all", "Show all sessions (default: last 7 days)").option("--days <n>", "Show last N days of sessions", "7").option("--detail <sessionId>", "Show full tool trace for a session").action((options) => {
|
|
24346
25524
|
console.log("");
|
|
24347
|
-
console.log(
|
|
25525
|
+
console.log(chalk27.cyan.bold("\u{1F4CB} node9 sessions") + chalk27.dim(" \u2014 what your AI agent did"));
|
|
24348
25526
|
console.log("");
|
|
24349
25527
|
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
24350
25528
|
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
24351
|
-
console.log(
|
|
25529
|
+
console.log(chalk27.dim(" " + rangeLabel));
|
|
24352
25530
|
console.log("");
|
|
24353
|
-
process.stdout.write(
|
|
25531
|
+
process.stdout.write(chalk27.dim(" Loading\u2026"));
|
|
24354
25532
|
const summaries = buildSessions(days);
|
|
24355
25533
|
if (process.stdout.isTTY) {
|
|
24356
25534
|
process.stdout.clearLine(0);
|
|
@@ -24363,8 +25541,8 @@ function registerSessionsCommand(program2) {
|
|
|
24363
25541
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
24364
25542
|
);
|
|
24365
25543
|
if (!target) {
|
|
24366
|
-
console.log(
|
|
24367
|
-
console.log(
|
|
25544
|
+
console.log(chalk27.red(` Session not found: ${options.detail}`));
|
|
25545
|
+
console.log(chalk27.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
24368
25546
|
return;
|
|
24369
25547
|
}
|
|
24370
25548
|
renderDetail(target);
|
|
@@ -24377,13 +25555,13 @@ function registerSessionsCommand(program2) {
|
|
|
24377
25555
|
}
|
|
24378
25556
|
|
|
24379
25557
|
// src/cli/commands/skill-pin.ts
|
|
24380
|
-
import
|
|
24381
|
-
import
|
|
24382
|
-
import
|
|
24383
|
-
import
|
|
25558
|
+
import chalk28 from "chalk";
|
|
25559
|
+
import fs52 from "fs";
|
|
25560
|
+
import os47 from "os";
|
|
25561
|
+
import path51 from "path";
|
|
24384
25562
|
function wipeSkillSessions() {
|
|
24385
25563
|
try {
|
|
24386
|
-
|
|
25564
|
+
fs52.rmSync(path51.join(os47.homedir(), ".node9", "skill-sessions"), {
|
|
24387
25565
|
recursive: true,
|
|
24388
25566
|
force: true
|
|
24389
25567
|
});
|
|
@@ -24397,29 +25575,29 @@ function registerSkillPinCommand(program2) {
|
|
|
24397
25575
|
const result = readSkillPinsSafe();
|
|
24398
25576
|
if (!result.ok) {
|
|
24399
25577
|
if (result.reason === "missing") {
|
|
24400
|
-
console.log(
|
|
25578
|
+
console.log(chalk28.gray("\nNo skill roots are pinned yet."));
|
|
24401
25579
|
console.log(
|
|
24402
|
-
|
|
25580
|
+
chalk28.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
24403
25581
|
);
|
|
24404
25582
|
return;
|
|
24405
25583
|
}
|
|
24406
|
-
console.error(
|
|
25584
|
+
console.error(chalk28.red(`
|
|
24407
25585
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
24408
|
-
console.error(
|
|
25586
|
+
console.error(chalk28.yellow(" Run: node9 skill pin reset\n"));
|
|
24409
25587
|
process.exit(1);
|
|
24410
25588
|
}
|
|
24411
25589
|
const entries = Object.entries(result.pins.roots);
|
|
24412
25590
|
if (entries.length === 0) {
|
|
24413
|
-
console.log(
|
|
25591
|
+
console.log(chalk28.gray("\nNo skill roots are pinned yet.\n"));
|
|
24414
25592
|
return;
|
|
24415
25593
|
}
|
|
24416
|
-
console.log(
|
|
25594
|
+
console.log(chalk28.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
24417
25595
|
for (const [key, entry] of entries) {
|
|
24418
|
-
const missing = entry.exists ? "" :
|
|
24419
|
-
console.log(` ${
|
|
25596
|
+
const missing = entry.exists ? "" : chalk28.yellow(" (not present at pin time)");
|
|
25597
|
+
console.log(` ${chalk28.cyan(key)} ${chalk28.gray(entry.rootPath)}${missing}`);
|
|
24420
25598
|
console.log(` Files (${entry.fileCount})`);
|
|
24421
|
-
console.log(` Hash: ${
|
|
24422
|
-
console.log(` Pinned: ${
|
|
25599
|
+
console.log(` Hash: ${chalk28.gray(entry.contentHash.slice(0, 16))}...`);
|
|
25600
|
+
console.log(` Pinned: ${chalk28.gray(entry.pinnedAt)}
|
|
24423
25601
|
`);
|
|
24424
25602
|
}
|
|
24425
25603
|
});
|
|
@@ -24428,52 +25606,52 @@ function registerSkillPinCommand(program2) {
|
|
|
24428
25606
|
try {
|
|
24429
25607
|
pins = readSkillPins();
|
|
24430
25608
|
} catch {
|
|
24431
|
-
console.error(
|
|
24432
|
-
console.error(
|
|
25609
|
+
console.error(chalk28.red("\n\u274C Pin file is corrupt."));
|
|
25610
|
+
console.error(chalk28.yellow(" Run: node9 skill pin reset\n"));
|
|
24433
25611
|
process.exit(1);
|
|
24434
25612
|
}
|
|
24435
25613
|
if (!pins.roots[rootKey]) {
|
|
24436
|
-
console.error(
|
|
25614
|
+
console.error(chalk28.red(`
|
|
24437
25615
|
\u274C No pin found for root key "${rootKey}"
|
|
24438
25616
|
`));
|
|
24439
|
-
console.error(`Run ${
|
|
25617
|
+
console.error(`Run ${chalk28.cyan("node9 skill pin list")} to see pinned roots.
|
|
24440
25618
|
`);
|
|
24441
25619
|
process.exit(1);
|
|
24442
25620
|
}
|
|
24443
25621
|
const rootPath = pins.roots[rootKey].rootPath;
|
|
24444
25622
|
removePin2(rootKey);
|
|
24445
25623
|
wipeSkillSessions();
|
|
24446
|
-
console.log(
|
|
24447
|
-
\u{1F513} Pin removed for ${
|
|
24448
|
-
console.log(
|
|
24449
|
-
console.log(
|
|
25624
|
+
console.log(chalk28.green(`
|
|
25625
|
+
\u{1F513} Pin removed for ${chalk28.cyan(rootKey)}`));
|
|
25626
|
+
console.log(chalk28.gray(` ${rootPath}`));
|
|
25627
|
+
console.log(chalk28.gray(" Next session will re-pin with current state.\n"));
|
|
24450
25628
|
});
|
|
24451
25629
|
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
24452
25630
|
const result = readSkillPinsSafe();
|
|
24453
25631
|
if (!result.ok && result.reason === "missing") {
|
|
24454
25632
|
wipeSkillSessions();
|
|
24455
|
-
console.log(
|
|
25633
|
+
console.log(chalk28.gray("\nNo pins to clear.\n"));
|
|
24456
25634
|
return;
|
|
24457
25635
|
}
|
|
24458
25636
|
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
24459
25637
|
clearAllPins2();
|
|
24460
25638
|
wipeSkillSessions();
|
|
24461
|
-
console.log(
|
|
25639
|
+
console.log(chalk28.green(`
|
|
24462
25640
|
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
24463
|
-
console.log(
|
|
25641
|
+
console.log(chalk28.gray(" Next session will re-pin with current state.\n"));
|
|
24464
25642
|
});
|
|
24465
25643
|
}
|
|
24466
25644
|
|
|
24467
25645
|
// src/cli/commands/decisions.ts
|
|
24468
|
-
import
|
|
24469
|
-
import
|
|
24470
|
-
import
|
|
24471
|
-
import
|
|
24472
|
-
var DECISIONS_FILE2 =
|
|
25646
|
+
import fs53 from "fs";
|
|
25647
|
+
import os48 from "os";
|
|
25648
|
+
import path52 from "path";
|
|
25649
|
+
import chalk29 from "chalk";
|
|
25650
|
+
var DECISIONS_FILE2 = path52.join(os48.homedir(), ".node9", "decisions.json");
|
|
24473
25651
|
function readDecisions() {
|
|
24474
25652
|
try {
|
|
24475
|
-
if (!
|
|
24476
|
-
const raw =
|
|
25653
|
+
if (!fs53.existsSync(DECISIONS_FILE2)) return {};
|
|
25654
|
+
const raw = fs53.readFileSync(DECISIONS_FILE2, "utf-8");
|
|
24477
25655
|
const parsed = JSON.parse(raw);
|
|
24478
25656
|
const out = {};
|
|
24479
25657
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -24485,11 +25663,11 @@ function readDecisions() {
|
|
|
24485
25663
|
}
|
|
24486
25664
|
}
|
|
24487
25665
|
function writeDecisions(d) {
|
|
24488
|
-
const dir =
|
|
24489
|
-
if (!
|
|
25666
|
+
const dir = path52.dirname(DECISIONS_FILE2);
|
|
25667
|
+
if (!fs53.existsSync(dir)) fs53.mkdirSync(dir, { recursive: true });
|
|
24490
25668
|
const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
|
|
24491
|
-
|
|
24492
|
-
|
|
25669
|
+
fs53.writeFileSync(tmp, JSON.stringify(d, null, 2));
|
|
25670
|
+
fs53.renameSync(tmp, DECISIONS_FILE2);
|
|
24493
25671
|
}
|
|
24494
25672
|
function registerDecisionsCommand(program2) {
|
|
24495
25673
|
const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
|
|
@@ -24497,67 +25675,67 @@ function registerDecisionsCommand(program2) {
|
|
|
24497
25675
|
const decisions = readDecisions();
|
|
24498
25676
|
const entries = Object.entries(decisions);
|
|
24499
25677
|
if (entries.length === 0) {
|
|
24500
|
-
console.log(
|
|
25678
|
+
console.log(chalk29.gray(" No persistent decisions stored."));
|
|
24501
25679
|
console.log(
|
|
24502
|
-
|
|
24503
|
-
`) +
|
|
25680
|
+
chalk29.gray(` File: ${DECISIONS_FILE2}
|
|
25681
|
+
`) + chalk29.gray(' Decisions are written when you click "Always Allow" or')
|
|
24504
25682
|
);
|
|
24505
|
-
console.log(
|
|
25683
|
+
console.log(chalk29.gray(' "Always Deny" in node9 tail or the native popup.'));
|
|
24506
25684
|
return;
|
|
24507
25685
|
}
|
|
24508
|
-
console.log(
|
|
25686
|
+
console.log(chalk29.bold(`
|
|
24509
25687
|
Persistent decisions (${entries.length})
|
|
24510
25688
|
`));
|
|
24511
25689
|
const w = Math.max(...entries.map(([k]) => k.length));
|
|
24512
25690
|
for (const [tool, verdict] of entries.sort()) {
|
|
24513
|
-
const colored = verdict === "allow" ?
|
|
25691
|
+
const colored = verdict === "allow" ? chalk29.green(verdict) : chalk29.red(verdict);
|
|
24514
25692
|
console.log(` ${tool.padEnd(w)} ${colored}`);
|
|
24515
25693
|
}
|
|
24516
25694
|
console.log(
|
|
24517
|
-
|
|
25695
|
+
chalk29.gray(`
|
|
24518
25696
|
Stored in ${DECISIONS_FILE2}
|
|
24519
|
-
`) +
|
|
25697
|
+
`) + chalk29.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
|
|
24520
25698
|
);
|
|
24521
25699
|
});
|
|
24522
25700
|
cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
|
|
24523
25701
|
const decisions = readDecisions();
|
|
24524
25702
|
if (!(toolName in decisions)) {
|
|
24525
|
-
console.log(
|
|
25703
|
+
console.log(chalk29.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
|
|
24526
25704
|
process.exitCode = 1;
|
|
24527
25705
|
return;
|
|
24528
25706
|
}
|
|
24529
25707
|
delete decisions[toolName];
|
|
24530
25708
|
writeDecisions(decisions);
|
|
24531
|
-
console.log(
|
|
25709
|
+
console.log(chalk29.green(` \u2713 Cleared persistent decision for "${toolName}".`));
|
|
24532
25710
|
});
|
|
24533
25711
|
cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
|
|
24534
25712
|
const decisions = readDecisions();
|
|
24535
25713
|
const count = Object.keys(decisions).length;
|
|
24536
25714
|
if (count === 0) {
|
|
24537
|
-
console.log(
|
|
25715
|
+
console.log(chalk29.gray(" Nothing to clear \u2014 no persistent decisions stored."));
|
|
24538
25716
|
return;
|
|
24539
25717
|
}
|
|
24540
25718
|
writeDecisions({});
|
|
24541
25719
|
console.log(
|
|
24542
|
-
|
|
25720
|
+
chalk29.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
|
|
24543
25721
|
);
|
|
24544
25722
|
});
|
|
24545
25723
|
}
|
|
24546
25724
|
|
|
24547
25725
|
// src/cli/commands/dlp.ts
|
|
24548
|
-
import
|
|
24549
|
-
import
|
|
24550
|
-
import
|
|
24551
|
-
import
|
|
24552
|
-
var AUDIT_LOG =
|
|
24553
|
-
var RESOLVED_FILE =
|
|
25726
|
+
import chalk30 from "chalk";
|
|
25727
|
+
import fs54 from "fs";
|
|
25728
|
+
import path53 from "path";
|
|
25729
|
+
import os49 from "os";
|
|
25730
|
+
var AUDIT_LOG = path53.join(os49.homedir(), ".node9", "audit.log");
|
|
25731
|
+
var RESOLVED_FILE = path53.join(os49.homedir(), ".node9", "dlp-resolved.json");
|
|
24554
25732
|
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
24555
25733
|
function stripAnsi(s) {
|
|
24556
25734
|
return s.replace(ANSI_RE, "");
|
|
24557
25735
|
}
|
|
24558
25736
|
function loadResolved() {
|
|
24559
25737
|
try {
|
|
24560
|
-
const raw = JSON.parse(
|
|
25738
|
+
const raw = JSON.parse(fs54.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
24561
25739
|
return new Set(raw);
|
|
24562
25740
|
} catch {
|
|
24563
25741
|
return /* @__PURE__ */ new Set();
|
|
@@ -24565,13 +25743,13 @@ function loadResolved() {
|
|
|
24565
25743
|
}
|
|
24566
25744
|
function saveResolved(resolved) {
|
|
24567
25745
|
try {
|
|
24568
|
-
|
|
25746
|
+
fs54.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
24569
25747
|
} catch {
|
|
24570
25748
|
}
|
|
24571
25749
|
}
|
|
24572
25750
|
function loadDlpFindings() {
|
|
24573
|
-
if (!
|
|
24574
|
-
return
|
|
25751
|
+
if (!fs54.existsSync(AUDIT_LOG)) return [];
|
|
25752
|
+
return fs54.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
24575
25753
|
if (!line.trim()) return [];
|
|
24576
25754
|
try {
|
|
24577
25755
|
const e = JSON.parse(line);
|
|
@@ -24600,14 +25778,14 @@ function registerDlpCommand(program2) {
|
|
|
24600
25778
|
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
24601
25779
|
const findings = loadDlpFindings();
|
|
24602
25780
|
if (findings.length === 0) {
|
|
24603
|
-
console.log(
|
|
25781
|
+
console.log(chalk30.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
24604
25782
|
return;
|
|
24605
25783
|
}
|
|
24606
25784
|
const resolved = loadResolved();
|
|
24607
25785
|
for (const e of findings) resolved.add(entryKey2(e));
|
|
24608
25786
|
saveResolved(resolved);
|
|
24609
25787
|
console.log(
|
|
24610
|
-
|
|
25788
|
+
chalk30.green(
|
|
24611
25789
|
`
|
|
24612
25790
|
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
24613
25791
|
`
|
|
@@ -24621,47 +25799,47 @@ function registerDlpCommand(program2) {
|
|
|
24621
25799
|
const resolvedCount = findings.length - open.length;
|
|
24622
25800
|
console.log("");
|
|
24623
25801
|
console.log(
|
|
24624
|
-
|
|
25802
|
+
chalk30.bold.cyan("\u{1F510} node9 dlp") + chalk30.dim(" \u2014 secrets found in Claude response text")
|
|
24625
25803
|
);
|
|
24626
25804
|
console.log("");
|
|
24627
25805
|
if (open.length === 0) {
|
|
24628
25806
|
if (resolvedCount > 0) {
|
|
24629
|
-
console.log(
|
|
25807
|
+
console.log(chalk30.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
24630
25808
|
} else {
|
|
24631
25809
|
console.log(
|
|
24632
|
-
|
|
25810
|
+
chalk30.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
24633
25811
|
);
|
|
24634
25812
|
}
|
|
24635
25813
|
console.log("");
|
|
24636
25814
|
return;
|
|
24637
25815
|
}
|
|
24638
25816
|
console.log(
|
|
24639
|
-
|
|
25817
|
+
chalk30.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk30.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
24640
25818
|
);
|
|
24641
25819
|
console.log("");
|
|
24642
25820
|
console.log(
|
|
24643
|
-
|
|
25821
|
+
chalk30.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
24644
25822
|
);
|
|
24645
|
-
console.log(
|
|
25823
|
+
console.log(chalk30.dim(" Rotate each affected key immediately.\n"));
|
|
24646
25824
|
for (const e of open) {
|
|
24647
25825
|
console.log(
|
|
24648
|
-
" " +
|
|
25826
|
+
" " + chalk30.red("\u25CF") + " " + chalk30.white(e.dlpPattern ?? "Secret") + chalk30.dim(" " + fmtDate3(e.ts))
|
|
24649
25827
|
);
|
|
24650
25828
|
if (e.dlpSample) {
|
|
24651
|
-
console.log(" " +
|
|
25829
|
+
console.log(" " + chalk30.dim("Sample: ") + chalk30.yellow(stripAnsi(e.dlpSample)));
|
|
24652
25830
|
}
|
|
24653
25831
|
if (e.project) {
|
|
24654
|
-
console.log(" " +
|
|
25832
|
+
console.log(" " + chalk30.dim("Project: ") + chalk30.dim(stripAnsi(e.project)));
|
|
24655
25833
|
}
|
|
24656
25834
|
console.log("");
|
|
24657
25835
|
}
|
|
24658
|
-
console.log(" " +
|
|
24659
|
-
console.log(" " +
|
|
25836
|
+
console.log(" " + chalk30.bold("Next steps:"));
|
|
25837
|
+
console.log(" " + chalk30.cyan("1.") + " Rotate any exposed keys shown above");
|
|
24660
25838
|
console.log(
|
|
24661
|
-
" " +
|
|
25839
|
+
" " + chalk30.cyan("2.") + " Run " + chalk30.white("node9 dlp resolve") + " to acknowledge"
|
|
24662
25840
|
);
|
|
24663
25841
|
console.log(
|
|
24664
|
-
" " +
|
|
25842
|
+
" " + chalk30.cyan("3.") + " Run " + chalk30.white("node9 report") + " for full audit history"
|
|
24665
25843
|
);
|
|
24666
25844
|
console.log("");
|
|
24667
25845
|
});
|
|
@@ -24669,15 +25847,15 @@ function registerDlpCommand(program2) {
|
|
|
24669
25847
|
|
|
24670
25848
|
// src/cli/commands/mask.ts
|
|
24671
25849
|
init_dlp();
|
|
24672
|
-
import
|
|
24673
|
-
import
|
|
24674
|
-
import
|
|
24675
|
-
import
|
|
25850
|
+
import chalk31 from "chalk";
|
|
25851
|
+
import fs55 from "fs";
|
|
25852
|
+
import path54 from "path";
|
|
25853
|
+
import os50 from "os";
|
|
24676
25854
|
function findJsonlFiles(dir) {
|
|
24677
25855
|
const results = [];
|
|
24678
|
-
if (!
|
|
24679
|
-
for (const entry of
|
|
24680
|
-
const full =
|
|
25856
|
+
if (!fs55.existsSync(dir)) return results;
|
|
25857
|
+
for (const entry of fs55.readdirSync(dir, { withFileTypes: true })) {
|
|
25858
|
+
const full = path54.join(dir, entry.name);
|
|
24681
25859
|
if (entry.isDirectory()) results.push(...findJsonlFiles(full));
|
|
24682
25860
|
else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
|
|
24683
25861
|
}
|
|
@@ -24720,7 +25898,7 @@ function redactJson(obj) {
|
|
|
24720
25898
|
function processFile(filePath, dryRun) {
|
|
24721
25899
|
let raw;
|
|
24722
25900
|
try {
|
|
24723
|
-
raw =
|
|
25901
|
+
raw = fs55.readFileSync(filePath, "utf-8");
|
|
24724
25902
|
} catch {
|
|
24725
25903
|
return { redactedLines: 0, patterns: [] };
|
|
24726
25904
|
}
|
|
@@ -24752,14 +25930,14 @@ function processFile(filePath, dryRun) {
|
|
|
24752
25930
|
}
|
|
24753
25931
|
}
|
|
24754
25932
|
if (!dryRun && redactedLines > 0) {
|
|
24755
|
-
|
|
25933
|
+
fs55.writeFileSync(filePath, newLines.join("\n"), "utf-8");
|
|
24756
25934
|
}
|
|
24757
25935
|
return { redactedLines, patterns };
|
|
24758
25936
|
}
|
|
24759
25937
|
function processJsonFile(filePath, dryRun) {
|
|
24760
25938
|
let raw;
|
|
24761
25939
|
try {
|
|
24762
|
-
raw =
|
|
25940
|
+
raw = fs55.readFileSync(filePath, "utf-8");
|
|
24763
25941
|
} catch {
|
|
24764
25942
|
return { redactedLines: 0, patterns: [] };
|
|
24765
25943
|
}
|
|
@@ -24772,15 +25950,15 @@ function processJsonFile(filePath, dryRun) {
|
|
|
24772
25950
|
const { value, modified, found } = redactJson(parsed);
|
|
24773
25951
|
if (!modified) return { redactedLines: 0, patterns: [] };
|
|
24774
25952
|
if (!dryRun) {
|
|
24775
|
-
|
|
25953
|
+
fs55.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
24776
25954
|
}
|
|
24777
25955
|
return { redactedLines: 1, patterns: found };
|
|
24778
25956
|
}
|
|
24779
25957
|
function findJsonFiles(dir) {
|
|
24780
25958
|
const results = [];
|
|
24781
|
-
if (!
|
|
24782
|
-
for (const entry of
|
|
24783
|
-
const full =
|
|
25959
|
+
if (!fs55.existsSync(dir)) return results;
|
|
25960
|
+
for (const entry of fs55.readdirSync(dir, { withFileTypes: true })) {
|
|
25961
|
+
const full = path54.join(dir, entry.name);
|
|
24784
25962
|
if (entry.isDirectory()) results.push(...findJsonFiles(full));
|
|
24785
25963
|
else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
|
|
24786
25964
|
}
|
|
@@ -24789,9 +25967,9 @@ function findJsonFiles(dir) {
|
|
|
24789
25967
|
function registerMaskCommand(program2) {
|
|
24790
25968
|
program2.command("mask").description("Redact plaintext secrets from local AI session history files").option("--dry-run", "show what would be redacted without making changes").option("--all", "scan all history (default: last 30 days)").action(async (options) => {
|
|
24791
25969
|
const dryRun = !!options.dryRun;
|
|
24792
|
-
const home =
|
|
24793
|
-
const claudeDir =
|
|
24794
|
-
const geminiDir =
|
|
25970
|
+
const home = os50.homedir();
|
|
25971
|
+
const claudeDir = path54.join(home, ".claude", "projects");
|
|
25972
|
+
const geminiDir = path54.join(home, ".gemini", "tmp");
|
|
24795
25973
|
const allFiles = [
|
|
24796
25974
|
...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
|
|
24797
25975
|
...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
|
|
@@ -24799,18 +25977,18 @@ function registerMaskCommand(program2) {
|
|
|
24799
25977
|
const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
24800
25978
|
const filtered = cutoff ? allFiles.filter((f) => {
|
|
24801
25979
|
try {
|
|
24802
|
-
return
|
|
25980
|
+
return fs55.statSync(f.path).mtime >= cutoff;
|
|
24803
25981
|
} catch {
|
|
24804
25982
|
return false;
|
|
24805
25983
|
}
|
|
24806
25984
|
}) : allFiles;
|
|
24807
25985
|
if (filtered.length === 0) {
|
|
24808
|
-
console.log(
|
|
25986
|
+
console.log(chalk31.yellow(" No session files found."));
|
|
24809
25987
|
return;
|
|
24810
25988
|
}
|
|
24811
25989
|
console.log("");
|
|
24812
25990
|
if (dryRun) {
|
|
24813
|
-
console.log(
|
|
25991
|
+
console.log(chalk31.dim(" Dry run \u2014 no files will be modified.\n"));
|
|
24814
25992
|
}
|
|
24815
25993
|
let totalFiles = 0;
|
|
24816
25994
|
let totalLines = 0;
|
|
@@ -24826,23 +26004,23 @@ function registerMaskCommand(program2) {
|
|
|
24826
26004
|
});
|
|
24827
26005
|
const verb = dryRun ? "Would redact" : "Redacted";
|
|
24828
26006
|
console.log(
|
|
24829
|
-
" " +
|
|
26007
|
+
" " + chalk31.dim(shortPath.slice(0, 60).padEnd(62)) + chalk31.red(`${verb}: `) + chalk31.yellow(patterns.join(", ")) + chalk31.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
|
|
24830
26008
|
);
|
|
24831
26009
|
}
|
|
24832
26010
|
}
|
|
24833
26011
|
console.log("");
|
|
24834
26012
|
if (totalFiles === 0) {
|
|
24835
|
-
console.log(
|
|
26013
|
+
console.log(chalk31.green(" No secrets found in session history."));
|
|
24836
26014
|
} else {
|
|
24837
26015
|
const verb = dryRun ? "would be modified" : "modified";
|
|
24838
26016
|
console.log(
|
|
24839
|
-
|
|
26017
|
+
chalk31.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk31.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
|
|
24840
26018
|
);
|
|
24841
|
-
console.log(" Patterns: " +
|
|
26019
|
+
console.log(" Patterns: " + chalk31.yellow(totalPatterns.join(", ")));
|
|
24842
26020
|
if (!dryRun) {
|
|
24843
26021
|
console.log("");
|
|
24844
26022
|
console.log(
|
|
24845
|
-
|
|
26023
|
+
chalk31.dim(
|
|
24846
26024
|
" Note: secrets were already sent to the AI provider during the active session.\n This cleans your local disk only. Rotate any exposed keys."
|
|
24847
26025
|
)
|
|
24848
26026
|
);
|
|
@@ -24855,20 +26033,20 @@ function registerMaskCommand(program2) {
|
|
|
24855
26033
|
// src/cli.ts
|
|
24856
26034
|
init_blast();
|
|
24857
26035
|
var { version } = JSON.parse(
|
|
24858
|
-
|
|
26036
|
+
fs58.readFileSync(path57.join(__dirname, "../package.json"), "utf-8")
|
|
24859
26037
|
);
|
|
24860
26038
|
var program = new Command();
|
|
24861
26039
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
24862
26040
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
24863
26041
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
24864
|
-
const credPath =
|
|
24865
|
-
if (!
|
|
24866
|
-
|
|
26042
|
+
const credPath = path57.join(os53.homedir(), ".node9", "credentials.json");
|
|
26043
|
+
if (!fs58.existsSync(path57.dirname(credPath)))
|
|
26044
|
+
fs58.mkdirSync(path57.dirname(credPath), { recursive: true });
|
|
24867
26045
|
const profileName = options.profile || "default";
|
|
24868
26046
|
let existingCreds = {};
|
|
24869
26047
|
try {
|
|
24870
|
-
if (
|
|
24871
|
-
const raw = JSON.parse(
|
|
26048
|
+
if (fs58.existsSync(credPath)) {
|
|
26049
|
+
const raw = JSON.parse(fs58.readFileSync(credPath, "utf-8"));
|
|
24872
26050
|
if (raw.apiKey) {
|
|
24873
26051
|
existingCreds = {
|
|
24874
26052
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -24880,14 +26058,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
24880
26058
|
} catch {
|
|
24881
26059
|
}
|
|
24882
26060
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
24883
|
-
|
|
26061
|
+
fs58.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
24884
26062
|
let effectiveCloud = null;
|
|
24885
26063
|
if (profileName === "default") {
|
|
24886
|
-
const
|
|
26064
|
+
const configPath2 = path57.join(os53.homedir(), ".node9", "config.json");
|
|
24887
26065
|
let config = {};
|
|
24888
26066
|
try {
|
|
24889
|
-
if (
|
|
24890
|
-
config = JSON.parse(
|
|
26067
|
+
if (fs58.existsSync(configPath2))
|
|
26068
|
+
config = JSON.parse(fs58.readFileSync(configPath2, "utf-8"));
|
|
24891
26069
|
} catch {
|
|
24892
26070
|
}
|
|
24893
26071
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -24902,28 +26080,28 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
24902
26080
|
approvers.cloud = false;
|
|
24903
26081
|
}
|
|
24904
26082
|
s.approvers = approvers;
|
|
24905
|
-
if (!
|
|
24906
|
-
|
|
24907
|
-
|
|
26083
|
+
if (!fs58.existsSync(path57.dirname(configPath2)))
|
|
26084
|
+
fs58.mkdirSync(path57.dirname(configPath2), { recursive: true });
|
|
26085
|
+
fs58.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
|
|
24908
26086
|
effectiveCloud = approvers.cloud === true;
|
|
24909
26087
|
}
|
|
24910
26088
|
if (options.profile && profileName !== "default") {
|
|
24911
|
-
console.log(
|
|
24912
|
-
console.log(
|
|
26089
|
+
console.log(chalk33.green(`\u2705 Profile "${profileName}" saved`));
|
|
26090
|
+
console.log(chalk33.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
24913
26091
|
} else if (options.local || effectiveCloud === false) {
|
|
24914
|
-
console.log(
|
|
24915
|
-
console.log(
|
|
26092
|
+
console.log(chalk33.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
|
|
26093
|
+
console.log(chalk33.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
|
|
24916
26094
|
if (!options.local) {
|
|
24917
26095
|
console.log(
|
|
24918
|
-
|
|
26096
|
+
chalk33.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
|
|
24919
26097
|
);
|
|
24920
26098
|
console.log(
|
|
24921
|
-
|
|
26099
|
+
chalk33.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
|
|
24922
26100
|
);
|
|
24923
26101
|
}
|
|
24924
26102
|
} else {
|
|
24925
|
-
console.log(
|
|
24926
|
-
console.log(
|
|
26103
|
+
console.log(chalk33.green(`\u2705 Logged in \u2014 agent mode`));
|
|
26104
|
+
console.log(chalk33.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
24927
26105
|
}
|
|
24928
26106
|
});
|
|
24929
26107
|
program.command("addto", { hidden: true }).description("Integrate Node9 with an AI agent").addHelpText(
|
|
@@ -24944,7 +26122,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
|
|
|
24944
26122
|
if (target === "hermes") return setupHermes();
|
|
24945
26123
|
if (target === "hud") return setupHud();
|
|
24946
26124
|
console.error(
|
|
24947
|
-
|
|
26125
|
+
chalk33.red(
|
|
24948
26126
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
24949
26127
|
)
|
|
24950
26128
|
);
|
|
@@ -24958,20 +26136,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
24958
26136
|
"The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
|
|
24959
26137
|
).action(async (target) => {
|
|
24960
26138
|
if (!target) {
|
|
24961
|
-
console.log(
|
|
24962
|
-
console.log(" Usage: " +
|
|
26139
|
+
console.log(chalk33.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
26140
|
+
console.log(" Usage: " + chalk33.white("node9 setup <target>") + "\n");
|
|
24963
26141
|
console.log(" Targets:");
|
|
24964
|
-
console.log(" " +
|
|
24965
|
-
console.log(" " +
|
|
24966
|
-
console.log(" " +
|
|
24967
|
-
console.log(" " +
|
|
24968
|
-
console.log(" " +
|
|
24969
|
-
console.log(" " +
|
|
24970
|
-
console.log(" " +
|
|
24971
|
-
console.log(" " +
|
|
24972
|
-
console.log(" " +
|
|
26142
|
+
console.log(" " + chalk33.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
26143
|
+
console.log(" " + chalk33.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
26144
|
+
console.log(" " + chalk33.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
|
|
26145
|
+
console.log(" " + chalk33.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
|
|
26146
|
+
console.log(" " + chalk33.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
26147
|
+
console.log(" " + chalk33.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
|
|
26148
|
+
console.log(" " + chalk33.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
26149
|
+
console.log(" " + chalk33.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
26150
|
+
console.log(" " + chalk33.green("hermes") + " \u2014 Hermes Agent (hook mode)");
|
|
24973
26151
|
process.stdout.write(
|
|
24974
|
-
" " +
|
|
26152
|
+
" " + chalk33.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
24975
26153
|
);
|
|
24976
26154
|
console.log("");
|
|
24977
26155
|
return;
|
|
@@ -24988,7 +26166,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
24988
26166
|
if (t === "hermes") return setupHermes();
|
|
24989
26167
|
if (t === "hud") return setupHud();
|
|
24990
26168
|
console.error(
|
|
24991
|
-
|
|
26169
|
+
chalk33.red(
|
|
24992
26170
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
24993
26171
|
)
|
|
24994
26172
|
);
|
|
@@ -25014,35 +26192,35 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
|
|
|
25014
26192
|
else if (target === "hud") fn = teardownHud;
|
|
25015
26193
|
else {
|
|
25016
26194
|
console.error(
|
|
25017
|
-
|
|
26195
|
+
chalk33.red(
|
|
25018
26196
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
25019
26197
|
)
|
|
25020
26198
|
);
|
|
25021
26199
|
process.exit(1);
|
|
25022
26200
|
}
|
|
25023
|
-
console.log(
|
|
26201
|
+
console.log(chalk33.cyan(`
|
|
25024
26202
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
25025
26203
|
`));
|
|
25026
26204
|
try {
|
|
25027
26205
|
fn();
|
|
25028
26206
|
} catch (err2) {
|
|
25029
|
-
console.error(
|
|
26207
|
+
console.error(chalk33.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
25030
26208
|
process.exit(1);
|
|
25031
26209
|
}
|
|
25032
|
-
console.log(
|
|
26210
|
+
console.log(chalk33.gray("\n Restart the agent for changes to take effect."));
|
|
25033
26211
|
});
|
|
25034
26212
|
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
25035
|
-
console.log(
|
|
25036
|
-
console.log(
|
|
26213
|
+
console.log(chalk33.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
26214
|
+
console.log(chalk33.bold("Stopping daemon..."));
|
|
25037
26215
|
try {
|
|
25038
26216
|
stopDaemon();
|
|
25039
|
-
console.log(
|
|
26217
|
+
console.log(chalk33.green(" \u2705 Daemon stopped"));
|
|
25040
26218
|
} catch {
|
|
25041
|
-
console.log(
|
|
26219
|
+
console.log(chalk33.blue(" \u2139\uFE0F Daemon was not running"));
|
|
25042
26220
|
}
|
|
25043
|
-
console.log(
|
|
26221
|
+
console.log(chalk33.bold("\nRemoving hooks..."));
|
|
25044
26222
|
let teardownFailed = false;
|
|
25045
|
-
for (const [
|
|
26223
|
+
for (const [label2, fn] of [
|
|
25046
26224
|
["Claude", teardownClaude],
|
|
25047
26225
|
["Gemini", teardownGemini],
|
|
25048
26226
|
["Cursor", teardownCursor],
|
|
@@ -25056,45 +26234,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
25056
26234
|
} catch (err2) {
|
|
25057
26235
|
teardownFailed = true;
|
|
25058
26236
|
console.error(
|
|
25059
|
-
|
|
25060
|
-
` \u26A0\uFE0F Failed to remove ${
|
|
26237
|
+
chalk33.red(
|
|
26238
|
+
` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
25061
26239
|
)
|
|
25062
26240
|
);
|
|
25063
26241
|
}
|
|
25064
26242
|
}
|
|
25065
26243
|
if (options.purge) {
|
|
25066
|
-
const node9Dir =
|
|
25067
|
-
if (
|
|
26244
|
+
const node9Dir = path57.join(os53.homedir(), ".node9");
|
|
26245
|
+
if (fs58.existsSync(node9Dir)) {
|
|
25068
26246
|
const confirmed = await confirm2({
|
|
25069
26247
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
25070
26248
|
default: false
|
|
25071
26249
|
});
|
|
25072
26250
|
if (confirmed) {
|
|
25073
|
-
|
|
25074
|
-
if (
|
|
26251
|
+
fs58.rmSync(node9Dir, { recursive: true });
|
|
26252
|
+
if (fs58.existsSync(node9Dir)) {
|
|
25075
26253
|
console.error(
|
|
25076
|
-
|
|
26254
|
+
chalk33.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
25077
26255
|
);
|
|
25078
26256
|
} else {
|
|
25079
|
-
console.log(
|
|
26257
|
+
console.log(chalk33.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
25080
26258
|
}
|
|
25081
26259
|
} else {
|
|
25082
|
-
console.log(
|
|
26260
|
+
console.log(chalk33.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
25083
26261
|
}
|
|
25084
26262
|
} else {
|
|
25085
|
-
console.log(
|
|
26263
|
+
console.log(chalk33.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
25086
26264
|
}
|
|
25087
26265
|
} else {
|
|
25088
26266
|
console.log(
|
|
25089
|
-
|
|
26267
|
+
chalk33.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
25090
26268
|
);
|
|
25091
26269
|
}
|
|
25092
26270
|
if (teardownFailed) {
|
|
25093
|
-
console.error(
|
|
26271
|
+
console.error(chalk33.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
25094
26272
|
process.exit(1);
|
|
25095
26273
|
}
|
|
25096
|
-
console.log(
|
|
25097
|
-
console.log(
|
|
26274
|
+
console.log(chalk33.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
|
|
26275
|
+
console.log(chalk33.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
25098
26276
|
});
|
|
25099
26277
|
registerDoctorCommand(program, version);
|
|
25100
26278
|
program.command("explain").description(
|
|
@@ -25107,7 +26285,7 @@ program.command("explain").description(
|
|
|
25107
26285
|
try {
|
|
25108
26286
|
args = JSON.parse(trimmed);
|
|
25109
26287
|
} catch {
|
|
25110
|
-
console.error(
|
|
26288
|
+
console.error(chalk33.red(`
|
|
25111
26289
|
\u274C Invalid JSON: ${trimmed}
|
|
25112
26290
|
`));
|
|
25113
26291
|
process.exit(1);
|
|
@@ -25118,54 +26296,54 @@ program.command("explain").description(
|
|
|
25118
26296
|
}
|
|
25119
26297
|
const result = await explainPolicy(tool, args);
|
|
25120
26298
|
console.log("");
|
|
25121
|
-
console.log(
|
|
26299
|
+
console.log(chalk33.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
25122
26300
|
console.log("");
|
|
25123
|
-
console.log(` ${
|
|
26301
|
+
console.log(` ${chalk33.bold("Tool:")} ${chalk33.white(result.tool)}`);
|
|
25124
26302
|
if (argsRaw) {
|
|
25125
26303
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
25126
|
-
console.log(` ${
|
|
26304
|
+
console.log(` ${chalk33.bold("Input:")} ${chalk33.gray(preview2)}`);
|
|
25127
26305
|
}
|
|
25128
26306
|
console.log("");
|
|
25129
|
-
console.log(
|
|
26307
|
+
console.log(chalk33.bold("Config Sources (Waterfall):"));
|
|
25130
26308
|
for (const tier of result.waterfall) {
|
|
25131
|
-
const num3 =
|
|
25132
|
-
const
|
|
26309
|
+
const num3 = chalk33.gray(` ${tier.tier}.`);
|
|
26310
|
+
const label2 = tier.label.padEnd(16);
|
|
25133
26311
|
let statusStr;
|
|
25134
26312
|
if (tier.tier === 1) {
|
|
25135
|
-
statusStr =
|
|
26313
|
+
statusStr = chalk33.gray(tier.note ?? "");
|
|
25136
26314
|
} else if (tier.status === "active") {
|
|
25137
|
-
const loc = tier.path ?
|
|
25138
|
-
const note = tier.note ?
|
|
25139
|
-
statusStr =
|
|
26315
|
+
const loc = tier.path ? chalk33.gray(tier.path) : "";
|
|
26316
|
+
const note = tier.note ? chalk33.gray(`(${tier.note})`) : "";
|
|
26317
|
+
statusStr = chalk33.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
25140
26318
|
} else {
|
|
25141
|
-
statusStr =
|
|
26319
|
+
statusStr = chalk33.gray("\u25CB " + (tier.note ?? "not found"));
|
|
25142
26320
|
}
|
|
25143
|
-
console.log(`${num3} ${
|
|
26321
|
+
console.log(`${num3} ${chalk33.white(label2)} ${statusStr}`);
|
|
25144
26322
|
}
|
|
25145
26323
|
console.log("");
|
|
25146
|
-
console.log(
|
|
26324
|
+
console.log(chalk33.bold("Policy Evaluation:"));
|
|
25147
26325
|
for (const step of result.steps) {
|
|
25148
26326
|
const isFinal = step.isFinal;
|
|
25149
26327
|
let icon;
|
|
25150
|
-
if (step.outcome === "allow") icon =
|
|
25151
|
-
else if (step.outcome === "review") icon =
|
|
25152
|
-
else if (step.outcome === "skip") icon =
|
|
25153
|
-
else icon =
|
|
26328
|
+
if (step.outcome === "allow") icon = chalk33.green(" \u2705");
|
|
26329
|
+
else if (step.outcome === "review") icon = chalk33.red(" \u{1F534}");
|
|
26330
|
+
else if (step.outcome === "skip") icon = chalk33.gray(" \u2500 ");
|
|
26331
|
+
else icon = chalk33.gray(" \u25CB ");
|
|
25154
26332
|
const name = step.name.padEnd(18);
|
|
25155
|
-
const nameStr = isFinal ?
|
|
25156
|
-
const detail = isFinal ?
|
|
25157
|
-
const arrow = isFinal ?
|
|
26333
|
+
const nameStr = isFinal ? chalk33.white.bold(name) : chalk33.white(name);
|
|
26334
|
+
const detail = isFinal ? chalk33.white(step.detail) : chalk33.gray(step.detail);
|
|
26335
|
+
const arrow = isFinal ? chalk33.yellow(" \u2190 STOP") : "";
|
|
25158
26336
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
25159
26337
|
}
|
|
25160
26338
|
console.log("");
|
|
25161
26339
|
if (result.decision === "allow") {
|
|
25162
|
-
console.log(
|
|
26340
|
+
console.log(chalk33.green.bold(" Decision: \u2705 ALLOW") + chalk33.gray(" \u2014 no approval needed"));
|
|
25163
26341
|
} else {
|
|
25164
26342
|
console.log(
|
|
25165
|
-
|
|
26343
|
+
chalk33.red.bold(" Decision: \u{1F534} REVIEW") + chalk33.gray(" \u2014 human approval required")
|
|
25166
26344
|
);
|
|
25167
26345
|
if (result.blockedByLabel) {
|
|
25168
|
-
console.log(
|
|
26346
|
+
console.log(chalk33.gray(` Reason: ${result.blockedByLabel}`));
|
|
25169
26347
|
}
|
|
25170
26348
|
}
|
|
25171
26349
|
console.log("");
|
|
@@ -25180,18 +26358,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
25180
26358
|
try {
|
|
25181
26359
|
await startTail2(options);
|
|
25182
26360
|
} catch (err2) {
|
|
25183
|
-
console.error(
|
|
26361
|
+
console.error(chalk33.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
25184
26362
|
process.exit(1);
|
|
25185
26363
|
}
|
|
25186
26364
|
});
|
|
25187
26365
|
program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
|
|
25188
26366
|
try {
|
|
25189
|
-
const dashboardPath =
|
|
26367
|
+
const dashboardPath = path57.join(__dirname, "dashboard.mjs");
|
|
25190
26368
|
const dynamicImport = new Function("id", "return import(id)");
|
|
25191
26369
|
const mod = await dynamicImport(`file://${dashboardPath}`);
|
|
25192
26370
|
await mod.startMonitor();
|
|
25193
26371
|
} catch (err2) {
|
|
25194
|
-
console.error(
|
|
26372
|
+
console.error(chalk33.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
25195
26373
|
process.exit(1);
|
|
25196
26374
|
}
|
|
25197
26375
|
});
|
|
@@ -25224,14 +26402,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
25224
26402
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
25225
26403
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
25226
26404
|
if (subcommand === "debug") {
|
|
25227
|
-
const flagFile =
|
|
26405
|
+
const flagFile = path57.join(os53.homedir(), ".node9", "hud-debug");
|
|
25228
26406
|
if (state === "on") {
|
|
25229
|
-
|
|
25230
|
-
|
|
26407
|
+
fs58.mkdirSync(path57.dirname(flagFile), { recursive: true });
|
|
26408
|
+
fs58.writeFileSync(flagFile, "");
|
|
25231
26409
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
25232
26410
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
25233
26411
|
} else if (state === "off") {
|
|
25234
|
-
if (
|
|
26412
|
+
if (fs58.existsSync(flagFile)) fs58.unlinkSync(flagFile);
|
|
25235
26413
|
console.log("HUD debug logging disabled.");
|
|
25236
26414
|
} else {
|
|
25237
26415
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -25246,7 +26424,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
25246
26424
|
const ms = parseDuration(options.duration);
|
|
25247
26425
|
if (ms === null) {
|
|
25248
26426
|
console.error(
|
|
25249
|
-
|
|
26427
|
+
chalk33.red(`
|
|
25250
26428
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
25251
26429
|
`)
|
|
25252
26430
|
);
|
|
@@ -25254,20 +26432,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
25254
26432
|
}
|
|
25255
26433
|
pauseNode9(ms, options.duration);
|
|
25256
26434
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
25257
|
-
console.log(
|
|
26435
|
+
console.log(chalk33.yellow(`
|
|
25258
26436
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
25259
|
-
console.log(
|
|
25260
|
-
console.log(
|
|
26437
|
+
console.log(chalk33.gray(` All tool calls will be allowed without review.`));
|
|
26438
|
+
console.log(chalk33.gray(` Run "node9 resume" to re-enable early.
|
|
25261
26439
|
`));
|
|
25262
26440
|
});
|
|
25263
26441
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
25264
26442
|
const { paused } = checkPause();
|
|
25265
26443
|
if (!paused) {
|
|
25266
|
-
console.log(
|
|
26444
|
+
console.log(chalk33.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
25267
26445
|
return;
|
|
25268
26446
|
}
|
|
25269
26447
|
resumeNode9();
|
|
25270
|
-
console.log(
|
|
26448
|
+
console.log(chalk33.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
25271
26449
|
});
|
|
25272
26450
|
var HOOK_BASED_AGENTS = {
|
|
25273
26451
|
claude: "claude",
|
|
@@ -25283,15 +26461,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
25283
26461
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
25284
26462
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
25285
26463
|
console.error(
|
|
25286
|
-
|
|
26464
|
+
chalk33.yellow(`
|
|
25287
26465
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
25288
26466
|
);
|
|
25289
|
-
console.error(
|
|
26467
|
+
console.error(chalk33.white(`
|
|
25290
26468
|
"${target}" uses its own hook system. Use:`));
|
|
25291
26469
|
console.error(
|
|
25292
|
-
|
|
26470
|
+
chalk33.green(` node9 addto ${target} `) + chalk33.gray("# one-time setup")
|
|
25293
26471
|
);
|
|
25294
|
-
console.error(
|
|
26472
|
+
console.error(chalk33.green(` ${target} `) + chalk33.gray("# run normally"));
|
|
25295
26473
|
process.exit(1);
|
|
25296
26474
|
}
|
|
25297
26475
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -25308,7 +26486,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
25308
26486
|
}
|
|
25309
26487
|
);
|
|
25310
26488
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
25311
|
-
console.error(
|
|
26489
|
+
console.error(chalk33.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
25312
26490
|
const daemonReady = await autoStartDaemonAndWait();
|
|
25313
26491
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
25314
26492
|
}
|
|
@@ -25321,12 +26499,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
25321
26499
|
}
|
|
25322
26500
|
if (!result.approved) {
|
|
25323
26501
|
console.error(
|
|
25324
|
-
|
|
26502
|
+
chalk33.red(`
|
|
25325
26503
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
25326
26504
|
);
|
|
25327
26505
|
process.exit(1);
|
|
25328
26506
|
}
|
|
25329
|
-
console.error(
|
|
26507
|
+
console.error(chalk33.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
25330
26508
|
await runProxy(fullCommand);
|
|
25331
26509
|
} else {
|
|
25332
26510
|
program.help();
|
|
@@ -25339,6 +26517,8 @@ registerTrustCommand(program);
|
|
|
25339
26517
|
registerSyncCommand(program);
|
|
25340
26518
|
registerAgentsCommand(program);
|
|
25341
26519
|
registerScanCommand(program);
|
|
26520
|
+
registerPostureCommand(program);
|
|
26521
|
+
registerEgressCommand(program);
|
|
25342
26522
|
registerSessionsCommand(program);
|
|
25343
26523
|
registerDlpCommand(program);
|
|
25344
26524
|
registerMaskCommand(program);
|
|
@@ -25348,9 +26528,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
25348
26528
|
const isCheckHook = process.argv[2] === "check";
|
|
25349
26529
|
if (isCheckHook) {
|
|
25350
26530
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
25351
|
-
const logPath =
|
|
26531
|
+
const logPath = path57.join(os53.homedir(), ".node9", "hook-debug.log");
|
|
25352
26532
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
25353
|
-
|
|
26533
|
+
fs58.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
25354
26534
|
`);
|
|
25355
26535
|
}
|
|
25356
26536
|
process.exit(0);
|