@node9/proxy 2.14.1 → 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 +1548 -1386
- package/dist/cli.mjs +1479 -1319
- package/dist/dashboard.mjs +91 -11
- package/dist/index.js +230 -92
- package/dist/index.mjs +230 -92
- 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
|
}
|
|
@@ -4454,6 +4456,15 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4454
4456
|
function isIgnoredTool(toolName, config) {
|
|
4455
4457
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
4456
4458
|
}
|
|
4459
|
+
var TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
4460
|
+
function stripTerminalEscapes(s) {
|
|
4461
|
+
return s.replace(TERMINAL_ESCAPE_RE, "");
|
|
4462
|
+
}
|
|
4463
|
+
function safeMessage(value, max = 300) {
|
|
4464
|
+
const raw = typeof value === "string" ? value : value instanceof Error ? value.message : String(value ?? "");
|
|
4465
|
+
const s = stripTerminalEscapes(raw).replace(/\s+/g, " ").trim();
|
|
4466
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
4467
|
+
}
|
|
4457
4468
|
var aws_default = {
|
|
4458
4469
|
name: "aws",
|
|
4459
4470
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -5827,6 +5838,45 @@ function isTrustedHost(host) {
|
|
|
5827
5838
|
);
|
|
5828
5839
|
}
|
|
5829
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
|
+
|
|
5830
5880
|
// src/config/index.ts
|
|
5831
5881
|
function sanitizeSsrfAllow(entries, source) {
|
|
5832
5882
|
const kept = [];
|
|
@@ -6115,12 +6165,27 @@ var ADVISORY_SMART_RULES = [
|
|
|
6115
6165
|
}
|
|
6116
6166
|
];
|
|
6117
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
|
+
}
|
|
6118
6182
|
function getCredentials() {
|
|
6119
|
-
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
6120
6183
|
if (process.env.NODE9_API_KEY) {
|
|
6121
6184
|
return {
|
|
6122
6185
|
apiKey: process.env.NODE9_API_KEY,
|
|
6123
|
-
|
|
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)
|
|
6124
6189
|
};
|
|
6125
6190
|
}
|
|
6126
6191
|
try {
|
|
@@ -6132,14 +6197,14 @@ function getCredentials() {
|
|
|
6132
6197
|
if (profile?.apiKey) {
|
|
6133
6198
|
return {
|
|
6134
6199
|
apiKey: profile.apiKey,
|
|
6135
|
-
apiUrl: profile.apiUrl || DEFAULT_API_URL,
|
|
6200
|
+
apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
|
|
6136
6201
|
localOnly: profile.localOnly === true || profileName !== "default"
|
|
6137
6202
|
};
|
|
6138
6203
|
}
|
|
6139
6204
|
if (creds.apiKey) {
|
|
6140
6205
|
return {
|
|
6141
6206
|
apiKey: creds.apiKey,
|
|
6142
|
-
apiUrl: creds.apiUrl || DEFAULT_API_URL,
|
|
6207
|
+
apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
|
|
6143
6208
|
localOnly: creds.localOnly === true
|
|
6144
6209
|
};
|
|
6145
6210
|
}
|
|
@@ -6572,13 +6637,13 @@ function getConfig(cwd) {
|
|
|
6572
6637
|
}
|
|
6573
6638
|
if (Array.isArray(mc.jailPaths)) {
|
|
6574
6639
|
for (const jp of mc.jailPaths) {
|
|
6575
|
-
const
|
|
6576
|
-
if (!
|
|
6640
|
+
const path16 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
6641
|
+
if (!path16) continue;
|
|
6577
6642
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
6578
|
-
for (const r of pathRules(
|
|
6643
|
+
for (const r of pathRules(path16, verdict, "org-managed jail")) {
|
|
6579
6644
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
6580
6645
|
}
|
|
6581
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
6646
|
+
mergedPolicy.managedJailPaths.push({ path: path16, verdict });
|
|
6582
6647
|
}
|
|
6583
6648
|
}
|
|
6584
6649
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -6915,11 +6980,41 @@ function isIgnoredTool2(toolName) {
|
|
|
6915
6980
|
}
|
|
6916
6981
|
|
|
6917
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
|
|
6918
6988
|
import fs7 from "fs";
|
|
6919
6989
|
import path7 from "path";
|
|
6920
|
-
import
|
|
6921
|
-
|
|
6922
|
-
|
|
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");
|
|
6923
7018
|
function extractCommandPattern(toolName, args) {
|
|
6924
7019
|
const lower = toolName.toLowerCase();
|
|
6925
7020
|
if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
|
|
@@ -6931,11 +7026,11 @@ function extractCommandPattern(toolName, args) {
|
|
|
6931
7026
|
}
|
|
6932
7027
|
function checkPause() {
|
|
6933
7028
|
try {
|
|
6934
|
-
if (!
|
|
6935
|
-
const state = JSON.parse(
|
|
7029
|
+
if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
|
|
7030
|
+
const state = JSON.parse(fs8.readFileSync(PAUSED_FILE, "utf-8"));
|
|
6936
7031
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
6937
7032
|
try {
|
|
6938
|
-
|
|
7033
|
+
fs8.unlinkSync(PAUSED_FILE);
|
|
6939
7034
|
} catch {
|
|
6940
7035
|
}
|
|
6941
7036
|
return { paused: false };
|
|
@@ -6945,21 +7040,14 @@ function checkPause() {
|
|
|
6945
7040
|
return { paused: false };
|
|
6946
7041
|
}
|
|
6947
7042
|
}
|
|
6948
|
-
function atomicWriteSync(filePath, data, options) {
|
|
6949
|
-
const dir = path7.dirname(filePath);
|
|
6950
|
-
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
6951
|
-
const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
|
|
6952
|
-
fs7.writeFileSync(tmpPath, data, options);
|
|
6953
|
-
fs7.renameSync(tmpPath, filePath);
|
|
6954
|
-
}
|
|
6955
7043
|
function getActiveTrustSession(toolName, args) {
|
|
6956
7044
|
try {
|
|
6957
|
-
if (!
|
|
6958
|
-
const trust = JSON.parse(
|
|
7045
|
+
if (!fs8.existsSync(TRUST_FILE)) return false;
|
|
7046
|
+
const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
6959
7047
|
const now = Date.now();
|
|
6960
7048
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
6961
7049
|
if (active.length !== trust.entries.length) {
|
|
6962
|
-
|
|
7050
|
+
atomicWriteSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
6963
7051
|
}
|
|
6964
7052
|
return active.some((e) => {
|
|
6965
7053
|
if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
|
|
@@ -6978,8 +7066,8 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
6978
7066
|
try {
|
|
6979
7067
|
let trust = { entries: [] };
|
|
6980
7068
|
try {
|
|
6981
|
-
if (
|
|
6982
|
-
trust = JSON.parse(
|
|
7069
|
+
if (fs8.existsSync(TRUST_FILE)) {
|
|
7070
|
+
trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
6983
7071
|
}
|
|
6984
7072
|
} catch {
|
|
6985
7073
|
}
|
|
@@ -7001,9 +7089,9 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
7001
7089
|
}
|
|
7002
7090
|
function getPersistentDecision(toolName) {
|
|
7003
7091
|
try {
|
|
7004
|
-
const file =
|
|
7005
|
-
if (!
|
|
7006
|
-
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"));
|
|
7007
7095
|
const d = decisions[toolName];
|
|
7008
7096
|
if (d === "allow" || d === "deny") return d;
|
|
7009
7097
|
} catch {
|
|
@@ -7012,11 +7100,11 @@ function getPersistentDecision(toolName) {
|
|
|
7012
7100
|
}
|
|
7013
7101
|
|
|
7014
7102
|
// src/auth/daemon.ts
|
|
7015
|
-
import
|
|
7103
|
+
import fs9 from "fs";
|
|
7016
7104
|
import net from "net";
|
|
7017
|
-
import
|
|
7105
|
+
import path9 from "path";
|
|
7018
7106
|
import os7 from "os";
|
|
7019
|
-
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");
|
|
7020
7108
|
function notifyActivitySocket(data) {
|
|
7021
7109
|
return new Promise((resolve) => {
|
|
7022
7110
|
try {
|
|
@@ -7049,9 +7137,9 @@ var DAEMON_PORT = 7391;
|
|
|
7049
7137
|
var DAEMON_HOST = "127.0.0.1";
|
|
7050
7138
|
function getInternalToken() {
|
|
7051
7139
|
try {
|
|
7052
|
-
const pidFile =
|
|
7053
|
-
if (!
|
|
7054
|
-
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"));
|
|
7055
7143
|
process.kill(data.pid, 0);
|
|
7056
7144
|
return data.internalToken ?? null;
|
|
7057
7145
|
} catch {
|
|
@@ -7059,12 +7147,12 @@ function getInternalToken() {
|
|
|
7059
7147
|
}
|
|
7060
7148
|
}
|
|
7061
7149
|
function isDaemonRunning() {
|
|
7062
|
-
const pidFile =
|
|
7063
|
-
if (
|
|
7150
|
+
const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
|
|
7151
|
+
if (fs9.existsSync(pidFile)) {
|
|
7064
7152
|
let pid;
|
|
7065
7153
|
let port;
|
|
7066
7154
|
try {
|
|
7067
|
-
const data = JSON.parse(
|
|
7155
|
+
const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
|
|
7068
7156
|
pid = data.pid;
|
|
7069
7157
|
port = data.port;
|
|
7070
7158
|
} catch {
|
|
@@ -7082,7 +7170,7 @@ function isDaemonRunning() {
|
|
|
7082
7170
|
} catch (err) {
|
|
7083
7171
|
if (err instanceof Error && "code" in err && err.code === "ESRCH") {
|
|
7084
7172
|
try {
|
|
7085
|
-
|
|
7173
|
+
fs9.unlinkSync(pidFile);
|
|
7086
7174
|
} catch {
|
|
7087
7175
|
}
|
|
7088
7176
|
}
|
|
@@ -7248,14 +7336,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
7248
7336
|
}
|
|
7249
7337
|
|
|
7250
7338
|
// src/auth/orchestrator.ts
|
|
7251
|
-
import { randomUUID } from "crypto";
|
|
7339
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
7252
7340
|
|
|
7253
7341
|
// src/ui/native.ts
|
|
7254
7342
|
import { spawn } from "child_process";
|
|
7255
|
-
import
|
|
7343
|
+
import path11 from "path";
|
|
7256
7344
|
|
|
7257
7345
|
// src/context-sniper.ts
|
|
7258
|
-
import
|
|
7346
|
+
import path10 from "path";
|
|
7259
7347
|
function smartTruncate(str, maxLen = 500) {
|
|
7260
7348
|
if (str.length <= maxLen) return str;
|
|
7261
7349
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -7323,7 +7411,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
7323
7411
|
intent = "EDIT";
|
|
7324
7412
|
if (obj.file_path) {
|
|
7325
7413
|
editFilePath = String(obj.file_path);
|
|
7326
|
-
editFileName =
|
|
7414
|
+
editFileName = path10.basename(editFilePath);
|
|
7327
7415
|
}
|
|
7328
7416
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
7329
7417
|
contextSnippet = result.snippet;
|
|
@@ -7387,7 +7475,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
7387
7475
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
7388
7476
|
const obj = parsed;
|
|
7389
7477
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
7390
|
-
const file = obj.file_path ?
|
|
7478
|
+
const file = obj.file_path ? path11.basename(String(obj.file_path)) : "file";
|
|
7391
7479
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
7392
7480
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
7393
7481
|
return {
|
|
@@ -7453,7 +7541,7 @@ function escapePango(text) {
|
|
|
7453
7541
|
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
7454
7542
|
const lines = [];
|
|
7455
7543
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
7456
|
-
const safeAgent = (agent ?? "AI Agent"
|
|
7544
|
+
const safeAgent = safeMessage(agent ?? "AI Agent", 80);
|
|
7457
7545
|
lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
|
|
7458
7546
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
7459
7547
|
if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
|
|
@@ -7585,22 +7673,22 @@ end run`;
|
|
|
7585
7673
|
}
|
|
7586
7674
|
|
|
7587
7675
|
// src/canary/registry.ts
|
|
7588
|
-
import
|
|
7676
|
+
import fs11 from "fs";
|
|
7589
7677
|
import os9 from "os";
|
|
7590
|
-
import
|
|
7678
|
+
import path13 from "path";
|
|
7591
7679
|
|
|
7592
7680
|
// src/shields/jail.ts
|
|
7593
|
-
import
|
|
7681
|
+
import fs10 from "fs";
|
|
7594
7682
|
import os8 from "os";
|
|
7595
|
-
import
|
|
7683
|
+
import path12 from "path";
|
|
7596
7684
|
var USER_JAIL_SHIELD = "user-jail";
|
|
7597
7685
|
function jailStorePath() {
|
|
7598
|
-
return
|
|
7686
|
+
return path12.join(os8.homedir(), ".node9", "jail-paths.json");
|
|
7599
7687
|
}
|
|
7600
7688
|
function readJailPaths() {
|
|
7601
7689
|
let text;
|
|
7602
7690
|
try {
|
|
7603
|
-
text =
|
|
7691
|
+
text = fs10.readFileSync(jailStorePath(), "utf8");
|
|
7604
7692
|
} catch (err) {
|
|
7605
7693
|
if (err.code === "ENOENT") return [];
|
|
7606
7694
|
throw err;
|
|
@@ -7629,7 +7717,7 @@ function findJailedPathIn(candidate, paths) {
|
|
|
7629
7717
|
|
|
7630
7718
|
// src/canary/registry.ts
|
|
7631
7719
|
function canaryStorePath() {
|
|
7632
|
-
return
|
|
7720
|
+
return path13.join(os9.homedir(), ".node9", "canaries.json");
|
|
7633
7721
|
}
|
|
7634
7722
|
function isRecord(x) {
|
|
7635
7723
|
if (!x || typeof x !== "object") return false;
|
|
@@ -7640,7 +7728,7 @@ function loadCanaries(opts) {
|
|
|
7640
7728
|
const p = canaryStorePath();
|
|
7641
7729
|
let raw;
|
|
7642
7730
|
try {
|
|
7643
|
-
raw =
|
|
7731
|
+
raw = fs11.readFileSync(p, "utf-8");
|
|
7644
7732
|
} catch (e) {
|
|
7645
7733
|
if (e.code === "ENOENT") return [];
|
|
7646
7734
|
throw e;
|
|
@@ -7667,9 +7755,50 @@ init_audit();
|
|
|
7667
7755
|
|
|
7668
7756
|
// src/auth/cloud.ts
|
|
7669
7757
|
init_audit();
|
|
7670
|
-
import
|
|
7758
|
+
import fs13 from "fs";
|
|
7671
7759
|
import os10 from "os";
|
|
7672
|
-
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
|
|
7673
7802
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
|
|
7674
7803
|
const controller = new AbortController();
|
|
7675
7804
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -7677,10 +7806,11 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
7677
7806
|
let ciContext;
|
|
7678
7807
|
if (process.env.CI) {
|
|
7679
7808
|
try {
|
|
7680
|
-
const ciContextPath =
|
|
7681
|
-
const
|
|
7682
|
-
if (
|
|
7683
|
-
|
|
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;
|
|
7684
7814
|
const parsed = JSON.parse(raw);
|
|
7685
7815
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
7686
7816
|
throw new Error("ci-context.json is not a plain object");
|
|
@@ -7779,32 +7909,32 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
7779
7909
|
});
|
|
7780
7910
|
clearTimeout(timer);
|
|
7781
7911
|
if (!res.ok) {
|
|
7782
|
-
|
|
7912
|
+
fs13.appendFileSync(
|
|
7783
7913
|
HOOK_DEBUG_LOG,
|
|
7784
|
-
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
7914
|
+
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
7785
7915
|
`
|
|
7786
7916
|
);
|
|
7787
7917
|
}
|
|
7788
7918
|
} catch (err) {
|
|
7789
|
-
|
|
7919
|
+
fs13.appendFileSync(
|
|
7790
7920
|
HOOK_DEBUG_LOG,
|
|
7791
|
-
`[resolve-cloud] PATCH failed for ${requestId}: ${err
|
|
7921
|
+
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
7792
7922
|
`
|
|
7793
7923
|
);
|
|
7794
7924
|
}
|
|
7795
7925
|
}
|
|
7796
7926
|
|
|
7797
7927
|
// src/loop-detector.ts
|
|
7798
|
-
import
|
|
7799
|
-
import
|
|
7928
|
+
import fs14 from "fs";
|
|
7929
|
+
import path15 from "path";
|
|
7800
7930
|
import os11 from "os";
|
|
7801
7931
|
function loopStateFile() {
|
|
7802
|
-
return
|
|
7932
|
+
return path15.join(os11.homedir(), ".node9", "loop-state.json");
|
|
7803
7933
|
}
|
|
7804
7934
|
function readState() {
|
|
7805
7935
|
try {
|
|
7806
|
-
if (!
|
|
7807
|
-
const raw =
|
|
7936
|
+
if (!fs14.existsSync(loopStateFile())) return [];
|
|
7937
|
+
const raw = fs14.readFileSync(loopStateFile(), "utf-8");
|
|
7808
7938
|
const parsed = JSON.parse(raw);
|
|
7809
7939
|
if (!Array.isArray(parsed)) return [];
|
|
7810
7940
|
return parsed;
|
|
@@ -7813,11 +7943,11 @@ function readState() {
|
|
|
7813
7943
|
}
|
|
7814
7944
|
}
|
|
7815
7945
|
function writeState(records) {
|
|
7816
|
-
const dir =
|
|
7817
|
-
if (!
|
|
7946
|
+
const dir = path15.dirname(loopStateFile());
|
|
7947
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
7818
7948
|
const tmpPath = `${loopStateFile()}.${os11.hostname()}.${process.pid}.tmp`;
|
|
7819
|
-
|
|
7820
|
-
|
|
7949
|
+
fs14.writeFileSync(tmpPath, JSON.stringify(records));
|
|
7950
|
+
fs14.renameSync(tmpPath, loopStateFile());
|
|
7821
7951
|
}
|
|
7822
7952
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
7823
7953
|
try {
|
|
@@ -7885,13 +8015,16 @@ async function hasReachableHumanApprover(opts) {
|
|
|
7885
8015
|
if (nativeReachable) return true;
|
|
7886
8016
|
return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
|
|
7887
8017
|
}
|
|
8018
|
+
function mayDowngradeHardBlock(opts) {
|
|
8019
|
+
if (opts.overridable === false) return false;
|
|
8020
|
+
return opts.daemonUp && !opts.isTestEnv && opts.humanApproverReachable;
|
|
8021
|
+
}
|
|
7888
8022
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
7889
8023
|
if (!options?.calledFromDaemon) {
|
|
7890
|
-
const actId =
|
|
8024
|
+
const actId = randomUUID2();
|
|
7891
8025
|
const actTs = Date.now();
|
|
7892
|
-
const
|
|
7893
|
-
const
|
|
7894
|
-
const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
|
|
8026
|
+
const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
|
|
8027
|
+
const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
|
|
7895
8028
|
const socketOk = await notifyActivity({
|
|
7896
8029
|
id: actId,
|
|
7897
8030
|
ts: actTs,
|
|
@@ -8276,7 +8409,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8276
8409
|
calledFromDaemon: options?.calledFromDaemon
|
|
8277
8410
|
});
|
|
8278
8411
|
}
|
|
8279
|
-
const mayDowngrade =
|
|
8412
|
+
const mayDowngrade = mayDowngradeHardBlock({
|
|
8413
|
+
daemonUp,
|
|
8414
|
+
isTestEnv: isTestEnv2,
|
|
8415
|
+
humanApproverReachable,
|
|
8416
|
+
overridable: policyResult.overridable
|
|
8417
|
+
});
|
|
8280
8418
|
const hardBlock = () => {
|
|
8281
8419
|
if (!isManual)
|
|
8282
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",
|