@node9/proxy 1.6.0 → 1.7.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/README.md +0 -4
- package/dist/cli.js +291 -31
- package/dist/cli.mjs +291 -31
- package/dist/index.js +2 -1
- package/dist/index.mjs +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,10 +32,6 @@ While others try to _guess_ if a prompt is malicious (Semantic Security), Node9
|
|
|
32
32
|
|
|
33
33
|
**AIs are literal.** When you ask an agent to "Fix my disk space," it might decide to run `docker system prune -af`.
|
|
34
34
|
|
|
35
|
-
<p align="center">
|
|
36
|
-
<img src="https://github.com/user-attachments/assets/afae9caa-0605-4cac-929a-c14198383169" width="100%">
|
|
37
|
-
</p>
|
|
38
|
-
|
|
39
35
|
**With Node9, the interaction looks like this:**
|
|
40
36
|
|
|
41
37
|
1. **🤖 AI attempts a "Nuke":** `Bash("docker system prune -af --volumes")`
|
package/dist/cli.js
CHANGED
|
@@ -799,7 +799,7 @@ var init_config = __esm({
|
|
|
799
799
|
DEFAULT_CONFIG = {
|
|
800
800
|
version: "1.0",
|
|
801
801
|
settings: {
|
|
802
|
-
mode: "
|
|
802
|
+
mode: "standard",
|
|
803
803
|
autoStartDaemon: true,
|
|
804
804
|
enableUndo: true,
|
|
805
805
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
@@ -3198,6 +3198,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3198
3198
|
await notifyActivity({
|
|
3199
3199
|
id: actId,
|
|
3200
3200
|
tool: toolName,
|
|
3201
|
+
args,
|
|
3201
3202
|
ts: actTs,
|
|
3202
3203
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
3203
3204
|
label: result.blockedByLabel,
|
|
@@ -5476,6 +5477,7 @@ var init_session_counters = __esm({
|
|
|
5476
5477
|
_blocked = 0;
|
|
5477
5478
|
_dlpHits = 0;
|
|
5478
5479
|
_wouldBlock = 0;
|
|
5480
|
+
_estimatedCost = 0;
|
|
5479
5481
|
_lastRuleHit = null;
|
|
5480
5482
|
_lastBlockedTool = null;
|
|
5481
5483
|
incrementAllowed() {
|
|
@@ -5490,6 +5492,10 @@ var init_session_counters = __esm({
|
|
|
5490
5492
|
incrementWouldBlock() {
|
|
5491
5493
|
this._wouldBlock++;
|
|
5492
5494
|
}
|
|
5495
|
+
addCost(amount) {
|
|
5496
|
+
if (!isFinite(amount) || amount < 0) return;
|
|
5497
|
+
this._estimatedCost += amount;
|
|
5498
|
+
}
|
|
5493
5499
|
recordRuleHit(label) {
|
|
5494
5500
|
this._lastRuleHit = label;
|
|
5495
5501
|
}
|
|
@@ -5502,6 +5508,7 @@ var init_session_counters = __esm({
|
|
|
5502
5508
|
blocked: this._blocked,
|
|
5503
5509
|
dlpHits: this._dlpHits,
|
|
5504
5510
|
wouldBlock: this._wouldBlock,
|
|
5511
|
+
estimatedCost: this._estimatedCost,
|
|
5505
5512
|
lastRuleHit: this._lastRuleHit,
|
|
5506
5513
|
lastBlockedTool: this._lastBlockedTool
|
|
5507
5514
|
};
|
|
@@ -5511,6 +5518,7 @@ var init_session_counters = __esm({
|
|
|
5511
5518
|
this._blocked = 0;
|
|
5512
5519
|
this._dlpHits = 0;
|
|
5513
5520
|
this._wouldBlock = 0;
|
|
5521
|
+
this._estimatedCost = 0;
|
|
5514
5522
|
this._lastRuleHit = null;
|
|
5515
5523
|
this._lastBlockedTool = null;
|
|
5516
5524
|
}
|
|
@@ -5735,15 +5743,38 @@ function openBrowser(url) {
|
|
|
5735
5743
|
} catch {
|
|
5736
5744
|
}
|
|
5737
5745
|
}
|
|
5746
|
+
function estimateToolCost(tool, args) {
|
|
5747
|
+
const a = args ?? {};
|
|
5748
|
+
const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
5749
|
+
if (t.includes("read") || t === "glob" || t === "grep") {
|
|
5750
|
+
const filePath = a.file_path ?? a.path;
|
|
5751
|
+
if (filePath) {
|
|
5752
|
+
try {
|
|
5753
|
+
const bytes = import_fs13.default.statSync(filePath).size;
|
|
5754
|
+
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5755
|
+
} catch {
|
|
5756
|
+
}
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
if (t.includes("write")) {
|
|
5760
|
+
const content = a.content ?? "";
|
|
5761
|
+
return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5762
|
+
}
|
|
5763
|
+
if (t.includes("edit") || t === "str_replace_based_edit_tool") {
|
|
5764
|
+
const newStr = a.new_string ?? "";
|
|
5765
|
+
return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5766
|
+
}
|
|
5767
|
+
return void 0;
|
|
5768
|
+
}
|
|
5738
5769
|
function broadcast(event, data) {
|
|
5739
5770
|
if (event === "activity") {
|
|
5740
5771
|
activityRing.push({ event, data });
|
|
5741
5772
|
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
5742
5773
|
} else if (event === "activity-result") {
|
|
5743
|
-
const { id, status, label } = data;
|
|
5774
|
+
const { id, status, label, costEstimate } = data;
|
|
5744
5775
|
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
5745
5776
|
if (activityRing[i].data.id === id) {
|
|
5746
|
-
Object.assign(activityRing[i].data, { status, label });
|
|
5777
|
+
Object.assign(activityRing[i].data, { status, label, costEstimate });
|
|
5747
5778
|
break;
|
|
5748
5779
|
}
|
|
5749
5780
|
}
|
|
@@ -5845,10 +5876,13 @@ function startActivitySocket() {
|
|
|
5845
5876
|
sessionCounters.incrementBlocked();
|
|
5846
5877
|
sessionCounters.recordBlockedTool(data.tool);
|
|
5847
5878
|
}
|
|
5879
|
+
const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
|
|
5880
|
+
if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
|
|
5848
5881
|
broadcast("activity-result", {
|
|
5849
5882
|
id: data.id,
|
|
5850
5883
|
status: data.status,
|
|
5851
|
-
label: data.label
|
|
5884
|
+
label: data.label,
|
|
5885
|
+
costEstimate
|
|
5852
5886
|
});
|
|
5853
5887
|
}
|
|
5854
5888
|
} catch {
|
|
@@ -5865,7 +5899,7 @@ function startActivitySocket() {
|
|
|
5865
5899
|
}
|
|
5866
5900
|
});
|
|
5867
5901
|
}
|
|
5868
|
-
var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
|
|
5902
|
+
var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
|
|
5869
5903
|
var init_state2 = __esm({
|
|
5870
5904
|
"src/daemon/state.ts"() {
|
|
5871
5905
|
"use strict";
|
|
@@ -5909,6 +5943,9 @@ var init_state2 = __esm({
|
|
|
5909
5943
|
ACTIVITY_RING_SIZE = 100;
|
|
5910
5944
|
activityRing = [];
|
|
5911
5945
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
5946
|
+
INPUT_PRICE_PER_1M = 3;
|
|
5947
|
+
OUTPUT_PRICE_PER_1M = 15;
|
|
5948
|
+
BYTES_PER_TOKEN = 4;
|
|
5912
5949
|
WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5913
5950
|
"write",
|
|
5914
5951
|
"write_file",
|
|
@@ -6366,7 +6403,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6366
6403
|
allowed: counters.allowed,
|
|
6367
6404
|
blocked: counters.blocked,
|
|
6368
6405
|
dlpHits: counters.dlpHits,
|
|
6369
|
-
wouldBlock: counters.wouldBlock
|
|
6406
|
+
wouldBlock: counters.wouldBlock,
|
|
6407
|
+
estimatedCost: counters.estimatedCost
|
|
6370
6408
|
},
|
|
6371
6409
|
taintedCount: taintStore.list().length,
|
|
6372
6410
|
lastRuleHit: counters.lastRuleHit,
|
|
@@ -6498,6 +6536,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6498
6536
|
}
|
|
6499
6537
|
if (req.method === "POST" && pathname === "/events/clear") {
|
|
6500
6538
|
activityRing.length = 0;
|
|
6539
|
+
sessionCounters.reset();
|
|
6501
6540
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6502
6541
|
return res.end(JSON.stringify({ ok: true }));
|
|
6503
6542
|
}
|
|
@@ -6811,7 +6850,7 @@ function formatBase(activity) {
|
|
|
6811
6850
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6812
6851
|
const icon = getIcon(activity.tool);
|
|
6813
6852
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6814
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
6853
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os21.default.homedir(), "~");
|
|
6815
6854
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6816
6855
|
return `${import_chalk17.default.gray(time)} ${icon} ${import_chalk17.default.white.bold(toolName)} ${import_chalk17.default.dim(argsPreview)}`;
|
|
6817
6856
|
}
|
|
@@ -6825,11 +6864,13 @@ function renderResult(activity, result) {
|
|
|
6825
6864
|
} else {
|
|
6826
6865
|
status = import_chalk17.default.red("\u2717 BLOCK");
|
|
6827
6866
|
}
|
|
6867
|
+
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6868
|
+
const costSuffix = cost == null ? "" : import_chalk17.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6828
6869
|
if (process.stdout.isTTY) {
|
|
6829
6870
|
import_readline5.default.clearLine(process.stdout, 0);
|
|
6830
6871
|
import_readline5.default.cursorTo(process.stdout, 0);
|
|
6831
6872
|
}
|
|
6832
|
-
console.log(`${base} ${status}`);
|
|
6873
|
+
console.log(`${base} ${status}${costSuffix}`);
|
|
6833
6874
|
}
|
|
6834
6875
|
function renderPending(activity) {
|
|
6835
6876
|
if (!process.stdout.isTTY) return;
|
|
@@ -6958,6 +6999,39 @@ function buildRecoveryCardLines(req) {
|
|
|
6958
6999
|
``
|
|
6959
7000
|
];
|
|
6960
7001
|
}
|
|
7002
|
+
function readApproversFromDisk() {
|
|
7003
|
+
const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
|
|
7004
|
+
try {
|
|
7005
|
+
const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
|
|
7006
|
+
const settings = raw.settings ?? {};
|
|
7007
|
+
return settings.approvers ?? {};
|
|
7008
|
+
} catch {
|
|
7009
|
+
return {};
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
function approverStatusLine() {
|
|
7013
|
+
const a = readApproversFromDisk();
|
|
7014
|
+
const fmt = (label, key) => {
|
|
7015
|
+
const on = a[key] !== false;
|
|
7016
|
+
return `[${key[0]}]${label.slice(1)} ${on ? import_chalk17.default.green("\u2713") : import_chalk17.default.dim("\u2717")}`;
|
|
7017
|
+
};
|
|
7018
|
+
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7019
|
+
}
|
|
7020
|
+
function toggleApprover(channel) {
|
|
7021
|
+
const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
|
|
7022
|
+
try {
|
|
7023
|
+
const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
|
|
7024
|
+
const settings = raw.settings ?? {};
|
|
7025
|
+
const approvers = settings.approvers ?? {};
|
|
7026
|
+
approvers[channel] = approvers[channel] === false;
|
|
7027
|
+
settings.approvers = approvers;
|
|
7028
|
+
raw.settings = settings;
|
|
7029
|
+
import_fs25.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7030
|
+
} catch (err2) {
|
|
7031
|
+
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7032
|
+
`);
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
6961
7035
|
async function startTail(options = {}) {
|
|
6962
7036
|
const port = await ensureDaemon();
|
|
6963
7037
|
if (options.clear) {
|
|
@@ -7004,6 +7078,44 @@ async function startTail(options = {}) {
|
|
|
7004
7078
|
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
7005
7079
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
7006
7080
|
if (canApprove) import_readline5.default.emitKeypressEvents(process.stdin);
|
|
7081
|
+
let idleKeypressHandler = null;
|
|
7082
|
+
function enterIdleMode() {
|
|
7083
|
+
if (!canApprove || idleKeypressHandler !== null) return;
|
|
7084
|
+
try {
|
|
7085
|
+
process.stdin.setRawMode(true);
|
|
7086
|
+
} catch {
|
|
7087
|
+
return;
|
|
7088
|
+
}
|
|
7089
|
+
process.stdin.resume();
|
|
7090
|
+
idleKeypressHandler = (_str, key) => {
|
|
7091
|
+
const name = key?.name ?? "";
|
|
7092
|
+
if (key?.ctrl && name === "c") {
|
|
7093
|
+
process.kill(process.pid, "SIGINT");
|
|
7094
|
+
return;
|
|
7095
|
+
}
|
|
7096
|
+
if (name === "q") {
|
|
7097
|
+
process.kill(process.pid, "SIGINT");
|
|
7098
|
+
return;
|
|
7099
|
+
}
|
|
7100
|
+
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7101
|
+
if (channel) {
|
|
7102
|
+
toggleApprover(channel);
|
|
7103
|
+
console.log(import_chalk17.default.dim(` Approvers: ${approverStatusLine()}`));
|
|
7104
|
+
}
|
|
7105
|
+
};
|
|
7106
|
+
process.stdin.on("keypress", idleKeypressHandler);
|
|
7107
|
+
}
|
|
7108
|
+
function exitIdleMode() {
|
|
7109
|
+
if (idleKeypressHandler) {
|
|
7110
|
+
process.stdin.removeListener("keypress", idleKeypressHandler);
|
|
7111
|
+
idleKeypressHandler = null;
|
|
7112
|
+
}
|
|
7113
|
+
try {
|
|
7114
|
+
process.stdin.setRawMode(false);
|
|
7115
|
+
} catch {
|
|
7116
|
+
}
|
|
7117
|
+
process.stdin.pause();
|
|
7118
|
+
}
|
|
7007
7119
|
function clearCard() {
|
|
7008
7120
|
if (cardLineCount > 0) {
|
|
7009
7121
|
import_readline5.default.moveCursor(process.stdout, 0, -cardLineCount);
|
|
@@ -7022,10 +7134,12 @@ async function startTail(options = {}) {
|
|
|
7022
7134
|
}
|
|
7023
7135
|
function showNextCard() {
|
|
7024
7136
|
if (cardActive || approvalQueue.length === 0 || !canApprove) return;
|
|
7137
|
+
exitIdleMode();
|
|
7025
7138
|
try {
|
|
7026
7139
|
process.stdin.setRawMode(true);
|
|
7027
7140
|
} catch {
|
|
7028
7141
|
cardActive = false;
|
|
7142
|
+
enterIdleMode();
|
|
7029
7143
|
return;
|
|
7030
7144
|
}
|
|
7031
7145
|
cardActive = true;
|
|
@@ -7037,12 +7151,8 @@ async function startTail(options = {}) {
|
|
|
7037
7151
|
const handler = onKeypress;
|
|
7038
7152
|
onKeypress = null;
|
|
7039
7153
|
if (handler) process.stdin.removeListener("keypress", handler);
|
|
7040
|
-
try {
|
|
7041
|
-
process.stdin.setRawMode(false);
|
|
7042
|
-
} catch {
|
|
7043
|
-
}
|
|
7044
|
-
process.stdin.pause();
|
|
7045
7154
|
cancelActiveCard = null;
|
|
7155
|
+
enterIdleMode();
|
|
7046
7156
|
};
|
|
7047
7157
|
const settle = (action) => {
|
|
7048
7158
|
if (settled) return;
|
|
@@ -7169,18 +7279,16 @@ async function startTail(options = {}) {
|
|
|
7169
7279
|
console.log(import_chalk17.default.cyan.bold(`
|
|
7170
7280
|
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk17.default.dim(`\u2192 ${dashboardUrl}`));
|
|
7171
7281
|
if (canApprove) {
|
|
7172
|
-
console.log(
|
|
7173
|
-
|
|
7174
|
-
);
|
|
7282
|
+
console.log(import_chalk17.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7283
|
+
console.log(import_chalk17.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7175
7284
|
}
|
|
7176
7285
|
if (options.history) {
|
|
7177
|
-
console.log(import_chalk17.default.dim("Showing history + live events
|
|
7286
|
+
console.log(import_chalk17.default.dim("Showing history + live events.\n"));
|
|
7178
7287
|
} else {
|
|
7179
|
-
console.log(
|
|
7180
|
-
import_chalk17.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
7181
|
-
);
|
|
7288
|
+
console.log(import_chalk17.default.dim("Showing live events only. Use --history to include past.\n"));
|
|
7182
7289
|
}
|
|
7183
7290
|
process.on("SIGINT", () => {
|
|
7291
|
+
exitIdleMode();
|
|
7184
7292
|
clearCard();
|
|
7185
7293
|
process.stdout.write(SHOW_CURSOR);
|
|
7186
7294
|
if (process.stdout.isTTY) {
|
|
@@ -7196,6 +7304,7 @@ async function startTail(options = {}) {
|
|
|
7196
7304
|
console.error(import_chalk17.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7197
7305
|
process.exit(1);
|
|
7198
7306
|
}
|
|
7307
|
+
if (canApprove) enterIdleMode();
|
|
7199
7308
|
let currentEvent = "";
|
|
7200
7309
|
let currentData = "";
|
|
7201
7310
|
res.on("error", () => {
|
|
@@ -7589,6 +7698,11 @@ function renderSecurityLine(status) {
|
|
|
7589
7698
|
parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
|
|
7590
7699
|
}
|
|
7591
7700
|
}
|
|
7701
|
+
if (status.session.estimatedCost > 0) {
|
|
7702
|
+
const cost = status.session.estimatedCost;
|
|
7703
|
+
const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
|
|
7704
|
+
parts.push(color(DIM, `~${costStr}`));
|
|
7705
|
+
}
|
|
7592
7706
|
if (status.taintedCount > 0) {
|
|
7593
7707
|
parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
|
|
7594
7708
|
}
|
|
@@ -8621,6 +8735,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8621
8735
|
if (filesRes.status === 0) {
|
|
8622
8736
|
capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8623
8737
|
}
|
|
8738
|
+
if (capturedFiles.length === 0) {
|
|
8739
|
+
return prevEntry.hash;
|
|
8740
|
+
}
|
|
8624
8741
|
const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
|
|
8625
8742
|
env: shadowEnv,
|
|
8626
8743
|
timeout: GIT_TIMEOUT
|
|
@@ -9817,25 +9934,79 @@ var import_chalk11 = __toESM(require("chalk"));
|
|
|
9817
9934
|
var import_fs23 = __toESM(require("fs"));
|
|
9818
9935
|
var import_path25 = __toESM(require("path"));
|
|
9819
9936
|
var import_os19 = __toESM(require("os"));
|
|
9937
|
+
var import_https = __toESM(require("https"));
|
|
9820
9938
|
init_core();
|
|
9939
|
+
function fireTelemetryPing(agents) {
|
|
9940
|
+
try {
|
|
9941
|
+
const body = JSON.stringify({
|
|
9942
|
+
event: "init_completed",
|
|
9943
|
+
agents_detected: agents,
|
|
9944
|
+
os: process.platform,
|
|
9945
|
+
node9_version: process.env.npm_package_version ?? "unknown"
|
|
9946
|
+
});
|
|
9947
|
+
const req = import_https.default.request(
|
|
9948
|
+
{
|
|
9949
|
+
hostname: "api.node9.ai",
|
|
9950
|
+
path: "/api/v1/telemetry",
|
|
9951
|
+
method: "POST",
|
|
9952
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
9953
|
+
timeout: 3e3
|
|
9954
|
+
},
|
|
9955
|
+
(res) => {
|
|
9956
|
+
res.resume();
|
|
9957
|
+
}
|
|
9958
|
+
);
|
|
9959
|
+
req.on("error", () => {
|
|
9960
|
+
});
|
|
9961
|
+
req.on("timeout", () => {
|
|
9962
|
+
req.destroy();
|
|
9963
|
+
});
|
|
9964
|
+
req.end(body);
|
|
9965
|
+
} catch {
|
|
9966
|
+
}
|
|
9967
|
+
}
|
|
9821
9968
|
function registerInitCommand(program2) {
|
|
9822
9969
|
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
9823
9970
|
console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
9971
|
+
let chosenMode = options.mode.toLowerCase();
|
|
9972
|
+
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
9973
|
+
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
9974
|
+
}
|
|
9975
|
+
{
|
|
9976
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
9977
|
+
const enableShields = await confirm3({
|
|
9978
|
+
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
9979
|
+
default: true
|
|
9980
|
+
});
|
|
9981
|
+
if (enableShields) chosenMode = "standard";
|
|
9982
|
+
console.log("");
|
|
9983
|
+
}
|
|
9824
9984
|
const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
9825
9985
|
if (import_fs23.default.existsSync(configPath) && !options.force) {
|
|
9826
|
-
|
|
9986
|
+
try {
|
|
9987
|
+
const existing = JSON.parse(import_fs23.default.readFileSync(configPath, "utf-8"));
|
|
9988
|
+
const settings = existing.settings ?? {};
|
|
9989
|
+
if (settings.mode !== chosenMode) {
|
|
9990
|
+
settings.mode = chosenMode;
|
|
9991
|
+
existing.settings = settings;
|
|
9992
|
+
import_fs23.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
9993
|
+
console.log(import_chalk11.default.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
9994
|
+
} else {
|
|
9995
|
+
console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
9996
|
+
}
|
|
9997
|
+
} catch {
|
|
9998
|
+
console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
9999
|
+
}
|
|
9827
10000
|
} else {
|
|
9828
|
-
const requestedMode = options.mode.toLowerCase();
|
|
9829
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
9830
10001
|
const configToSave = {
|
|
9831
10002
|
...DEFAULT_CONFIG,
|
|
9832
|
-
settings: { ...DEFAULT_CONFIG.settings, mode:
|
|
10003
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
9833
10004
|
};
|
|
9834
10005
|
const dir = import_path25.default.dirname(configPath);
|
|
9835
10006
|
if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
|
|
9836
|
-
import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
10007
|
+
import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
9837
10008
|
console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
|
|
9838
|
-
console.log(import_chalk11.default.gray(` Mode: ${
|
|
10009
|
+
console.log(import_chalk11.default.gray(` Mode: ${chosenMode}`));
|
|
9839
10010
|
}
|
|
9840
10011
|
if (options.skipSetup) return;
|
|
9841
10012
|
console.log("");
|
|
@@ -9862,14 +10033,20 @@ function registerInitCommand(program2) {
|
|
|
9862
10033
|
else if (agent === "cursor") await setupCursor();
|
|
9863
10034
|
console.log("");
|
|
9864
10035
|
}
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
10036
|
+
{
|
|
10037
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10038
|
+
const sendTelemetry = await confirm3({
|
|
10039
|
+
message: "Send anonymous usage stats to help improve node9? (no code, no args)",
|
|
10040
|
+
default: true
|
|
10041
|
+
});
|
|
10042
|
+
if (sendTelemetry) fireTelemetryPing(found);
|
|
9869
10043
|
console.log("");
|
|
9870
10044
|
}
|
|
9871
10045
|
console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
9872
|
-
console.log(
|
|
10046
|
+
console.log("");
|
|
10047
|
+
console.log(import_chalk11.default.white(" Start watching: ") + import_chalk11.default.cyan("node9 tail"));
|
|
10048
|
+
console.log(import_chalk11.default.white(" Browser view: ") + import_chalk11.default.cyan("node9 daemon --openui"));
|
|
10049
|
+
console.log(import_chalk11.default.white(" Cloud dashboard: ") + import_chalk11.default.cyan("node9.ai"));
|
|
9873
10050
|
});
|
|
9874
10051
|
}
|
|
9875
10052
|
|
|
@@ -10475,6 +10652,30 @@ var TOOLS = [
|
|
|
10475
10652
|
required: ["service"]
|
|
10476
10653
|
}
|
|
10477
10654
|
},
|
|
10655
|
+
{
|
|
10656
|
+
name: "node9_approver_list",
|
|
10657
|
+
description: "List all node9 approver channels and their current enabled/disabled state. Approvers are the channels through which node9 asks a human to approve risky tool calls. Channels: native (OS popup), browser (web UI), cloud (team policy server), terminal (stdin).",
|
|
10658
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10659
|
+
},
|
|
10660
|
+
{
|
|
10661
|
+
name: "node9_approver_set",
|
|
10662
|
+
description: "Enable or disable a specific node9 approver channel in the global config (~/.node9/config.json). Use this to turn individual channels on or off without touching other settings. Channels: native, browser, cloud, terminal. WARNING: disabling all approvers means node9 cannot prompt for human approval \u2014 use with care.",
|
|
10663
|
+
inputSchema: {
|
|
10664
|
+
type: "object",
|
|
10665
|
+
properties: {
|
|
10666
|
+
channel: {
|
|
10667
|
+
type: "string",
|
|
10668
|
+
enum: ["native", "browser", "cloud", "terminal"],
|
|
10669
|
+
description: "Approver channel to configure."
|
|
10670
|
+
},
|
|
10671
|
+
enabled: {
|
|
10672
|
+
type: "boolean",
|
|
10673
|
+
description: "true to enable the channel, false to disable it."
|
|
10674
|
+
}
|
|
10675
|
+
},
|
|
10676
|
+
required: ["channel", "enabled"]
|
|
10677
|
+
}
|
|
10678
|
+
},
|
|
10478
10679
|
{
|
|
10479
10680
|
name: "node9_undo_list",
|
|
10480
10681
|
description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert.",
|
|
@@ -10580,6 +10781,61 @@ function handleShieldEnable(args) {
|
|
|
10580
10781
|
const shield = getShield(name);
|
|
10581
10782
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10582
10783
|
}
|
|
10784
|
+
var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
10785
|
+
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
10786
|
+
function readGlobalConfigRaw() {
|
|
10787
|
+
try {
|
|
10788
|
+
if (import_fs24.default.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
10789
|
+
return JSON.parse(import_fs24.default.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
10790
|
+
}
|
|
10791
|
+
} catch {
|
|
10792
|
+
}
|
|
10793
|
+
return {};
|
|
10794
|
+
}
|
|
10795
|
+
function writeGlobalConfigRaw(data) {
|
|
10796
|
+
const dir = import_path27.default.dirname(GLOBAL_CONFIG_PATH2);
|
|
10797
|
+
if (!import_fs24.default.existsSync(dir)) import_fs24.default.mkdirSync(dir, { recursive: true });
|
|
10798
|
+
import_fs24.default.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
10799
|
+
}
|
|
10800
|
+
function handleApproverList() {
|
|
10801
|
+
const config = getConfig();
|
|
10802
|
+
const approvers = config.settings.approvers;
|
|
10803
|
+
const lines = ["Approver channels:\n"];
|
|
10804
|
+
for (const ch of APPROVER_CHANNELS) {
|
|
10805
|
+
const on = approvers[ch];
|
|
10806
|
+
lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
|
|
10807
|
+
}
|
|
10808
|
+
const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
|
|
10809
|
+
if (enabledCount === 0) {
|
|
10810
|
+
lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
|
|
10811
|
+
}
|
|
10812
|
+
return lines.join("\n");
|
|
10813
|
+
}
|
|
10814
|
+
function handleApproverSet(args) {
|
|
10815
|
+
const channel = args.channel;
|
|
10816
|
+
const enabled = args.enabled;
|
|
10817
|
+
if (!channel || !APPROVER_CHANNELS.includes(channel)) {
|
|
10818
|
+
throw new Error(
|
|
10819
|
+
`Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
|
|
10820
|
+
);
|
|
10821
|
+
}
|
|
10822
|
+
if (typeof enabled !== "boolean") {
|
|
10823
|
+
throw new Error("enabled must be a boolean (true or false).");
|
|
10824
|
+
}
|
|
10825
|
+
const raw = readGlobalConfigRaw();
|
|
10826
|
+
const settings = raw.settings ?? {};
|
|
10827
|
+
const approvers = settings.approvers ?? {};
|
|
10828
|
+
approvers[channel] = enabled;
|
|
10829
|
+
settings.approvers = approvers;
|
|
10830
|
+
raw.settings = settings;
|
|
10831
|
+
writeGlobalConfigRaw(raw);
|
|
10832
|
+
const currentApprovers = getConfig().settings.approvers;
|
|
10833
|
+
const anyEnabled = APPROVER_CHANNELS.some(
|
|
10834
|
+
(ch) => ch === channel ? enabled : currentApprovers[ch]
|
|
10835
|
+
);
|
|
10836
|
+
const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
|
|
10837
|
+
return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
|
|
10838
|
+
}
|
|
10583
10839
|
function handleUndoList() {
|
|
10584
10840
|
const history = getSnapshotHistory();
|
|
10585
10841
|
if (history.length === 0) {
|
|
@@ -10653,6 +10909,10 @@ function runMcpServer() {
|
|
|
10653
10909
|
text = handleShieldList();
|
|
10654
10910
|
} else if (toolName === "node9_shield_enable") {
|
|
10655
10911
|
text = handleShieldEnable(toolArgs);
|
|
10912
|
+
} else if (toolName === "node9_approver_list") {
|
|
10913
|
+
text = handleApproverList();
|
|
10914
|
+
} else if (toolName === "node9_approver_set") {
|
|
10915
|
+
text = handleApproverSet(toolArgs);
|
|
10656
10916
|
} else if (toolName === "node9_undo_list") {
|
|
10657
10917
|
text = handleUndoList();
|
|
10658
10918
|
} else if (toolName === "node9_undo_revert") {
|
package/dist/cli.mjs
CHANGED
|
@@ -777,7 +777,7 @@ var init_config = __esm({
|
|
|
777
777
|
DEFAULT_CONFIG = {
|
|
778
778
|
version: "1.0",
|
|
779
779
|
settings: {
|
|
780
|
-
mode: "
|
|
780
|
+
mode: "standard",
|
|
781
781
|
autoStartDaemon: true,
|
|
782
782
|
enableUndo: true,
|
|
783
783
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
@@ -3176,6 +3176,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3176
3176
|
await notifyActivity({
|
|
3177
3177
|
id: actId,
|
|
3178
3178
|
tool: toolName,
|
|
3179
|
+
args,
|
|
3179
3180
|
ts: actTs,
|
|
3180
3181
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
3181
3182
|
label: result.blockedByLabel,
|
|
@@ -5453,6 +5454,7 @@ var init_session_counters = __esm({
|
|
|
5453
5454
|
_blocked = 0;
|
|
5454
5455
|
_dlpHits = 0;
|
|
5455
5456
|
_wouldBlock = 0;
|
|
5457
|
+
_estimatedCost = 0;
|
|
5456
5458
|
_lastRuleHit = null;
|
|
5457
5459
|
_lastBlockedTool = null;
|
|
5458
5460
|
incrementAllowed() {
|
|
@@ -5467,6 +5469,10 @@ var init_session_counters = __esm({
|
|
|
5467
5469
|
incrementWouldBlock() {
|
|
5468
5470
|
this._wouldBlock++;
|
|
5469
5471
|
}
|
|
5472
|
+
addCost(amount) {
|
|
5473
|
+
if (!isFinite(amount) || amount < 0) return;
|
|
5474
|
+
this._estimatedCost += amount;
|
|
5475
|
+
}
|
|
5470
5476
|
recordRuleHit(label) {
|
|
5471
5477
|
this._lastRuleHit = label;
|
|
5472
5478
|
}
|
|
@@ -5479,6 +5485,7 @@ var init_session_counters = __esm({
|
|
|
5479
5485
|
blocked: this._blocked,
|
|
5480
5486
|
dlpHits: this._dlpHits,
|
|
5481
5487
|
wouldBlock: this._wouldBlock,
|
|
5488
|
+
estimatedCost: this._estimatedCost,
|
|
5482
5489
|
lastRuleHit: this._lastRuleHit,
|
|
5483
5490
|
lastBlockedTool: this._lastBlockedTool
|
|
5484
5491
|
};
|
|
@@ -5488,6 +5495,7 @@ var init_session_counters = __esm({
|
|
|
5488
5495
|
this._blocked = 0;
|
|
5489
5496
|
this._dlpHits = 0;
|
|
5490
5497
|
this._wouldBlock = 0;
|
|
5498
|
+
this._estimatedCost = 0;
|
|
5491
5499
|
this._lastRuleHit = null;
|
|
5492
5500
|
this._lastBlockedTool = null;
|
|
5493
5501
|
}
|
|
@@ -5718,15 +5726,38 @@ function openBrowser(url) {
|
|
|
5718
5726
|
} catch {
|
|
5719
5727
|
}
|
|
5720
5728
|
}
|
|
5729
|
+
function estimateToolCost(tool, args) {
|
|
5730
|
+
const a = args ?? {};
|
|
5731
|
+
const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
5732
|
+
if (t.includes("read") || t === "glob" || t === "grep") {
|
|
5733
|
+
const filePath = a.file_path ?? a.path;
|
|
5734
|
+
if (filePath) {
|
|
5735
|
+
try {
|
|
5736
|
+
const bytes = fs13.statSync(filePath).size;
|
|
5737
|
+
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5738
|
+
} catch {
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
if (t.includes("write")) {
|
|
5743
|
+
const content = a.content ?? "";
|
|
5744
|
+
return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5745
|
+
}
|
|
5746
|
+
if (t.includes("edit") || t === "str_replace_based_edit_tool") {
|
|
5747
|
+
const newStr = a.new_string ?? "";
|
|
5748
|
+
return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5749
|
+
}
|
|
5750
|
+
return void 0;
|
|
5751
|
+
}
|
|
5721
5752
|
function broadcast(event, data) {
|
|
5722
5753
|
if (event === "activity") {
|
|
5723
5754
|
activityRing.push({ event, data });
|
|
5724
5755
|
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
5725
5756
|
} else if (event === "activity-result") {
|
|
5726
|
-
const { id, status, label } = data;
|
|
5757
|
+
const { id, status, label, costEstimate } = data;
|
|
5727
5758
|
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
5728
5759
|
if (activityRing[i].data.id === id) {
|
|
5729
|
-
Object.assign(activityRing[i].data, { status, label });
|
|
5760
|
+
Object.assign(activityRing[i].data, { status, label, costEstimate });
|
|
5730
5761
|
break;
|
|
5731
5762
|
}
|
|
5732
5763
|
}
|
|
@@ -5828,10 +5859,13 @@ function startActivitySocket() {
|
|
|
5828
5859
|
sessionCounters.incrementBlocked();
|
|
5829
5860
|
sessionCounters.recordBlockedTool(data.tool);
|
|
5830
5861
|
}
|
|
5862
|
+
const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
|
|
5863
|
+
if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
|
|
5831
5864
|
broadcast("activity-result", {
|
|
5832
5865
|
id: data.id,
|
|
5833
5866
|
status: data.status,
|
|
5834
|
-
label: data.label
|
|
5867
|
+
label: data.label,
|
|
5868
|
+
costEstimate
|
|
5835
5869
|
});
|
|
5836
5870
|
}
|
|
5837
5871
|
} catch {
|
|
@@ -5848,7 +5882,7 @@ function startActivitySocket() {
|
|
|
5848
5882
|
}
|
|
5849
5883
|
});
|
|
5850
5884
|
}
|
|
5851
|
-
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
|
|
5885
|
+
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
|
|
5852
5886
|
var init_state2 = __esm({
|
|
5853
5887
|
"src/daemon/state.ts"() {
|
|
5854
5888
|
"use strict";
|
|
@@ -5886,6 +5920,9 @@ var init_state2 = __esm({
|
|
|
5886
5920
|
ACTIVITY_RING_SIZE = 100;
|
|
5887
5921
|
activityRing = [];
|
|
5888
5922
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
5923
|
+
INPUT_PRICE_PER_1M = 3;
|
|
5924
|
+
OUTPUT_PRICE_PER_1M = 15;
|
|
5925
|
+
BYTES_PER_TOKEN = 4;
|
|
5889
5926
|
WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5890
5927
|
"write",
|
|
5891
5928
|
"write_file",
|
|
@@ -6349,7 +6386,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6349
6386
|
allowed: counters.allowed,
|
|
6350
6387
|
blocked: counters.blocked,
|
|
6351
6388
|
dlpHits: counters.dlpHits,
|
|
6352
|
-
wouldBlock: counters.wouldBlock
|
|
6389
|
+
wouldBlock: counters.wouldBlock,
|
|
6390
|
+
estimatedCost: counters.estimatedCost
|
|
6353
6391
|
},
|
|
6354
6392
|
taintedCount: taintStore.list().length,
|
|
6355
6393
|
lastRuleHit: counters.lastRuleHit,
|
|
@@ -6481,6 +6519,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6481
6519
|
}
|
|
6482
6520
|
if (req.method === "POST" && pathname === "/events/clear") {
|
|
6483
6521
|
activityRing.length = 0;
|
|
6522
|
+
sessionCounters.reset();
|
|
6484
6523
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6485
6524
|
return res.end(JSON.stringify({ ok: true }));
|
|
6486
6525
|
}
|
|
@@ -6793,7 +6832,7 @@ function formatBase(activity) {
|
|
|
6793
6832
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6794
6833
|
const icon = getIcon(activity.tool);
|
|
6795
6834
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6796
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
6835
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os21.homedir(), "~");
|
|
6797
6836
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6798
6837
|
return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
|
|
6799
6838
|
}
|
|
@@ -6807,11 +6846,13 @@ function renderResult(activity, result) {
|
|
|
6807
6846
|
} else {
|
|
6808
6847
|
status = chalk17.red("\u2717 BLOCK");
|
|
6809
6848
|
}
|
|
6849
|
+
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6850
|
+
const costSuffix = cost == null ? "" : chalk17.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6810
6851
|
if (process.stdout.isTTY) {
|
|
6811
6852
|
readline5.clearLine(process.stdout, 0);
|
|
6812
6853
|
readline5.cursorTo(process.stdout, 0);
|
|
6813
6854
|
}
|
|
6814
|
-
console.log(`${base} ${status}`);
|
|
6855
|
+
console.log(`${base} ${status}${costSuffix}`);
|
|
6815
6856
|
}
|
|
6816
6857
|
function renderPending(activity) {
|
|
6817
6858
|
if (!process.stdout.isTTY) return;
|
|
@@ -6940,6 +6981,39 @@ function buildRecoveryCardLines(req) {
|
|
|
6940
6981
|
``
|
|
6941
6982
|
];
|
|
6942
6983
|
}
|
|
6984
|
+
function readApproversFromDisk() {
|
|
6985
|
+
const configPath = path28.join(os21.homedir(), ".node9", "config.json");
|
|
6986
|
+
try {
|
|
6987
|
+
const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
6988
|
+
const settings = raw.settings ?? {};
|
|
6989
|
+
return settings.approvers ?? {};
|
|
6990
|
+
} catch {
|
|
6991
|
+
return {};
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
function approverStatusLine() {
|
|
6995
|
+
const a = readApproversFromDisk();
|
|
6996
|
+
const fmt = (label, key) => {
|
|
6997
|
+
const on = a[key] !== false;
|
|
6998
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk17.green("\u2713") : chalk17.dim("\u2717")}`;
|
|
6999
|
+
};
|
|
7000
|
+
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7001
|
+
}
|
|
7002
|
+
function toggleApprover(channel) {
|
|
7003
|
+
const configPath = path28.join(os21.homedir(), ".node9", "config.json");
|
|
7004
|
+
try {
|
|
7005
|
+
const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
7006
|
+
const settings = raw.settings ?? {};
|
|
7007
|
+
const approvers = settings.approvers ?? {};
|
|
7008
|
+
approvers[channel] = approvers[channel] === false;
|
|
7009
|
+
settings.approvers = approvers;
|
|
7010
|
+
raw.settings = settings;
|
|
7011
|
+
fs25.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7012
|
+
} catch (err2) {
|
|
7013
|
+
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7014
|
+
`);
|
|
7015
|
+
}
|
|
7016
|
+
}
|
|
6943
7017
|
async function startTail(options = {}) {
|
|
6944
7018
|
const port = await ensureDaemon();
|
|
6945
7019
|
if (options.clear) {
|
|
@@ -6986,6 +7060,44 @@ async function startTail(options = {}) {
|
|
|
6986
7060
|
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
6987
7061
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
6988
7062
|
if (canApprove) readline5.emitKeypressEvents(process.stdin);
|
|
7063
|
+
let idleKeypressHandler = null;
|
|
7064
|
+
function enterIdleMode() {
|
|
7065
|
+
if (!canApprove || idleKeypressHandler !== null) return;
|
|
7066
|
+
try {
|
|
7067
|
+
process.stdin.setRawMode(true);
|
|
7068
|
+
} catch {
|
|
7069
|
+
return;
|
|
7070
|
+
}
|
|
7071
|
+
process.stdin.resume();
|
|
7072
|
+
idleKeypressHandler = (_str, key) => {
|
|
7073
|
+
const name = key?.name ?? "";
|
|
7074
|
+
if (key?.ctrl && name === "c") {
|
|
7075
|
+
process.kill(process.pid, "SIGINT");
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
if (name === "q") {
|
|
7079
|
+
process.kill(process.pid, "SIGINT");
|
|
7080
|
+
return;
|
|
7081
|
+
}
|
|
7082
|
+
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7083
|
+
if (channel) {
|
|
7084
|
+
toggleApprover(channel);
|
|
7085
|
+
console.log(chalk17.dim(` Approvers: ${approverStatusLine()}`));
|
|
7086
|
+
}
|
|
7087
|
+
};
|
|
7088
|
+
process.stdin.on("keypress", idleKeypressHandler);
|
|
7089
|
+
}
|
|
7090
|
+
function exitIdleMode() {
|
|
7091
|
+
if (idleKeypressHandler) {
|
|
7092
|
+
process.stdin.removeListener("keypress", idleKeypressHandler);
|
|
7093
|
+
idleKeypressHandler = null;
|
|
7094
|
+
}
|
|
7095
|
+
try {
|
|
7096
|
+
process.stdin.setRawMode(false);
|
|
7097
|
+
} catch {
|
|
7098
|
+
}
|
|
7099
|
+
process.stdin.pause();
|
|
7100
|
+
}
|
|
6989
7101
|
function clearCard() {
|
|
6990
7102
|
if (cardLineCount > 0) {
|
|
6991
7103
|
readline5.moveCursor(process.stdout, 0, -cardLineCount);
|
|
@@ -7004,10 +7116,12 @@ async function startTail(options = {}) {
|
|
|
7004
7116
|
}
|
|
7005
7117
|
function showNextCard() {
|
|
7006
7118
|
if (cardActive || approvalQueue.length === 0 || !canApprove) return;
|
|
7119
|
+
exitIdleMode();
|
|
7007
7120
|
try {
|
|
7008
7121
|
process.stdin.setRawMode(true);
|
|
7009
7122
|
} catch {
|
|
7010
7123
|
cardActive = false;
|
|
7124
|
+
enterIdleMode();
|
|
7011
7125
|
return;
|
|
7012
7126
|
}
|
|
7013
7127
|
cardActive = true;
|
|
@@ -7019,12 +7133,8 @@ async function startTail(options = {}) {
|
|
|
7019
7133
|
const handler = onKeypress;
|
|
7020
7134
|
onKeypress = null;
|
|
7021
7135
|
if (handler) process.stdin.removeListener("keypress", handler);
|
|
7022
|
-
try {
|
|
7023
|
-
process.stdin.setRawMode(false);
|
|
7024
|
-
} catch {
|
|
7025
|
-
}
|
|
7026
|
-
process.stdin.pause();
|
|
7027
7136
|
cancelActiveCard = null;
|
|
7137
|
+
enterIdleMode();
|
|
7028
7138
|
};
|
|
7029
7139
|
const settle = (action) => {
|
|
7030
7140
|
if (settled) return;
|
|
@@ -7151,18 +7261,16 @@ async function startTail(options = {}) {
|
|
|
7151
7261
|
console.log(chalk17.cyan.bold(`
|
|
7152
7262
|
\u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
|
|
7153
7263
|
if (canApprove) {
|
|
7154
|
-
console.log(
|
|
7155
|
-
|
|
7156
|
-
);
|
|
7264
|
+
console.log(chalk17.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7265
|
+
console.log(chalk17.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7157
7266
|
}
|
|
7158
7267
|
if (options.history) {
|
|
7159
|
-
console.log(chalk17.dim("Showing history + live events
|
|
7268
|
+
console.log(chalk17.dim("Showing history + live events.\n"));
|
|
7160
7269
|
} else {
|
|
7161
|
-
console.log(
|
|
7162
|
-
chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
7163
|
-
);
|
|
7270
|
+
console.log(chalk17.dim("Showing live events only. Use --history to include past.\n"));
|
|
7164
7271
|
}
|
|
7165
7272
|
process.on("SIGINT", () => {
|
|
7273
|
+
exitIdleMode();
|
|
7166
7274
|
clearCard();
|
|
7167
7275
|
process.stdout.write(SHOW_CURSOR);
|
|
7168
7276
|
if (process.stdout.isTTY) {
|
|
@@ -7178,6 +7286,7 @@ async function startTail(options = {}) {
|
|
|
7178
7286
|
console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7179
7287
|
process.exit(1);
|
|
7180
7288
|
}
|
|
7289
|
+
if (canApprove) enterIdleMode();
|
|
7181
7290
|
let currentEvent = "";
|
|
7182
7291
|
let currentData = "";
|
|
7183
7292
|
res.on("error", () => {
|
|
@@ -7568,6 +7677,11 @@ function renderSecurityLine(status) {
|
|
|
7568
7677
|
parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
|
|
7569
7678
|
}
|
|
7570
7679
|
}
|
|
7680
|
+
if (status.session.estimatedCost > 0) {
|
|
7681
|
+
const cost = status.session.estimatedCost;
|
|
7682
|
+
const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
|
|
7683
|
+
parts.push(color(DIM, `~${costStr}`));
|
|
7684
|
+
}
|
|
7571
7685
|
if (status.taintedCount > 0) {
|
|
7572
7686
|
parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
|
|
7573
7687
|
}
|
|
@@ -8596,6 +8710,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8596
8710
|
if (filesRes.status === 0) {
|
|
8597
8711
|
capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8598
8712
|
}
|
|
8713
|
+
if (capturedFiles.length === 0) {
|
|
8714
|
+
return prevEntry.hash;
|
|
8715
|
+
}
|
|
8599
8716
|
const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
|
|
8600
8717
|
env: shadowEnv,
|
|
8601
8718
|
timeout: GIT_TIMEOUT
|
|
@@ -9793,24 +9910,78 @@ import chalk11 from "chalk";
|
|
|
9793
9910
|
import fs23 from "fs";
|
|
9794
9911
|
import path25 from "path";
|
|
9795
9912
|
import os19 from "os";
|
|
9913
|
+
import https from "https";
|
|
9914
|
+
function fireTelemetryPing(agents) {
|
|
9915
|
+
try {
|
|
9916
|
+
const body = JSON.stringify({
|
|
9917
|
+
event: "init_completed",
|
|
9918
|
+
agents_detected: agents,
|
|
9919
|
+
os: process.platform,
|
|
9920
|
+
node9_version: process.env.npm_package_version ?? "unknown"
|
|
9921
|
+
});
|
|
9922
|
+
const req = https.request(
|
|
9923
|
+
{
|
|
9924
|
+
hostname: "api.node9.ai",
|
|
9925
|
+
path: "/api/v1/telemetry",
|
|
9926
|
+
method: "POST",
|
|
9927
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
9928
|
+
timeout: 3e3
|
|
9929
|
+
},
|
|
9930
|
+
(res) => {
|
|
9931
|
+
res.resume();
|
|
9932
|
+
}
|
|
9933
|
+
);
|
|
9934
|
+
req.on("error", () => {
|
|
9935
|
+
});
|
|
9936
|
+
req.on("timeout", () => {
|
|
9937
|
+
req.destroy();
|
|
9938
|
+
});
|
|
9939
|
+
req.end(body);
|
|
9940
|
+
} catch {
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9796
9943
|
function registerInitCommand(program2) {
|
|
9797
9944
|
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
9798
9945
|
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
9946
|
+
let chosenMode = options.mode.toLowerCase();
|
|
9947
|
+
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
9948
|
+
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
9949
|
+
}
|
|
9950
|
+
{
|
|
9951
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
9952
|
+
const enableShields = await confirm3({
|
|
9953
|
+
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
9954
|
+
default: true
|
|
9955
|
+
});
|
|
9956
|
+
if (enableShields) chosenMode = "standard";
|
|
9957
|
+
console.log("");
|
|
9958
|
+
}
|
|
9799
9959
|
const configPath = path25.join(os19.homedir(), ".node9", "config.json");
|
|
9800
9960
|
if (fs23.existsSync(configPath) && !options.force) {
|
|
9801
|
-
|
|
9961
|
+
try {
|
|
9962
|
+
const existing = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
|
|
9963
|
+
const settings = existing.settings ?? {};
|
|
9964
|
+
if (settings.mode !== chosenMode) {
|
|
9965
|
+
settings.mode = chosenMode;
|
|
9966
|
+
existing.settings = settings;
|
|
9967
|
+
fs23.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
9968
|
+
console.log(chalk11.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
9969
|
+
} else {
|
|
9970
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
9971
|
+
}
|
|
9972
|
+
} catch {
|
|
9973
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
9974
|
+
}
|
|
9802
9975
|
} else {
|
|
9803
|
-
const requestedMode = options.mode.toLowerCase();
|
|
9804
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
9805
9976
|
const configToSave = {
|
|
9806
9977
|
...DEFAULT_CONFIG,
|
|
9807
|
-
settings: { ...DEFAULT_CONFIG.settings, mode:
|
|
9978
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
9808
9979
|
};
|
|
9809
9980
|
const dir = path25.dirname(configPath);
|
|
9810
9981
|
if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
|
|
9811
|
-
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
9982
|
+
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
9812
9983
|
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
9813
|
-
console.log(chalk11.gray(` Mode: ${
|
|
9984
|
+
console.log(chalk11.gray(` Mode: ${chosenMode}`));
|
|
9814
9985
|
}
|
|
9815
9986
|
if (options.skipSetup) return;
|
|
9816
9987
|
console.log("");
|
|
@@ -9837,14 +10008,20 @@ function registerInitCommand(program2) {
|
|
|
9837
10008
|
else if (agent === "cursor") await setupCursor();
|
|
9838
10009
|
console.log("");
|
|
9839
10010
|
}
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
10011
|
+
{
|
|
10012
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10013
|
+
const sendTelemetry = await confirm3({
|
|
10014
|
+
message: "Send anonymous usage stats to help improve node9? (no code, no args)",
|
|
10015
|
+
default: true
|
|
10016
|
+
});
|
|
10017
|
+
if (sendTelemetry) fireTelemetryPing(found);
|
|
9844
10018
|
console.log("");
|
|
9845
10019
|
}
|
|
9846
10020
|
console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
9847
|
-
console.log(
|
|
10021
|
+
console.log("");
|
|
10022
|
+
console.log(chalk11.white(" Start watching: ") + chalk11.cyan("node9 tail"));
|
|
10023
|
+
console.log(chalk11.white(" Browser view: ") + chalk11.cyan("node9 daemon --openui"));
|
|
10024
|
+
console.log(chalk11.white(" Cloud dashboard: ") + chalk11.cyan("node9.ai"));
|
|
9848
10025
|
});
|
|
9849
10026
|
}
|
|
9850
10027
|
|
|
@@ -10450,6 +10627,30 @@ var TOOLS = [
|
|
|
10450
10627
|
required: ["service"]
|
|
10451
10628
|
}
|
|
10452
10629
|
},
|
|
10630
|
+
{
|
|
10631
|
+
name: "node9_approver_list",
|
|
10632
|
+
description: "List all node9 approver channels and their current enabled/disabled state. Approvers are the channels through which node9 asks a human to approve risky tool calls. Channels: native (OS popup), browser (web UI), cloud (team policy server), terminal (stdin).",
|
|
10633
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10634
|
+
},
|
|
10635
|
+
{
|
|
10636
|
+
name: "node9_approver_set",
|
|
10637
|
+
description: "Enable or disable a specific node9 approver channel in the global config (~/.node9/config.json). Use this to turn individual channels on or off without touching other settings. Channels: native, browser, cloud, terminal. WARNING: disabling all approvers means node9 cannot prompt for human approval \u2014 use with care.",
|
|
10638
|
+
inputSchema: {
|
|
10639
|
+
type: "object",
|
|
10640
|
+
properties: {
|
|
10641
|
+
channel: {
|
|
10642
|
+
type: "string",
|
|
10643
|
+
enum: ["native", "browser", "cloud", "terminal"],
|
|
10644
|
+
description: "Approver channel to configure."
|
|
10645
|
+
},
|
|
10646
|
+
enabled: {
|
|
10647
|
+
type: "boolean",
|
|
10648
|
+
description: "true to enable the channel, false to disable it."
|
|
10649
|
+
}
|
|
10650
|
+
},
|
|
10651
|
+
required: ["channel", "enabled"]
|
|
10652
|
+
}
|
|
10653
|
+
},
|
|
10453
10654
|
{
|
|
10454
10655
|
name: "node9_undo_list",
|
|
10455
10656
|
description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert.",
|
|
@@ -10555,6 +10756,61 @@ function handleShieldEnable(args) {
|
|
|
10555
10756
|
const shield = getShield(name);
|
|
10556
10757
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10557
10758
|
}
|
|
10759
|
+
var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
|
|
10760
|
+
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
10761
|
+
function readGlobalConfigRaw() {
|
|
10762
|
+
try {
|
|
10763
|
+
if (fs24.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
10764
|
+
return JSON.parse(fs24.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
10765
|
+
}
|
|
10766
|
+
} catch {
|
|
10767
|
+
}
|
|
10768
|
+
return {};
|
|
10769
|
+
}
|
|
10770
|
+
function writeGlobalConfigRaw(data) {
|
|
10771
|
+
const dir = path27.dirname(GLOBAL_CONFIG_PATH2);
|
|
10772
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
10773
|
+
fs24.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
10774
|
+
}
|
|
10775
|
+
function handleApproverList() {
|
|
10776
|
+
const config = getConfig();
|
|
10777
|
+
const approvers = config.settings.approvers;
|
|
10778
|
+
const lines = ["Approver channels:\n"];
|
|
10779
|
+
for (const ch of APPROVER_CHANNELS) {
|
|
10780
|
+
const on = approvers[ch];
|
|
10781
|
+
lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
|
|
10782
|
+
}
|
|
10783
|
+
const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
|
|
10784
|
+
if (enabledCount === 0) {
|
|
10785
|
+
lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
|
|
10786
|
+
}
|
|
10787
|
+
return lines.join("\n");
|
|
10788
|
+
}
|
|
10789
|
+
function handleApproverSet(args) {
|
|
10790
|
+
const channel = args.channel;
|
|
10791
|
+
const enabled = args.enabled;
|
|
10792
|
+
if (!channel || !APPROVER_CHANNELS.includes(channel)) {
|
|
10793
|
+
throw new Error(
|
|
10794
|
+
`Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
|
|
10795
|
+
);
|
|
10796
|
+
}
|
|
10797
|
+
if (typeof enabled !== "boolean") {
|
|
10798
|
+
throw new Error("enabled must be a boolean (true or false).");
|
|
10799
|
+
}
|
|
10800
|
+
const raw = readGlobalConfigRaw();
|
|
10801
|
+
const settings = raw.settings ?? {};
|
|
10802
|
+
const approvers = settings.approvers ?? {};
|
|
10803
|
+
approvers[channel] = enabled;
|
|
10804
|
+
settings.approvers = approvers;
|
|
10805
|
+
raw.settings = settings;
|
|
10806
|
+
writeGlobalConfigRaw(raw);
|
|
10807
|
+
const currentApprovers = getConfig().settings.approvers;
|
|
10808
|
+
const anyEnabled = APPROVER_CHANNELS.some(
|
|
10809
|
+
(ch) => ch === channel ? enabled : currentApprovers[ch]
|
|
10810
|
+
);
|
|
10811
|
+
const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
|
|
10812
|
+
return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
|
|
10813
|
+
}
|
|
10558
10814
|
function handleUndoList() {
|
|
10559
10815
|
const history = getSnapshotHistory();
|
|
10560
10816
|
if (history.length === 0) {
|
|
@@ -10628,6 +10884,10 @@ function runMcpServer() {
|
|
|
10628
10884
|
text = handleShieldList();
|
|
10629
10885
|
} else if (toolName === "node9_shield_enable") {
|
|
10630
10886
|
text = handleShieldEnable(toolArgs);
|
|
10887
|
+
} else if (toolName === "node9_approver_list") {
|
|
10888
|
+
text = handleApproverList();
|
|
10889
|
+
} else if (toolName === "node9_approver_set") {
|
|
10890
|
+
text = handleApproverSet(toolArgs);
|
|
10631
10891
|
} else if (toolName === "node9_undo_list") {
|
|
10632
10892
|
text = handleUndoList();
|
|
10633
10893
|
} else if (toolName === "node9_undo_revert") {
|
package/dist/index.js
CHANGED
|
@@ -517,7 +517,7 @@ var DANGEROUS_WORDS = [
|
|
|
517
517
|
var DEFAULT_CONFIG = {
|
|
518
518
|
version: "1.0",
|
|
519
519
|
settings: {
|
|
520
|
-
mode: "
|
|
520
|
+
mode: "standard",
|
|
521
521
|
autoStartDaemon: true,
|
|
522
522
|
enableUndo: true,
|
|
523
523
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
@@ -2721,6 +2721,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2721
2721
|
await notifyActivity({
|
|
2722
2722
|
id: actId,
|
|
2723
2723
|
tool: toolName,
|
|
2724
|
+
args,
|
|
2724
2725
|
ts: actTs,
|
|
2725
2726
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2726
2727
|
label: result.blockedByLabel,
|
package/dist/index.mjs
CHANGED
|
@@ -487,7 +487,7 @@ var DANGEROUS_WORDS = [
|
|
|
487
487
|
var DEFAULT_CONFIG = {
|
|
488
488
|
version: "1.0",
|
|
489
489
|
settings: {
|
|
490
|
-
mode: "
|
|
490
|
+
mode: "standard",
|
|
491
491
|
autoStartDaemon: true,
|
|
492
492
|
enableUndo: true,
|
|
493
493
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
@@ -2691,6 +2691,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2691
2691
|
await notifyActivity({
|
|
2692
2692
|
id: actId,
|
|
2693
2693
|
tool: toolName,
|
|
2694
|
+
args,
|
|
2694
2695
|
ts: actTs,
|
|
2695
2696
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2696
2697
|
label: result.blockedByLabel,
|