@node9/proxy 1.0.0 → 1.0.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 +200 -223
- package/dist/cli.mjs +200 -223
- package/dist/index.js +109 -132
- package/dist/index.mjs +109 -132
- package/package.json +30 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
|
-
import
|
|
2
|
+
import chalk2 from "chalk";
|
|
3
3
|
import { confirm } from "@inquirer/prompts";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
@@ -9,19 +9,69 @@ import { parse } from "sh-syntax";
|
|
|
9
9
|
|
|
10
10
|
// src/ui/native.ts
|
|
11
11
|
import { spawn } from "child_process";
|
|
12
|
+
import chalk from "chalk";
|
|
12
13
|
var isTestEnv = () => {
|
|
13
14
|
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
14
15
|
};
|
|
16
|
+
function smartTruncate(str, maxLen = 500) {
|
|
17
|
+
if (str.length <= maxLen) return str;
|
|
18
|
+
const edge = Math.floor(maxLen / 2) - 3;
|
|
19
|
+
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
20
|
+
}
|
|
21
|
+
function formatArgs(args) {
|
|
22
|
+
if (args === null || args === void 0) return "(none)";
|
|
23
|
+
let parsed = args;
|
|
24
|
+
if (typeof args === "string") {
|
|
25
|
+
const trimmed = args.trim();
|
|
26
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(trimmed);
|
|
29
|
+
} catch {
|
|
30
|
+
parsed = args;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
return smartTruncate(args, 600);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
37
|
+
const obj = parsed;
|
|
38
|
+
const codeKeys = [
|
|
39
|
+
"command",
|
|
40
|
+
"cmd",
|
|
41
|
+
"shell_command",
|
|
42
|
+
"bash_command",
|
|
43
|
+
"script",
|
|
44
|
+
"code",
|
|
45
|
+
"input",
|
|
46
|
+
"sql",
|
|
47
|
+
"query",
|
|
48
|
+
"arguments",
|
|
49
|
+
"args",
|
|
50
|
+
"param",
|
|
51
|
+
"params",
|
|
52
|
+
"text"
|
|
53
|
+
];
|
|
54
|
+
const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
|
|
55
|
+
if (foundKey) {
|
|
56
|
+
const val = obj[foundKey];
|
|
57
|
+
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
58
|
+
return `[${foundKey.toUpperCase()}]:
|
|
59
|
+
${smartTruncate(str, 500)}`;
|
|
60
|
+
}
|
|
61
|
+
return Object.entries(obj).slice(0, 5).map(
|
|
62
|
+
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
63
|
+
).join("\n");
|
|
64
|
+
}
|
|
65
|
+
return smartTruncate(JSON.stringify(parsed), 200);
|
|
66
|
+
}
|
|
15
67
|
function sendDesktopNotification(title, body) {
|
|
16
68
|
if (isTestEnv()) return;
|
|
17
69
|
try {
|
|
18
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
19
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
20
70
|
if (process.platform === "darwin") {
|
|
21
|
-
const script = `display notification "${
|
|
71
|
+
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
22
72
|
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
23
73
|
} else if (process.platform === "linux") {
|
|
24
|
-
spawn("notify-send", [
|
|
74
|
+
spawn("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
25
75
|
detached: true,
|
|
26
76
|
stdio: "ignore"
|
|
27
77
|
}).unref();
|
|
@@ -29,69 +79,28 @@ function sendDesktopNotification(title, body) {
|
|
|
29
79
|
} catch {
|
|
30
80
|
}
|
|
31
81
|
}
|
|
32
|
-
function formatArgs(args) {
|
|
33
|
-
if (args === null || args === void 0) return "(none)";
|
|
34
|
-
if (typeof args !== "object" || Array.isArray(args)) {
|
|
35
|
-
const str = typeof args === "string" ? args : JSON.stringify(args);
|
|
36
|
-
return str.length > 200 ? str.slice(0, 200) + "\u2026" : str;
|
|
37
|
-
}
|
|
38
|
-
const entries = Object.entries(args).filter(
|
|
39
|
-
([, v]) => v !== null && v !== void 0 && v !== ""
|
|
40
|
-
);
|
|
41
|
-
if (entries.length === 0) return "(none)";
|
|
42
|
-
const MAX_FIELDS = 5;
|
|
43
|
-
const MAX_VALUE_LEN = 120;
|
|
44
|
-
const lines = entries.slice(0, MAX_FIELDS).map(([key, val]) => {
|
|
45
|
-
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
46
|
-
const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
|
|
47
|
-
return ` ${key}: ${truncated}`;
|
|
48
|
-
});
|
|
49
|
-
if (entries.length > MAX_FIELDS) {
|
|
50
|
-
lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
|
|
51
|
-
}
|
|
52
|
-
return lines.join("\n");
|
|
53
|
-
}
|
|
54
82
|
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
55
83
|
if (isTestEnv()) return "deny";
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log(`[DEBUG Native] isTestEnv check:`, {
|
|
59
|
-
VITEST: process.env.VITEST,
|
|
60
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
61
|
-
CI: process.env.CI,
|
|
62
|
-
isTest: isTestEnv()
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
|
|
84
|
+
const formattedArgs = formatArgs(args);
|
|
85
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
66
86
|
let message = "";
|
|
67
|
-
if (locked)
|
|
68
|
-
message += `\u26A1 Awaiting remote approval via Slack. Local override is disabled.
|
|
87
|
+
if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
|
|
69
88
|
`;
|
|
70
|
-
|
|
89
|
+
message += `Tool: ${toolName}
|
|
71
90
|
`;
|
|
72
|
-
}
|
|
73
|
-
message += `Tool: ${toolName}
|
|
74
|
-
`;
|
|
75
|
-
message += `Agent: ${agent || "AI Agent"}
|
|
76
|
-
`;
|
|
77
|
-
if (explainableLabel) {
|
|
78
|
-
message += `Reason: ${explainableLabel}
|
|
91
|
+
message += `Agent: ${agent || "AI Agent"}
|
|
79
92
|
`;
|
|
80
|
-
}
|
|
81
|
-
message += `
|
|
82
|
-
Arguments:
|
|
83
|
-
${formatArgs(args)}`;
|
|
84
|
-
if (!locked) {
|
|
85
|
-
message += `
|
|
93
|
+
message += `Rule: ${explainableLabel || "Security Policy"}
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
`;
|
|
96
|
+
message += `${formattedArgs}`;
|
|
97
|
+
process.stderr.write(chalk.yellow(`
|
|
98
|
+
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
99
|
+
`));
|
|
91
100
|
return new Promise((resolve) => {
|
|
92
101
|
let childProcess = null;
|
|
93
102
|
const onAbort = () => {
|
|
94
|
-
if (childProcess) {
|
|
103
|
+
if (childProcess && childProcess.pid) {
|
|
95
104
|
try {
|
|
96
105
|
process.kill(childProcess.pid, "SIGKILL");
|
|
97
106
|
} catch {
|
|
@@ -103,83 +112,51 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
103
112
|
if (signal.aborted) return resolve("deny");
|
|
104
113
|
signal.addEventListener("abort", onAbort);
|
|
105
114
|
}
|
|
106
|
-
const cleanup = () => {
|
|
107
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
108
|
-
};
|
|
109
115
|
try {
|
|
110
116
|
if (process.platform === "darwin") {
|
|
111
117
|
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
112
|
-
const script = `
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
120
|
-
childProcess.on("close", (code) => {
|
|
121
|
-
cleanup();
|
|
122
|
-
if (locked) return resolve("deny");
|
|
123
|
-
if (code === 0) {
|
|
124
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
125
|
-
if (output.includes("Allow")) return resolve("allow");
|
|
126
|
-
}
|
|
127
|
-
resolve("deny");
|
|
128
|
-
});
|
|
118
|
+
const script = `on run argv
|
|
119
|
+
tell application "System Events"
|
|
120
|
+
activate
|
|
121
|
+
display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
|
|
122
|
+
end tell
|
|
123
|
+
end run`;
|
|
124
|
+
childProcess = spawn("osascript", ["-e", script, "--", message, title]);
|
|
129
125
|
} else if (process.platform === "linux") {
|
|
130
|
-
const argsList =
|
|
131
|
-
"--info",
|
|
132
|
-
"--
|
|
133
|
-
|
|
134
|
-
"--text",
|
|
135
|
-
safeMessage,
|
|
136
|
-
"--ok-label",
|
|
137
|
-
"Waiting for Slack\u2026",
|
|
138
|
-
"--timeout",
|
|
139
|
-
"300"
|
|
140
|
-
] : [
|
|
141
|
-
"--question",
|
|
126
|
+
const argsList = [
|
|
127
|
+
locked ? "--info" : "--question",
|
|
128
|
+
"--modal",
|
|
129
|
+
"--width=450",
|
|
142
130
|
"--title",
|
|
143
131
|
title,
|
|
144
132
|
"--text",
|
|
145
|
-
|
|
133
|
+
message,
|
|
146
134
|
"--ok-label",
|
|
147
|
-
"Allow",
|
|
148
|
-
"--cancel-label",
|
|
149
|
-
"Block",
|
|
150
|
-
"--extra-button",
|
|
151
|
-
"Always Allow",
|
|
135
|
+
locked ? "Waiting..." : "Allow",
|
|
152
136
|
"--timeout",
|
|
153
137
|
"300"
|
|
154
138
|
];
|
|
139
|
+
if (!locked) {
|
|
140
|
+
argsList.push("--cancel-label", "Block");
|
|
141
|
+
argsList.push("--extra-button", "Always Allow");
|
|
142
|
+
}
|
|
155
143
|
childProcess = spawn("zenity", argsList);
|
|
156
|
-
let output = "";
|
|
157
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
158
|
-
childProcess.on("close", (code) => {
|
|
159
|
-
cleanup();
|
|
160
|
-
if (locked) return resolve("deny");
|
|
161
|
-
if (output.trim() === "Always Allow") return resolve("always_allow");
|
|
162
|
-
if (code === 0) return resolve("allow");
|
|
163
|
-
resolve("deny");
|
|
164
|
-
});
|
|
165
144
|
} else if (process.platform === "win32") {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
$res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
|
|
170
|
-
if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
145
|
+
const b64Msg = Buffer.from(message).toString("base64");
|
|
146
|
+
const b64Title = Buffer.from(title).toString("base64");
|
|
147
|
+
const ps = `Add-Type -AssemblyName PresentationFramework; $msg = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Msg}")); $title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Title}")); $res = [System.Windows.MessageBox]::Show($msg, $title, "${locked ? "OK" : "YesNo"}", "Warning", "Button2", "DefaultDesktopOnly"); if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
171
148
|
childProcess = spawn("powershell", ["-Command", ps]);
|
|
172
|
-
childProcess.on("close", (code) => {
|
|
173
|
-
cleanup();
|
|
174
|
-
if (locked) return resolve("deny");
|
|
175
|
-
resolve(code === 0 ? "allow" : "deny");
|
|
176
|
-
});
|
|
177
|
-
} else {
|
|
178
|
-
cleanup();
|
|
179
|
-
resolve("deny");
|
|
180
149
|
}
|
|
150
|
+
let output = "";
|
|
151
|
+
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
152
|
+
childProcess?.on("close", (code) => {
|
|
153
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
154
|
+
if (locked) return resolve("deny");
|
|
155
|
+
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
156
|
+
if (code === 0) return resolve("allow");
|
|
157
|
+
resolve("deny");
|
|
158
|
+
});
|
|
181
159
|
} catch {
|
|
182
|
-
cleanup();
|
|
183
160
|
resolve("deny");
|
|
184
161
|
}
|
|
185
162
|
});
|
|
@@ -657,8 +634,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
657
634
|
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
658
635
|
const reason = isAuthError ? "Invalid or missing API key. Run `node9 login` to generate a key (must start with n9_live_)." : isNetworkError ? "Could not reach the Node9 cloud. Check your network or API URL." : error.message;
|
|
659
636
|
console.error(
|
|
660
|
-
|
|
661
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) +
|
|
637
|
+
chalk2.yellow(`
|
|
638
|
+
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
|
|
662
639
|
Falling back to local rules...
|
|
663
640
|
`)
|
|
664
641
|
);
|
|
@@ -666,13 +643,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
666
643
|
}
|
|
667
644
|
if (cloudEnforced && cloudRequestId) {
|
|
668
645
|
console.error(
|
|
669
|
-
|
|
646
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
670
647
|
);
|
|
671
|
-
console.error(
|
|
648
|
+
console.error(chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n"));
|
|
672
649
|
} else if (!cloudEnforced) {
|
|
673
650
|
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
674
651
|
console.error(
|
|
675
|
-
|
|
652
|
+
chalk2.dim(`
|
|
676
653
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
677
654
|
`)
|
|
678
655
|
);
|
|
@@ -737,9 +714,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
737
714
|
try {
|
|
738
715
|
if (!approvers.native && !cloudEnforced) {
|
|
739
716
|
console.error(
|
|
740
|
-
|
|
717
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
|
|
741
718
|
);
|
|
742
|
-
console.error(
|
|
719
|
+
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
743
720
|
`));
|
|
744
721
|
}
|
|
745
722
|
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
@@ -762,11 +739,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
762
739
|
racePromises.push(
|
|
763
740
|
(async () => {
|
|
764
741
|
try {
|
|
765
|
-
console.log(
|
|
766
|
-
console.log(`${
|
|
767
|
-
console.log(`${
|
|
742
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
743
|
+
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
744
|
+
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
768
745
|
if (isRemoteLocked) {
|
|
769
|
-
console.log(
|
|
746
|
+
console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
770
747
|
`));
|
|
771
748
|
await new Promise((_, reject) => {
|
|
772
749
|
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
@@ -1017,11 +994,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
1017
994
|
if (!statusRes.ok) continue;
|
|
1018
995
|
const { status, reason } = await statusRes.json();
|
|
1019
996
|
if (status === "APPROVED") {
|
|
1020
|
-
console.error(
|
|
997
|
+
console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
|
|
1021
998
|
return { approved: true, reason };
|
|
1022
999
|
}
|
|
1023
1000
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
1024
|
-
console.error(
|
|
1001
|
+
console.error(chalk2.red("\u274C Denied via Cloud.\n"));
|
|
1025
1002
|
return { approved: false, reason };
|
|
1026
1003
|
}
|
|
1027
1004
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -90,5 +90,34 @@
|
|
|
90
90
|
},
|
|
91
91
|
"publishConfig": {
|
|
92
92
|
"access": "public"
|
|
93
|
+
},
|
|
94
|
+
"release": {
|
|
95
|
+
"branches": [
|
|
96
|
+
"main"
|
|
97
|
+
],
|
|
98
|
+
"plugins": [
|
|
99
|
+
"@semantic-release/commit-analyzer",
|
|
100
|
+
"@semantic-release/release-notes-generator",
|
|
101
|
+
"@semantic-release/npm",
|
|
102
|
+
[
|
|
103
|
+
"@semantic-release/github",
|
|
104
|
+
{
|
|
105
|
+
"assets": [
|
|
106
|
+
"dist/*.js",
|
|
107
|
+
"dist/*.mjs"
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"@semantic-release/git",
|
|
113
|
+
{
|
|
114
|
+
"assets": [
|
|
115
|
+
"package.json",
|
|
116
|
+
"package-lock.json"
|
|
117
|
+
],
|
|
118
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
]
|
|
93
122
|
}
|
|
94
123
|
}
|