@node9/proxy 1.0.1 → 1.0.3
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 +28 -4
- package/dist/cli.js +211 -148
- package/dist/cli.mjs +211 -148
- package/dist/index.js +194 -76
- package/dist/index.mjs +194 -76
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -84,21 +84,47 @@ function sendDesktopNotification(title, body) {
|
|
|
84
84
|
} catch {
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
function escapePango(text) {
|
|
88
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
89
|
+
}
|
|
90
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
93
|
+
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
94
|
+
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push(formattedArgs);
|
|
97
|
+
if (!locked) {
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
104
|
+
const lines = [];
|
|
105
|
+
if (locked) {
|
|
106
|
+
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
lines.push(
|
|
110
|
+
`<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
|
|
111
|
+
);
|
|
112
|
+
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
115
|
+
if (!locked) {
|
|
116
|
+
lines.push("");
|
|
117
|
+
lines.push(
|
|
118
|
+
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
87
123
|
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
88
124
|
if (isTestEnv()) return "deny";
|
|
89
125
|
const formattedArgs = formatArgs(args);
|
|
90
126
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
91
|
-
|
|
92
|
-
if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
|
|
93
|
-
`;
|
|
94
|
-
message += `Tool: ${toolName}
|
|
95
|
-
`;
|
|
96
|
-
message += `Agent: ${agent || "AI Agent"}
|
|
97
|
-
`;
|
|
98
|
-
message += `Rule: ${explainableLabel || "Security Policy"}
|
|
99
|
-
|
|
100
|
-
`;
|
|
101
|
-
message += `${formattedArgs}`;
|
|
127
|
+
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
102
128
|
process.stderr.write(chalk.yellow(`
|
|
103
129
|
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
104
130
|
`));
|
|
@@ -119,7 +145,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
119
145
|
}
|
|
120
146
|
try {
|
|
121
147
|
if (process.platform === "darwin") {
|
|
122
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
148
|
+
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "Always Allow", "Allow \u21B5"} default button "Allow \u21B5" cancel button "Block \u238B"`;
|
|
123
149
|
const script = `on run argv
|
|
124
150
|
tell application "System Events"
|
|
125
151
|
activate
|
|
@@ -128,21 +154,28 @@ end tell
|
|
|
128
154
|
end run`;
|
|
129
155
|
childProcess = spawn("osascript", ["-e", script, "--", message, title]);
|
|
130
156
|
} else if (process.platform === "linux") {
|
|
157
|
+
const pangoMessage = buildPangoMessage(
|
|
158
|
+
toolName,
|
|
159
|
+
formattedArgs,
|
|
160
|
+
agent,
|
|
161
|
+
explainableLabel,
|
|
162
|
+
locked
|
|
163
|
+
);
|
|
131
164
|
const argsList = [
|
|
132
165
|
locked ? "--info" : "--question",
|
|
133
166
|
"--modal",
|
|
134
|
-
"--width=
|
|
167
|
+
"--width=480",
|
|
135
168
|
"--title",
|
|
136
169
|
title,
|
|
137
170
|
"--text",
|
|
138
|
-
|
|
171
|
+
pangoMessage,
|
|
139
172
|
"--ok-label",
|
|
140
|
-
locked ? "Waiting..." : "Allow",
|
|
173
|
+
locked ? "Waiting..." : "Allow \u21B5",
|
|
141
174
|
"--timeout",
|
|
142
175
|
"300"
|
|
143
176
|
];
|
|
144
177
|
if (!locked) {
|
|
145
|
-
argsList.push("--cancel-label", "Block");
|
|
178
|
+
argsList.push("--cancel-label", "Block \u238B");
|
|
146
179
|
argsList.push("--extra-button", "Always Allow");
|
|
147
180
|
}
|
|
148
181
|
childProcess = spawn("zenity", argsList);
|
|
@@ -170,6 +203,8 @@ end run`;
|
|
|
170
203
|
// src/core.ts
|
|
171
204
|
var PAUSED_FILE = path.join(os.homedir(), ".node9", "PAUSED");
|
|
172
205
|
var TRUST_FILE = path.join(os.homedir(), ".node9", "trust.json");
|
|
206
|
+
var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
207
|
+
var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
173
208
|
function checkPause() {
|
|
174
209
|
try {
|
|
175
210
|
if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -236,36 +271,39 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
236
271
|
}
|
|
237
272
|
}
|
|
238
273
|
}
|
|
239
|
-
function
|
|
274
|
+
function appendToLog(logPath, entry) {
|
|
240
275
|
try {
|
|
241
|
-
const entry = JSON.stringify({
|
|
242
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
243
|
-
tool: toolName,
|
|
244
|
-
args,
|
|
245
|
-
decision: "would-have-blocked",
|
|
246
|
-
source: "audit-mode"
|
|
247
|
-
});
|
|
248
|
-
const logPath = path.join(os.homedir(), ".node9", "audit.log");
|
|
249
276
|
const dir = path.dirname(logPath);
|
|
250
277
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
251
|
-
fs.appendFileSync(logPath, entry + "\n");
|
|
278
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
252
279
|
} catch {
|
|
253
280
|
}
|
|
254
281
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
282
|
+
function appendHookDebug(toolName, args, meta) {
|
|
283
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
284
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
285
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
286
|
+
tool: toolName,
|
|
287
|
+
args: safeArgs,
|
|
288
|
+
agent: meta?.agent,
|
|
289
|
+
mcpServer: meta?.mcpServer,
|
|
290
|
+
hostname: os.hostname(),
|
|
291
|
+
cwd: process.cwd()
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
295
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
296
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
297
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
298
|
+
tool: toolName,
|
|
299
|
+
args: safeArgs,
|
|
300
|
+
decision,
|
|
301
|
+
checkedBy,
|
|
302
|
+
agent: meta?.agent,
|
|
303
|
+
mcpServer: meta?.mcpServer,
|
|
304
|
+
hostname: os.hostname()
|
|
305
|
+
});
|
|
306
|
+
}
|
|
269
307
|
function tokenize(toolName) {
|
|
270
308
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
271
309
|
}
|
|
@@ -372,16 +410,28 @@ function redactSecrets(text) {
|
|
|
372
410
|
);
|
|
373
411
|
return redacted;
|
|
374
412
|
}
|
|
413
|
+
var DANGEROUS_WORDS = [
|
|
414
|
+
"drop",
|
|
415
|
+
"truncate",
|
|
416
|
+
"purge",
|
|
417
|
+
"format",
|
|
418
|
+
"destroy",
|
|
419
|
+
"terminate",
|
|
420
|
+
"revoke",
|
|
421
|
+
"docker",
|
|
422
|
+
"psql"
|
|
423
|
+
];
|
|
375
424
|
var DEFAULT_CONFIG = {
|
|
376
425
|
settings: {
|
|
377
426
|
mode: "standard",
|
|
378
427
|
autoStartDaemon: true,
|
|
379
|
-
enableUndo:
|
|
428
|
+
enableUndo: true,
|
|
429
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
380
430
|
enableHookLogDebug: false,
|
|
381
431
|
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
382
432
|
},
|
|
383
433
|
policy: {
|
|
384
|
-
sandboxPaths: [],
|
|
434
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
385
435
|
dangerousWords: DANGEROUS_WORDS,
|
|
386
436
|
ignoredTools: [
|
|
387
437
|
"list_*",
|
|
@@ -389,12 +439,44 @@ var DEFAULT_CONFIG = {
|
|
|
389
439
|
"read_*",
|
|
390
440
|
"describe_*",
|
|
391
441
|
"read",
|
|
442
|
+
"glob",
|
|
392
443
|
"grep",
|
|
393
444
|
"ls",
|
|
394
|
-
"
|
|
445
|
+
"notebookread",
|
|
446
|
+
"notebookedit",
|
|
447
|
+
"webfetch",
|
|
448
|
+
"websearch",
|
|
449
|
+
"exitplanmode",
|
|
450
|
+
"askuserquestion",
|
|
451
|
+
"agent",
|
|
452
|
+
"task*",
|
|
453
|
+
"toolsearch",
|
|
454
|
+
"mcp__ide__*",
|
|
455
|
+
"getDiagnostics"
|
|
395
456
|
],
|
|
396
|
-
toolInspection: {
|
|
397
|
-
|
|
457
|
+
toolInspection: {
|
|
458
|
+
bash: "command",
|
|
459
|
+
shell: "command",
|
|
460
|
+
run_shell_command: "command",
|
|
461
|
+
"terminal.execute": "command",
|
|
462
|
+
"postgres:query": "sql"
|
|
463
|
+
},
|
|
464
|
+
rules: [
|
|
465
|
+
{
|
|
466
|
+
action: "rm",
|
|
467
|
+
allowPaths: [
|
|
468
|
+
"**/node_modules/**",
|
|
469
|
+
"dist/**",
|
|
470
|
+
"build/**",
|
|
471
|
+
".next/**",
|
|
472
|
+
"coverage/**",
|
|
473
|
+
".cache/**",
|
|
474
|
+
"tmp/**",
|
|
475
|
+
"temp/**",
|
|
476
|
+
".DS_Store"
|
|
477
|
+
]
|
|
478
|
+
}
|
|
479
|
+
]
|
|
398
480
|
},
|
|
399
481
|
environments: {}
|
|
400
482
|
};
|
|
@@ -459,20 +541,15 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
459
541
|
}
|
|
460
542
|
const isManual = agent === "Terminal";
|
|
461
543
|
if (isManual) {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
"
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"revoke",
|
|
472
|
-
"docker"
|
|
473
|
-
];
|
|
474
|
-
const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
|
|
475
|
-
if (!hasNuclear) return { decision: "allow" };
|
|
544
|
+
const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
|
|
545
|
+
const hasSystemDisaster = allTokens.some(
|
|
546
|
+
(t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
|
|
547
|
+
);
|
|
548
|
+
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
549
|
+
if (hasSystemDisaster || isRootWipe) {
|
|
550
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
551
|
+
}
|
|
552
|
+
return { decision: "allow" };
|
|
476
553
|
}
|
|
477
554
|
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
478
555
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
@@ -486,27 +563,39 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
486
563
|
if (pathTokens.length > 0) {
|
|
487
564
|
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
488
565
|
if (anyBlocked)
|
|
489
|
-
return {
|
|
566
|
+
return {
|
|
567
|
+
decision: "review",
|
|
568
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
569
|
+
};
|
|
490
570
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
491
571
|
if (allAllowed) return { decision: "allow" };
|
|
492
572
|
}
|
|
493
|
-
return {
|
|
573
|
+
return {
|
|
574
|
+
decision: "review",
|
|
575
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
576
|
+
};
|
|
494
577
|
}
|
|
495
578
|
}
|
|
579
|
+
let matchedDangerousWord;
|
|
496
580
|
const isDangerous = allTokens.some(
|
|
497
581
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
498
582
|
const w = word.toLowerCase();
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
583
|
+
const hit = token === w || (() => {
|
|
584
|
+
try {
|
|
585
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
})();
|
|
590
|
+
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
591
|
+
return hit;
|
|
505
592
|
})
|
|
506
593
|
);
|
|
507
594
|
if (isDangerous) {
|
|
508
|
-
|
|
509
|
-
|
|
595
|
+
return {
|
|
596
|
+
decision: "review",
|
|
597
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
|
|
598
|
+
};
|
|
510
599
|
}
|
|
511
600
|
if (config.settings.mode === "strict") {
|
|
512
601
|
const envConfig = getActiveEnvironment(config);
|
|
@@ -621,13 +710,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
621
710
|
approvers.browser = false;
|
|
622
711
|
approvers.terminal = false;
|
|
623
712
|
}
|
|
713
|
+
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
714
|
+
appendHookDebug(toolName, args, meta);
|
|
715
|
+
}
|
|
624
716
|
const isManual = meta?.agent === "Terminal";
|
|
625
717
|
let explainableLabel = "Local Config";
|
|
626
718
|
if (config.settings.mode === "audit") {
|
|
627
719
|
if (!isIgnoredTool(toolName)) {
|
|
628
720
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
629
721
|
if (policyResult.decision === "review") {
|
|
630
|
-
|
|
722
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
631
723
|
sendDesktopNotification(
|
|
632
724
|
"Node9 Audit Mode",
|
|
633
725
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -639,20 +731,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
639
731
|
if (!isIgnoredTool(toolName)) {
|
|
640
732
|
if (getActiveTrustSession(toolName)) {
|
|
641
733
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
734
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
642
735
|
return { approved: true, checkedBy: "trust" };
|
|
643
736
|
}
|
|
644
737
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
645
738
|
if (policyResult.decision === "allow") {
|
|
646
739
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
740
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
647
741
|
return { approved: true, checkedBy: "local-policy" };
|
|
648
742
|
}
|
|
649
743
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
650
744
|
const persistent = getPersistentDecision(toolName);
|
|
651
745
|
if (persistent === "allow") {
|
|
652
746
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
747
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
653
748
|
return { approved: true, checkedBy: "persistent" };
|
|
654
749
|
}
|
|
655
750
|
if (persistent === "deny") {
|
|
751
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
|
|
656
752
|
return {
|
|
657
753
|
approved: false,
|
|
658
754
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -662,6 +758,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
662
758
|
}
|
|
663
759
|
} else {
|
|
664
760
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
761
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
665
762
|
return { approved: true };
|
|
666
763
|
}
|
|
667
764
|
let cloudRequestId = null;
|
|
@@ -669,8 +766,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
669
766
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
670
767
|
if (cloudEnforced) {
|
|
671
768
|
try {
|
|
672
|
-
const
|
|
673
|
-
const initResult = await initNode9SaaS(toolName, args, creds, envConfig?.slackChannel, meta);
|
|
769
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
674
770
|
if (!initResult.pending) {
|
|
675
771
|
return {
|
|
676
772
|
approved: !!initResult.approved,
|
|
@@ -886,6 +982,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
886
982
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
887
983
|
await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
|
|
888
984
|
}
|
|
985
|
+
if (!isManual) {
|
|
986
|
+
appendLocalAudit(
|
|
987
|
+
toolName,
|
|
988
|
+
args,
|
|
989
|
+
finalResult.approved ? "allow" : "deny",
|
|
990
|
+
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
991
|
+
meta
|
|
992
|
+
);
|
|
993
|
+
}
|
|
889
994
|
return finalResult;
|
|
890
995
|
}
|
|
891
996
|
function getConfig() {
|
|
@@ -915,9 +1020,10 @@ function getConfig() {
|
|
|
915
1020
|
if (s.enableHookLogDebug !== void 0)
|
|
916
1021
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
917
1022
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
1023
|
+
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
918
1024
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
919
|
-
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
920
1025
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
1026
|
+
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
921
1027
|
if (p.toolInspection)
|
|
922
1028
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
923
1029
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
@@ -944,7 +1050,7 @@ function tryLoadConfig(filePath) {
|
|
|
944
1050
|
}
|
|
945
1051
|
}
|
|
946
1052
|
function getActiveEnvironment(config) {
|
|
947
|
-
const env = process.env.NODE_ENV || "development";
|
|
1053
|
+
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
948
1054
|
return config.environments[env] ?? null;
|
|
949
1055
|
}
|
|
950
1056
|
function getCredentials() {
|
|
@@ -1000,7 +1106,7 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1000
1106
|
}).catch(() => {
|
|
1001
1107
|
});
|
|
1002
1108
|
}
|
|
1003
|
-
async function initNode9SaaS(toolName, args, creds,
|
|
1109
|
+
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1004
1110
|
const controller = new AbortController();
|
|
1005
1111
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1006
1112
|
try {
|
|
@@ -1010,7 +1116,6 @@ async function initNode9SaaS(toolName, args, creds, slackChannel, meta) {
|
|
|
1010
1116
|
body: JSON.stringify({
|
|
1011
1117
|
toolName,
|
|
1012
1118
|
args,
|
|
1013
|
-
slackChannel,
|
|
1014
1119
|
context: {
|
|
1015
1120
|
agent: meta?.agent,
|
|
1016
1121
|
mcpServer: meta?.mcpServer,
|
|
@@ -3054,75 +3159,30 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
|
|
|
3054
3159
|
console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
3055
3160
|
process.exit(1);
|
|
3056
3161
|
});
|
|
3057
|
-
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
|
|
3162
|
+
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
3058
3163
|
const configPath = path5.join(os5.homedir(), ".node9", "config.json");
|
|
3059
3164
|
if (fs5.existsSync(configPath) && !options.force) {
|
|
3060
3165
|
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3061
3166
|
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
3062
3167
|
return;
|
|
3063
3168
|
}
|
|
3064
|
-
const
|
|
3065
|
-
|
|
3169
|
+
const requestedMode = options.mode.toLowerCase();
|
|
3170
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
3171
|
+
const configToSave = {
|
|
3172
|
+
...DEFAULT_CONFIG,
|
|
3066
3173
|
settings: {
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
enableUndo: true,
|
|
3070
|
-
enableHookLogDebug: false,
|
|
3071
|
-
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
3072
|
-
},
|
|
3073
|
-
policy: {
|
|
3074
|
-
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
3075
|
-
dangerousWords: DANGEROUS_WORDS,
|
|
3076
|
-
ignoredTools: [
|
|
3077
|
-
"list_*",
|
|
3078
|
-
"get_*",
|
|
3079
|
-
"read_*",
|
|
3080
|
-
"describe_*",
|
|
3081
|
-
"read",
|
|
3082
|
-
"write",
|
|
3083
|
-
"edit",
|
|
3084
|
-
"glob",
|
|
3085
|
-
"grep",
|
|
3086
|
-
"ls",
|
|
3087
|
-
"notebookread",
|
|
3088
|
-
"notebookedit",
|
|
3089
|
-
"webfetch",
|
|
3090
|
-
"websearch",
|
|
3091
|
-
"exitplanmode",
|
|
3092
|
-
"askuserquestion",
|
|
3093
|
-
"agent",
|
|
3094
|
-
"task*"
|
|
3095
|
-
],
|
|
3096
|
-
toolInspection: {
|
|
3097
|
-
bash: "command",
|
|
3098
|
-
shell: "command",
|
|
3099
|
-
run_shell_command: "command",
|
|
3100
|
-
"terminal.execute": "command",
|
|
3101
|
-
"postgres:query": "sql"
|
|
3102
|
-
},
|
|
3103
|
-
rules: [
|
|
3104
|
-
{
|
|
3105
|
-
action: "rm",
|
|
3106
|
-
allowPaths: [
|
|
3107
|
-
"**/node_modules/**",
|
|
3108
|
-
"dist/**",
|
|
3109
|
-
"build/**",
|
|
3110
|
-
".next/**",
|
|
3111
|
-
"coverage/**",
|
|
3112
|
-
".cache/**",
|
|
3113
|
-
"tmp/**",
|
|
3114
|
-
"temp/**",
|
|
3115
|
-
".DS_Store"
|
|
3116
|
-
]
|
|
3117
|
-
}
|
|
3118
|
-
]
|
|
3174
|
+
...DEFAULT_CONFIG.settings,
|
|
3175
|
+
mode: safeMode
|
|
3119
3176
|
}
|
|
3120
3177
|
};
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
fs5.writeFileSync(configPath, JSON.stringify(
|
|
3178
|
+
const dir = path5.dirname(configPath);
|
|
3179
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
3180
|
+
fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3124
3181
|
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
3125
|
-
console.log(chalk5.
|
|
3182
|
+
console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
|
|
3183
|
+
console.log(
|
|
3184
|
+
chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
3185
|
+
);
|
|
3126
3186
|
});
|
|
3127
3187
|
program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
3128
3188
|
const creds = getCredentials();
|
|
@@ -3269,23 +3329,24 @@ RAW: ${raw}
|
|
|
3269
3329
|
let aiFeedbackMessage = "";
|
|
3270
3330
|
if (isHumanDecision) {
|
|
3271
3331
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
|
|
3272
|
-
|
|
3332
|
+
REASON: ${msg || "No specific reason provided by user."}
|
|
3273
3333
|
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3334
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3335
|
+
- Do NOT retry this exact command immediately.
|
|
3336
|
+
- Explain to the user that you understand they blocked the action.
|
|
3337
|
+
- Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
|
|
3338
|
+
- If you believe this action is critical, explain your reasoning to the user and ask them to run 'node9 pause 15m' to allow you to proceed.`;
|
|
3279
3339
|
} else {
|
|
3280
3340
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
|
|
3281
|
-
|
|
3341
|
+
REASON: ${msg}
|
|
3282
3342
|
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3343
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3344
|
+
- This command violates the current security configuration.
|
|
3345
|
+
- Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
|
|
3346
|
+
- Pivot to a non-destructive or read-only alternative.
|
|
3347
|
+
- Inform the user which security rule was triggered.`;
|
|
3288
3348
|
}
|
|
3349
|
+
console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
|
|
3289
3350
|
process.stdout.write(
|
|
3290
3351
|
JSON.stringify({
|
|
3291
3352
|
decision: "block",
|
|
@@ -3480,7 +3541,9 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3480
3541
|
process.exit(1);
|
|
3481
3542
|
}
|
|
3482
3543
|
const fullCommand = commandArgs.join(" ");
|
|
3483
|
-
let result = await authorizeHeadless("shell", { command: fullCommand }
|
|
3544
|
+
let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
|
|
3545
|
+
agent: "Terminal"
|
|
3546
|
+
});
|
|
3484
3547
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
3485
3548
|
console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3486
3549
|
const daemonReady = await autoStartDaemonAndWait();
|