@node9/proxy 1.7.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/dist/cli.js +255 -7
- package/dist/cli.mjs +255 -7
- package/dist/index.js +78 -0
- package/dist/index.mjs +78 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -513,6 +513,84 @@ var init_shields = __esm({
|
|
|
513
513
|
],
|
|
514
514
|
dangerousWords: []
|
|
515
515
|
},
|
|
516
|
+
"bash-safe": {
|
|
517
|
+
name: "bash-safe",
|
|
518
|
+
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
519
|
+
aliases: ["bash", "shell"],
|
|
520
|
+
smartRules: [
|
|
521
|
+
{
|
|
522
|
+
name: "shield:bash-safe:block-pipe-to-shell",
|
|
523
|
+
tool: "bash",
|
|
524
|
+
conditions: [
|
|
525
|
+
{
|
|
526
|
+
field: "command",
|
|
527
|
+
op: "matches",
|
|
528
|
+
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
529
|
+
flags: "i"
|
|
530
|
+
}
|
|
531
|
+
],
|
|
532
|
+
verdict: "block",
|
|
533
|
+
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "shield:bash-safe:block-obfuscated-exec",
|
|
537
|
+
tool: "bash",
|
|
538
|
+
conditions: [
|
|
539
|
+
{
|
|
540
|
+
field: "command",
|
|
541
|
+
op: "matches",
|
|
542
|
+
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
543
|
+
flags: "i"
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
verdict: "block",
|
|
547
|
+
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "shield:bash-safe:block-rm-root",
|
|
551
|
+
tool: "bash",
|
|
552
|
+
conditions: [
|
|
553
|
+
{
|
|
554
|
+
field: "command",
|
|
555
|
+
op: "matches",
|
|
556
|
+
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*$",
|
|
557
|
+
flags: "i"
|
|
558
|
+
}
|
|
559
|
+
],
|
|
560
|
+
verdict: "block",
|
|
561
|
+
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "shield:bash-safe:block-disk-overwrite",
|
|
565
|
+
tool: "bash",
|
|
566
|
+
conditions: [
|
|
567
|
+
{
|
|
568
|
+
field: "command",
|
|
569
|
+
op: "matches",
|
|
570
|
+
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
571
|
+
flags: "i"
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
verdict: "block",
|
|
575
|
+
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: "shield:bash-safe:review-eval",
|
|
579
|
+
tool: "bash",
|
|
580
|
+
conditions: [
|
|
581
|
+
{
|
|
582
|
+
field: "command",
|
|
583
|
+
op: "matches",
|
|
584
|
+
value: '\\beval\\s+[\\$`("]',
|
|
585
|
+
flags: "i"
|
|
586
|
+
}
|
|
587
|
+
],
|
|
588
|
+
verdict: "review",
|
|
589
|
+
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
590
|
+
}
|
|
591
|
+
],
|
|
592
|
+
dangerousWords: []
|
|
593
|
+
},
|
|
516
594
|
filesystem: {
|
|
517
595
|
name: "filesystem",
|
|
518
596
|
description: "Protects the local filesystem from dangerous AI operations",
|
|
@@ -7070,6 +7148,7 @@ async function startTail(options = {}) {
|
|
|
7070
7148
|
}
|
|
7071
7149
|
const connectionTime = Date.now();
|
|
7072
7150
|
const activityPending = /* @__PURE__ */ new Map();
|
|
7151
|
+
const orphanedResults = /* @__PURE__ */ new Map();
|
|
7073
7152
|
let csrfToken = "";
|
|
7074
7153
|
const approvalQueue = [];
|
|
7075
7154
|
let cardActive = false;
|
|
@@ -7404,9 +7483,14 @@ async function startTail(options = {}) {
|
|
|
7404
7483
|
renderResult(data, data);
|
|
7405
7484
|
return;
|
|
7406
7485
|
}
|
|
7486
|
+
const orphaned = orphanedResults.get(data.id);
|
|
7487
|
+
if (orphaned) {
|
|
7488
|
+
orphanedResults.delete(data.id);
|
|
7489
|
+
renderResult(data, orphaned);
|
|
7490
|
+
return;
|
|
7491
|
+
}
|
|
7407
7492
|
activityPending.set(data.id, data);
|
|
7408
|
-
|
|
7409
|
-
if (slowTool) renderPending(data);
|
|
7493
|
+
renderPending(data);
|
|
7410
7494
|
}
|
|
7411
7495
|
if (event === "snapshot") {
|
|
7412
7496
|
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
@@ -7425,6 +7509,8 @@ async function startTail(options = {}) {
|
|
|
7425
7509
|
if (original) {
|
|
7426
7510
|
renderResult(original, data);
|
|
7427
7511
|
activityPending.delete(data.id);
|
|
7512
|
+
} else {
|
|
7513
|
+
orphanedResults.set(data.id, data);
|
|
7428
7514
|
}
|
|
7429
7515
|
}
|
|
7430
7516
|
}
|
|
@@ -7667,6 +7753,29 @@ function renderOffline() {
|
|
|
7667
7753
|
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7668
7754
|
`);
|
|
7669
7755
|
}
|
|
7756
|
+
function readActiveShieldsHud() {
|
|
7757
|
+
const now = Date.now();
|
|
7758
|
+
if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
|
|
7759
|
+
return shieldsCache.value;
|
|
7760
|
+
}
|
|
7761
|
+
try {
|
|
7762
|
+
const shieldsPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "shields.json");
|
|
7763
|
+
if (!import_fs26.default.existsSync(shieldsPath)) {
|
|
7764
|
+
shieldsCache = { value: [], ts: now };
|
|
7765
|
+
return [];
|
|
7766
|
+
}
|
|
7767
|
+
const parsed = JSON.parse(import_fs26.default.readFileSync(shieldsPath, "utf-8"));
|
|
7768
|
+
if (!Array.isArray(parsed.active)) {
|
|
7769
|
+
shieldsCache = { value: [], ts: now };
|
|
7770
|
+
return [];
|
|
7771
|
+
}
|
|
7772
|
+
const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
|
|
7773
|
+
shieldsCache = { value, ts: now };
|
|
7774
|
+
return value;
|
|
7775
|
+
} catch {
|
|
7776
|
+
return [];
|
|
7777
|
+
}
|
|
7778
|
+
}
|
|
7670
7779
|
function renderSecurityLine(status) {
|
|
7671
7780
|
const parts = [];
|
|
7672
7781
|
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
@@ -7684,6 +7793,18 @@ function renderSecurityLine(status) {
|
|
|
7684
7793
|
};
|
|
7685
7794
|
const mc = modeColors[status.mode] ?? WHITE;
|
|
7686
7795
|
parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
|
|
7796
|
+
const activeShields = readActiveShieldsHud();
|
|
7797
|
+
if (activeShields.length > 0) {
|
|
7798
|
+
const shieldAbbrevs = {
|
|
7799
|
+
"bash-safe": "bash",
|
|
7800
|
+
filesystem: "fs",
|
|
7801
|
+
postgres: "pg",
|
|
7802
|
+
github: "gh",
|
|
7803
|
+
aws: "aws"
|
|
7804
|
+
};
|
|
7805
|
+
const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
|
|
7806
|
+
parts.push(color(DIM, `[${labels}]`));
|
|
7807
|
+
}
|
|
7687
7808
|
if (status.mode === "observe") {
|
|
7688
7809
|
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7689
7810
|
if (status.session.wouldBlock > 0) {
|
|
@@ -7780,7 +7901,7 @@ async function main() {
|
|
|
7780
7901
|
renderOffline();
|
|
7781
7902
|
}
|
|
7782
7903
|
}
|
|
7783
|
-
var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7904
|
+
var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
|
|
7784
7905
|
var init_hud = __esm({
|
|
7785
7906
|
"src/cli/hud.ts"() {
|
|
7786
7907
|
"use strict";
|
|
@@ -7802,6 +7923,8 @@ var init_hud = __esm({
|
|
|
7802
7923
|
BAR_FILLED = "\u2588";
|
|
7803
7924
|
BAR_EMPTY = "\u2591";
|
|
7804
7925
|
BAR_WIDTH = 10;
|
|
7926
|
+
shieldsCache = null;
|
|
7927
|
+
SHIELDS_CACHE_TTL_MS = 2e3;
|
|
7805
7928
|
}
|
|
7806
7929
|
});
|
|
7807
7930
|
|
|
@@ -7815,6 +7938,7 @@ var import_path14 = __toESM(require("path"));
|
|
|
7815
7938
|
var import_os10 = __toESM(require("os"));
|
|
7816
7939
|
var import_chalk = __toESM(require("chalk"));
|
|
7817
7940
|
var import_prompts = require("@inquirer/prompts");
|
|
7941
|
+
var import_smol_toml = require("smol-toml");
|
|
7818
7942
|
var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
|
|
7819
7943
|
function hasNode9McpServer(servers) {
|
|
7820
7944
|
const entry = servers["node9"];
|
|
@@ -8178,7 +8302,8 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
|
|
|
8178
8302
|
return {
|
|
8179
8303
|
claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
|
|
8180
8304
|
gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
|
|
8181
|
-
cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
|
|
8305
|
+
cursor: exists(import_path14.default.join(homeDir2, ".cursor")),
|
|
8306
|
+
codex: exists(import_path14.default.join(homeDir2, ".codex"))
|
|
8182
8307
|
};
|
|
8183
8308
|
}
|
|
8184
8309
|
async function setupCursor() {
|
|
@@ -8243,6 +8368,82 @@ async function setupCursor() {
|
|
|
8243
8368
|
printDaemonTip();
|
|
8244
8369
|
}
|
|
8245
8370
|
}
|
|
8371
|
+
function readToml(filePath) {
|
|
8372
|
+
try {
|
|
8373
|
+
if (import_fs11.default.existsSync(filePath)) {
|
|
8374
|
+
return (0, import_smol_toml.parse)(import_fs11.default.readFileSync(filePath, "utf-8"));
|
|
8375
|
+
}
|
|
8376
|
+
} catch {
|
|
8377
|
+
}
|
|
8378
|
+
return null;
|
|
8379
|
+
}
|
|
8380
|
+
function writeToml(filePath, data) {
|
|
8381
|
+
const dir = import_path14.default.dirname(filePath);
|
|
8382
|
+
if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
|
|
8383
|
+
import_fs11.default.writeFileSync(filePath, (0, import_smol_toml.stringify)(data));
|
|
8384
|
+
}
|
|
8385
|
+
async function setupCodex() {
|
|
8386
|
+
const homeDir2 = import_os10.default.homedir();
|
|
8387
|
+
const configPath = import_path14.default.join(homeDir2, ".codex", "config.toml");
|
|
8388
|
+
const config = readToml(configPath) ?? {};
|
|
8389
|
+
const servers = config.mcp_servers ?? {};
|
|
8390
|
+
let anythingChanged = false;
|
|
8391
|
+
if (!hasNode9McpServer(servers)) {
|
|
8392
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8393
|
+
config.mcp_servers = servers;
|
|
8394
|
+
writeToml(configPath, config);
|
|
8395
|
+
console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8396
|
+
anythingChanged = true;
|
|
8397
|
+
}
|
|
8398
|
+
const serversToWrap = [];
|
|
8399
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
8400
|
+
if (!server.command || server.command === "node9") continue;
|
|
8401
|
+
const parts = [server.command, ...server.args ?? []];
|
|
8402
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
8403
|
+
}
|
|
8404
|
+
if (serversToWrap.length > 0) {
|
|
8405
|
+
console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
|
|
8406
|
+
console.log(import_chalk.default.white(` ${configPath}`));
|
|
8407
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
8408
|
+
console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
8409
|
+
}
|
|
8410
|
+
console.log("");
|
|
8411
|
+
const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
8412
|
+
if (proceed) {
|
|
8413
|
+
for (const { name, parts } of serversToWrap) {
|
|
8414
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
8415
|
+
}
|
|
8416
|
+
config.mcp_servers = servers;
|
|
8417
|
+
writeToml(configPath, config);
|
|
8418
|
+
console.log(import_chalk.default.green(`
|
|
8419
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8420
|
+
anythingChanged = true;
|
|
8421
|
+
} else {
|
|
8422
|
+
console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
|
|
8423
|
+
}
|
|
8424
|
+
console.log("");
|
|
8425
|
+
}
|
|
8426
|
+
console.log(
|
|
8427
|
+
import_chalk.default.yellow(
|
|
8428
|
+
" \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."
|
|
8429
|
+
)
|
|
8430
|
+
);
|
|
8431
|
+
console.log("");
|
|
8432
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
8433
|
+
console.log(
|
|
8434
|
+
import_chalk.default.blue(
|
|
8435
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
|
|
8436
|
+
)
|
|
8437
|
+
);
|
|
8438
|
+
printDaemonTip();
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
if (anythingChanged) {
|
|
8442
|
+
console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
|
|
8443
|
+
console.log(import_chalk.default.gray(" Restart Codex for changes to take effect."));
|
|
8444
|
+
printDaemonTip();
|
|
8445
|
+
}
|
|
8446
|
+
}
|
|
8246
8447
|
function setupHud() {
|
|
8247
8448
|
const homeDir2 = import_os10.default.homedir();
|
|
8248
8449
|
const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
|
|
@@ -9936,6 +10137,8 @@ var import_path25 = __toESM(require("path"));
|
|
|
9936
10137
|
var import_os19 = __toESM(require("os"));
|
|
9937
10138
|
var import_https = __toESM(require("https"));
|
|
9938
10139
|
init_core();
|
|
10140
|
+
init_shields();
|
|
10141
|
+
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
9939
10142
|
function fireTelemetryPing(agents) {
|
|
9940
10143
|
try {
|
|
9941
10144
|
const body = JSON.stringify({
|
|
@@ -9978,7 +10181,17 @@ function registerInitCommand(program2) {
|
|
|
9978
10181
|
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
9979
10182
|
default: true
|
|
9980
10183
|
});
|
|
9981
|
-
if (enableShields)
|
|
10184
|
+
if (enableShields) {
|
|
10185
|
+
chosenMode = "standard";
|
|
10186
|
+
try {
|
|
10187
|
+
const current = readActiveShields();
|
|
10188
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
|
|
10189
|
+
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10190
|
+
if (hasNewShields) writeActiveShields(merged);
|
|
10191
|
+
} catch (err2) {
|
|
10192
|
+
console.log(import_chalk11.default.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10193
|
+
}
|
|
10194
|
+
}
|
|
9982
10195
|
console.log("");
|
|
9983
10196
|
}
|
|
9984
10197
|
const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
@@ -10016,9 +10229,9 @@ function registerInitCommand(program2) {
|
|
|
10016
10229
|
);
|
|
10017
10230
|
if (found.length === 0) {
|
|
10018
10231
|
console.log(
|
|
10019
|
-
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or
|
|
10232
|
+
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
10020
10233
|
);
|
|
10021
|
-
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
10234
|
+
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
10022
10235
|
return;
|
|
10023
10236
|
}
|
|
10024
10237
|
console.log(import_chalk11.default.bold("Detected agents:"));
|
|
@@ -10031,6 +10244,7 @@ function registerInitCommand(program2) {
|
|
|
10031
10244
|
if (agent === "claude") await setupClaude();
|
|
10032
10245
|
else if (agent === "gemini") await setupGemini();
|
|
10033
10246
|
else if (agent === "cursor") await setupCursor();
|
|
10247
|
+
else if (agent === "codex") await setupCodex();
|
|
10034
10248
|
console.log("");
|
|
10035
10249
|
}
|
|
10036
10250
|
{
|
|
@@ -10652,6 +10866,20 @@ var TOOLS = [
|
|
|
10652
10866
|
required: ["service"]
|
|
10653
10867
|
}
|
|
10654
10868
|
},
|
|
10869
|
+
{
|
|
10870
|
+
name: "node9_shield_disable",
|
|
10871
|
+
description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
|
|
10872
|
+
inputSchema: {
|
|
10873
|
+
type: "object",
|
|
10874
|
+
properties: {
|
|
10875
|
+
service: {
|
|
10876
|
+
type: "string",
|
|
10877
|
+
description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
|
|
10878
|
+
}
|
|
10879
|
+
},
|
|
10880
|
+
required: ["service"]
|
|
10881
|
+
}
|
|
10882
|
+
},
|
|
10655
10883
|
{
|
|
10656
10884
|
name: "node9_approver_list",
|
|
10657
10885
|
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).",
|
|
@@ -10781,6 +11009,24 @@ function handleShieldEnable(args) {
|
|
|
10781
11009
|
const shield = getShield(name);
|
|
10782
11010
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10783
11011
|
}
|
|
11012
|
+
function handleShieldDisable(args) {
|
|
11013
|
+
const service = args.service;
|
|
11014
|
+
if (typeof service !== "string" || !service) {
|
|
11015
|
+
throw new Error("service is required");
|
|
11016
|
+
}
|
|
11017
|
+
const name = resolveShieldName(service);
|
|
11018
|
+
if (!name) {
|
|
11019
|
+
throw new Error(
|
|
11020
|
+
`Unknown shield: "${service}". Run node9_shield_list to see available shields.`
|
|
11021
|
+
);
|
|
11022
|
+
}
|
|
11023
|
+
const active = readActiveShields();
|
|
11024
|
+
if (!active.includes(name)) {
|
|
11025
|
+
return `Shield "${name}" is not active.`;
|
|
11026
|
+
}
|
|
11027
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
11028
|
+
return `Shield "${name}" disabled.`;
|
|
11029
|
+
}
|
|
10784
11030
|
var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
10785
11031
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
10786
11032
|
function readGlobalConfigRaw() {
|
|
@@ -10909,6 +11155,8 @@ function runMcpServer() {
|
|
|
10909
11155
|
text = handleShieldList();
|
|
10910
11156
|
} else if (toolName === "node9_shield_enable") {
|
|
10911
11157
|
text = handleShieldEnable(toolArgs);
|
|
11158
|
+
} else if (toolName === "node9_shield_disable") {
|
|
11159
|
+
text = handleShieldDisable(toolArgs);
|
|
10912
11160
|
} else if (toolName === "node9_approver_list") {
|
|
10913
11161
|
text = handleApproverList();
|
|
10914
11162
|
} else if (toolName === "node9_approver_set") {
|
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",
|
|
@@ -7052,6 +7130,7 @@ async function startTail(options = {}) {
|
|
|
7052
7130
|
}
|
|
7053
7131
|
const connectionTime = Date.now();
|
|
7054
7132
|
const activityPending = /* @__PURE__ */ new Map();
|
|
7133
|
+
const orphanedResults = /* @__PURE__ */ new Map();
|
|
7055
7134
|
let csrfToken = "";
|
|
7056
7135
|
const approvalQueue = [];
|
|
7057
7136
|
let cardActive = false;
|
|
@@ -7386,9 +7465,14 @@ async function startTail(options = {}) {
|
|
|
7386
7465
|
renderResult(data, data);
|
|
7387
7466
|
return;
|
|
7388
7467
|
}
|
|
7468
|
+
const orphaned = orphanedResults.get(data.id);
|
|
7469
|
+
if (orphaned) {
|
|
7470
|
+
orphanedResults.delete(data.id);
|
|
7471
|
+
renderResult(data, orphaned);
|
|
7472
|
+
return;
|
|
7473
|
+
}
|
|
7389
7474
|
activityPending.set(data.id, data);
|
|
7390
|
-
|
|
7391
|
-
if (slowTool) renderPending(data);
|
|
7475
|
+
renderPending(data);
|
|
7392
7476
|
}
|
|
7393
7477
|
if (event === "snapshot") {
|
|
7394
7478
|
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
@@ -7407,6 +7491,8 @@ async function startTail(options = {}) {
|
|
|
7407
7491
|
if (original) {
|
|
7408
7492
|
renderResult(original, data);
|
|
7409
7493
|
activityPending.delete(data.id);
|
|
7494
|
+
} else {
|
|
7495
|
+
orphanedResults.set(data.id, data);
|
|
7410
7496
|
}
|
|
7411
7497
|
}
|
|
7412
7498
|
}
|
|
@@ -7646,6 +7732,29 @@ function renderOffline() {
|
|
|
7646
7732
|
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7647
7733
|
`);
|
|
7648
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
|
+
}
|
|
7649
7758
|
function renderSecurityLine(status) {
|
|
7650
7759
|
const parts = [];
|
|
7651
7760
|
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
@@ -7663,6 +7772,18 @@ function renderSecurityLine(status) {
|
|
|
7663
7772
|
};
|
|
7664
7773
|
const mc = modeColors[status.mode] ?? WHITE;
|
|
7665
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
|
+
}
|
|
7666
7787
|
if (status.mode === "observe") {
|
|
7667
7788
|
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7668
7789
|
if (status.session.wouldBlock > 0) {
|
|
@@ -7759,7 +7880,7 @@ async function main() {
|
|
|
7759
7880
|
renderOffline();
|
|
7760
7881
|
}
|
|
7761
7882
|
}
|
|
7762
|
-
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;
|
|
7763
7884
|
var init_hud = __esm({
|
|
7764
7885
|
"src/cli/hud.ts"() {
|
|
7765
7886
|
"use strict";
|
|
@@ -7777,6 +7898,8 @@ var init_hud = __esm({
|
|
|
7777
7898
|
BAR_FILLED = "\u2588";
|
|
7778
7899
|
BAR_EMPTY = "\u2591";
|
|
7779
7900
|
BAR_WIDTH = 10;
|
|
7901
|
+
shieldsCache = null;
|
|
7902
|
+
SHIELDS_CACHE_TTL_MS = 2e3;
|
|
7780
7903
|
}
|
|
7781
7904
|
});
|
|
7782
7905
|
|
|
@@ -7790,6 +7913,7 @@ import path14 from "path";
|
|
|
7790
7913
|
import os10 from "os";
|
|
7791
7914
|
import chalk from "chalk";
|
|
7792
7915
|
import { confirm } from "@inquirer/prompts";
|
|
7916
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7793
7917
|
var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
|
|
7794
7918
|
function hasNode9McpServer(servers) {
|
|
7795
7919
|
const entry = servers["node9"];
|
|
@@ -8153,7 +8277,8 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
8153
8277
|
return {
|
|
8154
8278
|
claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
|
|
8155
8279
|
gemini: exists(path14.join(homeDir2, ".gemini")),
|
|
8156
|
-
cursor: exists(path14.join(homeDir2, ".cursor"))
|
|
8280
|
+
cursor: exists(path14.join(homeDir2, ".cursor")),
|
|
8281
|
+
codex: exists(path14.join(homeDir2, ".codex"))
|
|
8157
8282
|
};
|
|
8158
8283
|
}
|
|
8159
8284
|
async function setupCursor() {
|
|
@@ -8218,6 +8343,82 @@ async function setupCursor() {
|
|
|
8218
8343
|
printDaemonTip();
|
|
8219
8344
|
}
|
|
8220
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
|
+
}
|
|
8221
8422
|
function setupHud() {
|
|
8222
8423
|
const homeDir2 = os10.homedir();
|
|
8223
8424
|
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
@@ -9911,6 +10112,8 @@ import fs23 from "fs";
|
|
|
9911
10112
|
import path25 from "path";
|
|
9912
10113
|
import os19 from "os";
|
|
9913
10114
|
import https from "https";
|
|
10115
|
+
init_shields();
|
|
10116
|
+
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
9914
10117
|
function fireTelemetryPing(agents) {
|
|
9915
10118
|
try {
|
|
9916
10119
|
const body = JSON.stringify({
|
|
@@ -9953,7 +10156,17 @@ function registerInitCommand(program2) {
|
|
|
9953
10156
|
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
9954
10157
|
default: true
|
|
9955
10158
|
});
|
|
9956
|
-
if (enableShields)
|
|
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
|
+
}
|
|
9957
10170
|
console.log("");
|
|
9958
10171
|
}
|
|
9959
10172
|
const configPath = path25.join(os19.homedir(), ".node9", "config.json");
|
|
@@ -9991,9 +10204,9 @@ function registerInitCommand(program2) {
|
|
|
9991
10204
|
);
|
|
9992
10205
|
if (found.length === 0) {
|
|
9993
10206
|
console.log(
|
|
9994
|
-
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")
|
|
9995
10208
|
);
|
|
9996
|
-
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
10209
|
+
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
9997
10210
|
return;
|
|
9998
10211
|
}
|
|
9999
10212
|
console.log(chalk11.bold("Detected agents:"));
|
|
@@ -10006,6 +10219,7 @@ function registerInitCommand(program2) {
|
|
|
10006
10219
|
if (agent === "claude") await setupClaude();
|
|
10007
10220
|
else if (agent === "gemini") await setupGemini();
|
|
10008
10221
|
else if (agent === "cursor") await setupCursor();
|
|
10222
|
+
else if (agent === "codex") await setupCodex();
|
|
10009
10223
|
console.log("");
|
|
10010
10224
|
}
|
|
10011
10225
|
{
|
|
@@ -10627,6 +10841,20 @@ var TOOLS = [
|
|
|
10627
10841
|
required: ["service"]
|
|
10628
10842
|
}
|
|
10629
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
|
+
},
|
|
10630
10858
|
{
|
|
10631
10859
|
name: "node9_approver_list",
|
|
10632
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).",
|
|
@@ -10756,6 +10984,24 @@ function handleShieldEnable(args) {
|
|
|
10756
10984
|
const shield = getShield(name);
|
|
10757
10985
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10758
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
|
+
}
|
|
10759
11005
|
var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
|
|
10760
11006
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
10761
11007
|
function readGlobalConfigRaw() {
|
|
@@ -10884,6 +11130,8 @@ function runMcpServer() {
|
|
|
10884
11130
|
text = handleShieldList();
|
|
10885
11131
|
} else if (toolName === "node9_shield_enable") {
|
|
10886
11132
|
text = handleShieldEnable(toolArgs);
|
|
11133
|
+
} else if (toolName === "node9_shield_disable") {
|
|
11134
|
+
text = handleShieldDisable(toolArgs);
|
|
10887
11135
|
} else if (toolName === "node9_approver_list") {
|
|
10888
11136
|
text = handleApproverList();
|
|
10889
11137
|
} else if (toolName === "node9_approver_set") {
|
package/dist/index.js
CHANGED
|
@@ -411,6 +411,84 @@ var SHIELDS = {
|
|
|
411
411
|
],
|
|
412
412
|
dangerousWords: []
|
|
413
413
|
},
|
|
414
|
+
"bash-safe": {
|
|
415
|
+
name: "bash-safe",
|
|
416
|
+
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
417
|
+
aliases: ["bash", "shell"],
|
|
418
|
+
smartRules: [
|
|
419
|
+
{
|
|
420
|
+
name: "shield:bash-safe:block-pipe-to-shell",
|
|
421
|
+
tool: "bash",
|
|
422
|
+
conditions: [
|
|
423
|
+
{
|
|
424
|
+
field: "command",
|
|
425
|
+
op: "matches",
|
|
426
|
+
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
427
|
+
flags: "i"
|
|
428
|
+
}
|
|
429
|
+
],
|
|
430
|
+
verdict: "block",
|
|
431
|
+
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "shield:bash-safe:block-obfuscated-exec",
|
|
435
|
+
tool: "bash",
|
|
436
|
+
conditions: [
|
|
437
|
+
{
|
|
438
|
+
field: "command",
|
|
439
|
+
op: "matches",
|
|
440
|
+
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
441
|
+
flags: "i"
|
|
442
|
+
}
|
|
443
|
+
],
|
|
444
|
+
verdict: "block",
|
|
445
|
+
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "shield:bash-safe:block-rm-root",
|
|
449
|
+
tool: "bash",
|
|
450
|
+
conditions: [
|
|
451
|
+
{
|
|
452
|
+
field: "command",
|
|
453
|
+
op: "matches",
|
|
454
|
+
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*$",
|
|
455
|
+
flags: "i"
|
|
456
|
+
}
|
|
457
|
+
],
|
|
458
|
+
verdict: "block",
|
|
459
|
+
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "shield:bash-safe:block-disk-overwrite",
|
|
463
|
+
tool: "bash",
|
|
464
|
+
conditions: [
|
|
465
|
+
{
|
|
466
|
+
field: "command",
|
|
467
|
+
op: "matches",
|
|
468
|
+
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
469
|
+
flags: "i"
|
|
470
|
+
}
|
|
471
|
+
],
|
|
472
|
+
verdict: "block",
|
|
473
|
+
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "shield:bash-safe:review-eval",
|
|
477
|
+
tool: "bash",
|
|
478
|
+
conditions: [
|
|
479
|
+
{
|
|
480
|
+
field: "command",
|
|
481
|
+
op: "matches",
|
|
482
|
+
value: '\\beval\\s+[\\$`("]',
|
|
483
|
+
flags: "i"
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
verdict: "review",
|
|
487
|
+
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
488
|
+
}
|
|
489
|
+
],
|
|
490
|
+
dangerousWords: []
|
|
491
|
+
},
|
|
414
492
|
filesystem: {
|
|
415
493
|
name: "filesystem",
|
|
416
494
|
description: "Protects the local filesystem from dangerous AI operations",
|
package/dist/index.mjs
CHANGED
|
@@ -381,6 +381,84 @@ var SHIELDS = {
|
|
|
381
381
|
],
|
|
382
382
|
dangerousWords: []
|
|
383
383
|
},
|
|
384
|
+
"bash-safe": {
|
|
385
|
+
name: "bash-safe",
|
|
386
|
+
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
387
|
+
aliases: ["bash", "shell"],
|
|
388
|
+
smartRules: [
|
|
389
|
+
{
|
|
390
|
+
name: "shield:bash-safe:block-pipe-to-shell",
|
|
391
|
+
tool: "bash",
|
|
392
|
+
conditions: [
|
|
393
|
+
{
|
|
394
|
+
field: "command",
|
|
395
|
+
op: "matches",
|
|
396
|
+
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
397
|
+
flags: "i"
|
|
398
|
+
}
|
|
399
|
+
],
|
|
400
|
+
verdict: "block",
|
|
401
|
+
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "shield:bash-safe:block-obfuscated-exec",
|
|
405
|
+
tool: "bash",
|
|
406
|
+
conditions: [
|
|
407
|
+
{
|
|
408
|
+
field: "command",
|
|
409
|
+
op: "matches",
|
|
410
|
+
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
411
|
+
flags: "i"
|
|
412
|
+
}
|
|
413
|
+
],
|
|
414
|
+
verdict: "block",
|
|
415
|
+
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "shield:bash-safe:block-rm-root",
|
|
419
|
+
tool: "bash",
|
|
420
|
+
conditions: [
|
|
421
|
+
{
|
|
422
|
+
field: "command",
|
|
423
|
+
op: "matches",
|
|
424
|
+
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*$",
|
|
425
|
+
flags: "i"
|
|
426
|
+
}
|
|
427
|
+
],
|
|
428
|
+
verdict: "block",
|
|
429
|
+
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "shield:bash-safe:block-disk-overwrite",
|
|
433
|
+
tool: "bash",
|
|
434
|
+
conditions: [
|
|
435
|
+
{
|
|
436
|
+
field: "command",
|
|
437
|
+
op: "matches",
|
|
438
|
+
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
439
|
+
flags: "i"
|
|
440
|
+
}
|
|
441
|
+
],
|
|
442
|
+
verdict: "block",
|
|
443
|
+
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "shield:bash-safe:review-eval",
|
|
447
|
+
tool: "bash",
|
|
448
|
+
conditions: [
|
|
449
|
+
{
|
|
450
|
+
field: "command",
|
|
451
|
+
op: "matches",
|
|
452
|
+
value: '\\beval\\s+[\\$`("]',
|
|
453
|
+
flags: "i"
|
|
454
|
+
}
|
|
455
|
+
],
|
|
456
|
+
verdict: "review",
|
|
457
|
+
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
458
|
+
}
|
|
459
|
+
],
|
|
460
|
+
dangerousWords: []
|
|
461
|
+
},
|
|
384
462
|
filesystem: {
|
|
385
463
|
name: "filesystem",
|
|
386
464
|
description: "Protects the local filesystem from dangerous AI operations",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"picomatch": "^4.0.3",
|
|
76
76
|
"safe-regex2": "^5.1.0",
|
|
77
77
|
"sh-syntax": "^0.5.8",
|
|
78
|
+
"smol-toml": "^1.6.1",
|
|
78
79
|
"zod": "^3.25.76"
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|