@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/README.md
CHANGED
|
@@ -11,6 +11,23 @@ While others try to _guess_ if a prompt is malicious (Semantic Security), Node9
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## 💎 The "Aha!" Moment
|
|
15
|
+
|
|
16
|
+
**AIs are literal.** When you ask an agent to "Fix my disk space," it might decide to run `docker system prune -af`.
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="https://github.com/user-attachments/assets/c3a8f3ae-f0aa-4c57-869a-5e1e2e356d35" width="100%">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
**With Node9, the interaction looks like this:**
|
|
23
|
+
|
|
24
|
+
1. **🤖 AI attempts a "Nuke":** `Bash("docker system prune -af --volumes")`
|
|
25
|
+
2. **🛡️ Node9 Intercepts:** An OS-native popup appears immediately.
|
|
26
|
+
3. **🛑 User Blocks:** You click "Block" in the popup.
|
|
27
|
+
4. **🧠 AI Negotiates:** Node9 explains the block to the AI. The AI responds: _"I understand. I will pivot to a safer cleanup, like removing only large log files instead."_
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
14
31
|
## ⚡ Key Architectural Upgrades
|
|
15
32
|
|
|
16
33
|
### 🏁 The Multi-Channel Race Engine
|
|
@@ -26,6 +43,14 @@ Node9 initiates a **Concurrent Race** across all enabled channels. The first cha
|
|
|
26
43
|
|
|
27
44
|
Node9 doesn't just "cut the wire." When a command is blocked, it injects a **Structured Negotiation Prompt** back into the AI’s context window. This teaches the AI why it was stopped and instructs it to pivot to a safer alternative or apologize to the human.
|
|
28
45
|
|
|
46
|
+
### ⏪ Shadow Git Snapshots (Auto-Undo)
|
|
47
|
+
|
|
48
|
+
Node9 takes silent, lightweight Git snapshots right before an AI agent is allowed to edit or delete files. If the AI hallucinates and ruins your code, don't waste time manualy fixing it. Just run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node9 undo
|
|
52
|
+
```
|
|
53
|
+
|
|
29
54
|
### 🌊 The Resolution Waterfall
|
|
30
55
|
|
|
31
56
|
Security posture is resolved using a strict 5-tier waterfall:
|
|
@@ -47,8 +72,8 @@ npm install -g @node9/proxy
|
|
|
47
72
|
node9 addto claude
|
|
48
73
|
node9 addto gemini
|
|
49
74
|
|
|
50
|
-
# 2.
|
|
51
|
-
node9
|
|
75
|
+
# 2. Initialize your local safety net
|
|
76
|
+
node9 init
|
|
52
77
|
|
|
53
78
|
# 3. Check your status
|
|
54
79
|
node9 status
|
|
@@ -121,9 +146,8 @@ A corporate policy has locked this action. You must click the "Approve" button i
|
|
|
121
146
|
- [x] **AI Negotiation Loop** (Instructional feedback loop to guide LLM behavior)
|
|
122
147
|
- [x] **Resolution Waterfall** (Cascading configuration: Env > Cloud > Project > Global)
|
|
123
148
|
- [x] **Native OS Dialogs** (Sub-second approval via Mac/Win/Linux system windows)
|
|
124
|
-
- [x] **
|
|
149
|
+
- [x] **Shadow Git Snapshots** (1-click Undo for AI hallucinations)
|
|
125
150
|
- [x] **Identity-Aware Execution** (Differentiates between Human vs. AI risk levels)
|
|
126
|
-
- [ ] **Shadow Git Snapshots** (1-click Undo for AI hallucinations)
|
|
127
151
|
- [ ] **Execution Sandboxing** (Simulate dangerous commands in a virtual FS before applying)
|
|
128
152
|
- [ ] **Multi-Admin Quorum** (Require 2+ human signatures for high-stakes production actions)
|
|
129
153
|
- [ ] **SOC2 Tamper-proof Audit Trail** (Cryptographically signed, cloud-managed logs)
|
package/dist/cli.js
CHANGED
|
@@ -107,21 +107,47 @@ function sendDesktopNotification(title, body) {
|
|
|
107
107
|
} catch {
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
function escapePango(text) {
|
|
111
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
112
|
+
}
|
|
113
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
114
|
+
const lines = [];
|
|
115
|
+
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
116
|
+
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
117
|
+
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
lines.push(formattedArgs);
|
|
120
|
+
if (!locked) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
if (locked) {
|
|
129
|
+
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
130
|
+
lines.push("");
|
|
131
|
+
}
|
|
132
|
+
lines.push(
|
|
133
|
+
`<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
|
|
134
|
+
);
|
|
135
|
+
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
138
|
+
if (!locked) {
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(
|
|
141
|
+
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return lines.join("\n");
|
|
145
|
+
}
|
|
110
146
|
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
111
147
|
if (isTestEnv()) return "deny";
|
|
112
148
|
const formattedArgs = formatArgs(args);
|
|
113
149
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
114
|
-
|
|
115
|
-
if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
|
|
116
|
-
`;
|
|
117
|
-
message += `Tool: ${toolName}
|
|
118
|
-
`;
|
|
119
|
-
message += `Agent: ${agent || "AI Agent"}
|
|
120
|
-
`;
|
|
121
|
-
message += `Rule: ${explainableLabel || "Security Policy"}
|
|
122
|
-
|
|
123
|
-
`;
|
|
124
|
-
message += `${formattedArgs}`;
|
|
150
|
+
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
125
151
|
process.stderr.write(import_chalk.default.yellow(`
|
|
126
152
|
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
127
153
|
`));
|
|
@@ -142,7 +168,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
142
168
|
}
|
|
143
169
|
try {
|
|
144
170
|
if (process.platform === "darwin") {
|
|
145
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
171
|
+
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"`;
|
|
146
172
|
const script = `on run argv
|
|
147
173
|
tell application "System Events"
|
|
148
174
|
activate
|
|
@@ -151,21 +177,28 @@ end tell
|
|
|
151
177
|
end run`;
|
|
152
178
|
childProcess = (0, import_child_process.spawn)("osascript", ["-e", script, "--", message, title]);
|
|
153
179
|
} else if (process.platform === "linux") {
|
|
180
|
+
const pangoMessage = buildPangoMessage(
|
|
181
|
+
toolName,
|
|
182
|
+
formattedArgs,
|
|
183
|
+
agent,
|
|
184
|
+
explainableLabel,
|
|
185
|
+
locked
|
|
186
|
+
);
|
|
154
187
|
const argsList = [
|
|
155
188
|
locked ? "--info" : "--question",
|
|
156
189
|
"--modal",
|
|
157
|
-
"--width=
|
|
190
|
+
"--width=480",
|
|
158
191
|
"--title",
|
|
159
192
|
title,
|
|
160
193
|
"--text",
|
|
161
|
-
|
|
194
|
+
pangoMessage,
|
|
162
195
|
"--ok-label",
|
|
163
|
-
locked ? "Waiting..." : "Allow",
|
|
196
|
+
locked ? "Waiting..." : "Allow \u21B5",
|
|
164
197
|
"--timeout",
|
|
165
198
|
"300"
|
|
166
199
|
];
|
|
167
200
|
if (!locked) {
|
|
168
|
-
argsList.push("--cancel-label", "Block");
|
|
201
|
+
argsList.push("--cancel-label", "Block \u238B");
|
|
169
202
|
argsList.push("--extra-button", "Always Allow");
|
|
170
203
|
}
|
|
171
204
|
childProcess = (0, import_child_process.spawn)("zenity", argsList);
|
|
@@ -193,6 +226,8 @@ end run`;
|
|
|
193
226
|
// src/core.ts
|
|
194
227
|
var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
195
228
|
var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
229
|
+
var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
230
|
+
var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
196
231
|
function checkPause() {
|
|
197
232
|
try {
|
|
198
233
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -259,36 +294,39 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
259
294
|
}
|
|
260
295
|
}
|
|
261
296
|
}
|
|
262
|
-
function
|
|
297
|
+
function appendToLog(logPath, entry) {
|
|
263
298
|
try {
|
|
264
|
-
const entry = JSON.stringify({
|
|
265
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
266
|
-
tool: toolName,
|
|
267
|
-
args,
|
|
268
|
-
decision: "would-have-blocked",
|
|
269
|
-
source: "audit-mode"
|
|
270
|
-
});
|
|
271
|
-
const logPath = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
272
299
|
const dir = import_path.default.dirname(logPath);
|
|
273
300
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
274
|
-
import_fs.default.appendFileSync(logPath, entry + "\n");
|
|
301
|
+
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
275
302
|
} catch {
|
|
276
303
|
}
|
|
277
304
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
305
|
+
function appendHookDebug(toolName, args, meta) {
|
|
306
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
307
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
308
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
309
|
+
tool: toolName,
|
|
310
|
+
args: safeArgs,
|
|
311
|
+
agent: meta?.agent,
|
|
312
|
+
mcpServer: meta?.mcpServer,
|
|
313
|
+
hostname: import_os.default.hostname(),
|
|
314
|
+
cwd: process.cwd()
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
318
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
319
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
320
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
321
|
+
tool: toolName,
|
|
322
|
+
args: safeArgs,
|
|
323
|
+
decision,
|
|
324
|
+
checkedBy,
|
|
325
|
+
agent: meta?.agent,
|
|
326
|
+
mcpServer: meta?.mcpServer,
|
|
327
|
+
hostname: import_os.default.hostname()
|
|
328
|
+
});
|
|
329
|
+
}
|
|
292
330
|
function tokenize(toolName) {
|
|
293
331
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
294
332
|
}
|
|
@@ -395,16 +433,28 @@ function redactSecrets(text) {
|
|
|
395
433
|
);
|
|
396
434
|
return redacted;
|
|
397
435
|
}
|
|
436
|
+
var DANGEROUS_WORDS = [
|
|
437
|
+
"drop",
|
|
438
|
+
"truncate",
|
|
439
|
+
"purge",
|
|
440
|
+
"format",
|
|
441
|
+
"destroy",
|
|
442
|
+
"terminate",
|
|
443
|
+
"revoke",
|
|
444
|
+
"docker",
|
|
445
|
+
"psql"
|
|
446
|
+
];
|
|
398
447
|
var DEFAULT_CONFIG = {
|
|
399
448
|
settings: {
|
|
400
449
|
mode: "standard",
|
|
401
450
|
autoStartDaemon: true,
|
|
402
|
-
enableUndo:
|
|
451
|
+
enableUndo: true,
|
|
452
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
403
453
|
enableHookLogDebug: false,
|
|
404
454
|
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
405
455
|
},
|
|
406
456
|
policy: {
|
|
407
|
-
sandboxPaths: [],
|
|
457
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
408
458
|
dangerousWords: DANGEROUS_WORDS,
|
|
409
459
|
ignoredTools: [
|
|
410
460
|
"list_*",
|
|
@@ -412,12 +462,44 @@ var DEFAULT_CONFIG = {
|
|
|
412
462
|
"read_*",
|
|
413
463
|
"describe_*",
|
|
414
464
|
"read",
|
|
465
|
+
"glob",
|
|
415
466
|
"grep",
|
|
416
467
|
"ls",
|
|
417
|
-
"
|
|
468
|
+
"notebookread",
|
|
469
|
+
"notebookedit",
|
|
470
|
+
"webfetch",
|
|
471
|
+
"websearch",
|
|
472
|
+
"exitplanmode",
|
|
473
|
+
"askuserquestion",
|
|
474
|
+
"agent",
|
|
475
|
+
"task*",
|
|
476
|
+
"toolsearch",
|
|
477
|
+
"mcp__ide__*",
|
|
478
|
+
"getDiagnostics"
|
|
418
479
|
],
|
|
419
|
-
toolInspection: {
|
|
420
|
-
|
|
480
|
+
toolInspection: {
|
|
481
|
+
bash: "command",
|
|
482
|
+
shell: "command",
|
|
483
|
+
run_shell_command: "command",
|
|
484
|
+
"terminal.execute": "command",
|
|
485
|
+
"postgres:query": "sql"
|
|
486
|
+
},
|
|
487
|
+
rules: [
|
|
488
|
+
{
|
|
489
|
+
action: "rm",
|
|
490
|
+
allowPaths: [
|
|
491
|
+
"**/node_modules/**",
|
|
492
|
+
"dist/**",
|
|
493
|
+
"build/**",
|
|
494
|
+
".next/**",
|
|
495
|
+
"coverage/**",
|
|
496
|
+
".cache/**",
|
|
497
|
+
"tmp/**",
|
|
498
|
+
"temp/**",
|
|
499
|
+
".DS_Store"
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
]
|
|
421
503
|
},
|
|
422
504
|
environments: {}
|
|
423
505
|
};
|
|
@@ -482,20 +564,15 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
482
564
|
}
|
|
483
565
|
const isManual = agent === "Terminal";
|
|
484
566
|
if (isManual) {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"revoke",
|
|
495
|
-
"docker"
|
|
496
|
-
];
|
|
497
|
-
const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
|
|
498
|
-
if (!hasNuclear) return { decision: "allow" };
|
|
567
|
+
const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
|
|
568
|
+
const hasSystemDisaster = allTokens.some(
|
|
569
|
+
(t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
|
|
570
|
+
);
|
|
571
|
+
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
572
|
+
if (hasSystemDisaster || isRootWipe) {
|
|
573
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
574
|
+
}
|
|
575
|
+
return { decision: "allow" };
|
|
499
576
|
}
|
|
500
577
|
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
501
578
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
@@ -509,27 +586,39 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
509
586
|
if (pathTokens.length > 0) {
|
|
510
587
|
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
511
588
|
if (anyBlocked)
|
|
512
|
-
return {
|
|
589
|
+
return {
|
|
590
|
+
decision: "review",
|
|
591
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
592
|
+
};
|
|
513
593
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
514
594
|
if (allAllowed) return { decision: "allow" };
|
|
515
595
|
}
|
|
516
|
-
return {
|
|
596
|
+
return {
|
|
597
|
+
decision: "review",
|
|
598
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
599
|
+
};
|
|
517
600
|
}
|
|
518
601
|
}
|
|
602
|
+
let matchedDangerousWord;
|
|
519
603
|
const isDangerous = allTokens.some(
|
|
520
604
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
521
605
|
const w = word.toLowerCase();
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
606
|
+
const hit = token === w || (() => {
|
|
607
|
+
try {
|
|
608
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
613
|
+
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
614
|
+
return hit;
|
|
528
615
|
})
|
|
529
616
|
);
|
|
530
617
|
if (isDangerous) {
|
|
531
|
-
|
|
532
|
-
|
|
618
|
+
return {
|
|
619
|
+
decision: "review",
|
|
620
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
|
|
621
|
+
};
|
|
533
622
|
}
|
|
534
623
|
if (config.settings.mode === "strict") {
|
|
535
624
|
const envConfig = getActiveEnvironment(config);
|
|
@@ -644,13 +733,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
644
733
|
approvers.browser = false;
|
|
645
734
|
approvers.terminal = false;
|
|
646
735
|
}
|
|
736
|
+
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
737
|
+
appendHookDebug(toolName, args, meta);
|
|
738
|
+
}
|
|
647
739
|
const isManual = meta?.agent === "Terminal";
|
|
648
740
|
let explainableLabel = "Local Config";
|
|
649
741
|
if (config.settings.mode === "audit") {
|
|
650
742
|
if (!isIgnoredTool(toolName)) {
|
|
651
743
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
652
744
|
if (policyResult.decision === "review") {
|
|
653
|
-
|
|
745
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
654
746
|
sendDesktopNotification(
|
|
655
747
|
"Node9 Audit Mode",
|
|
656
748
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -662,20 +754,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
662
754
|
if (!isIgnoredTool(toolName)) {
|
|
663
755
|
if (getActiveTrustSession(toolName)) {
|
|
664
756
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
757
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
665
758
|
return { approved: true, checkedBy: "trust" };
|
|
666
759
|
}
|
|
667
760
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
668
761
|
if (policyResult.decision === "allow") {
|
|
669
762
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
763
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
670
764
|
return { approved: true, checkedBy: "local-policy" };
|
|
671
765
|
}
|
|
672
766
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
673
767
|
const persistent = getPersistentDecision(toolName);
|
|
674
768
|
if (persistent === "allow") {
|
|
675
769
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
770
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
676
771
|
return { approved: true, checkedBy: "persistent" };
|
|
677
772
|
}
|
|
678
773
|
if (persistent === "deny") {
|
|
774
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
|
|
679
775
|
return {
|
|
680
776
|
approved: false,
|
|
681
777
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -685,6 +781,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
685
781
|
}
|
|
686
782
|
} else {
|
|
687
783
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
784
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
688
785
|
return { approved: true };
|
|
689
786
|
}
|
|
690
787
|
let cloudRequestId = null;
|
|
@@ -909,6 +1006,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
909
1006
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
910
1007
|
await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
|
|
911
1008
|
}
|
|
1009
|
+
if (!isManual) {
|
|
1010
|
+
appendLocalAudit(
|
|
1011
|
+
toolName,
|
|
1012
|
+
args,
|
|
1013
|
+
finalResult.approved ? "allow" : "deny",
|
|
1014
|
+
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
1015
|
+
meta
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
912
1018
|
return finalResult;
|
|
913
1019
|
}
|
|
914
1020
|
function getConfig() {
|
|
@@ -939,8 +1045,8 @@ function getConfig() {
|
|
|
939
1045
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
940
1046
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
941
1047
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
942
|
-
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
943
1048
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
1049
|
+
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
944
1050
|
if (p.toolInspection)
|
|
945
1051
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
946
1052
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
@@ -3077,75 +3183,30 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
|
|
|
3077
3183
|
console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
3078
3184
|
process.exit(1);
|
|
3079
3185
|
});
|
|
3080
|
-
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
|
|
3186
|
+
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) => {
|
|
3081
3187
|
const configPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3082
3188
|
if (import_fs5.default.existsSync(configPath) && !options.force) {
|
|
3083
3189
|
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3084
3190
|
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
3085
3191
|
return;
|
|
3086
3192
|
}
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3193
|
+
const requestedMode = options.mode.toLowerCase();
|
|
3194
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
3195
|
+
const configToSave = {
|
|
3196
|
+
...DEFAULT_CONFIG,
|
|
3089
3197
|
settings: {
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
enableUndo: true,
|
|
3093
|
-
enableHookLogDebug: false,
|
|
3094
|
-
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
3095
|
-
},
|
|
3096
|
-
policy: {
|
|
3097
|
-
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
3098
|
-
dangerousWords: DANGEROUS_WORDS,
|
|
3099
|
-
ignoredTools: [
|
|
3100
|
-
"list_*",
|
|
3101
|
-
"get_*",
|
|
3102
|
-
"read_*",
|
|
3103
|
-
"describe_*",
|
|
3104
|
-
"read",
|
|
3105
|
-
"write",
|
|
3106
|
-
"edit",
|
|
3107
|
-
"glob",
|
|
3108
|
-
"grep",
|
|
3109
|
-
"ls",
|
|
3110
|
-
"notebookread",
|
|
3111
|
-
"notebookedit",
|
|
3112
|
-
"webfetch",
|
|
3113
|
-
"websearch",
|
|
3114
|
-
"exitplanmode",
|
|
3115
|
-
"askuserquestion",
|
|
3116
|
-
"agent",
|
|
3117
|
-
"task*"
|
|
3118
|
-
],
|
|
3119
|
-
toolInspection: {
|
|
3120
|
-
bash: "command",
|
|
3121
|
-
shell: "command",
|
|
3122
|
-
run_shell_command: "command",
|
|
3123
|
-
"terminal.execute": "command",
|
|
3124
|
-
"postgres:query": "sql"
|
|
3125
|
-
},
|
|
3126
|
-
rules: [
|
|
3127
|
-
{
|
|
3128
|
-
action: "rm",
|
|
3129
|
-
allowPaths: [
|
|
3130
|
-
"**/node_modules/**",
|
|
3131
|
-
"dist/**",
|
|
3132
|
-
"build/**",
|
|
3133
|
-
".next/**",
|
|
3134
|
-
"coverage/**",
|
|
3135
|
-
".cache/**",
|
|
3136
|
-
"tmp/**",
|
|
3137
|
-
"temp/**",
|
|
3138
|
-
".DS_Store"
|
|
3139
|
-
]
|
|
3140
|
-
}
|
|
3141
|
-
]
|
|
3198
|
+
...DEFAULT_CONFIG.settings,
|
|
3199
|
+
mode: safeMode
|
|
3142
3200
|
}
|
|
3143
3201
|
};
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
import_fs5.default.writeFileSync(configPath, JSON.stringify(
|
|
3202
|
+
const dir = import_path5.default.dirname(configPath);
|
|
3203
|
+
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
3204
|
+
import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3147
3205
|
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
3148
|
-
console.log(import_chalk5.default.
|
|
3206
|
+
console.log(import_chalk5.default.cyan(` Mode set to: ${safeMode}`));
|
|
3207
|
+
console.log(
|
|
3208
|
+
import_chalk5.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
3209
|
+
);
|
|
3149
3210
|
});
|
|
3150
3211
|
program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
3151
3212
|
const creds = getCredentials();
|
|
@@ -3292,23 +3353,24 @@ RAW: ${raw}
|
|
|
3292
3353
|
let aiFeedbackMessage = "";
|
|
3293
3354
|
if (isHumanDecision) {
|
|
3294
3355
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
|
|
3295
|
-
|
|
3356
|
+
REASON: ${msg || "No specific reason provided by user."}
|
|
3296
3357
|
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3358
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3359
|
+
- Do NOT retry this exact command immediately.
|
|
3360
|
+
- Explain to the user that you understand they blocked the action.
|
|
3361
|
+
- Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
|
|
3362
|
+
- 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.`;
|
|
3302
3363
|
} else {
|
|
3303
3364
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
|
|
3304
|
-
|
|
3365
|
+
REASON: ${msg}
|
|
3305
3366
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3367
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3368
|
+
- This command violates the current security configuration.
|
|
3369
|
+
- Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
|
|
3370
|
+
- Pivot to a non-destructive or read-only alternative.
|
|
3371
|
+
- Inform the user which security rule was triggered.`;
|
|
3311
3372
|
}
|
|
3373
|
+
console.error(import_chalk5.default.dim(` (Detailed instructions sent to AI agent)`));
|
|
3312
3374
|
process.stdout.write(
|
|
3313
3375
|
JSON.stringify({
|
|
3314
3376
|
decision: "block",
|
|
@@ -3503,7 +3565,9 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3503
3565
|
process.exit(1);
|
|
3504
3566
|
}
|
|
3505
3567
|
const fullCommand = commandArgs.join(" ");
|
|
3506
|
-
let result = await authorizeHeadless("shell", { command: fullCommand }
|
|
3568
|
+
let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
|
|
3569
|
+
agent: "Terminal"
|
|
3570
|
+
});
|
|
3507
3571
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
3508
3572
|
console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3509
3573
|
const daemonReady = await autoStartDaemonAndWait();
|