@node9/proxy 2.14.2 → 2.14.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/dist/cli.js +1464 -1334
- package/dist/cli.mjs +1394 -1266
- package/dist/dashboard.mjs +75 -5
- package/dist/index.js +216 -86
- package/dist/index.mjs +216 -86
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -377,8 +377,8 @@ var ConfigFileSchema = z.object({
|
|
|
377
377
|
}).strict();
|
|
378
378
|
function formatIssues(issues) {
|
|
379
379
|
const lines = issues.map((issue) => {
|
|
380
|
-
const
|
|
381
|
-
return ` \u2022 ${
|
|
380
|
+
const path16 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
381
|
+
return ` \u2022 ${path16}: ${issue.message}`;
|
|
382
382
|
});
|
|
383
383
|
return `Invalid config:
|
|
384
384
|
${lines.join("\n")}`;
|
|
@@ -391,9 +391,9 @@ function prunePaths(root, paths) {
|
|
|
391
391
|
const yi = y[y.length - 1];
|
|
392
392
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
393
393
|
});
|
|
394
|
-
for (const
|
|
394
|
+
for (const path16 of ordered) {
|
|
395
395
|
let cur = root;
|
|
396
|
-
for (const key of
|
|
396
|
+
for (const key of path16.slice(0, -1)) {
|
|
397
397
|
if (cur === null || typeof cur !== "object") {
|
|
398
398
|
cur = void 0;
|
|
399
399
|
break;
|
|
@@ -401,7 +401,7 @@ function prunePaths(root, paths) {
|
|
|
401
401
|
cur = cur[key];
|
|
402
402
|
}
|
|
403
403
|
if (cur === null || typeof cur !== "object") continue;
|
|
404
|
-
const last =
|
|
404
|
+
const last = path16[path16.length - 1];
|
|
405
405
|
if (Array.isArray(cur)) {
|
|
406
406
|
const i = Number(last);
|
|
407
407
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -437,7 +437,7 @@ function sanitizeConfig(raw) {
|
|
|
437
437
|
return issue.keys.map((k) => [...at, k]);
|
|
438
438
|
}
|
|
439
439
|
return [at.slice(0, -level || void 0)];
|
|
440
|
-
}).filter((
|
|
440
|
+
}).filter((path16) => path16.length > 0);
|
|
441
441
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
442
442
|
}
|
|
443
443
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -1349,9 +1349,9 @@ function matchesPattern(text, patterns) {
|
|
|
1349
1349
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1350
1350
|
}
|
|
1351
1351
|
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1352
|
-
function getNestedValue(obj,
|
|
1352
|
+
function getNestedValue(obj, path16) {
|
|
1353
1353
|
if (!obj || typeof obj !== "object") return null;
|
|
1354
|
-
const segments2 =
|
|
1354
|
+
const segments2 = path16.split(".");
|
|
1355
1355
|
for (const seg of segments2) {
|
|
1356
1356
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1357
1357
|
}
|
|
@@ -4016,9 +4016,9 @@ function bareToolName(toolName) {
|
|
|
4016
4016
|
const parts = toolName.split("__");
|
|
4017
4017
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
4018
4018
|
}
|
|
4019
|
-
function valuesAt(args,
|
|
4019
|
+
function valuesAt(args, path16) {
|
|
4020
4020
|
let cursors = [args];
|
|
4021
|
-
for (const rawSegment of
|
|
4021
|
+
for (const rawSegment of path16.split(".")) {
|
|
4022
4022
|
const isArray = rawSegment.endsWith("[]");
|
|
4023
4023
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
4024
4024
|
const next = [];
|
|
@@ -4050,15 +4050,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
4050
4050
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4051
4051
|
if (!paths) return null;
|
|
4052
4052
|
const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
4053
|
-
for (const
|
|
4054
|
-
for (const value of valuesAt(args,
|
|
4053
|
+
for (const path16 of paths) {
|
|
4054
|
+
for (const value of valuesAt(args, path16)) {
|
|
4055
4055
|
const host = hostOf(value);
|
|
4056
4056
|
if (!host) continue;
|
|
4057
4057
|
const m = classifySsrf(host);
|
|
4058
4058
|
if (!m) continue;
|
|
4059
4059
|
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
4060
4060
|
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
4061
|
-
return { ...m, argPath:
|
|
4061
|
+
return { ...m, argPath: path16, host, reason: ssrfReason(m, host) };
|
|
4062
4062
|
}
|
|
4063
4063
|
}
|
|
4064
4064
|
return null;
|
|
@@ -4181,7 +4181,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4181
4181
|
reason: dest.reason,
|
|
4182
4182
|
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
4183
4183
|
ruleDescription: dest.reason,
|
|
4184
|
-
tier: 3
|
|
4184
|
+
tier: 3,
|
|
4185
|
+
overridable: dest.overridable
|
|
4185
4186
|
};
|
|
4186
4187
|
}
|
|
4187
4188
|
}
|
|
@@ -4330,7 +4331,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4330
4331
|
reason: ssrf.reason,
|
|
4331
4332
|
ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
|
|
4332
4333
|
ruleDescription: ssrf.reason,
|
|
4333
|
-
tier: 3
|
|
4334
|
+
tier: 3,
|
|
4335
|
+
overridable: ssrf.overridable
|
|
4334
4336
|
};
|
|
4335
4337
|
}
|
|
4336
4338
|
}
|
|
@@ -5836,6 +5838,45 @@ function isTrustedHost(host) {
|
|
|
5836
5838
|
);
|
|
5837
5839
|
}
|
|
5838
5840
|
|
|
5841
|
+
// src/auth/api-url.ts
|
|
5842
|
+
var DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
5843
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"]);
|
|
5844
|
+
var HOST_ALLOW_ENV = "NODE9_API_HOST_ALLOW";
|
|
5845
|
+
function defaultHostSuffixes() {
|
|
5846
|
+
const host = new URL(DEFAULT_API_URL).hostname.toLowerCase();
|
|
5847
|
+
const parent = host.split(".").slice(1).join(".");
|
|
5848
|
+
return parent.includes(".") ? [host, parent] : [host];
|
|
5849
|
+
}
|
|
5850
|
+
function allowedSuffixes() {
|
|
5851
|
+
const extra = (process.env[HOST_ALLOW_ENV] ?? "").split(",").map(
|
|
5852
|
+
(s) => s.trim().toLowerCase().replace(/^\*?\./, "")
|
|
5853
|
+
).filter(Boolean);
|
|
5854
|
+
return [...defaultHostSuffixes(), ...extra];
|
|
5855
|
+
}
|
|
5856
|
+
function validateApiUrl(raw) {
|
|
5857
|
+
if (typeof raw !== "string" || raw.length === 0 || raw.length > 2048) return null;
|
|
5858
|
+
let u;
|
|
5859
|
+
try {
|
|
5860
|
+
u = new URL(raw);
|
|
5861
|
+
} catch {
|
|
5862
|
+
return null;
|
|
5863
|
+
}
|
|
5864
|
+
if (u.username || u.password) return null;
|
|
5865
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
|
|
5866
|
+
const host = u.hostname.toLowerCase();
|
|
5867
|
+
if (LOOPBACK.has(host)) return u;
|
|
5868
|
+
if (u.protocol !== "https:") return null;
|
|
5869
|
+
const suffixes = allowedSuffixes();
|
|
5870
|
+
const ok = suffixes.some((s) => host === s || host.endsWith("." + s));
|
|
5871
|
+
return ok ? u : null;
|
|
5872
|
+
}
|
|
5873
|
+
function safeApiUrl(raw, onReject) {
|
|
5874
|
+
const ok = validateApiUrl(raw);
|
|
5875
|
+
if (ok) return typeof raw === "string" ? raw : ok.toString();
|
|
5876
|
+
onReject?.(raw);
|
|
5877
|
+
return DEFAULT_API_URL;
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5839
5880
|
// src/config/index.ts
|
|
5840
5881
|
function sanitizeSsrfAllow(entries, source) {
|
|
5841
5882
|
const kept = [];
|
|
@@ -6124,12 +6165,27 @@ var ADVISORY_SMART_RULES = [
|
|
|
6124
6165
|
}
|
|
6125
6166
|
];
|
|
6126
6167
|
var cachedConfig = null;
|
|
6168
|
+
var rejectedApiUrlsSeen = /* @__PURE__ */ new Set();
|
|
6169
|
+
function noteRejectedApiUrl(raw) {
|
|
6170
|
+
const key = String(raw).slice(0, 200);
|
|
6171
|
+
if (rejectedApiUrlsSeen.has(key)) return;
|
|
6172
|
+
rejectedApiUrlsSeen.add(key);
|
|
6173
|
+
try {
|
|
6174
|
+
fs4.appendFileSync(
|
|
6175
|
+
path4.join(os4.homedir(), ".node9", "hook-debug.log"),
|
|
6176
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
|
|
6177
|
+
`
|
|
6178
|
+
);
|
|
6179
|
+
} catch {
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
6127
6182
|
function getCredentials() {
|
|
6128
|
-
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
6129
6183
|
if (process.env.NODE9_API_KEY) {
|
|
6130
6184
|
return {
|
|
6131
6185
|
apiKey: process.env.NODE9_API_KEY,
|
|
6132
|
-
|
|
6186
|
+
// NODE9_API_URL is env, and a hook inherits the agent's env, so it is
|
|
6187
|
+
// no more trusted than the file below.
|
|
6188
|
+
apiUrl: safeApiUrl(process.env.NODE9_API_URL || DEFAULT_API_URL, noteRejectedApiUrl)
|
|
6133
6189
|
};
|
|
6134
6190
|
}
|
|
6135
6191
|
try {
|
|
@@ -6141,14 +6197,14 @@ function getCredentials() {
|
|
|
6141
6197
|
if (profile?.apiKey) {
|
|
6142
6198
|
return {
|
|
6143
6199
|
apiKey: profile.apiKey,
|
|
6144
|
-
apiUrl: profile.apiUrl || DEFAULT_API_URL,
|
|
6200
|
+
apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
|
|
6145
6201
|
localOnly: profile.localOnly === true || profileName !== "default"
|
|
6146
6202
|
};
|
|
6147
6203
|
}
|
|
6148
6204
|
if (creds.apiKey) {
|
|
6149
6205
|
return {
|
|
6150
6206
|
apiKey: creds.apiKey,
|
|
6151
|
-
apiUrl: creds.apiUrl || DEFAULT_API_URL,
|
|
6207
|
+
apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
|
|
6152
6208
|
localOnly: creds.localOnly === true
|
|
6153
6209
|
};
|
|
6154
6210
|
}
|
|
@@ -6581,13 +6637,13 @@ function getConfig(cwd) {
|
|
|
6581
6637
|
}
|
|
6582
6638
|
if (Array.isArray(mc.jailPaths)) {
|
|
6583
6639
|
for (const jp of mc.jailPaths) {
|
|
6584
|
-
const
|
|
6585
|
-
if (!
|
|
6640
|
+
const path16 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
6641
|
+
if (!path16) continue;
|
|
6586
6642
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
6587
|
-
for (const r of pathRules(
|
|
6643
|
+
for (const r of pathRules(path16, verdict, "org-managed jail")) {
|
|
6588
6644
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
6589
6645
|
}
|
|
6590
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
6646
|
+
mergedPolicy.managedJailPaths.push({ path: path16, verdict });
|
|
6591
6647
|
}
|
|
6592
6648
|
}
|
|
6593
6649
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -6924,11 +6980,41 @@ function isIgnoredTool2(toolName) {
|
|
|
6924
6980
|
}
|
|
6925
6981
|
|
|
6926
6982
|
// src/auth/state.ts
|
|
6983
|
+
import fs8 from "fs";
|
|
6984
|
+
import path8 from "path";
|
|
6985
|
+
import os6 from "os";
|
|
6986
|
+
|
|
6987
|
+
// src/utils/atomic-write.ts
|
|
6927
6988
|
import fs7 from "fs";
|
|
6928
6989
|
import path7 from "path";
|
|
6929
|
-
import
|
|
6930
|
-
|
|
6931
|
-
|
|
6990
|
+
import { randomUUID } from "crypto";
|
|
6991
|
+
function atomicWriteSync(filePath, data, options) {
|
|
6992
|
+
const dir = path7.dirname(filePath);
|
|
6993
|
+
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
6994
|
+
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
6995
|
+
try {
|
|
6996
|
+
fs7.writeFileSync(tmpPath, data, options);
|
|
6997
|
+
} catch (err) {
|
|
6998
|
+
try {
|
|
6999
|
+
fs7.unlinkSync(tmpPath);
|
|
7000
|
+
} catch {
|
|
7001
|
+
}
|
|
7002
|
+
throw err;
|
|
7003
|
+
}
|
|
7004
|
+
try {
|
|
7005
|
+
fs7.renameSync(tmpPath, filePath);
|
|
7006
|
+
} catch (err) {
|
|
7007
|
+
try {
|
|
7008
|
+
fs7.unlinkSync(tmpPath);
|
|
7009
|
+
} catch {
|
|
7010
|
+
}
|
|
7011
|
+
throw err;
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
|
|
7015
|
+
// src/auth/state.ts
|
|
7016
|
+
var PAUSED_FILE = path8.join(os6.homedir(), ".node9", "PAUSED");
|
|
7017
|
+
var TRUST_FILE = path8.join(os6.homedir(), ".node9", "trust.json");
|
|
6932
7018
|
function extractCommandPattern(toolName, args) {
|
|
6933
7019
|
const lower = toolName.toLowerCase();
|
|
6934
7020
|
if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
|
|
@@ -6940,11 +7026,11 @@ function extractCommandPattern(toolName, args) {
|
|
|
6940
7026
|
}
|
|
6941
7027
|
function checkPause() {
|
|
6942
7028
|
try {
|
|
6943
|
-
if (!
|
|
6944
|
-
const state = JSON.parse(
|
|
7029
|
+
if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
|
|
7030
|
+
const state = JSON.parse(fs8.readFileSync(PAUSED_FILE, "utf-8"));
|
|
6945
7031
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
6946
7032
|
try {
|
|
6947
|
-
|
|
7033
|
+
fs8.unlinkSync(PAUSED_FILE);
|
|
6948
7034
|
} catch {
|
|
6949
7035
|
}
|
|
6950
7036
|
return { paused: false };
|
|
@@ -6954,21 +7040,14 @@ function checkPause() {
|
|
|
6954
7040
|
return { paused: false };
|
|
6955
7041
|
}
|
|
6956
7042
|
}
|
|
6957
|
-
function atomicWriteSync(filePath, data, options) {
|
|
6958
|
-
const dir = path7.dirname(filePath);
|
|
6959
|
-
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
6960
|
-
const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
|
|
6961
|
-
fs7.writeFileSync(tmpPath, data, options);
|
|
6962
|
-
fs7.renameSync(tmpPath, filePath);
|
|
6963
|
-
}
|
|
6964
7043
|
function getActiveTrustSession(toolName, args) {
|
|
6965
7044
|
try {
|
|
6966
|
-
if (!
|
|
6967
|
-
const trust = JSON.parse(
|
|
7045
|
+
if (!fs8.existsSync(TRUST_FILE)) return false;
|
|
7046
|
+
const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
6968
7047
|
const now = Date.now();
|
|
6969
7048
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
6970
7049
|
if (active.length !== trust.entries.length) {
|
|
6971
|
-
|
|
7050
|
+
atomicWriteSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
6972
7051
|
}
|
|
6973
7052
|
return active.some((e) => {
|
|
6974
7053
|
if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
|
|
@@ -6987,8 +7066,8 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
6987
7066
|
try {
|
|
6988
7067
|
let trust = { entries: [] };
|
|
6989
7068
|
try {
|
|
6990
|
-
if (
|
|
6991
|
-
trust = JSON.parse(
|
|
7069
|
+
if (fs8.existsSync(TRUST_FILE)) {
|
|
7070
|
+
trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
6992
7071
|
}
|
|
6993
7072
|
} catch {
|
|
6994
7073
|
}
|
|
@@ -7010,9 +7089,9 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
7010
7089
|
}
|
|
7011
7090
|
function getPersistentDecision(toolName) {
|
|
7012
7091
|
try {
|
|
7013
|
-
const file =
|
|
7014
|
-
if (!
|
|
7015
|
-
const decisions = JSON.parse(
|
|
7092
|
+
const file = path8.join(os6.homedir(), ".node9", "decisions.json");
|
|
7093
|
+
if (!fs8.existsSync(file)) return null;
|
|
7094
|
+
const decisions = JSON.parse(fs8.readFileSync(file, "utf-8"));
|
|
7016
7095
|
const d = decisions[toolName];
|
|
7017
7096
|
if (d === "allow" || d === "deny") return d;
|
|
7018
7097
|
} catch {
|
|
@@ -7021,11 +7100,11 @@ function getPersistentDecision(toolName) {
|
|
|
7021
7100
|
}
|
|
7022
7101
|
|
|
7023
7102
|
// src/auth/daemon.ts
|
|
7024
|
-
import
|
|
7103
|
+
import fs9 from "fs";
|
|
7025
7104
|
import net from "net";
|
|
7026
|
-
import
|
|
7105
|
+
import path9 from "path";
|
|
7027
7106
|
import os7 from "os";
|
|
7028
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
7107
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path9.join(os7.tmpdir(), "node9-activity.sock");
|
|
7029
7108
|
function notifyActivitySocket(data) {
|
|
7030
7109
|
return new Promise((resolve) => {
|
|
7031
7110
|
try {
|
|
@@ -7058,9 +7137,9 @@ var DAEMON_PORT = 7391;
|
|
|
7058
7137
|
var DAEMON_HOST = "127.0.0.1";
|
|
7059
7138
|
function getInternalToken() {
|
|
7060
7139
|
try {
|
|
7061
|
-
const pidFile =
|
|
7062
|
-
if (!
|
|
7063
|
-
const data = JSON.parse(
|
|
7140
|
+
const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
|
|
7141
|
+
if (!fs9.existsSync(pidFile)) return null;
|
|
7142
|
+
const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
|
|
7064
7143
|
process.kill(data.pid, 0);
|
|
7065
7144
|
return data.internalToken ?? null;
|
|
7066
7145
|
} catch {
|
|
@@ -7068,12 +7147,12 @@ function getInternalToken() {
|
|
|
7068
7147
|
}
|
|
7069
7148
|
}
|
|
7070
7149
|
function isDaemonRunning() {
|
|
7071
|
-
const pidFile =
|
|
7072
|
-
if (
|
|
7150
|
+
const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
|
|
7151
|
+
if (fs9.existsSync(pidFile)) {
|
|
7073
7152
|
let pid;
|
|
7074
7153
|
let port;
|
|
7075
7154
|
try {
|
|
7076
|
-
const data = JSON.parse(
|
|
7155
|
+
const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
|
|
7077
7156
|
pid = data.pid;
|
|
7078
7157
|
port = data.port;
|
|
7079
7158
|
} catch {
|
|
@@ -7091,7 +7170,7 @@ function isDaemonRunning() {
|
|
|
7091
7170
|
} catch (err) {
|
|
7092
7171
|
if (err instanceof Error && "code" in err && err.code === "ESRCH") {
|
|
7093
7172
|
try {
|
|
7094
|
-
|
|
7173
|
+
fs9.unlinkSync(pidFile);
|
|
7095
7174
|
} catch {
|
|
7096
7175
|
}
|
|
7097
7176
|
}
|
|
@@ -7257,14 +7336,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
7257
7336
|
}
|
|
7258
7337
|
|
|
7259
7338
|
// src/auth/orchestrator.ts
|
|
7260
|
-
import { randomUUID } from "crypto";
|
|
7339
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
7261
7340
|
|
|
7262
7341
|
// src/ui/native.ts
|
|
7263
7342
|
import { spawn } from "child_process";
|
|
7264
|
-
import
|
|
7343
|
+
import path11 from "path";
|
|
7265
7344
|
|
|
7266
7345
|
// src/context-sniper.ts
|
|
7267
|
-
import
|
|
7346
|
+
import path10 from "path";
|
|
7268
7347
|
function smartTruncate(str, maxLen = 500) {
|
|
7269
7348
|
if (str.length <= maxLen) return str;
|
|
7270
7349
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -7332,7 +7411,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
7332
7411
|
intent = "EDIT";
|
|
7333
7412
|
if (obj.file_path) {
|
|
7334
7413
|
editFilePath = String(obj.file_path);
|
|
7335
|
-
editFileName =
|
|
7414
|
+
editFileName = path10.basename(editFilePath);
|
|
7336
7415
|
}
|
|
7337
7416
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
7338
7417
|
contextSnippet = result.snippet;
|
|
@@ -7396,7 +7475,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
7396
7475
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
7397
7476
|
const obj = parsed;
|
|
7398
7477
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
7399
|
-
const file = obj.file_path ?
|
|
7478
|
+
const file = obj.file_path ? path11.basename(String(obj.file_path)) : "file";
|
|
7400
7479
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
7401
7480
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
7402
7481
|
return {
|
|
@@ -7594,22 +7673,22 @@ end run`;
|
|
|
7594
7673
|
}
|
|
7595
7674
|
|
|
7596
7675
|
// src/canary/registry.ts
|
|
7597
|
-
import
|
|
7676
|
+
import fs11 from "fs";
|
|
7598
7677
|
import os9 from "os";
|
|
7599
|
-
import
|
|
7678
|
+
import path13 from "path";
|
|
7600
7679
|
|
|
7601
7680
|
// src/shields/jail.ts
|
|
7602
|
-
import
|
|
7681
|
+
import fs10 from "fs";
|
|
7603
7682
|
import os8 from "os";
|
|
7604
|
-
import
|
|
7683
|
+
import path12 from "path";
|
|
7605
7684
|
var USER_JAIL_SHIELD = "user-jail";
|
|
7606
7685
|
function jailStorePath() {
|
|
7607
|
-
return
|
|
7686
|
+
return path12.join(os8.homedir(), ".node9", "jail-paths.json");
|
|
7608
7687
|
}
|
|
7609
7688
|
function readJailPaths() {
|
|
7610
7689
|
let text;
|
|
7611
7690
|
try {
|
|
7612
|
-
text =
|
|
7691
|
+
text = fs10.readFileSync(jailStorePath(), "utf8");
|
|
7613
7692
|
} catch (err) {
|
|
7614
7693
|
if (err.code === "ENOENT") return [];
|
|
7615
7694
|
throw err;
|
|
@@ -7638,7 +7717,7 @@ function findJailedPathIn(candidate, paths) {
|
|
|
7638
7717
|
|
|
7639
7718
|
// src/canary/registry.ts
|
|
7640
7719
|
function canaryStorePath() {
|
|
7641
|
-
return
|
|
7720
|
+
return path13.join(os9.homedir(), ".node9", "canaries.json");
|
|
7642
7721
|
}
|
|
7643
7722
|
function isRecord(x) {
|
|
7644
7723
|
if (!x || typeof x !== "object") return false;
|
|
@@ -7649,7 +7728,7 @@ function loadCanaries(opts) {
|
|
|
7649
7728
|
const p = canaryStorePath();
|
|
7650
7729
|
let raw;
|
|
7651
7730
|
try {
|
|
7652
|
-
raw =
|
|
7731
|
+
raw = fs11.readFileSync(p, "utf-8");
|
|
7653
7732
|
} catch (e) {
|
|
7654
7733
|
if (e.code === "ENOENT") return [];
|
|
7655
7734
|
throw e;
|
|
@@ -7676,9 +7755,50 @@ init_audit();
|
|
|
7676
7755
|
|
|
7677
7756
|
// src/auth/cloud.ts
|
|
7678
7757
|
init_audit();
|
|
7679
|
-
import
|
|
7758
|
+
import fs13 from "fs";
|
|
7680
7759
|
import os10 from "os";
|
|
7681
|
-
import
|
|
7760
|
+
import path14 from "path";
|
|
7761
|
+
|
|
7762
|
+
// src/utils/read-capped.ts
|
|
7763
|
+
import fs12 from "fs";
|
|
7764
|
+
function readCapped(file, maxBytes) {
|
|
7765
|
+
if (maxBytes < 0) return null;
|
|
7766
|
+
let fd;
|
|
7767
|
+
try {
|
|
7768
|
+
fd = fs12.openSync(file, "r");
|
|
7769
|
+
} catch {
|
|
7770
|
+
return null;
|
|
7771
|
+
}
|
|
7772
|
+
try {
|
|
7773
|
+
const st = fs12.fstatSync(fd);
|
|
7774
|
+
if (!st.isFile()) return null;
|
|
7775
|
+
const want = Math.min(st.size, maxBytes + 1);
|
|
7776
|
+
const buf = Buffer.alloc(want);
|
|
7777
|
+
let read = 0;
|
|
7778
|
+
while (read < want) {
|
|
7779
|
+
const n = fs12.readSync(fd, buf, read, want - read, read);
|
|
7780
|
+
if (n <= 0) break;
|
|
7781
|
+
read += n;
|
|
7782
|
+
}
|
|
7783
|
+
return {
|
|
7784
|
+
bytes: buf.subarray(0, Math.min(read, maxBytes)),
|
|
7785
|
+
truncated: read > maxBytes
|
|
7786
|
+
};
|
|
7787
|
+
} catch {
|
|
7788
|
+
return null;
|
|
7789
|
+
} finally {
|
|
7790
|
+
try {
|
|
7791
|
+
fs12.closeSync(fd);
|
|
7792
|
+
} catch {
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
function readCappedText(file, maxBytes) {
|
|
7797
|
+
const r = readCapped(file, maxBytes);
|
|
7798
|
+
return r ? { text: r.bytes.toString("utf8"), truncated: r.truncated } : null;
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
// src/auth/cloud.ts
|
|
7682
7802
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
|
|
7683
7803
|
const controller = new AbortController();
|
|
7684
7804
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -7686,10 +7806,11 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
7686
7806
|
let ciContext;
|
|
7687
7807
|
if (process.env.CI) {
|
|
7688
7808
|
try {
|
|
7689
|
-
const ciContextPath =
|
|
7690
|
-
const
|
|
7691
|
-
if (
|
|
7692
|
-
|
|
7809
|
+
const ciContextPath = path14.join(os10.homedir(), ".node9", "ci-context.json");
|
|
7810
|
+
const ci = readCappedText(ciContextPath, 1e4);
|
|
7811
|
+
if (!ci) throw new Error("ci-context.json unreadable");
|
|
7812
|
+
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
7813
|
+
const raw = ci.text;
|
|
7693
7814
|
const parsed = JSON.parse(raw);
|
|
7694
7815
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
7695
7816
|
throw new Error("ci-context.json is not a plain object");
|
|
@@ -7788,14 +7909,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
7788
7909
|
});
|
|
7789
7910
|
clearTimeout(timer);
|
|
7790
7911
|
if (!res.ok) {
|
|
7791
|
-
|
|
7912
|
+
fs13.appendFileSync(
|
|
7792
7913
|
HOOK_DEBUG_LOG,
|
|
7793
7914
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
7794
7915
|
`
|
|
7795
7916
|
);
|
|
7796
7917
|
}
|
|
7797
7918
|
} catch (err) {
|
|
7798
|
-
|
|
7919
|
+
fs13.appendFileSync(
|
|
7799
7920
|
HOOK_DEBUG_LOG,
|
|
7800
7921
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
7801
7922
|
`
|
|
@@ -7804,16 +7925,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
7804
7925
|
}
|
|
7805
7926
|
|
|
7806
7927
|
// src/loop-detector.ts
|
|
7807
|
-
import
|
|
7808
|
-
import
|
|
7928
|
+
import fs14 from "fs";
|
|
7929
|
+
import path15 from "path";
|
|
7809
7930
|
import os11 from "os";
|
|
7810
7931
|
function loopStateFile() {
|
|
7811
|
-
return
|
|
7932
|
+
return path15.join(os11.homedir(), ".node9", "loop-state.json");
|
|
7812
7933
|
}
|
|
7813
7934
|
function readState() {
|
|
7814
7935
|
try {
|
|
7815
|
-
if (!
|
|
7816
|
-
const raw =
|
|
7936
|
+
if (!fs14.existsSync(loopStateFile())) return [];
|
|
7937
|
+
const raw = fs14.readFileSync(loopStateFile(), "utf-8");
|
|
7817
7938
|
const parsed = JSON.parse(raw);
|
|
7818
7939
|
if (!Array.isArray(parsed)) return [];
|
|
7819
7940
|
return parsed;
|
|
@@ -7822,11 +7943,11 @@ function readState() {
|
|
|
7822
7943
|
}
|
|
7823
7944
|
}
|
|
7824
7945
|
function writeState(records) {
|
|
7825
|
-
const dir =
|
|
7826
|
-
if (!
|
|
7946
|
+
const dir = path15.dirname(loopStateFile());
|
|
7947
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
7827
7948
|
const tmpPath = `${loopStateFile()}.${os11.hostname()}.${process.pid}.tmp`;
|
|
7828
|
-
|
|
7829
|
-
|
|
7949
|
+
fs14.writeFileSync(tmpPath, JSON.stringify(records));
|
|
7950
|
+
fs14.renameSync(tmpPath, loopStateFile());
|
|
7830
7951
|
}
|
|
7831
7952
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
7832
7953
|
try {
|
|
@@ -7894,9 +8015,13 @@ async function hasReachableHumanApprover(opts) {
|
|
|
7894
8015
|
if (nativeReachable) return true;
|
|
7895
8016
|
return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
|
|
7896
8017
|
}
|
|
8018
|
+
function mayDowngradeHardBlock(opts) {
|
|
8019
|
+
if (opts.overridable === false) return false;
|
|
8020
|
+
return opts.daemonUp && !opts.isTestEnv && opts.humanApproverReachable;
|
|
8021
|
+
}
|
|
7897
8022
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
7898
8023
|
if (!options?.calledFromDaemon) {
|
|
7899
|
-
const actId =
|
|
8024
|
+
const actId = randomUUID2();
|
|
7900
8025
|
const actTs = Date.now();
|
|
7901
8026
|
const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
|
|
7902
8027
|
const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
|
|
@@ -8284,7 +8409,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8284
8409
|
calledFromDaemon: options?.calledFromDaemon
|
|
8285
8410
|
});
|
|
8286
8411
|
}
|
|
8287
|
-
const mayDowngrade =
|
|
8412
|
+
const mayDowngrade = mayDowngradeHardBlock({
|
|
8413
|
+
daemonUp,
|
|
8414
|
+
isTestEnv: isTestEnv2,
|
|
8415
|
+
humanApproverReachable,
|
|
8416
|
+
overridable: policyResult.overridable
|
|
8417
|
+
});
|
|
8288
8418
|
const hardBlock = () => {
|
|
8289
8419
|
if (!isManual)
|
|
8290
8420
|
appendLocalAudit(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.3",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|