@node9/proxy 1.0.1 → 1.0.2
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 +207 -143
- package/dist/cli.mjs +207 -143
- package/dist/index.js +190 -71
- package/dist/index.mjs +190 -71
- 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;
|
|
@@ -886,6 +983,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
886
983
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
887
984
|
await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
|
|
888
985
|
}
|
|
986
|
+
if (!isManual) {
|
|
987
|
+
appendLocalAudit(
|
|
988
|
+
toolName,
|
|
989
|
+
args,
|
|
990
|
+
finalResult.approved ? "allow" : "deny",
|
|
991
|
+
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
992
|
+
meta
|
|
993
|
+
);
|
|
994
|
+
}
|
|
889
995
|
return finalResult;
|
|
890
996
|
}
|
|
891
997
|
function getConfig() {
|
|
@@ -916,8 +1022,8 @@ function getConfig() {
|
|
|
916
1022
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
917
1023
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
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);
|
|
@@ -3054,75 +3160,30 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
|
|
|
3054
3160
|
console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
3055
3161
|
process.exit(1);
|
|
3056
3162
|
});
|
|
3057
|
-
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
|
|
3163
|
+
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
3164
|
const configPath = path5.join(os5.homedir(), ".node9", "config.json");
|
|
3059
3165
|
if (fs5.existsSync(configPath) && !options.force) {
|
|
3060
3166
|
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3061
3167
|
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
3062
3168
|
return;
|
|
3063
3169
|
}
|
|
3064
|
-
const
|
|
3065
|
-
|
|
3170
|
+
const requestedMode = options.mode.toLowerCase();
|
|
3171
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
3172
|
+
const configToSave = {
|
|
3173
|
+
...DEFAULT_CONFIG,
|
|
3066
3174
|
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
|
-
]
|
|
3175
|
+
...DEFAULT_CONFIG.settings,
|
|
3176
|
+
mode: safeMode
|
|
3119
3177
|
}
|
|
3120
3178
|
};
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
fs5.writeFileSync(configPath, JSON.stringify(
|
|
3179
|
+
const dir = path5.dirname(configPath);
|
|
3180
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
3181
|
+
fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3124
3182
|
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
3125
|
-
console.log(chalk5.
|
|
3183
|
+
console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
|
|
3184
|
+
console.log(
|
|
3185
|
+
chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
3186
|
+
);
|
|
3126
3187
|
});
|
|
3127
3188
|
program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
3128
3189
|
const creds = getCredentials();
|
|
@@ -3269,23 +3330,24 @@ RAW: ${raw}
|
|
|
3269
3330
|
let aiFeedbackMessage = "";
|
|
3270
3331
|
if (isHumanDecision) {
|
|
3271
3332
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
|
|
3272
|
-
|
|
3333
|
+
REASON: ${msg || "No specific reason provided by user."}
|
|
3273
3334
|
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3335
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3336
|
+
- Do NOT retry this exact command immediately.
|
|
3337
|
+
- Explain to the user that you understand they blocked the action.
|
|
3338
|
+
- Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
|
|
3339
|
+
- 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
3340
|
} else {
|
|
3280
3341
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
|
|
3281
|
-
|
|
3342
|
+
REASON: ${msg}
|
|
3282
3343
|
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3344
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3345
|
+
- This command violates the current security configuration.
|
|
3346
|
+
- Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
|
|
3347
|
+
- Pivot to a non-destructive or read-only alternative.
|
|
3348
|
+
- Inform the user which security rule was triggered.`;
|
|
3288
3349
|
}
|
|
3350
|
+
console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
|
|
3289
3351
|
process.stdout.write(
|
|
3290
3352
|
JSON.stringify({
|
|
3291
3353
|
decision: "block",
|
|
@@ -3480,7 +3542,9 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3480
3542
|
process.exit(1);
|
|
3481
3543
|
}
|
|
3482
3544
|
const fullCommand = commandArgs.join(" ");
|
|
3483
|
-
let result = await authorizeHeadless("shell", { command: fullCommand }
|
|
3545
|
+
let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
|
|
3546
|
+
agent: "Terminal"
|
|
3547
|
+
});
|
|
3484
3548
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
3485
3549
|
console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3486
3550
|
const daemonReady = await autoStartDaemonAndWait();
|