@node9/proxy 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/dist/cli.js +545 -37
- package/dist/cli.mjs +545 -37
- package/dist/index.js +80 -1
- package/dist/index.mjs +80 -1
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -491,6 +491,84 @@ var init_shields = __esm({
|
|
|
491
491
|
],
|
|
492
492
|
dangerousWords: []
|
|
493
493
|
},
|
|
494
|
+
"bash-safe": {
|
|
495
|
+
name: "bash-safe",
|
|
496
|
+
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
497
|
+
aliases: ["bash", "shell"],
|
|
498
|
+
smartRules: [
|
|
499
|
+
{
|
|
500
|
+
name: "shield:bash-safe:block-pipe-to-shell",
|
|
501
|
+
tool: "bash",
|
|
502
|
+
conditions: [
|
|
503
|
+
{
|
|
504
|
+
field: "command",
|
|
505
|
+
op: "matches",
|
|
506
|
+
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
507
|
+
flags: "i"
|
|
508
|
+
}
|
|
509
|
+
],
|
|
510
|
+
verdict: "block",
|
|
511
|
+
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "shield:bash-safe:block-obfuscated-exec",
|
|
515
|
+
tool: "bash",
|
|
516
|
+
conditions: [
|
|
517
|
+
{
|
|
518
|
+
field: "command",
|
|
519
|
+
op: "matches",
|
|
520
|
+
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
521
|
+
flags: "i"
|
|
522
|
+
}
|
|
523
|
+
],
|
|
524
|
+
verdict: "block",
|
|
525
|
+
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: "shield:bash-safe:block-rm-root",
|
|
529
|
+
tool: "bash",
|
|
530
|
+
conditions: [
|
|
531
|
+
{
|
|
532
|
+
field: "command",
|
|
533
|
+
op: "matches",
|
|
534
|
+
value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
|
|
535
|
+
flags: "i"
|
|
536
|
+
}
|
|
537
|
+
],
|
|
538
|
+
verdict: "block",
|
|
539
|
+
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
name: "shield:bash-safe:block-disk-overwrite",
|
|
543
|
+
tool: "bash",
|
|
544
|
+
conditions: [
|
|
545
|
+
{
|
|
546
|
+
field: "command",
|
|
547
|
+
op: "matches",
|
|
548
|
+
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
549
|
+
flags: "i"
|
|
550
|
+
}
|
|
551
|
+
],
|
|
552
|
+
verdict: "block",
|
|
553
|
+
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "shield:bash-safe:review-eval",
|
|
557
|
+
tool: "bash",
|
|
558
|
+
conditions: [
|
|
559
|
+
{
|
|
560
|
+
field: "command",
|
|
561
|
+
op: "matches",
|
|
562
|
+
value: '\\beval\\s+[\\$`("]',
|
|
563
|
+
flags: "i"
|
|
564
|
+
}
|
|
565
|
+
],
|
|
566
|
+
verdict: "review",
|
|
567
|
+
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
dangerousWords: []
|
|
571
|
+
},
|
|
494
572
|
filesystem: {
|
|
495
573
|
name: "filesystem",
|
|
496
574
|
description: "Protects the local filesystem from dangerous AI operations",
|
|
@@ -777,7 +855,7 @@ var init_config = __esm({
|
|
|
777
855
|
DEFAULT_CONFIG = {
|
|
778
856
|
version: "1.0",
|
|
779
857
|
settings: {
|
|
780
|
-
mode: "
|
|
858
|
+
mode: "standard",
|
|
781
859
|
autoStartDaemon: true,
|
|
782
860
|
enableUndo: true,
|
|
783
861
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
@@ -3176,6 +3254,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3176
3254
|
await notifyActivity({
|
|
3177
3255
|
id: actId,
|
|
3178
3256
|
tool: toolName,
|
|
3257
|
+
args,
|
|
3179
3258
|
ts: actTs,
|
|
3180
3259
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
3181
3260
|
label: result.blockedByLabel,
|
|
@@ -5453,6 +5532,7 @@ var init_session_counters = __esm({
|
|
|
5453
5532
|
_blocked = 0;
|
|
5454
5533
|
_dlpHits = 0;
|
|
5455
5534
|
_wouldBlock = 0;
|
|
5535
|
+
_estimatedCost = 0;
|
|
5456
5536
|
_lastRuleHit = null;
|
|
5457
5537
|
_lastBlockedTool = null;
|
|
5458
5538
|
incrementAllowed() {
|
|
@@ -5467,6 +5547,10 @@ var init_session_counters = __esm({
|
|
|
5467
5547
|
incrementWouldBlock() {
|
|
5468
5548
|
this._wouldBlock++;
|
|
5469
5549
|
}
|
|
5550
|
+
addCost(amount) {
|
|
5551
|
+
if (!isFinite(amount) || amount < 0) return;
|
|
5552
|
+
this._estimatedCost += amount;
|
|
5553
|
+
}
|
|
5470
5554
|
recordRuleHit(label) {
|
|
5471
5555
|
this._lastRuleHit = label;
|
|
5472
5556
|
}
|
|
@@ -5479,6 +5563,7 @@ var init_session_counters = __esm({
|
|
|
5479
5563
|
blocked: this._blocked,
|
|
5480
5564
|
dlpHits: this._dlpHits,
|
|
5481
5565
|
wouldBlock: this._wouldBlock,
|
|
5566
|
+
estimatedCost: this._estimatedCost,
|
|
5482
5567
|
lastRuleHit: this._lastRuleHit,
|
|
5483
5568
|
lastBlockedTool: this._lastBlockedTool
|
|
5484
5569
|
};
|
|
@@ -5488,6 +5573,7 @@ var init_session_counters = __esm({
|
|
|
5488
5573
|
this._blocked = 0;
|
|
5489
5574
|
this._dlpHits = 0;
|
|
5490
5575
|
this._wouldBlock = 0;
|
|
5576
|
+
this._estimatedCost = 0;
|
|
5491
5577
|
this._lastRuleHit = null;
|
|
5492
5578
|
this._lastBlockedTool = null;
|
|
5493
5579
|
}
|
|
@@ -5718,15 +5804,38 @@ function openBrowser(url) {
|
|
|
5718
5804
|
} catch {
|
|
5719
5805
|
}
|
|
5720
5806
|
}
|
|
5807
|
+
function estimateToolCost(tool, args) {
|
|
5808
|
+
const a = args ?? {};
|
|
5809
|
+
const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
5810
|
+
if (t.includes("read") || t === "glob" || t === "grep") {
|
|
5811
|
+
const filePath = a.file_path ?? a.path;
|
|
5812
|
+
if (filePath) {
|
|
5813
|
+
try {
|
|
5814
|
+
const bytes = fs13.statSync(filePath).size;
|
|
5815
|
+
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5816
|
+
} catch {
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
if (t.includes("write")) {
|
|
5821
|
+
const content = a.content ?? "";
|
|
5822
|
+
return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5823
|
+
}
|
|
5824
|
+
if (t.includes("edit") || t === "str_replace_based_edit_tool") {
|
|
5825
|
+
const newStr = a.new_string ?? "";
|
|
5826
|
+
return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5827
|
+
}
|
|
5828
|
+
return void 0;
|
|
5829
|
+
}
|
|
5721
5830
|
function broadcast(event, data) {
|
|
5722
5831
|
if (event === "activity") {
|
|
5723
5832
|
activityRing.push({ event, data });
|
|
5724
5833
|
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
5725
5834
|
} else if (event === "activity-result") {
|
|
5726
|
-
const { id, status, label } = data;
|
|
5835
|
+
const { id, status, label, costEstimate } = data;
|
|
5727
5836
|
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
5728
5837
|
if (activityRing[i].data.id === id) {
|
|
5729
|
-
Object.assign(activityRing[i].data, { status, label });
|
|
5838
|
+
Object.assign(activityRing[i].data, { status, label, costEstimate });
|
|
5730
5839
|
break;
|
|
5731
5840
|
}
|
|
5732
5841
|
}
|
|
@@ -5828,10 +5937,13 @@ function startActivitySocket() {
|
|
|
5828
5937
|
sessionCounters.incrementBlocked();
|
|
5829
5938
|
sessionCounters.recordBlockedTool(data.tool);
|
|
5830
5939
|
}
|
|
5940
|
+
const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
|
|
5941
|
+
if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
|
|
5831
5942
|
broadcast("activity-result", {
|
|
5832
5943
|
id: data.id,
|
|
5833
5944
|
status: data.status,
|
|
5834
|
-
label: data.label
|
|
5945
|
+
label: data.label,
|
|
5946
|
+
costEstimate
|
|
5835
5947
|
});
|
|
5836
5948
|
}
|
|
5837
5949
|
} catch {
|
|
@@ -5848,7 +5960,7 @@ function startActivitySocket() {
|
|
|
5848
5960
|
}
|
|
5849
5961
|
});
|
|
5850
5962
|
}
|
|
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;
|
|
5963
|
+
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
5964
|
var init_state2 = __esm({
|
|
5853
5965
|
"src/daemon/state.ts"() {
|
|
5854
5966
|
"use strict";
|
|
@@ -5886,6 +5998,9 @@ var init_state2 = __esm({
|
|
|
5886
5998
|
ACTIVITY_RING_SIZE = 100;
|
|
5887
5999
|
activityRing = [];
|
|
5888
6000
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
6001
|
+
INPUT_PRICE_PER_1M = 3;
|
|
6002
|
+
OUTPUT_PRICE_PER_1M = 15;
|
|
6003
|
+
BYTES_PER_TOKEN = 4;
|
|
5889
6004
|
WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5890
6005
|
"write",
|
|
5891
6006
|
"write_file",
|
|
@@ -6349,7 +6464,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6349
6464
|
allowed: counters.allowed,
|
|
6350
6465
|
blocked: counters.blocked,
|
|
6351
6466
|
dlpHits: counters.dlpHits,
|
|
6352
|
-
wouldBlock: counters.wouldBlock
|
|
6467
|
+
wouldBlock: counters.wouldBlock,
|
|
6468
|
+
estimatedCost: counters.estimatedCost
|
|
6353
6469
|
},
|
|
6354
6470
|
taintedCount: taintStore.list().length,
|
|
6355
6471
|
lastRuleHit: counters.lastRuleHit,
|
|
@@ -6481,6 +6597,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6481
6597
|
}
|
|
6482
6598
|
if (req.method === "POST" && pathname === "/events/clear") {
|
|
6483
6599
|
activityRing.length = 0;
|
|
6600
|
+
sessionCounters.reset();
|
|
6484
6601
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6485
6602
|
return res.end(JSON.stringify({ ok: true }));
|
|
6486
6603
|
}
|
|
@@ -6793,7 +6910,7 @@ function formatBase(activity) {
|
|
|
6793
6910
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6794
6911
|
const icon = getIcon(activity.tool);
|
|
6795
6912
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6796
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
6913
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os21.homedir(), "~");
|
|
6797
6914
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6798
6915
|
return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
|
|
6799
6916
|
}
|
|
@@ -6807,11 +6924,13 @@ function renderResult(activity, result) {
|
|
|
6807
6924
|
} else {
|
|
6808
6925
|
status = chalk17.red("\u2717 BLOCK");
|
|
6809
6926
|
}
|
|
6927
|
+
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6928
|
+
const costSuffix = cost == null ? "" : chalk17.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6810
6929
|
if (process.stdout.isTTY) {
|
|
6811
6930
|
readline5.clearLine(process.stdout, 0);
|
|
6812
6931
|
readline5.cursorTo(process.stdout, 0);
|
|
6813
6932
|
}
|
|
6814
|
-
console.log(`${base} ${status}`);
|
|
6933
|
+
console.log(`${base} ${status}${costSuffix}`);
|
|
6815
6934
|
}
|
|
6816
6935
|
function renderPending(activity) {
|
|
6817
6936
|
if (!process.stdout.isTTY) return;
|
|
@@ -6940,6 +7059,39 @@ function buildRecoveryCardLines(req) {
|
|
|
6940
7059
|
``
|
|
6941
7060
|
];
|
|
6942
7061
|
}
|
|
7062
|
+
function readApproversFromDisk() {
|
|
7063
|
+
const configPath = path28.join(os21.homedir(), ".node9", "config.json");
|
|
7064
|
+
try {
|
|
7065
|
+
const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
7066
|
+
const settings = raw.settings ?? {};
|
|
7067
|
+
return settings.approvers ?? {};
|
|
7068
|
+
} catch {
|
|
7069
|
+
return {};
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
function approverStatusLine() {
|
|
7073
|
+
const a = readApproversFromDisk();
|
|
7074
|
+
const fmt = (label, key) => {
|
|
7075
|
+
const on = a[key] !== false;
|
|
7076
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk17.green("\u2713") : chalk17.dim("\u2717")}`;
|
|
7077
|
+
};
|
|
7078
|
+
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7079
|
+
}
|
|
7080
|
+
function toggleApprover(channel) {
|
|
7081
|
+
const configPath = path28.join(os21.homedir(), ".node9", "config.json");
|
|
7082
|
+
try {
|
|
7083
|
+
const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
7084
|
+
const settings = raw.settings ?? {};
|
|
7085
|
+
const approvers = settings.approvers ?? {};
|
|
7086
|
+
approvers[channel] = approvers[channel] === false;
|
|
7087
|
+
settings.approvers = approvers;
|
|
7088
|
+
raw.settings = settings;
|
|
7089
|
+
fs25.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7090
|
+
} catch (err2) {
|
|
7091
|
+
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7092
|
+
`);
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
6943
7095
|
async function startTail(options = {}) {
|
|
6944
7096
|
const port = await ensureDaemon();
|
|
6945
7097
|
if (options.clear) {
|
|
@@ -6978,6 +7130,7 @@ async function startTail(options = {}) {
|
|
|
6978
7130
|
}
|
|
6979
7131
|
const connectionTime = Date.now();
|
|
6980
7132
|
const activityPending = /* @__PURE__ */ new Map();
|
|
7133
|
+
const orphanedResults = /* @__PURE__ */ new Map();
|
|
6981
7134
|
let csrfToken = "";
|
|
6982
7135
|
const approvalQueue = [];
|
|
6983
7136
|
let cardActive = false;
|
|
@@ -6986,6 +7139,44 @@ async function startTail(options = {}) {
|
|
|
6986
7139
|
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
6987
7140
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
6988
7141
|
if (canApprove) readline5.emitKeypressEvents(process.stdin);
|
|
7142
|
+
let idleKeypressHandler = null;
|
|
7143
|
+
function enterIdleMode() {
|
|
7144
|
+
if (!canApprove || idleKeypressHandler !== null) return;
|
|
7145
|
+
try {
|
|
7146
|
+
process.stdin.setRawMode(true);
|
|
7147
|
+
} catch {
|
|
7148
|
+
return;
|
|
7149
|
+
}
|
|
7150
|
+
process.stdin.resume();
|
|
7151
|
+
idleKeypressHandler = (_str, key) => {
|
|
7152
|
+
const name = key?.name ?? "";
|
|
7153
|
+
if (key?.ctrl && name === "c") {
|
|
7154
|
+
process.kill(process.pid, "SIGINT");
|
|
7155
|
+
return;
|
|
7156
|
+
}
|
|
7157
|
+
if (name === "q") {
|
|
7158
|
+
process.kill(process.pid, "SIGINT");
|
|
7159
|
+
return;
|
|
7160
|
+
}
|
|
7161
|
+
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7162
|
+
if (channel) {
|
|
7163
|
+
toggleApprover(channel);
|
|
7164
|
+
console.log(chalk17.dim(` Approvers: ${approverStatusLine()}`));
|
|
7165
|
+
}
|
|
7166
|
+
};
|
|
7167
|
+
process.stdin.on("keypress", idleKeypressHandler);
|
|
7168
|
+
}
|
|
7169
|
+
function exitIdleMode() {
|
|
7170
|
+
if (idleKeypressHandler) {
|
|
7171
|
+
process.stdin.removeListener("keypress", idleKeypressHandler);
|
|
7172
|
+
idleKeypressHandler = null;
|
|
7173
|
+
}
|
|
7174
|
+
try {
|
|
7175
|
+
process.stdin.setRawMode(false);
|
|
7176
|
+
} catch {
|
|
7177
|
+
}
|
|
7178
|
+
process.stdin.pause();
|
|
7179
|
+
}
|
|
6989
7180
|
function clearCard() {
|
|
6990
7181
|
if (cardLineCount > 0) {
|
|
6991
7182
|
readline5.moveCursor(process.stdout, 0, -cardLineCount);
|
|
@@ -7004,10 +7195,12 @@ async function startTail(options = {}) {
|
|
|
7004
7195
|
}
|
|
7005
7196
|
function showNextCard() {
|
|
7006
7197
|
if (cardActive || approvalQueue.length === 0 || !canApprove) return;
|
|
7198
|
+
exitIdleMode();
|
|
7007
7199
|
try {
|
|
7008
7200
|
process.stdin.setRawMode(true);
|
|
7009
7201
|
} catch {
|
|
7010
7202
|
cardActive = false;
|
|
7203
|
+
enterIdleMode();
|
|
7011
7204
|
return;
|
|
7012
7205
|
}
|
|
7013
7206
|
cardActive = true;
|
|
@@ -7019,12 +7212,8 @@ async function startTail(options = {}) {
|
|
|
7019
7212
|
const handler = onKeypress;
|
|
7020
7213
|
onKeypress = null;
|
|
7021
7214
|
if (handler) process.stdin.removeListener("keypress", handler);
|
|
7022
|
-
try {
|
|
7023
|
-
process.stdin.setRawMode(false);
|
|
7024
|
-
} catch {
|
|
7025
|
-
}
|
|
7026
|
-
process.stdin.pause();
|
|
7027
7215
|
cancelActiveCard = null;
|
|
7216
|
+
enterIdleMode();
|
|
7028
7217
|
};
|
|
7029
7218
|
const settle = (action) => {
|
|
7030
7219
|
if (settled) return;
|
|
@@ -7151,18 +7340,16 @@ async function startTail(options = {}) {
|
|
|
7151
7340
|
console.log(chalk17.cyan.bold(`
|
|
7152
7341
|
\u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
|
|
7153
7342
|
if (canApprove) {
|
|
7154
|
-
console.log(
|
|
7155
|
-
|
|
7156
|
-
);
|
|
7343
|
+
console.log(chalk17.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7344
|
+
console.log(chalk17.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7157
7345
|
}
|
|
7158
7346
|
if (options.history) {
|
|
7159
|
-
console.log(chalk17.dim("Showing history + live events
|
|
7347
|
+
console.log(chalk17.dim("Showing history + live events.\n"));
|
|
7160
7348
|
} else {
|
|
7161
|
-
console.log(
|
|
7162
|
-
chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
7163
|
-
);
|
|
7349
|
+
console.log(chalk17.dim("Showing live events only. Use --history to include past.\n"));
|
|
7164
7350
|
}
|
|
7165
7351
|
process.on("SIGINT", () => {
|
|
7352
|
+
exitIdleMode();
|
|
7166
7353
|
clearCard();
|
|
7167
7354
|
process.stdout.write(SHOW_CURSOR);
|
|
7168
7355
|
if (process.stdout.isTTY) {
|
|
@@ -7178,6 +7365,7 @@ async function startTail(options = {}) {
|
|
|
7178
7365
|
console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7179
7366
|
process.exit(1);
|
|
7180
7367
|
}
|
|
7368
|
+
if (canApprove) enterIdleMode();
|
|
7181
7369
|
let currentEvent = "";
|
|
7182
7370
|
let currentData = "";
|
|
7183
7371
|
res.on("error", () => {
|
|
@@ -7277,9 +7465,14 @@ async function startTail(options = {}) {
|
|
|
7277
7465
|
renderResult(data, data);
|
|
7278
7466
|
return;
|
|
7279
7467
|
}
|
|
7468
|
+
const orphaned = orphanedResults.get(data.id);
|
|
7469
|
+
if (orphaned) {
|
|
7470
|
+
orphanedResults.delete(data.id);
|
|
7471
|
+
renderResult(data, orphaned);
|
|
7472
|
+
return;
|
|
7473
|
+
}
|
|
7280
7474
|
activityPending.set(data.id, data);
|
|
7281
|
-
|
|
7282
|
-
if (slowTool) renderPending(data);
|
|
7475
|
+
renderPending(data);
|
|
7283
7476
|
}
|
|
7284
7477
|
if (event === "snapshot") {
|
|
7285
7478
|
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
@@ -7298,6 +7491,8 @@ async function startTail(options = {}) {
|
|
|
7298
7491
|
if (original) {
|
|
7299
7492
|
renderResult(original, data);
|
|
7300
7493
|
activityPending.delete(data.id);
|
|
7494
|
+
} else {
|
|
7495
|
+
orphanedResults.set(data.id, data);
|
|
7301
7496
|
}
|
|
7302
7497
|
}
|
|
7303
7498
|
}
|
|
@@ -7537,6 +7732,29 @@ function renderOffline() {
|
|
|
7537
7732
|
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7538
7733
|
`);
|
|
7539
7734
|
}
|
|
7735
|
+
function readActiveShieldsHud() {
|
|
7736
|
+
const now = Date.now();
|
|
7737
|
+
if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
|
|
7738
|
+
return shieldsCache.value;
|
|
7739
|
+
}
|
|
7740
|
+
try {
|
|
7741
|
+
const shieldsPath = path29.join(os22.homedir(), ".node9", "shields.json");
|
|
7742
|
+
if (!fs26.existsSync(shieldsPath)) {
|
|
7743
|
+
shieldsCache = { value: [], ts: now };
|
|
7744
|
+
return [];
|
|
7745
|
+
}
|
|
7746
|
+
const parsed = JSON.parse(fs26.readFileSync(shieldsPath, "utf-8"));
|
|
7747
|
+
if (!Array.isArray(parsed.active)) {
|
|
7748
|
+
shieldsCache = { value: [], ts: now };
|
|
7749
|
+
return [];
|
|
7750
|
+
}
|
|
7751
|
+
const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
|
|
7752
|
+
shieldsCache = { value, ts: now };
|
|
7753
|
+
return value;
|
|
7754
|
+
} catch {
|
|
7755
|
+
return [];
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7540
7758
|
function renderSecurityLine(status) {
|
|
7541
7759
|
const parts = [];
|
|
7542
7760
|
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
@@ -7554,6 +7772,18 @@ function renderSecurityLine(status) {
|
|
|
7554
7772
|
};
|
|
7555
7773
|
const mc = modeColors[status.mode] ?? WHITE;
|
|
7556
7774
|
parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
|
|
7775
|
+
const activeShields = readActiveShieldsHud();
|
|
7776
|
+
if (activeShields.length > 0) {
|
|
7777
|
+
const shieldAbbrevs = {
|
|
7778
|
+
"bash-safe": "bash",
|
|
7779
|
+
filesystem: "fs",
|
|
7780
|
+
postgres: "pg",
|
|
7781
|
+
github: "gh",
|
|
7782
|
+
aws: "aws"
|
|
7783
|
+
};
|
|
7784
|
+
const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
|
|
7785
|
+
parts.push(color(DIM, `[${labels}]`));
|
|
7786
|
+
}
|
|
7557
7787
|
if (status.mode === "observe") {
|
|
7558
7788
|
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7559
7789
|
if (status.session.wouldBlock > 0) {
|
|
@@ -7568,6 +7798,11 @@ function renderSecurityLine(status) {
|
|
|
7568
7798
|
parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
|
|
7569
7799
|
}
|
|
7570
7800
|
}
|
|
7801
|
+
if (status.session.estimatedCost > 0) {
|
|
7802
|
+
const cost = status.session.estimatedCost;
|
|
7803
|
+
const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
|
|
7804
|
+
parts.push(color(DIM, `~${costStr}`));
|
|
7805
|
+
}
|
|
7571
7806
|
if (status.taintedCount > 0) {
|
|
7572
7807
|
parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
|
|
7573
7808
|
}
|
|
@@ -7645,7 +7880,7 @@ async function main() {
|
|
|
7645
7880
|
renderOffline();
|
|
7646
7881
|
}
|
|
7647
7882
|
}
|
|
7648
|
-
var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7883
|
+
var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
|
|
7649
7884
|
var init_hud = __esm({
|
|
7650
7885
|
"src/cli/hud.ts"() {
|
|
7651
7886
|
"use strict";
|
|
@@ -7663,6 +7898,8 @@ var init_hud = __esm({
|
|
|
7663
7898
|
BAR_FILLED = "\u2588";
|
|
7664
7899
|
BAR_EMPTY = "\u2591";
|
|
7665
7900
|
BAR_WIDTH = 10;
|
|
7901
|
+
shieldsCache = null;
|
|
7902
|
+
SHIELDS_CACHE_TTL_MS = 2e3;
|
|
7666
7903
|
}
|
|
7667
7904
|
});
|
|
7668
7905
|
|
|
@@ -7676,6 +7913,7 @@ import path14 from "path";
|
|
|
7676
7913
|
import os10 from "os";
|
|
7677
7914
|
import chalk from "chalk";
|
|
7678
7915
|
import { confirm } from "@inquirer/prompts";
|
|
7916
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7679
7917
|
var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
|
|
7680
7918
|
function hasNode9McpServer(servers) {
|
|
7681
7919
|
const entry = servers["node9"];
|
|
@@ -8039,7 +8277,8 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
8039
8277
|
return {
|
|
8040
8278
|
claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
|
|
8041
8279
|
gemini: exists(path14.join(homeDir2, ".gemini")),
|
|
8042
|
-
cursor: exists(path14.join(homeDir2, ".cursor"))
|
|
8280
|
+
cursor: exists(path14.join(homeDir2, ".cursor")),
|
|
8281
|
+
codex: exists(path14.join(homeDir2, ".codex"))
|
|
8043
8282
|
};
|
|
8044
8283
|
}
|
|
8045
8284
|
async function setupCursor() {
|
|
@@ -8104,6 +8343,82 @@ async function setupCursor() {
|
|
|
8104
8343
|
printDaemonTip();
|
|
8105
8344
|
}
|
|
8106
8345
|
}
|
|
8346
|
+
function readToml(filePath) {
|
|
8347
|
+
try {
|
|
8348
|
+
if (fs11.existsSync(filePath)) {
|
|
8349
|
+
return parseToml(fs11.readFileSync(filePath, "utf-8"));
|
|
8350
|
+
}
|
|
8351
|
+
} catch {
|
|
8352
|
+
}
|
|
8353
|
+
return null;
|
|
8354
|
+
}
|
|
8355
|
+
function writeToml(filePath, data) {
|
|
8356
|
+
const dir = path14.dirname(filePath);
|
|
8357
|
+
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
8358
|
+
fs11.writeFileSync(filePath, stringifyToml(data));
|
|
8359
|
+
}
|
|
8360
|
+
async function setupCodex() {
|
|
8361
|
+
const homeDir2 = os10.homedir();
|
|
8362
|
+
const configPath = path14.join(homeDir2, ".codex", "config.toml");
|
|
8363
|
+
const config = readToml(configPath) ?? {};
|
|
8364
|
+
const servers = config.mcp_servers ?? {};
|
|
8365
|
+
let anythingChanged = false;
|
|
8366
|
+
if (!hasNode9McpServer(servers)) {
|
|
8367
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8368
|
+
config.mcp_servers = servers;
|
|
8369
|
+
writeToml(configPath, config);
|
|
8370
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8371
|
+
anythingChanged = true;
|
|
8372
|
+
}
|
|
8373
|
+
const serversToWrap = [];
|
|
8374
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
8375
|
+
if (!server.command || server.command === "node9") continue;
|
|
8376
|
+
const parts = [server.command, ...server.args ?? []];
|
|
8377
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
8378
|
+
}
|
|
8379
|
+
if (serversToWrap.length > 0) {
|
|
8380
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8381
|
+
console.log(chalk.white(` ${configPath}`));
|
|
8382
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
8383
|
+
console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
8384
|
+
}
|
|
8385
|
+
console.log("");
|
|
8386
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
8387
|
+
if (proceed) {
|
|
8388
|
+
for (const { name, parts } of serversToWrap) {
|
|
8389
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
8390
|
+
}
|
|
8391
|
+
config.mcp_servers = servers;
|
|
8392
|
+
writeToml(configPath, config);
|
|
8393
|
+
console.log(chalk.green(`
|
|
8394
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8395
|
+
anythingChanged = true;
|
|
8396
|
+
} else {
|
|
8397
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
8398
|
+
}
|
|
8399
|
+
console.log("");
|
|
8400
|
+
}
|
|
8401
|
+
console.log(
|
|
8402
|
+
chalk.yellow(
|
|
8403
|
+
" \u26A0\uFE0F Note: Codex does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Codex.\n Native bash and file operations are not monitored."
|
|
8404
|
+
)
|
|
8405
|
+
);
|
|
8406
|
+
console.log("");
|
|
8407
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
8408
|
+
console.log(
|
|
8409
|
+
chalk.blue(
|
|
8410
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
|
|
8411
|
+
)
|
|
8412
|
+
);
|
|
8413
|
+
printDaemonTip();
|
|
8414
|
+
return;
|
|
8415
|
+
}
|
|
8416
|
+
if (anythingChanged) {
|
|
8417
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
|
|
8418
|
+
console.log(chalk.gray(" Restart Codex for changes to take effect."));
|
|
8419
|
+
printDaemonTip();
|
|
8420
|
+
}
|
|
8421
|
+
}
|
|
8107
8422
|
function setupHud() {
|
|
8108
8423
|
const homeDir2 = os10.homedir();
|
|
8109
8424
|
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
@@ -8596,6 +8911,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8596
8911
|
if (filesRes.status === 0) {
|
|
8597
8912
|
capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8598
8913
|
}
|
|
8914
|
+
if (capturedFiles.length === 0) {
|
|
8915
|
+
return prevEntry.hash;
|
|
8916
|
+
}
|
|
8599
8917
|
const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
|
|
8600
8918
|
env: shadowEnv,
|
|
8601
8919
|
timeout: GIT_TIMEOUT
|
|
@@ -9793,24 +10111,90 @@ import chalk11 from "chalk";
|
|
|
9793
10111
|
import fs23 from "fs";
|
|
9794
10112
|
import path25 from "path";
|
|
9795
10113
|
import os19 from "os";
|
|
10114
|
+
import https from "https";
|
|
10115
|
+
init_shields();
|
|
10116
|
+
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
10117
|
+
function fireTelemetryPing(agents) {
|
|
10118
|
+
try {
|
|
10119
|
+
const body = JSON.stringify({
|
|
10120
|
+
event: "init_completed",
|
|
10121
|
+
agents_detected: agents,
|
|
10122
|
+
os: process.platform,
|
|
10123
|
+
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10124
|
+
});
|
|
10125
|
+
const req = https.request(
|
|
10126
|
+
{
|
|
10127
|
+
hostname: "api.node9.ai",
|
|
10128
|
+
path: "/api/v1/telemetry",
|
|
10129
|
+
method: "POST",
|
|
10130
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
10131
|
+
timeout: 3e3
|
|
10132
|
+
},
|
|
10133
|
+
(res) => {
|
|
10134
|
+
res.resume();
|
|
10135
|
+
}
|
|
10136
|
+
);
|
|
10137
|
+
req.on("error", () => {
|
|
10138
|
+
});
|
|
10139
|
+
req.on("timeout", () => {
|
|
10140
|
+
req.destroy();
|
|
10141
|
+
});
|
|
10142
|
+
req.end(body);
|
|
10143
|
+
} catch {
|
|
10144
|
+
}
|
|
10145
|
+
}
|
|
9796
10146
|
function registerInitCommand(program2) {
|
|
9797
10147
|
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
10148
|
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
10149
|
+
let chosenMode = options.mode.toLowerCase();
|
|
10150
|
+
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
10151
|
+
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
10152
|
+
}
|
|
10153
|
+
{
|
|
10154
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10155
|
+
const enableShields = await confirm3({
|
|
10156
|
+
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
10157
|
+
default: true
|
|
10158
|
+
});
|
|
10159
|
+
if (enableShields) {
|
|
10160
|
+
chosenMode = "standard";
|
|
10161
|
+
try {
|
|
10162
|
+
const current = readActiveShields();
|
|
10163
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
|
|
10164
|
+
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10165
|
+
if (hasNewShields) writeActiveShields(merged);
|
|
10166
|
+
} catch (err2) {
|
|
10167
|
+
console.log(chalk11.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
console.log("");
|
|
10171
|
+
}
|
|
9799
10172
|
const configPath = path25.join(os19.homedir(), ".node9", "config.json");
|
|
9800
10173
|
if (fs23.existsSync(configPath) && !options.force) {
|
|
9801
|
-
|
|
10174
|
+
try {
|
|
10175
|
+
const existing = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
|
|
10176
|
+
const settings = existing.settings ?? {};
|
|
10177
|
+
if (settings.mode !== chosenMode) {
|
|
10178
|
+
settings.mode = chosenMode;
|
|
10179
|
+
existing.settings = settings;
|
|
10180
|
+
fs23.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
10181
|
+
console.log(chalk11.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
10182
|
+
} else {
|
|
10183
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10184
|
+
}
|
|
10185
|
+
} catch {
|
|
10186
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10187
|
+
}
|
|
9802
10188
|
} else {
|
|
9803
|
-
const requestedMode = options.mode.toLowerCase();
|
|
9804
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
9805
10189
|
const configToSave = {
|
|
9806
10190
|
...DEFAULT_CONFIG,
|
|
9807
|
-
settings: { ...DEFAULT_CONFIG.settings, mode:
|
|
10191
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
9808
10192
|
};
|
|
9809
10193
|
const dir = path25.dirname(configPath);
|
|
9810
10194
|
if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
|
|
9811
|
-
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
10195
|
+
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
9812
10196
|
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
9813
|
-
console.log(chalk11.gray(` Mode: ${
|
|
10197
|
+
console.log(chalk11.gray(` Mode: ${chosenMode}`));
|
|
9814
10198
|
}
|
|
9815
10199
|
if (options.skipSetup) return;
|
|
9816
10200
|
console.log("");
|
|
@@ -9820,9 +10204,9 @@ function registerInitCommand(program2) {
|
|
|
9820
10204
|
);
|
|
9821
10205
|
if (found.length === 0) {
|
|
9822
10206
|
console.log(
|
|
9823
|
-
chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or
|
|
10207
|
+
chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
9824
10208
|
);
|
|
9825
|
-
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
10209
|
+
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
9826
10210
|
return;
|
|
9827
10211
|
}
|
|
9828
10212
|
console.log(chalk11.bold("Detected agents:"));
|
|
@@ -9835,16 +10219,23 @@ function registerInitCommand(program2) {
|
|
|
9835
10219
|
if (agent === "claude") await setupClaude();
|
|
9836
10220
|
else if (agent === "gemini") await setupGemini();
|
|
9837
10221
|
else if (agent === "cursor") await setupCursor();
|
|
10222
|
+
else if (agent === "codex") await setupCodex();
|
|
9838
10223
|
console.log("");
|
|
9839
10224
|
}
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
10225
|
+
{
|
|
10226
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10227
|
+
const sendTelemetry = await confirm3({
|
|
10228
|
+
message: "Send anonymous usage stats to help improve node9? (no code, no args)",
|
|
10229
|
+
default: true
|
|
10230
|
+
});
|
|
10231
|
+
if (sendTelemetry) fireTelemetryPing(found);
|
|
9844
10232
|
console.log("");
|
|
9845
10233
|
}
|
|
9846
10234
|
console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
9847
|
-
console.log(
|
|
10235
|
+
console.log("");
|
|
10236
|
+
console.log(chalk11.white(" Start watching: ") + chalk11.cyan("node9 tail"));
|
|
10237
|
+
console.log(chalk11.white(" Browser view: ") + chalk11.cyan("node9 daemon --openui"));
|
|
10238
|
+
console.log(chalk11.white(" Cloud dashboard: ") + chalk11.cyan("node9.ai"));
|
|
9848
10239
|
});
|
|
9849
10240
|
}
|
|
9850
10241
|
|
|
@@ -10450,6 +10841,44 @@ var TOOLS = [
|
|
|
10450
10841
|
required: ["service"]
|
|
10451
10842
|
}
|
|
10452
10843
|
},
|
|
10844
|
+
{
|
|
10845
|
+
name: "node9_shield_disable",
|
|
10846
|
+
description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
|
|
10847
|
+
inputSchema: {
|
|
10848
|
+
type: "object",
|
|
10849
|
+
properties: {
|
|
10850
|
+
service: {
|
|
10851
|
+
type: "string",
|
|
10852
|
+
description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
|
|
10853
|
+
}
|
|
10854
|
+
},
|
|
10855
|
+
required: ["service"]
|
|
10856
|
+
}
|
|
10857
|
+
},
|
|
10858
|
+
{
|
|
10859
|
+
name: "node9_approver_list",
|
|
10860
|
+
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).",
|
|
10861
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10862
|
+
},
|
|
10863
|
+
{
|
|
10864
|
+
name: "node9_approver_set",
|
|
10865
|
+
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.",
|
|
10866
|
+
inputSchema: {
|
|
10867
|
+
type: "object",
|
|
10868
|
+
properties: {
|
|
10869
|
+
channel: {
|
|
10870
|
+
type: "string",
|
|
10871
|
+
enum: ["native", "browser", "cloud", "terminal"],
|
|
10872
|
+
description: "Approver channel to configure."
|
|
10873
|
+
},
|
|
10874
|
+
enabled: {
|
|
10875
|
+
type: "boolean",
|
|
10876
|
+
description: "true to enable the channel, false to disable it."
|
|
10877
|
+
}
|
|
10878
|
+
},
|
|
10879
|
+
required: ["channel", "enabled"]
|
|
10880
|
+
}
|
|
10881
|
+
},
|
|
10453
10882
|
{
|
|
10454
10883
|
name: "node9_undo_list",
|
|
10455
10884
|
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 +10984,79 @@ function handleShieldEnable(args) {
|
|
|
10555
10984
|
const shield = getShield(name);
|
|
10556
10985
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10557
10986
|
}
|
|
10987
|
+
function handleShieldDisable(args) {
|
|
10988
|
+
const service = args.service;
|
|
10989
|
+
if (typeof service !== "string" || !service) {
|
|
10990
|
+
throw new Error("service is required");
|
|
10991
|
+
}
|
|
10992
|
+
const name = resolveShieldName(service);
|
|
10993
|
+
if (!name) {
|
|
10994
|
+
throw new Error(
|
|
10995
|
+
`Unknown shield: "${service}". Run node9_shield_list to see available shields.`
|
|
10996
|
+
);
|
|
10997
|
+
}
|
|
10998
|
+
const active = readActiveShields();
|
|
10999
|
+
if (!active.includes(name)) {
|
|
11000
|
+
return `Shield "${name}" is not active.`;
|
|
11001
|
+
}
|
|
11002
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
11003
|
+
return `Shield "${name}" disabled.`;
|
|
11004
|
+
}
|
|
11005
|
+
var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
|
|
11006
|
+
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
11007
|
+
function readGlobalConfigRaw() {
|
|
11008
|
+
try {
|
|
11009
|
+
if (fs24.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
11010
|
+
return JSON.parse(fs24.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
11011
|
+
}
|
|
11012
|
+
} catch {
|
|
11013
|
+
}
|
|
11014
|
+
return {};
|
|
11015
|
+
}
|
|
11016
|
+
function writeGlobalConfigRaw(data) {
|
|
11017
|
+
const dir = path27.dirname(GLOBAL_CONFIG_PATH2);
|
|
11018
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
11019
|
+
fs24.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
11020
|
+
}
|
|
11021
|
+
function handleApproverList() {
|
|
11022
|
+
const config = getConfig();
|
|
11023
|
+
const approvers = config.settings.approvers;
|
|
11024
|
+
const lines = ["Approver channels:\n"];
|
|
11025
|
+
for (const ch of APPROVER_CHANNELS) {
|
|
11026
|
+
const on = approvers[ch];
|
|
11027
|
+
lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
|
|
11028
|
+
}
|
|
11029
|
+
const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
|
|
11030
|
+
if (enabledCount === 0) {
|
|
11031
|
+
lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
|
|
11032
|
+
}
|
|
11033
|
+
return lines.join("\n");
|
|
11034
|
+
}
|
|
11035
|
+
function handleApproverSet(args) {
|
|
11036
|
+
const channel = args.channel;
|
|
11037
|
+
const enabled = args.enabled;
|
|
11038
|
+
if (!channel || !APPROVER_CHANNELS.includes(channel)) {
|
|
11039
|
+
throw new Error(
|
|
11040
|
+
`Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
|
|
11041
|
+
);
|
|
11042
|
+
}
|
|
11043
|
+
if (typeof enabled !== "boolean") {
|
|
11044
|
+
throw new Error("enabled must be a boolean (true or false).");
|
|
11045
|
+
}
|
|
11046
|
+
const raw = readGlobalConfigRaw();
|
|
11047
|
+
const settings = raw.settings ?? {};
|
|
11048
|
+
const approvers = settings.approvers ?? {};
|
|
11049
|
+
approvers[channel] = enabled;
|
|
11050
|
+
settings.approvers = approvers;
|
|
11051
|
+
raw.settings = settings;
|
|
11052
|
+
writeGlobalConfigRaw(raw);
|
|
11053
|
+
const currentApprovers = getConfig().settings.approvers;
|
|
11054
|
+
const anyEnabled = APPROVER_CHANNELS.some(
|
|
11055
|
+
(ch) => ch === channel ? enabled : currentApprovers[ch]
|
|
11056
|
+
);
|
|
11057
|
+
const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
|
|
11058
|
+
return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
|
|
11059
|
+
}
|
|
10558
11060
|
function handleUndoList() {
|
|
10559
11061
|
const history = getSnapshotHistory();
|
|
10560
11062
|
if (history.length === 0) {
|
|
@@ -10628,6 +11130,12 @@ function runMcpServer() {
|
|
|
10628
11130
|
text = handleShieldList();
|
|
10629
11131
|
} else if (toolName === "node9_shield_enable") {
|
|
10630
11132
|
text = handleShieldEnable(toolArgs);
|
|
11133
|
+
} else if (toolName === "node9_shield_disable") {
|
|
11134
|
+
text = handleShieldDisable(toolArgs);
|
|
11135
|
+
} else if (toolName === "node9_approver_list") {
|
|
11136
|
+
text = handleApproverList();
|
|
11137
|
+
} else if (toolName === "node9_approver_set") {
|
|
11138
|
+
text = handleApproverSet(toolArgs);
|
|
10631
11139
|
} else if (toolName === "node9_undo_list") {
|
|
10632
11140
|
text = handleUndoList();
|
|
10633
11141
|
} else if (toolName === "node9_undo_revert") {
|