@node9/proxy 2.31.3 → 2.32.0
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 +4466 -2353
- package/dist/cli.mjs +4385 -2273
- package/dist/dashboard.mjs +179 -19
- package/dist/index.js +1645 -179
- package/dist/index.mjs +1642 -176
- package/dist/scan-ink.mjs +73 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -58,6 +58,7 @@ import safeRegex3 from "safe-regex2";
|
|
|
58
58
|
import { createHash as createHash2 } from "crypto";
|
|
59
59
|
import { createHash as createHash22 } from "crypto";
|
|
60
60
|
import mvdanSh3 from "mvdan-sh";
|
|
61
|
+
import mvdanSh4 from "mvdan-sh";
|
|
61
62
|
import crypto from "crypto";
|
|
62
63
|
function isShieldVerdict(v) {
|
|
63
64
|
return v === "allow" || v === "review" || v === "block";
|
|
@@ -207,10 +208,10 @@ function compressIpv6(g) {
|
|
|
207
208
|
if (bestLen < 2) return hex.join(":");
|
|
208
209
|
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
209
210
|
}
|
|
210
|
-
function normalizeIpLiteral(
|
|
211
|
+
function normalizeIpLiteral(host2) {
|
|
211
212
|
try {
|
|
212
|
-
if (typeof
|
|
213
|
-
let s =
|
|
213
|
+
if (typeof host2 !== "string") return null;
|
|
214
|
+
let s = host2.trim();
|
|
214
215
|
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
215
216
|
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
216
217
|
const zone = s.indexOf("%");
|
|
@@ -236,11 +237,11 @@ function isStrictGatedTier(tier) {
|
|
|
236
237
|
function ssrfReason(m, asWritten) {
|
|
237
238
|
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
238
239
|
}
|
|
239
|
-
function classifySsrf(
|
|
240
|
+
function classifySsrf(host2) {
|
|
240
241
|
try {
|
|
241
|
-
if (typeof
|
|
242
|
-
const lower2 =
|
|
243
|
-
const ip = normalizeIpLiteral(
|
|
242
|
+
if (typeof host2 !== "string" || !host2) return null;
|
|
243
|
+
const lower2 = host2.trim().toLowerCase().replace(/\.$/, "");
|
|
244
|
+
const ip = normalizeIpLiteral(host2);
|
|
244
245
|
if (ip === null) {
|
|
245
246
|
return METADATA_HOSTNAMES.has(lower2) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
246
247
|
}
|
|
@@ -442,6 +443,7 @@ function catalogSettingsFromConfig(settings, policy) {
|
|
|
442
443
|
injectionScan: policy.injectionScan,
|
|
443
444
|
skillPinning: policy.skillPinning,
|
|
444
445
|
packageCheck: policy.packageCheck,
|
|
446
|
+
untrustedContent: policy.untrustedContent,
|
|
445
447
|
appliedShields: policy.appliedShields ?? [],
|
|
446
448
|
checks: policy.checks
|
|
447
449
|
};
|
|
@@ -949,11 +951,11 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
949
951
|
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
950
952
|
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
951
953
|
}
|
|
952
|
-
let
|
|
954
|
+
let positionals2 = 0;
|
|
953
955
|
let selectedProgram = false;
|
|
954
956
|
for (const a of args) {
|
|
955
957
|
if (a == null) {
|
|
956
|
-
|
|
958
|
+
positionals2++;
|
|
957
959
|
selectedProgram = true;
|
|
958
960
|
continue;
|
|
959
961
|
}
|
|
@@ -964,13 +966,13 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
964
966
|
}
|
|
965
967
|
if (a === "-" && !selectedProgram) return true;
|
|
966
968
|
if (!a.startsWith("-")) {
|
|
967
|
-
|
|
969
|
+
positionals2++;
|
|
968
970
|
selectedProgram = true;
|
|
969
971
|
}
|
|
970
972
|
}
|
|
971
973
|
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
972
974
|
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
973
|
-
if (
|
|
975
|
+
if (positionals2 === 0 && (stdinFed || pipeFed)) {
|
|
974
976
|
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
975
977
|
}
|
|
976
978
|
return false;
|
|
@@ -1180,6 +1182,24 @@ function resolveWordLiteral(w) {
|
|
|
1180
1182
|
}
|
|
1181
1183
|
return s;
|
|
1182
1184
|
}
|
|
1185
|
+
function resolveWordPrefix(w) {
|
|
1186
|
+
const walk2 = (parts) => {
|
|
1187
|
+
let s = "";
|
|
1188
|
+
for (const p of parts) {
|
|
1189
|
+
if (p && typeof p === "object" && Array.isArray(p.Parts) && p.Dollar !== void 0) {
|
|
1190
|
+
const inner = walk2(p.Parts);
|
|
1191
|
+
s += inner.prefix;
|
|
1192
|
+
if (inner.dynamic) return { prefix: s, dynamic: true };
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const piece = resolvePart(p, false);
|
|
1196
|
+
if (piece === void 0 || piece === null) return { prefix: s, dynamic: true };
|
|
1197
|
+
s += piece;
|
|
1198
|
+
}
|
|
1199
|
+
return { prefix: s, dynamic: false };
|
|
1200
|
+
};
|
|
1201
|
+
return walk2(w?.Parts || []);
|
|
1202
|
+
}
|
|
1183
1203
|
function resolvePart(p, inQuotes) {
|
|
1184
1204
|
const t = syntax.NodeType(p);
|
|
1185
1205
|
if (t === "Lit") {
|
|
@@ -1227,7 +1247,7 @@ function parseDestHost(token) {
|
|
|
1227
1247
|
}
|
|
1228
1248
|
function destTokensForBinary(binary, args) {
|
|
1229
1249
|
const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
|
|
1230
|
-
const
|
|
1250
|
+
const positionals2 = [];
|
|
1231
1251
|
const urlFlagValues = [];
|
|
1232
1252
|
for (let i = 0; i < args.length; i++) {
|
|
1233
1253
|
const tok = args[i];
|
|
@@ -1247,21 +1267,21 @@ function destTokensForBinary(binary, args) {
|
|
|
1247
1267
|
if (valueFlags.has(tok)) i++;
|
|
1248
1268
|
continue;
|
|
1249
1269
|
}
|
|
1250
|
-
|
|
1270
|
+
positionals2.push(tok);
|
|
1251
1271
|
}
|
|
1252
1272
|
switch (binary) {
|
|
1253
1273
|
case "curl":
|
|
1254
1274
|
case "wget":
|
|
1255
|
-
return [...urlFlagValues, ...
|
|
1275
|
+
return [...urlFlagValues, ...positionals2];
|
|
1256
1276
|
case "ssh":
|
|
1257
|
-
return
|
|
1277
|
+
return positionals2.slice(0, 1);
|
|
1258
1278
|
case "scp":
|
|
1259
1279
|
case "rsync":
|
|
1260
|
-
return
|
|
1280
|
+
return positionals2.filter((p) => p.includes(":") || p.includes("@"));
|
|
1261
1281
|
case "nc":
|
|
1262
1282
|
case "ncat":
|
|
1263
1283
|
case "netcat":
|
|
1264
|
-
return
|
|
1284
|
+
return positionals2.slice(0, 1);
|
|
1265
1285
|
default:
|
|
1266
1286
|
return [];
|
|
1267
1287
|
}
|
|
@@ -1278,16 +1298,18 @@ function extractShellDestinations(command) {
|
|
|
1278
1298
|
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1279
1299
|
const callArgs = n.Args || [];
|
|
1280
1300
|
if (callArgs.length === 0) return true;
|
|
1281
|
-
const
|
|
1301
|
+
const words = callArgs.map((a) => resolveWordLiteral(a));
|
|
1302
|
+
const at = unwrapCommandHead(words);
|
|
1303
|
+
const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
|
|
1282
1304
|
if (!NET_BINARIES.has(name)) return true;
|
|
1283
|
-
const rest =
|
|
1305
|
+
const rest = words.slice(at + 1);
|
|
1284
1306
|
for (const raw of destTokensForBinary(name, rest)) {
|
|
1285
|
-
const
|
|
1286
|
-
if (!
|
|
1287
|
-
const key = `${name}:${
|
|
1307
|
+
const host2 = parseDestHost(raw);
|
|
1308
|
+
if (!host2) continue;
|
|
1309
|
+
const key = `${name}:${host2}`;
|
|
1288
1310
|
if (seen.has(key)) continue;
|
|
1289
1311
|
seen.add(key);
|
|
1290
|
-
out.push({ host, binary: name, raw });
|
|
1312
|
+
out.push({ host: host2, binary: name, raw });
|
|
1291
1313
|
}
|
|
1292
1314
|
return true;
|
|
1293
1315
|
});
|
|
@@ -2047,9 +2069,9 @@ function matchesPattern(text, patterns) {
|
|
|
2047
2069
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2048
2070
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2049
2071
|
}
|
|
2050
|
-
function getNestedValue(obj,
|
|
2072
|
+
function getNestedValue(obj, path26) {
|
|
2051
2073
|
if (!obj || typeof obj !== "object") return null;
|
|
2052
|
-
const segments2 =
|
|
2074
|
+
const segments2 = path26.split(".");
|
|
2053
2075
|
for (const seg of segments2) {
|
|
2054
2076
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2055
2077
|
}
|
|
@@ -2376,7 +2398,7 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2376
2398
|
if (!hasScript) hasScript = true;
|
|
2377
2399
|
else files.push(token);
|
|
2378
2400
|
}
|
|
2379
|
-
for (const
|
|
2401
|
+
for (const file2 of files) add(inplace ? "write" : "read", file2, state);
|
|
2380
2402
|
return;
|
|
2381
2403
|
}
|
|
2382
2404
|
if (FS_READ_TOOLS.has(name) && name !== "dd") {
|
|
@@ -2728,6 +2750,17 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2728
2750
|
if (program) statements(program.Stmts ?? [], initial, 0);
|
|
2729
2751
|
return out;
|
|
2730
2752
|
}
|
|
2753
|
+
function isAgentInstructionPath(absPath, root) {
|
|
2754
|
+
const r = root.replace(/\/$/, "");
|
|
2755
|
+
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
2756
|
+
const rel = absPath.slice(r.length + 1);
|
|
2757
|
+
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
2758
|
+
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
2759
|
+
}
|
|
2760
|
+
function isInside(root, p) {
|
|
2761
|
+
const r = root.replace(/\/$/, "") || "/";
|
|
2762
|
+
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
2763
|
+
}
|
|
2731
2764
|
function isPasswordHashPath(p) {
|
|
2732
2765
|
return PASSWORD_HASH_FILES.includes(p);
|
|
2733
2766
|
}
|
|
@@ -2797,8 +2830,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
|
|
|
2797
2830
|
}
|
|
2798
2831
|
return out;
|
|
2799
2832
|
}
|
|
2800
|
-
function within(
|
|
2801
|
-
return
|
|
2833
|
+
function within(path26, root) {
|
|
2834
|
+
return path26 === root || path26.startsWith(root === "/" ? "/" : root + "/");
|
|
2802
2835
|
}
|
|
2803
2836
|
function hits(access, paths, directories, context) {
|
|
2804
2837
|
const home = context.homeDirs?.[0] ?? "~";
|
|
@@ -3599,7 +3632,7 @@ function pipInstall(words, from, manager, out) {
|
|
|
3599
3632
|
if (v >= 0 && lower(words[v]) === "install")
|
|
3600
3633
|
pushPy(out, manager, operands(words, v + 1, PIP_VALUE_FLAGS));
|
|
3601
3634
|
}
|
|
3602
|
-
function localContexts(
|
|
3635
|
+
function localContexts(file2) {
|
|
3603
3636
|
const contexts = /* @__PURE__ */ new Map();
|
|
3604
3637
|
const sample = parseShared("cd . && true");
|
|
3605
3638
|
if (typeof sample === "symbol") return contexts;
|
|
@@ -3627,7 +3660,7 @@ function localContexts(file) {
|
|
|
3627
3660
|
contexts.set(cmd.Pos().Offset(), steps);
|
|
3628
3661
|
return null;
|
|
3629
3662
|
}
|
|
3630
|
-
if (
|
|
3663
|
+
if (file2.Stmts?.length === 1) visit(file2.Stmts[0], []);
|
|
3631
3664
|
return contexts;
|
|
3632
3665
|
}
|
|
3633
3666
|
function collect(command, out, depth) {
|
|
@@ -3765,6 +3798,13 @@ function checkToLegacyPolicy(policy, id, v) {
|
|
|
3765
3798
|
return offOr("packageCheck", () => {
|
|
3766
3799
|
obj("packageCheck").onMalicious = v === "review" ? "review" : "block";
|
|
3767
3800
|
});
|
|
3801
|
+
case "behavior.untrusted-content":
|
|
3802
|
+
if (v !== "off" && v !== "log" && v !== "review") return false;
|
|
3803
|
+
return offOr("untrustedContent", () => {
|
|
3804
|
+
const uc = obj("untrustedContent");
|
|
3805
|
+
if (v === "log") uc.afterExposure = "log-only";
|
|
3806
|
+
else if (uc.afterExposure !== "ask-every-time") uc.afterExposure = "ask-once";
|
|
3807
|
+
});
|
|
3768
3808
|
default:
|
|
3769
3809
|
return false;
|
|
3770
3810
|
}
|
|
@@ -3778,38 +3818,38 @@ function configurableValues(id) {
|
|
|
3778
3818
|
if (def.pack || MAP_GOVERNED.has(id)) return def.values;
|
|
3779
3819
|
return KNOB_GOVERNED[id] ?? [];
|
|
3780
3820
|
}
|
|
3781
|
-
function hostMatches(
|
|
3821
|
+
function hostMatches(host2, pattern) {
|
|
3782
3822
|
const p = pattern.trim().toLowerCase();
|
|
3783
3823
|
if (!p) return false;
|
|
3784
3824
|
if (p === "*") return true;
|
|
3785
|
-
const h = canonicalHost(
|
|
3825
|
+
const h = canonicalHost(host2);
|
|
3786
3826
|
if (p.startsWith("*.")) {
|
|
3787
3827
|
const suffix = canonicalHost(p.slice(2));
|
|
3788
3828
|
return h === suffix || h.endsWith("." + suffix);
|
|
3789
3829
|
}
|
|
3790
3830
|
return h === canonicalHost(p);
|
|
3791
3831
|
}
|
|
3792
|
-
function matchesAny(
|
|
3793
|
-
for (const p of patterns ?? []) if (hostMatches(
|
|
3832
|
+
function matchesAny(host2, patterns) {
|
|
3833
|
+
for (const p of patterns ?? []) if (hostMatches(host2, p)) return true;
|
|
3794
3834
|
return false;
|
|
3795
3835
|
}
|
|
3796
|
-
function isUniqueLocalV6(
|
|
3797
|
-
const ip = normalizeIpLiteral(
|
|
3836
|
+
function isUniqueLocalV6(host2) {
|
|
3837
|
+
const ip = normalizeIpLiteral(host2);
|
|
3798
3838
|
if (!ip || !ip.includes(":")) return false;
|
|
3799
3839
|
const g = expandIpv6(ip);
|
|
3800
3840
|
return g !== null && (g[0] & 65024) === 64512;
|
|
3801
3841
|
}
|
|
3802
|
-
function isPrivateHost(
|
|
3803
|
-
const h =
|
|
3842
|
+
function isPrivateHost(host2) {
|
|
3843
|
+
const h = host2.trim().toLowerCase().replace(/\.$/, "");
|
|
3804
3844
|
const m = classifySsrf(h);
|
|
3805
3845
|
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
3806
3846
|
if (h === "localhost") return true;
|
|
3807
3847
|
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
3808
3848
|
return isUniqueLocalV6(h);
|
|
3809
3849
|
}
|
|
3810
|
-
function canonicalHost(
|
|
3811
|
-
const ip = normalizeIpLiteral(
|
|
3812
|
-
return ip ??
|
|
3850
|
+
function canonicalHost(host2) {
|
|
3851
|
+
const ip = normalizeIpLiteral(host2);
|
|
3852
|
+
return ip ?? host2.trim().toLowerCase().replace(/\.$/, "");
|
|
3813
3853
|
}
|
|
3814
3854
|
function evaluateEgress(dests, policy) {
|
|
3815
3855
|
if (!policy.enabled) return null;
|
|
@@ -4033,9 +4073,9 @@ function bareToolName(toolName) {
|
|
|
4033
4073
|
const parts = toolName.split("__");
|
|
4034
4074
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
4035
4075
|
}
|
|
4036
|
-
function valuesAt(args,
|
|
4076
|
+
function valuesAt(args, path26) {
|
|
4037
4077
|
let cursors = [args];
|
|
4038
|
-
for (const rawSegment of
|
|
4078
|
+
for (const rawSegment of path26.split(".")) {
|
|
4039
4079
|
const isArray = rawSegment.endsWith("[]");
|
|
4040
4080
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
4041
4081
|
const next = [];
|
|
@@ -4067,10 +4107,10 @@ function extractToolDestinations(toolName, args) {
|
|
|
4067
4107
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4068
4108
|
if (!paths) return [];
|
|
4069
4109
|
const out = [];
|
|
4070
|
-
for (const
|
|
4071
|
-
for (const value of valuesAt(args,
|
|
4072
|
-
const
|
|
4073
|
-
if (
|
|
4110
|
+
for (const path26 of paths) {
|
|
4111
|
+
for (const value of valuesAt(args, path26)) {
|
|
4112
|
+
const host2 = hostOf(value);
|
|
4113
|
+
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
4074
4114
|
}
|
|
4075
4115
|
}
|
|
4076
4116
|
return out;
|
|
@@ -4082,15 +4122,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
4082
4122
|
try {
|
|
4083
4123
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4084
4124
|
if (!paths) return null;
|
|
4085
|
-
for (const
|
|
4086
|
-
for (const value of valuesAt(args,
|
|
4087
|
-
const
|
|
4088
|
-
if (!
|
|
4089
|
-
const m = classifySsrf(
|
|
4125
|
+
for (const path26 of paths) {
|
|
4126
|
+
for (const value of valuesAt(args, path26)) {
|
|
4127
|
+
const host2 = hostOf(value);
|
|
4128
|
+
if (!host2) continue;
|
|
4129
|
+
const m = classifySsrf(host2);
|
|
4090
4130
|
if (!m) continue;
|
|
4091
4131
|
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
4092
4132
|
if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
|
|
4093
|
-
return { ...m, argPath:
|
|
4133
|
+
return { ...m, argPath: path26, host: host2, reason: ssrfReason(m, host2) };
|
|
4094
4134
|
}
|
|
4095
4135
|
}
|
|
4096
4136
|
return null;
|
|
@@ -4132,16 +4172,16 @@ function* encodedRegions(text, budget) {
|
|
|
4132
4172
|
/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
|
|
4133
4173
|
);
|
|
4134
4174
|
if (!parts) continue;
|
|
4135
|
-
const
|
|
4175
|
+
const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
|
|
4136
4176
|
const regions = [
|
|
4137
4177
|
parts[1],
|
|
4138
|
-
...
|
|
4178
|
+
...host2.split("."),
|
|
4139
4179
|
...parts[2].split("/"),
|
|
4140
4180
|
...(parts[3] ?? "").split("&"),
|
|
4141
4181
|
parts[4] ?? ""
|
|
4142
4182
|
];
|
|
4143
4183
|
for (const region of regions) if (region) yield [region, false];
|
|
4144
|
-
const labels =
|
|
4184
|
+
const labels = host2.split(".");
|
|
4145
4185
|
for (let start = 0; start < labels.length; start++) {
|
|
4146
4186
|
let joined = labels[start];
|
|
4147
4187
|
for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
|
|
@@ -4377,7 +4417,7 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
4377
4417
|
if (!pipeAnalysis.isPipeline) return null;
|
|
4378
4418
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
4379
4419
|
const sinks = pipeAnalysis.sinkTargets;
|
|
4380
|
-
const allTrusted = sinks.length > 0 && sinks.every((
|
|
4420
|
+
const allTrusted = sinks.length > 0 && sinks.every((host2) => isTrustedHost2 ? isTrustedHost2(host2) : false);
|
|
4381
4421
|
if (pipeAnalysis.risk === "critical") {
|
|
4382
4422
|
if (allTrusted) {
|
|
4383
4423
|
return {
|
|
@@ -4854,6 +4894,487 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4854
4894
|
function isIgnoredTool(toolName, config) {
|
|
4855
4895
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
4856
4896
|
}
|
|
4897
|
+
function normalizeHost(raw) {
|
|
4898
|
+
let h = raw.trim().toLowerCase();
|
|
4899
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
4900
|
+
if (!h || h === "localhost") return null;
|
|
4901
|
+
if (/^[0-9a-f:]+$/.test(h) && h.includes(":")) return h;
|
|
4902
|
+
if (!h.includes(".") || !/^[\p{L}\p{N}.-]+$/u.test(h)) return null;
|
|
4903
|
+
return h;
|
|
4904
|
+
}
|
|
4905
|
+
function hostOfUrl(value) {
|
|
4906
|
+
const v = value.replace(/[.,;:!?)\]}>'"`]+$/, "");
|
|
4907
|
+
try {
|
|
4908
|
+
const u = new URL(v.toLowerCase().startsWith("www.") ? `http://${v}` : v);
|
|
4909
|
+
return normalizeHost(u.hostname);
|
|
4910
|
+
} catch {
|
|
4911
|
+
return null;
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
function addIbans(text, out) {
|
|
4915
|
+
for (const m of text.toUpperCase().matchAll(IBAN_RE)) {
|
|
4916
|
+
const compact = m[0].replace(/[ -]/g, "");
|
|
4917
|
+
if (validateIban(compact)) out.add(`iban:${compact}`);
|
|
4918
|
+
else if (/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) out.add(`account:${compact}`);
|
|
4919
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
function addEmails(text, out) {
|
|
4923
|
+
for (const m of text.matchAll(EMAIL_RE)) {
|
|
4924
|
+
out.add(`email:${m[0].toLowerCase()}`);
|
|
4925
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
function addUrlHosts(text, out) {
|
|
4929
|
+
for (const m of text.matchAll(URL_RE)) {
|
|
4930
|
+
const h = hostOfUrl(m[0]);
|
|
4931
|
+
if (h) out.add(`host:${h}`);
|
|
4932
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
function extractContentDestinations(text) {
|
|
4936
|
+
const out = /* @__PURE__ */ new Set();
|
|
4937
|
+
try {
|
|
4938
|
+
const t = text.length > MAX_SCAN_CHARS ? text.slice(0, MAX_SCAN_CHARS) : text;
|
|
4939
|
+
if (t.includes("@")) addEmails(t, out);
|
|
4940
|
+
if (out.size < MAX_DESTINATIONS) addUrlHosts(t, out);
|
|
4941
|
+
if (out.size < MAX_DESTINATIONS) addIbans(t, out);
|
|
4942
|
+
} catch {
|
|
4943
|
+
}
|
|
4944
|
+
return [...out];
|
|
4945
|
+
}
|
|
4946
|
+
function extractArgDestinations(args) {
|
|
4947
|
+
const out = /* @__PURE__ */ new Set();
|
|
4948
|
+
try {
|
|
4949
|
+
walk(args, void 0, out, 0);
|
|
4950
|
+
} catch {
|
|
4951
|
+
}
|
|
4952
|
+
return [...out];
|
|
4953
|
+
}
|
|
4954
|
+
function walk(value, key, out, depth) {
|
|
4955
|
+
if (out.size >= MAX_DESTINATIONS || depth > 12) return;
|
|
4956
|
+
if (typeof value === "string") {
|
|
4957
|
+
const s = value.length > MAX_SCAN_CHARS ? value.slice(0, MAX_SCAN_CHARS) : value;
|
|
4958
|
+
if (s.includes("@")) addEmails(s, out);
|
|
4959
|
+
addUrlHosts(s, out);
|
|
4960
|
+
addIbans(s, out);
|
|
4961
|
+
if (key && RECIPIENT_KEYS.has(key.toLowerCase()) && !/[\s@]/.test(s)) {
|
|
4962
|
+
const h = normalizeHost(
|
|
4963
|
+
s.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^\/\//, "").split(/[/?#]/)[0].replace(/:\d+$/, "")
|
|
4964
|
+
);
|
|
4965
|
+
if (h && (h.includes(":") || /\.\p{L}{2,}$/u.test(h))) out.add(`host:${h}`);
|
|
4966
|
+
const acct = s.replace(/[ .-]/g, "").toUpperCase();
|
|
4967
|
+
if (/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(acct) && !validateIban(acct))
|
|
4968
|
+
out.add(`account:${acct}`);
|
|
4969
|
+
}
|
|
4970
|
+
return;
|
|
4971
|
+
}
|
|
4972
|
+
if (Array.isArray(value)) {
|
|
4973
|
+
for (const v of value) walk(v, key, out, depth + 1);
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4976
|
+
if (value && typeof value === "object") {
|
|
4977
|
+
for (const [k, v] of Object.entries(value))
|
|
4978
|
+
walk(v, k, out, depth + 1);
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
function destinationDomain(dest) {
|
|
4982
|
+
if (!dest.startsWith("email:")) return null;
|
|
4983
|
+
const at = dest.lastIndexOf("@");
|
|
4984
|
+
return at > 0 ? dest.slice(at + 1) : null;
|
|
4985
|
+
}
|
|
4986
|
+
function destinationKind(dest) {
|
|
4987
|
+
const i = dest.indexOf(":");
|
|
4988
|
+
const k = i > 0 ? dest.slice(0, i) : "";
|
|
4989
|
+
return k === "email" || k === "host" || k === "iban" || k === "account" ? k : null;
|
|
4990
|
+
}
|
|
4991
|
+
function destinationValue(dest) {
|
|
4992
|
+
const i = dest.indexOf(":");
|
|
4993
|
+
return i > 0 ? dest.slice(i + 1) : dest;
|
|
4994
|
+
}
|
|
4995
|
+
function isLoopbackHost(host2) {
|
|
4996
|
+
let h = host2.trim().toLowerCase();
|
|
4997
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
4998
|
+
return h === "localhost" || h.endsWith(".localhost") || /^127(?:\.\d{1,3}){3}$/.test(h) || h === "::1" || h === "0:0:0:0:0:0:0:1" || h === "0.0.0.0";
|
|
4999
|
+
}
|
|
5000
|
+
function gitRepoPath(url) {
|
|
5001
|
+
const u = url.trim();
|
|
5002
|
+
if (!u) return null;
|
|
5003
|
+
let host2;
|
|
5004
|
+
let p;
|
|
5005
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
5006
|
+
try {
|
|
5007
|
+
const parsed = new URL(u);
|
|
5008
|
+
host2 = parsed.hostname.toLowerCase();
|
|
5009
|
+
p = decodeURIComponent(parsed.pathname);
|
|
5010
|
+
} catch {
|
|
5011
|
+
return null;
|
|
5012
|
+
}
|
|
5013
|
+
} else {
|
|
5014
|
+
const scp = /^(?:[^@/\s]+@)?([A-Za-z0-9.-]+):(?!\/\/)(.+)$/.exec(u);
|
|
5015
|
+
if (!scp || !scp[1].includes(".")) return null;
|
|
5016
|
+
host2 = scp[1].toLowerCase();
|
|
5017
|
+
p = scp[2];
|
|
5018
|
+
}
|
|
5019
|
+
let start = 0;
|
|
5020
|
+
let end = p.length;
|
|
5021
|
+
while (start < end && p[start] === "/") start++;
|
|
5022
|
+
while (end > start && p[end - 1] === "/") end--;
|
|
5023
|
+
const tail = p.slice(start, end).replace(/\.git$/i, "").toLowerCase();
|
|
5024
|
+
if (!host2 || !tail.includes("/")) return null;
|
|
5025
|
+
return `${host2}/${tail}`;
|
|
5026
|
+
}
|
|
5027
|
+
function isDestructiveToolName(bareTool) {
|
|
5028
|
+
return DESTRUCTIVE_VERB_RE.test(bareTool);
|
|
5029
|
+
}
|
|
5030
|
+
function host(token) {
|
|
5031
|
+
if (!token) return null;
|
|
5032
|
+
const h = parseDestHost(token);
|
|
5033
|
+
return h ? `host:${h}` : null;
|
|
5034
|
+
}
|
|
5035
|
+
function ghRepoArg(v, defaultHost) {
|
|
5036
|
+
if (/:\/\/|@/.test(v)) {
|
|
5037
|
+
const r = gitRepoPath(v);
|
|
5038
|
+
if (r) return { host: r.split("/")[0], repo: r };
|
|
5039
|
+
}
|
|
5040
|
+
const parts = v.toLowerCase().replace(/\.git$/, "").split("/").filter(Boolean);
|
|
5041
|
+
if (parts.length === 3) return { host: parts[0], repo: parts.join("/") };
|
|
5042
|
+
if (parts.length === 2) return { host: defaultHost, repo: `${defaultHost}/${parts.join("/")}` };
|
|
5043
|
+
return { host: defaultHost, repo: `${defaultHost}/*` };
|
|
5044
|
+
}
|
|
5045
|
+
function ghApiRepo(endpoint, ghHost) {
|
|
5046
|
+
if (endpoint === null) return `${ghHost}/*`;
|
|
5047
|
+
const e = endpoint.replace(/^https?:\/\/[^/]+\//i, "").replace(/^\/+/, "");
|
|
5048
|
+
const m = /^repos\/([^/]+)\/([^/?#]+)/i.exec(e);
|
|
5049
|
+
if (!m) return `${ghHost}/*`;
|
|
5050
|
+
if (m[1] === "{owner}" && m[2] === "{repo}") return void 0;
|
|
5051
|
+
return `${ghHost}/${m[1].toLowerCase()}/${m[2].toLowerCase()}`;
|
|
5052
|
+
}
|
|
5053
|
+
function sendToGitUrl(url, out) {
|
|
5054
|
+
out.sends = true;
|
|
5055
|
+
const h = typeof url === "string" ? host(url) : null;
|
|
5056
|
+
if (!url || !h) {
|
|
5057
|
+
out.unresolved = true;
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
5060
|
+
out.destinations.push(h);
|
|
5061
|
+
const r = gitRepoPath(url);
|
|
5062
|
+
if (r) out.repos.push(r);
|
|
5063
|
+
}
|
|
5064
|
+
function callWords(call) {
|
|
5065
|
+
const raw = call.Args || [];
|
|
5066
|
+
if (raw.length === 0) return null;
|
|
5067
|
+
const words = raw.map((a) => resolveWordLiteral(a));
|
|
5068
|
+
const prefixes = raw.map((a) => resolveWordPrefix(a).prefix);
|
|
5069
|
+
const first = (words[0] || "").toLowerCase().split("/").pop() ?? "";
|
|
5070
|
+
const at = OWN_HEADS.has(first) ? 0 : unwrapCommandHead(words);
|
|
5071
|
+
const head = (words[at] || "").toLowerCase().split("/").pop() ?? "";
|
|
5072
|
+
if (!head) return null;
|
|
5073
|
+
const assigns = call.Assigns || [];
|
|
5074
|
+
const env = [
|
|
5075
|
+
...assigns.map((a) => String(a?.Name?.Value ?? "")),
|
|
5076
|
+
...words.slice(0, at).map((w) => w === null ? "" : /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(w)?.[1] ?? "")
|
|
5077
|
+
].filter(Boolean);
|
|
5078
|
+
return { head, args: words.slice(at + 1), prefixes: prefixes.slice(at + 1), env };
|
|
5079
|
+
}
|
|
5080
|
+
function flagValue(args, names) {
|
|
5081
|
+
for (let i = 0; i < args.length; i++) {
|
|
5082
|
+
const w = args[i];
|
|
5083
|
+
if (w === null) continue;
|
|
5084
|
+
for (const n of names) {
|
|
5085
|
+
if (w === n && typeof args[i + 1] === "string") return args[i + 1];
|
|
5086
|
+
if (w.startsWith(`${n}=`)) return w.slice(n.length + 1);
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
return null;
|
|
5090
|
+
}
|
|
5091
|
+
function positionals(args, valueFlags) {
|
|
5092
|
+
const out = [];
|
|
5093
|
+
for (let i = 0; i < args.length; i++) {
|
|
5094
|
+
const w = args[i];
|
|
5095
|
+
if (w === null) {
|
|
5096
|
+
out.push(null);
|
|
5097
|
+
continue;
|
|
5098
|
+
}
|
|
5099
|
+
if (w === "--") {
|
|
5100
|
+
out.push(...args.slice(i + 1));
|
|
5101
|
+
break;
|
|
5102
|
+
}
|
|
5103
|
+
if (w.startsWith("-")) {
|
|
5104
|
+
if (!w.includes("=") && valueFlags.has(w)) i++;
|
|
5105
|
+
continue;
|
|
5106
|
+
}
|
|
5107
|
+
out.push(w);
|
|
5108
|
+
}
|
|
5109
|
+
return out;
|
|
5110
|
+
}
|
|
5111
|
+
function classifyCall(head, args, prefixes, env, state, out) {
|
|
5112
|
+
const lower2 = args.map((a) => a === null ? null : a.toLowerCase());
|
|
5113
|
+
switch (head) {
|
|
5114
|
+
case "curl":
|
|
5115
|
+
case "wget":
|
|
5116
|
+
case "iwr":
|
|
5117
|
+
case "invoke-webrequest":
|
|
5118
|
+
case "irm":
|
|
5119
|
+
case "invoke-restmethod": {
|
|
5120
|
+
const binary = head === "wget" ? "wget" : "curl";
|
|
5121
|
+
const sendFlags = head === "wget" ? WGET_SEND_FLAGS : head === "curl" ? CURL_SEND_FLAGS : IWR_SEND_FLAGS;
|
|
5122
|
+
let sends = false;
|
|
5123
|
+
for (let i = 0; i < args.length; i++) {
|
|
5124
|
+
const w = args[i];
|
|
5125
|
+
if (w === null) continue;
|
|
5126
|
+
const name = (head === "curl" || head === "wget" ? w : w.toLowerCase()).split("=")[0];
|
|
5127
|
+
if (sendFlags.has(name)) sends = true;
|
|
5128
|
+
const next = lower2[i + 1];
|
|
5129
|
+
if ((name === "-X" || name === "--request" || name === "-method") && next) {
|
|
5130
|
+
if (WRITE_METHODS.has(next)) sends = true;
|
|
5131
|
+
}
|
|
5132
|
+
if (/^(?:--request=|-X)(POST|PUT|PATCH|DELETE)$/i.test(w)) sends = true;
|
|
5133
|
+
if (name === "--method" && next && WRITE_METHODS.has(next)) sends = true;
|
|
5134
|
+
}
|
|
5135
|
+
const withPrefixes = args.map(
|
|
5136
|
+
(a, i) => a === null && /^[a-z][a-z0-9+.-]*:\/\//i.test(prefixes[i] ?? "") ? prefixes[i] : a
|
|
5137
|
+
);
|
|
5138
|
+
const dests = destTokensForBinary(binary, withPrefixes).map((t) => host(t)).filter((h) => h !== null);
|
|
5139
|
+
out.fetches = true;
|
|
5140
|
+
out.fetchKinds.push(head);
|
|
5141
|
+
out.fetchDestinations.push(...dests);
|
|
5142
|
+
if (sends) {
|
|
5143
|
+
out.sends = true;
|
|
5144
|
+
out.destinations.push(...dests);
|
|
5145
|
+
}
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
5148
|
+
case "scp":
|
|
5149
|
+
case "rsync": {
|
|
5150
|
+
const pos = positionals(args, VALUE_FLAGS[head] ?? /* @__PURE__ */ new Set()).filter(
|
|
5151
|
+
(p) => p !== null
|
|
5152
|
+
);
|
|
5153
|
+
if (pos.length < 2) return;
|
|
5154
|
+
const remote = (p) => /^[a-z][a-z0-9+.-]*:\/\//i.test(p) || /^[^/]*[^/:]:[^/]/.test(p) || p.includes("@");
|
|
5155
|
+
const target = pos[pos.length - 1];
|
|
5156
|
+
const sources = pos.slice(0, -1);
|
|
5157
|
+
if (remote(target)) {
|
|
5158
|
+
out.sends = true;
|
|
5159
|
+
const h = host(target);
|
|
5160
|
+
if (h) out.destinations.push(h);
|
|
5161
|
+
}
|
|
5162
|
+
if (sources.some(remote)) {
|
|
5163
|
+
out.fetches = true;
|
|
5164
|
+
out.fetchKinds.push(head);
|
|
5165
|
+
for (const src of sources.filter(remote)) {
|
|
5166
|
+
const h = host(src);
|
|
5167
|
+
if (h) out.fetchDestinations.push(h);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
return;
|
|
5171
|
+
}
|
|
5172
|
+
case "nc":
|
|
5173
|
+
case "ncat":
|
|
5174
|
+
case "netcat":
|
|
5175
|
+
case "ssh": {
|
|
5176
|
+
out.fetches = true;
|
|
5177
|
+
out.sends = true;
|
|
5178
|
+
out.fetchKinds.push(head);
|
|
5179
|
+
for (const t of destTokensForBinary(head, args)) {
|
|
5180
|
+
const h = host(t);
|
|
5181
|
+
if (h) {
|
|
5182
|
+
out.destinations.push(h);
|
|
5183
|
+
out.fetchDestinations.push(h);
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
return;
|
|
5187
|
+
}
|
|
5188
|
+
case "gh": {
|
|
5189
|
+
const pos = positionals(args, GH_HOSTNAME_FLAGS);
|
|
5190
|
+
const group = pos[0] ?? null;
|
|
5191
|
+
const verb = pos[1] ?? null;
|
|
5192
|
+
const hn = flagValue(args, ["--hostname"]);
|
|
5193
|
+
let dest = host(hn ?? "github.com") ?? "host:github.com";
|
|
5194
|
+
const ghHost = dest.slice("host:".length);
|
|
5195
|
+
const envRepo = env.some((n) => n === "GH_REPO" || n === "GH_HOST");
|
|
5196
|
+
if (group === "api") {
|
|
5197
|
+
const method = (flagValue(args, ["-X", "--method"]) ?? "get").toLowerCase();
|
|
5198
|
+
const hasBody = args.some(
|
|
5199
|
+
(w) => w !== null && /^(-f|--raw-field|-F|--field|--input)(=|$)/.test(w)
|
|
5200
|
+
);
|
|
5201
|
+
if (WRITE_METHODS.has(method) || hasBody) {
|
|
5202
|
+
out.sends = true;
|
|
5203
|
+
out.destinations.push(dest);
|
|
5204
|
+
const endpoint = positionals(args, GH_API_VALUE_FLAGS)[1] ?? null;
|
|
5205
|
+
const repo = ghApiRepo(endpoint, ghHost);
|
|
5206
|
+
if (repo !== void 0) out.repos.push(repo);
|
|
5207
|
+
else if (envRepo) out.repos.push(`${ghHost}/*`);
|
|
5208
|
+
} else {
|
|
5209
|
+
out.fetches = true;
|
|
5210
|
+
out.fetchKinds.push("gh api");
|
|
5211
|
+
out.fetchDestinations.push(dest);
|
|
5212
|
+
}
|
|
5213
|
+
return;
|
|
5214
|
+
}
|
|
5215
|
+
if (group === null || verb === null) return;
|
|
5216
|
+
if (GH_WRITE.has(verb)) {
|
|
5217
|
+
out.sends = true;
|
|
5218
|
+
const rIdx = args.findIndex((w) => w === "-R" || w === "--repo");
|
|
5219
|
+
const rEq = args.find((w) => w !== null && /^(-R|--repo)=/.test(w));
|
|
5220
|
+
const rVal = rEq !== void 0 && rEq !== null ? rEq.slice(rEq.indexOf("=") + 1) : rIdx >= 0 ? args[rIdx + 1] : void 0;
|
|
5221
|
+
let repo;
|
|
5222
|
+
if (rIdx >= 0 || rEq !== void 0) {
|
|
5223
|
+
if (typeof rVal === "string") {
|
|
5224
|
+
const named = ghRepoArg(rVal, ghHost);
|
|
5225
|
+
repo = named.repo;
|
|
5226
|
+
if (named.host !== ghHost) dest = `host:${named.host}`;
|
|
5227
|
+
} else repo = `${ghHost}/*`;
|
|
5228
|
+
} else if (group === "gist" || group === "repo" && (verb === "create" || verb === "fork")) {
|
|
5229
|
+
repo = `${ghHost}/*`;
|
|
5230
|
+
} else if (group === "repo" && verb !== "rename") {
|
|
5231
|
+
const operand = positionals(args, GH_REPO_VALUE_FLAGS)[2];
|
|
5232
|
+
if (operand === null) repo = `${ghHost}/*`;
|
|
5233
|
+
else if (operand !== void 0) repo = ghRepoArg(operand, ghHost).repo;
|
|
5234
|
+
else if (envRepo) repo = `${ghHost}/*`;
|
|
5235
|
+
} else if (envRepo) {
|
|
5236
|
+
repo = `${ghHost}/*`;
|
|
5237
|
+
}
|
|
5238
|
+
out.destinations.push(dest);
|
|
5239
|
+
if (repo !== void 0) out.repos.push(repo);
|
|
5240
|
+
} else if (GH_READ.has(verb)) {
|
|
5241
|
+
out.fetches = true;
|
|
5242
|
+
out.fetchKinds.push(`gh ${group}`);
|
|
5243
|
+
out.fetchDestinations.push(dest);
|
|
5244
|
+
}
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5247
|
+
case "git": {
|
|
5248
|
+
const pos = positionals(args, /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree"]));
|
|
5249
|
+
const verb = pos[0];
|
|
5250
|
+
if (verb === null || verb === void 0) return;
|
|
5251
|
+
for (let i = 0; i < args.length; i++) {
|
|
5252
|
+
if (args[i] !== "-c") continue;
|
|
5253
|
+
const v = args[i + 1];
|
|
5254
|
+
if (v === null || v === void 0) {
|
|
5255
|
+
if (verb === "push") out.unresolved = true;
|
|
5256
|
+
continue;
|
|
5257
|
+
}
|
|
5258
|
+
const m = /^remote\.[^=]+\.(?:push)?url=(.*)$/i.exec(v);
|
|
5259
|
+
if (m) sendToGitUrl(m[1], out);
|
|
5260
|
+
}
|
|
5261
|
+
if (verb === "remote") {
|
|
5262
|
+
const rp = positionals(args, /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "-t", "-m"]));
|
|
5263
|
+
const sub = rp[1];
|
|
5264
|
+
if (sub === "add" || sub === "set-url") {
|
|
5265
|
+
const url = rp.slice(2).filter((p) => p !== "--push" && p !== "--add")[1];
|
|
5266
|
+
sendToGitUrl(url, out);
|
|
5267
|
+
}
|
|
5268
|
+
return;
|
|
5269
|
+
}
|
|
5270
|
+
if (verb === "config") {
|
|
5271
|
+
const reads = args.some(
|
|
5272
|
+
(w) => w !== null && /^--(get|get-all|get-regexp|unset|unset-all|list|get-urlmatch)$/.test(w)
|
|
5273
|
+
);
|
|
5274
|
+
const k = pos.findIndex((p) => p !== null && /^remote\..+\.(?:push)?url$/i.test(p));
|
|
5275
|
+
if (k >= 0 && k + 1 < pos.length && !reads) sendToGitUrl(pos[k + 1], out);
|
|
5276
|
+
return;
|
|
5277
|
+
}
|
|
5278
|
+
if (GIT_FETCH.has(verb)) {
|
|
5279
|
+
out.fetches = true;
|
|
5280
|
+
out.fetchKinds.push(`git ${verb}`);
|
|
5281
|
+
for (const p of pos.slice(1)) {
|
|
5282
|
+
const h = p !== null && /:\/\/|@/.test(p) ? host(p) : null;
|
|
5283
|
+
if (h) out.fetchDestinations.push(h);
|
|
5284
|
+
}
|
|
5285
|
+
return;
|
|
5286
|
+
}
|
|
5287
|
+
if (verb === "push") {
|
|
5288
|
+
if (pos.length > 1 && pos[1] === null) {
|
|
5289
|
+
out.sends = true;
|
|
5290
|
+
out.unresolved = true;
|
|
5291
|
+
return;
|
|
5292
|
+
}
|
|
5293
|
+
const target = flagValue(args, ["--repo"]) ?? pos.slice(1).find((p) => p !== null && !p.startsWith("-")) ?? null;
|
|
5294
|
+
if (target !== null && (/:\/\/|@/.test(target) || /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+:/.test(target))) {
|
|
5295
|
+
sendToGitUrl(target, out);
|
|
5296
|
+
return;
|
|
5297
|
+
}
|
|
5298
|
+
if (target !== null && /^[./~]|\//.test(target)) return;
|
|
5299
|
+
out.sends = true;
|
|
5300
|
+
const elsewhere = state.cwdChanged || args.some((w) => w !== null && /^(-C|--git-dir|--work-tree)(=|$)/.test(w)) || env.some((n) => n === "GIT_DIR" || n === "GIT_WORK_TREE" || n === "GIT_CONFIG");
|
|
5301
|
+
if (elsewhere) out.unresolved = true;
|
|
5302
|
+
else out.gitRemotes.push(target ?? "");
|
|
5303
|
+
}
|
|
5304
|
+
return;
|
|
5305
|
+
}
|
|
5306
|
+
default: {
|
|
5307
|
+
const pub = PUBLISH[head];
|
|
5308
|
+
if (!pub) return;
|
|
5309
|
+
const pos = positionals(args, new Set(pub.flag ?? []));
|
|
5310
|
+
const verb = pos[0];
|
|
5311
|
+
if (verb === null || verb === void 0 || !pub.verbs.has(verb)) return;
|
|
5312
|
+
if (head === "yarn" && verb === "npm" && pos[1] !== "publish") return;
|
|
5313
|
+
out.sends = true;
|
|
5314
|
+
const named = pub.flag ? flagValue(args, pub.flag) : null;
|
|
5315
|
+
const h = named ? host(named) : null;
|
|
5316
|
+
if (named && !h) return;
|
|
5317
|
+
if (head === "docker") {
|
|
5318
|
+
const image = pos[1];
|
|
5319
|
+
const first = image?.split("/")[0] ?? "";
|
|
5320
|
+
const reg = first.includes(".") || first.includes(":") ? host(first) : null;
|
|
5321
|
+
out.destinations.push(reg ?? "host:docker.io");
|
|
5322
|
+
return;
|
|
5323
|
+
}
|
|
5324
|
+
out.destinations.push(h ?? `host:${pub.registry}`);
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
function walkCalls(command, out, depth, state) {
|
|
5329
|
+
const f = parseShared(command);
|
|
5330
|
+
if (typeof f === "symbol" || !f) return;
|
|
5331
|
+
syntax4.Walk(f, (node) => {
|
|
5332
|
+
if (!node) return false;
|
|
5333
|
+
const type = syntax4.NodeType(node);
|
|
5334
|
+
if (type === "DeclClause") {
|
|
5335
|
+
for (const a of node.Args || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
|
|
5336
|
+
return true;
|
|
5337
|
+
}
|
|
5338
|
+
if (type !== "CallExpr") return true;
|
|
5339
|
+
const call = node;
|
|
5340
|
+
if ((call.Args || []).length === 0) {
|
|
5341
|
+
for (const a of call.Assigns || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
|
|
5342
|
+
return true;
|
|
5343
|
+
}
|
|
5344
|
+
const c = callWords(node);
|
|
5345
|
+
if (!c) return true;
|
|
5346
|
+
if (SHELLS2.has(c.head) && depth < MAX_DEPTH3) {
|
|
5347
|
+
const i = c.args.findIndex((w) => w === "-c" || w === "-lc" || w === "-ec");
|
|
5348
|
+
const payload = i >= 0 ? c.args[i + 1] : null;
|
|
5349
|
+
if (typeof payload === "string") walkCalls(payload, out, depth + 1, state);
|
|
5350
|
+
return true;
|
|
5351
|
+
}
|
|
5352
|
+
if (CWD_CHANGERS.has(c.head)) state.cwdChanged = true;
|
|
5353
|
+
classifyCall(c.head, c.args, c.prefixes, [...c.env, ...state.env], state, out);
|
|
5354
|
+
return true;
|
|
5355
|
+
});
|
|
5356
|
+
}
|
|
5357
|
+
function classifyShellExposure(command) {
|
|
5358
|
+
const out = {
|
|
5359
|
+
...EMPTY,
|
|
5360
|
+
destinations: [],
|
|
5361
|
+
gitRemotes: [],
|
|
5362
|
+
repos: [],
|
|
5363
|
+
fetchDestinations: [],
|
|
5364
|
+
fetchKinds: []
|
|
5365
|
+
};
|
|
5366
|
+
try {
|
|
5367
|
+
walkCalls(command, out, 0, { cwdChanged: false, env: /* @__PURE__ */ new Set() });
|
|
5368
|
+
} catch {
|
|
5369
|
+
return { ...EMPTY };
|
|
5370
|
+
}
|
|
5371
|
+
out.destinations = [...new Set(out.destinations)];
|
|
5372
|
+
out.fetchDestinations = [...new Set(out.fetchDestinations)];
|
|
5373
|
+
out.gitRemotes = [...new Set(out.gitRemotes)];
|
|
5374
|
+
out.repos = [...new Set(out.repos)];
|
|
5375
|
+
out.fetchKinds = [...new Set(out.fetchKinds)];
|
|
5376
|
+
return out;
|
|
5377
|
+
}
|
|
4857
5378
|
function stripTerminalEscapes(s) {
|
|
4858
5379
|
return s.replace(TERMINAL_ESCAPE_RE, "");
|
|
4859
5380
|
}
|
|
@@ -5013,8 +5534,8 @@ function matchCanaryArgs(args, values) {
|
|
|
5013
5534
|
if (values.length === 0) return null;
|
|
5014
5535
|
const needles = prepare(values);
|
|
5015
5536
|
if (needles.length === 0) return null;
|
|
5016
|
-
const
|
|
5017
|
-
if (depth >
|
|
5537
|
+
const walk2 = (v, depth, fieldPath) => {
|
|
5538
|
+
if (depth > MAX_DEPTH4) return null;
|
|
5018
5539
|
if (typeof v === "string") {
|
|
5019
5540
|
const hit = matchPrepared(v, needles);
|
|
5020
5541
|
if (hit) return { id: hit.v.id, view: hit.view, fieldPath, retired: Boolean(hit.v.retired) };
|
|
@@ -5022,31 +5543,31 @@ function matchCanaryArgs(args, values) {
|
|
|
5022
5543
|
const trimmed = v.trim();
|
|
5023
5544
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
5024
5545
|
try {
|
|
5025
|
-
return
|
|
5546
|
+
return walk2(JSON.parse(v), depth + 1, fieldPath);
|
|
5026
5547
|
} catch {
|
|
5027
5548
|
}
|
|
5028
5549
|
}
|
|
5029
5550
|
}
|
|
5030
5551
|
return null;
|
|
5031
5552
|
}
|
|
5032
|
-
if (typeof v === "number" && Number.isFinite(v)) return
|
|
5553
|
+
if (typeof v === "number" && Number.isFinite(v)) return walk2(String(v), depth, fieldPath);
|
|
5033
5554
|
if (Array.isArray(v)) {
|
|
5034
5555
|
for (let i = 0; i < v.length; i++) {
|
|
5035
|
-
const h =
|
|
5556
|
+
const h = walk2(v[i], depth + 1, `${fieldPath}[${i}]`);
|
|
5036
5557
|
if (h) return h;
|
|
5037
5558
|
}
|
|
5038
5559
|
return null;
|
|
5039
5560
|
}
|
|
5040
5561
|
if (v && typeof v === "object") {
|
|
5041
5562
|
for (const [k, child] of Object.entries(v)) {
|
|
5042
|
-
const h =
|
|
5563
|
+
const h = walk2(child, depth + 1, fieldPath ? `${fieldPath}.${k}` : k);
|
|
5043
5564
|
if (h) return h;
|
|
5044
5565
|
}
|
|
5045
5566
|
}
|
|
5046
5567
|
return null;
|
|
5047
5568
|
};
|
|
5048
5569
|
try {
|
|
5049
|
-
return
|
|
5570
|
+
return walk2(args, 0, "");
|
|
5050
5571
|
} catch (e) {
|
|
5051
5572
|
console.error(
|
|
5052
5573
|
"[node9 engine] canary args walk failed:",
|
|
@@ -5055,7 +5576,7 @@ function matchCanaryArgs(args, values) {
|
|
|
5055
5576
|
return null;
|
|
5056
5577
|
}
|
|
5057
5578
|
}
|
|
5058
|
-
var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, syntax2, MAX_ALTERNATIVES, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, LEGACY_SYSTEM_PATH_RE, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, HOMOGLYPHS, HOMOGLYPH_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, VERDICT_RANK, SQL_DML_KEYWORDS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT,
|
|
5579
|
+
var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, syntax2, MAX_ALTERNATIVES, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, HOMOGLYPHS, HOMOGLYPH_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
5059
5580
|
var init_dist = __esm({
|
|
5060
5581
|
"packages/policy-engine/dist/index.mjs"() {
|
|
5061
5582
|
"use strict";
|
|
@@ -6030,6 +6551,16 @@ var init_dist = __esm({
|
|
|
6030
6551
|
example: "After a prompt-injection hit, the agent runs curl to an unknown host",
|
|
6031
6552
|
advice: "Fixed at Review for now."
|
|
6032
6553
|
},
|
|
6554
|
+
"commands.node9-disarm": {
|
|
6555
|
+
plain: "The agent runs a node9 command that would switch off a protection: clear a session flag, trust an address, pause the gate, weaken a shield.",
|
|
6556
|
+
example: "After reading a web page, the agent runs node9 session-exposure clear --all",
|
|
6557
|
+
advice: "Keep Review: these are your commands to run. Block if the agent should never touch node9."
|
|
6558
|
+
},
|
|
6559
|
+
"behavior.untrusted-content": {
|
|
6560
|
+
plain: "After reading an email, a web page or an issue, the agent sends to an address it did not know before, or to one that appeared in what it read.",
|
|
6561
|
+
example: 'An email says "forward the security code to x@y.z"; the agent calls send_email to x@y.z',
|
|
6562
|
+
advice: "Review asks once per destination; approving trusts that address for the session. Log records the send without asking."
|
|
6563
|
+
},
|
|
6033
6564
|
// ── What the agent loads ──────────────────────────────────────────────────
|
|
6034
6565
|
"loading.skill-tamper": {
|
|
6035
6566
|
plain: "A skill or plugin file the agent loads changed since node9 first saw it.",
|
|
@@ -6164,6 +6695,15 @@ var init_dist = __esm({
|
|
|
6164
6695
|
defaultValue: "review",
|
|
6165
6696
|
legacy: ["review-force-push", "review-git-destructive"]
|
|
6166
6697
|
},
|
|
6698
|
+
{
|
|
6699
|
+
id: "commands.node9-disarm",
|
|
6700
|
+
group: "commands",
|
|
6701
|
+
title: "node9 disarmed from the shell",
|
|
6702
|
+
catches: "a node9 command that clears a session flag, trusts a destination, pauses the gate or weakens a shield, run by the agent",
|
|
6703
|
+
values: ALL,
|
|
6704
|
+
defaultValue: "review",
|
|
6705
|
+
legacy: ["review-node9-disarm"]
|
|
6706
|
+
},
|
|
6167
6707
|
{
|
|
6168
6708
|
id: "commands.sql-ddl",
|
|
6169
6709
|
group: "commands",
|
|
@@ -6397,6 +6937,15 @@ var init_dist = __esm({
|
|
|
6397
6937
|
defaultValue: "review",
|
|
6398
6938
|
legacy: ["\u{1F534} Node9 Taint (Exfiltration Prevention)"]
|
|
6399
6939
|
},
|
|
6940
|
+
{
|
|
6941
|
+
id: "behavior.untrusted-content",
|
|
6942
|
+
group: "behavior",
|
|
6943
|
+
title: "Send after reading outside content",
|
|
6944
|
+
catches: "a send to an unknown destination, a destination found in what was read, or a destructive call after the session read an email, a web page or an issue",
|
|
6945
|
+
values: ["off", "log", "review"],
|
|
6946
|
+
defaultValue: "review",
|
|
6947
|
+
legacy: ["\u{1F6E1}\uFE0F Node9 Exposure", "exposure"]
|
|
6948
|
+
},
|
|
6400
6949
|
// ── What the agent loads ────────────────────────────────────────────────
|
|
6401
6950
|
{
|
|
6402
6951
|
id: "loading.skill-tamper",
|
|
@@ -6471,6 +7020,11 @@ var init_dist = __esm({
|
|
|
6471
7020
|
if (s.packageCheck?.enabled === void 0) return void 0;
|
|
6472
7021
|
if (!s.packageCheck.enabled) return "off";
|
|
6473
7022
|
return s.packageCheck.onMalicious === "review" ? "review" : "block";
|
|
7023
|
+
},
|
|
7024
|
+
"behavior.untrusted-content": (s) => {
|
|
7025
|
+
if (s.untrustedContent?.enabled === void 0) return void 0;
|
|
7026
|
+
if (!s.untrustedContent.enabled) return "off";
|
|
7027
|
+
return s.untrustedContent.afterExposure === "log-only" ? "log" : "review";
|
|
6474
7028
|
}
|
|
6475
7029
|
};
|
|
6476
7030
|
({ syntax } = mvdanSh);
|
|
@@ -7418,6 +7972,22 @@ var init_dist = __esm({
|
|
|
7418
7972
|
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
7419
7973
|
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
7420
7974
|
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
7975
|
+
AGENT_INSTRUCTION_FILES = [
|
|
7976
|
+
".gitlab-ci.yml",
|
|
7977
|
+
"CLAUDE.md",
|
|
7978
|
+
"AGENTS.md",
|
|
7979
|
+
".cursorrules",
|
|
7980
|
+
".mcp.json",
|
|
7981
|
+
".vscode/settings.json",
|
|
7982
|
+
"package.json"
|
|
7983
|
+
];
|
|
7984
|
+
AGENT_INSTRUCTION_DIRS = [
|
|
7985
|
+
".github/workflows",
|
|
7986
|
+
".claude",
|
|
7987
|
+
".cursor",
|
|
7988
|
+
".git/hooks",
|
|
7989
|
+
".husky"
|
|
7990
|
+
];
|
|
7421
7991
|
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
7422
7992
|
RANK = { allow: 0, review: 1, block: 2 };
|
|
7423
7993
|
BIP39_ENGLISH = `abandon
|
|
@@ -10517,7 +11087,25 @@ zoo`.split("\n");
|
|
|
10517
11087
|
name: "onlineFallback",
|
|
10518
11088
|
legacy: ["packageCheck", "onlineFallback"]
|
|
10519
11089
|
},
|
|
10520
|
-
{ checkId: "loading.malicious-package", name: "allow", legacy: ["packageCheck", "allow"] }
|
|
11090
|
+
{ checkId: "loading.malicious-package", name: "allow", legacy: ["packageCheck", "allow"] },
|
|
11091
|
+
// fw#318: the verdict carries off / log (log-only) / review; the tuning
|
|
11092
|
+
// carries whether a review asks once per destination or every time, the
|
|
11093
|
+
// frame around outside content, and the organisation's own domains.
|
|
11094
|
+
{
|
|
11095
|
+
checkId: "behavior.untrusted-content",
|
|
11096
|
+
name: "afterExposure",
|
|
11097
|
+
legacy: ["untrustedContent", "afterExposure"]
|
|
11098
|
+
},
|
|
11099
|
+
{
|
|
11100
|
+
checkId: "behavior.untrusted-content",
|
|
11101
|
+
name: "spotlight",
|
|
11102
|
+
legacy: ["untrustedContent", "spotlight"]
|
|
11103
|
+
},
|
|
11104
|
+
{
|
|
11105
|
+
checkId: "behavior.untrusted-content",
|
|
11106
|
+
name: "orgDomains",
|
|
11107
|
+
legacy: ["untrustedContent", "orgDomains"]
|
|
11108
|
+
}
|
|
10521
11109
|
];
|
|
10522
11110
|
MAP_GOVERNED = /* @__PURE__ */ new Set([
|
|
10523
11111
|
"files.shell-startup",
|
|
@@ -10534,6 +11122,7 @@ zoo`.split("\n");
|
|
|
10534
11122
|
"commands.temp-binary",
|
|
10535
11123
|
"commands.disk-destroy",
|
|
10536
11124
|
"commands.dangerous-word",
|
|
11125
|
+
"commands.node9-disarm",
|
|
10537
11126
|
"data.pipe-chain"
|
|
10538
11127
|
]);
|
|
10539
11128
|
KNOB_GOVERNED = {
|
|
@@ -10545,7 +11134,8 @@ zoo`.split("\n");
|
|
|
10545
11134
|
"behavior.loops": ["off", "block"],
|
|
10546
11135
|
"behavior.prompt-injection": ["off", "log"],
|
|
10547
11136
|
"loading.skill-tamper": ["off", "log", "block"],
|
|
10548
|
-
"loading.malicious-package": ["off", "review", "block"]
|
|
11137
|
+
"loading.malicious-package": ["off", "review", "block"],
|
|
11138
|
+
"behavior.untrusted-content": ["off", "log", "review"]
|
|
10549
11139
|
};
|
|
10550
11140
|
CONFIGURABLE_CHECK_IDS = CHECKS.filter(
|
|
10551
11141
|
(c) => configurableValues(c.id).length > 0
|
|
@@ -10736,6 +11326,152 @@ zoo`.split("\n");
|
|
|
10736
11326
|
block: 2
|
|
10737
11327
|
};
|
|
10738
11328
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
11329
|
+
MAX_DESTINATIONS = 200;
|
|
11330
|
+
MAX_SCAN_CHARS = 1e5;
|
|
11331
|
+
EMAIL_RE = /[\p{L}\p{N}._%+-]+@[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?(?:\.[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?)*\.\p{L}{2,}/gu;
|
|
11332
|
+
URL_RE = /\b(?:[a-z][a-z0-9+.-]*:\/\/|www\.)[^\s<>"'`){}]+/gi;
|
|
11333
|
+
IBAN_RE = /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]{4}){2,7}(?:[ -]?[A-Z0-9]{1,4})?\b/g;
|
|
11334
|
+
RECIPIENT_KEYS = /* @__PURE__ */ new Set([
|
|
11335
|
+
"to",
|
|
11336
|
+
"cc",
|
|
11337
|
+
"bcc",
|
|
11338
|
+
"recipient",
|
|
11339
|
+
"recipients",
|
|
11340
|
+
"email",
|
|
11341
|
+
"participants",
|
|
11342
|
+
"attendees",
|
|
11343
|
+
"destination",
|
|
11344
|
+
"iban",
|
|
11345
|
+
"account",
|
|
11346
|
+
// Where a request goes, when the value is a bare host or a scheme-less URL.
|
|
11347
|
+
"url",
|
|
11348
|
+
"uri",
|
|
11349
|
+
"host",
|
|
11350
|
+
"hostname",
|
|
11351
|
+
"endpoint",
|
|
11352
|
+
"server",
|
|
11353
|
+
"domain",
|
|
11354
|
+
"address",
|
|
11355
|
+
"webhook",
|
|
11356
|
+
"webhook_url",
|
|
11357
|
+
"callback",
|
|
11358
|
+
"callback_url",
|
|
11359
|
+
"target"
|
|
11360
|
+
]);
|
|
11361
|
+
DESTRUCTIVE_VERB_RE = /^(?:delete|remove|drop|destroy|purge|wipe|erase|transfer|pay|send_money|withdraw|change_password|update_password|reset_password|set_permission|grant|revoke|share)(?:_|$)/i;
|
|
11362
|
+
({ syntax: syntax4 } = mvdanSh4);
|
|
11363
|
+
EMPTY = {
|
|
11364
|
+
fetches: false,
|
|
11365
|
+
sends: false,
|
|
11366
|
+
destinations: [],
|
|
11367
|
+
gitRemotes: [],
|
|
11368
|
+
repos: [],
|
|
11369
|
+
unresolved: false,
|
|
11370
|
+
fetchDestinations: [],
|
|
11371
|
+
fetchKinds: []
|
|
11372
|
+
};
|
|
11373
|
+
CURL_SEND_FLAGS = /* @__PURE__ */ new Set([
|
|
11374
|
+
"-d",
|
|
11375
|
+
"--data",
|
|
11376
|
+
"--data-ascii",
|
|
11377
|
+
"--data-binary",
|
|
11378
|
+
"--data-raw",
|
|
11379
|
+
"--data-urlencode",
|
|
11380
|
+
"-F",
|
|
11381
|
+
"--form",
|
|
11382
|
+
"--form-string",
|
|
11383
|
+
"-T",
|
|
11384
|
+
"--upload-file",
|
|
11385
|
+
"--json"
|
|
11386
|
+
]);
|
|
11387
|
+
WGET_SEND_FLAGS = /* @__PURE__ */ new Set(["--post-data", "--post-file", "--body-data", "--body-file"]);
|
|
11388
|
+
IWR_SEND_FLAGS = /* @__PURE__ */ new Set(["-body", "-infile"]);
|
|
11389
|
+
WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
|
|
11390
|
+
GH_READ = /* @__PURE__ */ new Set(["view", "list", "diff", "checks", "status", "download", "clone"]);
|
|
11391
|
+
GH_WRITE = /* @__PURE__ */ new Set([
|
|
11392
|
+
"create",
|
|
11393
|
+
"comment",
|
|
11394
|
+
"edit",
|
|
11395
|
+
"close",
|
|
11396
|
+
"reopen",
|
|
11397
|
+
"merge",
|
|
11398
|
+
"review",
|
|
11399
|
+
"upload",
|
|
11400
|
+
"delete",
|
|
11401
|
+
"transfer",
|
|
11402
|
+
"pin",
|
|
11403
|
+
"lock",
|
|
11404
|
+
"ready",
|
|
11405
|
+
"set",
|
|
11406
|
+
"sync",
|
|
11407
|
+
"fork"
|
|
11408
|
+
]);
|
|
11409
|
+
GH_HOSTNAME_FLAGS = /* @__PURE__ */ new Set(["--hostname", "-h", "-R", "--repo"]);
|
|
11410
|
+
GH_REPO_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
11411
|
+
"--hostname",
|
|
11412
|
+
"-R",
|
|
11413
|
+
"--repo",
|
|
11414
|
+
"-d",
|
|
11415
|
+
"--description",
|
|
11416
|
+
"-h",
|
|
11417
|
+
"--homepage",
|
|
11418
|
+
"--visibility",
|
|
11419
|
+
"--default-branch",
|
|
11420
|
+
"--add-topic",
|
|
11421
|
+
"--remove-topic",
|
|
11422
|
+
"-p",
|
|
11423
|
+
"--template",
|
|
11424
|
+
"-s",
|
|
11425
|
+
"--source",
|
|
11426
|
+
"-b",
|
|
11427
|
+
"--branch",
|
|
11428
|
+
"-l",
|
|
11429
|
+
"--license",
|
|
11430
|
+
"-g",
|
|
11431
|
+
"--gitignore",
|
|
11432
|
+
"-t",
|
|
11433
|
+
"--team",
|
|
11434
|
+
"-r",
|
|
11435
|
+
"--remote",
|
|
11436
|
+
"--org"
|
|
11437
|
+
]);
|
|
11438
|
+
GH_API_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
11439
|
+
"--hostname",
|
|
11440
|
+
"-X",
|
|
11441
|
+
"--method",
|
|
11442
|
+
"-f",
|
|
11443
|
+
"--raw-field",
|
|
11444
|
+
"-F",
|
|
11445
|
+
"--field",
|
|
11446
|
+
"-H",
|
|
11447
|
+
"--header",
|
|
11448
|
+
"--input",
|
|
11449
|
+
"-q",
|
|
11450
|
+
"--jq",
|
|
11451
|
+
"-t",
|
|
11452
|
+
"--template",
|
|
11453
|
+
"--cache",
|
|
11454
|
+
"-p",
|
|
11455
|
+
"--preview"
|
|
11456
|
+
]);
|
|
11457
|
+
GIT_FETCH = /* @__PURE__ */ new Set(["clone", "fetch", "pull", "ls-remote"]);
|
|
11458
|
+
PUBLISH = {
|
|
11459
|
+
npm: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "registry.npmjs.org", flag: ["--registry"] },
|
|
11460
|
+
pnpm: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "registry.npmjs.org", flag: ["--registry"] },
|
|
11461
|
+
yarn: {
|
|
11462
|
+
verbs: /* @__PURE__ */ new Set(["publish", "npm"]),
|
|
11463
|
+
registry: "registry.npmjs.org",
|
|
11464
|
+
flag: ["--registry"]
|
|
11465
|
+
},
|
|
11466
|
+
twine: { verbs: /* @__PURE__ */ new Set(["upload"]), registry: "upload.pypi.org", flag: ["--repository-url"] },
|
|
11467
|
+
cargo: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "crates.io", flag: ["--registry", "--index"] },
|
|
11468
|
+
gem: { verbs: /* @__PURE__ */ new Set(["push"]), registry: "rubygems.org", flag: ["--host"] },
|
|
11469
|
+
docker: { verbs: /* @__PURE__ */ new Set(["push"]), registry: "docker.io" }
|
|
11470
|
+
};
|
|
11471
|
+
SHELLS2 = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
11472
|
+
OWN_HEADS = /* @__PURE__ */ new Set([...Object.keys(PUBLISH), "git", "gh"]);
|
|
11473
|
+
MAX_DEPTH3 = 3;
|
|
11474
|
+
CWD_CHANGERS = /* @__PURE__ */ new Set(["cd", "pushd", "popd"]);
|
|
10739
11475
|
TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
10740
11476
|
LOOP_MAX_RECORDS = 500;
|
|
10741
11477
|
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
@@ -10748,7 +11484,7 @@ zoo`.split("\n");
|
|
|
10748
11484
|
MAX_PII_SCAN_BYTES = 1e5;
|
|
10749
11485
|
CANARY_MIN_LENGTH = 16;
|
|
10750
11486
|
MAX_TEXT = 1e5;
|
|
10751
|
-
|
|
11487
|
+
MAX_DEPTH4 = 6;
|
|
10752
11488
|
MAX_JSON_PARSE = 1e4;
|
|
10753
11489
|
URL_DEPTH = 4;
|
|
10754
11490
|
B64_DEPTH = 3;
|
|
@@ -10860,8 +11596,8 @@ function truncateAuditPreview(text) {
|
|
|
10860
11596
|
}
|
|
10861
11597
|
function appendToLog(logPath, entry) {
|
|
10862
11598
|
try {
|
|
10863
|
-
const
|
|
10864
|
-
if (!fs.existsSync(
|
|
11599
|
+
const dir2 = path.dirname(logPath);
|
|
11600
|
+
if (!fs.existsSync(dir2)) fs.mkdirSync(dir2, { recursive: true });
|
|
10865
11601
|
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
10866
11602
|
} catch {
|
|
10867
11603
|
}
|
|
@@ -11154,6 +11890,13 @@ var ConfigFileSchema = z.object({
|
|
|
11154
11890
|
onlineFallback: z.boolean().optional(),
|
|
11155
11891
|
allow: z.array(z.string()).optional()
|
|
11156
11892
|
}).optional(),
|
|
11893
|
+
untrustedContent: z.object({
|
|
11894
|
+
enabled: z.boolean().optional(),
|
|
11895
|
+
spotlight: z.boolean().optional(),
|
|
11896
|
+
afterExposure: z.enum(["ask-once", "ask-every-time", "log-only"]).optional(),
|
|
11897
|
+
orgDomains: z.array(z.string()).optional(),
|
|
11898
|
+
roles: z.record(z.string(), z.record(z.string(), z.enum(["source", "action", "read"]))).optional()
|
|
11899
|
+
}).optional(),
|
|
11157
11900
|
skillPinning: z.object({
|
|
11158
11901
|
enabled: z.boolean().optional(),
|
|
11159
11902
|
mode: z.enum(["warn", "block"]).optional(),
|
|
@@ -11164,8 +11907,8 @@ var ConfigFileSchema = z.object({
|
|
|
11164
11907
|
}).strict();
|
|
11165
11908
|
function formatIssues(issues) {
|
|
11166
11909
|
const lines = issues.map((issue) => {
|
|
11167
|
-
const
|
|
11168
|
-
return ` \u2022 ${
|
|
11910
|
+
const path26 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
11911
|
+
return ` \u2022 ${path26}: ${issue.message}`;
|
|
11169
11912
|
});
|
|
11170
11913
|
return `Invalid config:
|
|
11171
11914
|
${lines.join("\n")}`;
|
|
@@ -11178,9 +11921,9 @@ function prunePaths(root, paths) {
|
|
|
11178
11921
|
const yi = y[y.length - 1];
|
|
11179
11922
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
11180
11923
|
});
|
|
11181
|
-
for (const
|
|
11924
|
+
for (const path26 of ordered) {
|
|
11182
11925
|
let cur = root;
|
|
11183
|
-
for (const key of
|
|
11926
|
+
for (const key of path26.slice(0, -1)) {
|
|
11184
11927
|
if (cur === null || typeof cur !== "object") {
|
|
11185
11928
|
cur = void 0;
|
|
11186
11929
|
break;
|
|
@@ -11188,7 +11931,7 @@ function prunePaths(root, paths) {
|
|
|
11188
11931
|
cur = cur[key];
|
|
11189
11932
|
}
|
|
11190
11933
|
if (cur === null || typeof cur !== "object") continue;
|
|
11191
|
-
const last =
|
|
11934
|
+
const last = path26[path26.length - 1];
|
|
11192
11935
|
if (Array.isArray(cur)) {
|
|
11193
11936
|
const i = Number(last);
|
|
11194
11937
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -11224,7 +11967,7 @@ function sanitizeConfig(raw) {
|
|
|
11224
11967
|
return issue.keys.map((k) => [...at, k]);
|
|
11225
11968
|
}
|
|
11226
11969
|
return [at.slice(0, -level || void 0)];
|
|
11227
|
-
}).filter((
|
|
11970
|
+
}).filter((path26) => path26.length > 0);
|
|
11228
11971
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
11229
11972
|
}
|
|
11230
11973
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -11266,8 +12009,8 @@ function loadUserShields() {
|
|
|
11266
12009
|
}
|
|
11267
12010
|
return result;
|
|
11268
12011
|
}
|
|
11269
|
-
for (const
|
|
11270
|
-
const filePath = path2.join(USER_SHIELDS_DIR,
|
|
12012
|
+
for (const file2 of entries) {
|
|
12013
|
+
const filePath = path2.join(USER_SHIELDS_DIR, file2);
|
|
11271
12014
|
try {
|
|
11272
12015
|
const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
11273
12016
|
const v = validateShieldDefinition(raw);
|
|
@@ -11278,7 +12021,7 @@ function loadUserShields() {
|
|
|
11278
12021
|
}
|
|
11279
12022
|
result[v.ok.name] = v.ok;
|
|
11280
12023
|
} catch (err) {
|
|
11281
|
-
process.stderr.write(`[node9] Failed to load user shield ${
|
|
12024
|
+
process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
|
|
11282
12025
|
`);
|
|
11283
12026
|
}
|
|
11284
12027
|
}
|
|
@@ -11549,11 +12292,11 @@ function getCachedHosts() {
|
|
|
11549
12292
|
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
11550
12293
|
return hosts;
|
|
11551
12294
|
}
|
|
11552
|
-
function
|
|
12295
|
+
function normalizeHost2(raw) {
|
|
11553
12296
|
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
11554
12297
|
}
|
|
11555
|
-
function matchesTrustedHost(
|
|
11556
|
-
const normalized =
|
|
12298
|
+
function matchesTrustedHost(host2, list) {
|
|
12299
|
+
const normalized = normalizeHost2(host2);
|
|
11557
12300
|
return list.some((raw) => {
|
|
11558
12301
|
const entryHost = raw.toLowerCase();
|
|
11559
12302
|
if (entryHost.startsWith("*.")) {
|
|
@@ -11563,9 +12306,9 @@ function matchesTrustedHost(host, list) {
|
|
|
11563
12306
|
return normalized === entryHost;
|
|
11564
12307
|
});
|
|
11565
12308
|
}
|
|
11566
|
-
function isTrustedHost(
|
|
12309
|
+
function isTrustedHost(host2) {
|
|
11567
12310
|
return matchesTrustedHost(
|
|
11568
|
-
|
|
12311
|
+
host2,
|
|
11569
12312
|
getCachedHosts().map((entry) => entry.host)
|
|
11570
12313
|
);
|
|
11571
12314
|
}
|
|
@@ -11683,6 +12426,10 @@ function v2ToLegacy(raw, options = {}) {
|
|
|
11683
12426
|
const [id] = pair.split(".", 1);
|
|
11684
12427
|
if (CHECK_BY_ID.has(id)) warnings.push(`tuning.${pair}: no such setting`);
|
|
11685
12428
|
}
|
|
12429
|
+
if (stated["behavior.untrusted-content"] === "log") {
|
|
12430
|
+
const uc = policy.untrustedContent;
|
|
12431
|
+
if (uc) uc.afterExposure = "log-only";
|
|
12432
|
+
}
|
|
11686
12433
|
const legacy = { version: "1.0" };
|
|
11687
12434
|
if (Object.keys(settings).length) legacy.settings = settings;
|
|
11688
12435
|
if (Object.keys(policy).length) legacy.policy = policy;
|
|
@@ -11700,9 +12447,9 @@ var DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
|
11700
12447
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"]);
|
|
11701
12448
|
var HOST_ALLOW_ENV = "NODE9_API_HOST_ALLOW";
|
|
11702
12449
|
function defaultHostSuffixes() {
|
|
11703
|
-
const
|
|
11704
|
-
const parent =
|
|
11705
|
-
return parent.includes(".") ? [
|
|
12450
|
+
const host2 = new URL(DEFAULT_API_URL).hostname.toLowerCase();
|
|
12451
|
+
const parent = host2.split(".").slice(1).join(".");
|
|
12452
|
+
return parent.includes(".") ? [host2, parent] : [host2];
|
|
11706
12453
|
}
|
|
11707
12454
|
function allowedSuffixes() {
|
|
11708
12455
|
const extra = (process.env[HOST_ALLOW_ENV] ?? "").split(",").map(
|
|
@@ -11720,11 +12467,11 @@ function validateApiUrl(raw) {
|
|
|
11720
12467
|
}
|
|
11721
12468
|
if (u.username || u.password) return null;
|
|
11722
12469
|
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
|
|
11723
|
-
const
|
|
11724
|
-
if (LOOPBACK.has(
|
|
12470
|
+
const host2 = u.hostname.toLowerCase();
|
|
12471
|
+
if (LOOPBACK.has(host2)) return u;
|
|
11725
12472
|
if (u.protocol !== "https:") return null;
|
|
11726
12473
|
const suffixes = allowedSuffixes();
|
|
11727
|
-
const ok = suffixes.some((s) =>
|
|
12474
|
+
const ok = suffixes.some((s) => host2 === s || host2.endsWith("." + s));
|
|
11728
12475
|
return ok ? u : null;
|
|
11729
12476
|
}
|
|
11730
12477
|
function safeApiUrl(raw, onReject) {
|
|
@@ -11866,6 +12613,29 @@ var DEFAULT_CONFIG = {
|
|
|
11866
12613
|
reason: "SQL DDL destructive statement inside a shell command",
|
|
11867
12614
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
11868
12615
|
},
|
|
12616
|
+
// ── node9 itself ──────────────────────────────────────────────────────
|
|
12617
|
+
// The agent runs as the user, so a `node9 ...` command from Bash can
|
|
12618
|
+
// clear the session's taint or exposure, trust a destination or pause
|
|
12619
|
+
// the gate. Those are the user's own commands; from the agent they are
|
|
12620
|
+
// a review (fw#318 review finding 1).
|
|
12621
|
+
{
|
|
12622
|
+
name: "review-node9-disarm",
|
|
12623
|
+
tool: "bash",
|
|
12624
|
+
conditions: [
|
|
12625
|
+
{
|
|
12626
|
+
field: "command",
|
|
12627
|
+
op: "matches",
|
|
12628
|
+
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
12629
|
+
// fails the engine's ReDoS check and the rule would be dropped.
|
|
12630
|
+
value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|egress allow|approver set)\\b",
|
|
12631
|
+
flags: "i"
|
|
12632
|
+
}
|
|
12633
|
+
],
|
|
12634
|
+
conditionMode: "all",
|
|
12635
|
+
verdict: "review",
|
|
12636
|
+
reason: "node9 self-weakening command run by the agent",
|
|
12637
|
+
description: "The AI wants to run a node9 command that clears a session flag, trusts a destination, pauses the gate or weakens a shield. These are your commands to run, not the agent's."
|
|
12638
|
+
},
|
|
11869
12639
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
11870
12640
|
{
|
|
11871
12641
|
name: "review-force-push",
|
|
@@ -11967,6 +12737,13 @@ var DEFAULT_CONFIG = {
|
|
|
11967
12737
|
allow: []
|
|
11968
12738
|
},
|
|
11969
12739
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
12740
|
+
untrustedContent: {
|
|
12741
|
+
enabled: true,
|
|
12742
|
+
spotlight: true,
|
|
12743
|
+
afterExposure: "ask-once",
|
|
12744
|
+
orgDomains: [],
|
|
12745
|
+
roles: {}
|
|
12746
|
+
},
|
|
11970
12747
|
trustedHosts: [],
|
|
11971
12748
|
trustedHostsManaged: false,
|
|
11972
12749
|
appPermissions: {},
|
|
@@ -12191,6 +12968,11 @@ function getConfig(cwd) {
|
|
|
12191
12968
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
12192
12969
|
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
12193
12970
|
},
|
|
12971
|
+
untrustedContent: {
|
|
12972
|
+
...DEFAULT_CONFIG.policy.untrustedContent,
|
|
12973
|
+
orgDomains: [...DEFAULT_CONFIG.policy.untrustedContent.orgDomains],
|
|
12974
|
+
roles: {}
|
|
12975
|
+
},
|
|
12194
12976
|
// Left empty on purpose: the local file is read fresh at policy-eval time
|
|
12195
12977
|
// (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
|
|
12196
12978
|
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
@@ -12350,6 +13132,9 @@ function getConfig(cwd) {
|
|
|
12350
13132
|
}
|
|
12351
13133
|
}
|
|
12352
13134
|
}
|
|
13135
|
+
if (p.untrustedContent && typeof p.untrustedContent === "object") {
|
|
13136
|
+
applyUntrustedContent(mergedPolicy.untrustedContent, p.untrustedContent, false, isProject);
|
|
13137
|
+
}
|
|
12353
13138
|
const envs = source.environments || {};
|
|
12354
13139
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
12355
13140
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -12549,6 +13334,9 @@ function getConfig(cwd) {
|
|
|
12549
13334
|
allow: Array.isArray(pc.allow) ? pc.allow.filter((a) => typeof a === "string" && a.length > 0) : cur.allow
|
|
12550
13335
|
};
|
|
12551
13336
|
}
|
|
13337
|
+
if (mc.untrustedContent && typeof mc.untrustedContent === "object") {
|
|
13338
|
+
applyUntrustedContent(mergedPolicy.untrustedContent, mc.untrustedContent, true);
|
|
13339
|
+
}
|
|
12552
13340
|
if (mc.loopDetection && typeof mc.loopDetection === "object") {
|
|
12553
13341
|
const l = mc.loopDetection;
|
|
12554
13342
|
const cur = mergedPolicy.loopDetection;
|
|
@@ -12569,18 +13357,18 @@ function getConfig(cwd) {
|
|
|
12569
13357
|
}
|
|
12570
13358
|
if (Array.isArray(mc.jailPaths)) {
|
|
12571
13359
|
for (const jp of mc.jailPaths) {
|
|
12572
|
-
const
|
|
12573
|
-
if (!
|
|
13360
|
+
const path26 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
13361
|
+
if (!path26) continue;
|
|
12574
13362
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
12575
|
-
for (const r of pathRules(
|
|
13363
|
+
for (const r of pathRules(path26, verdict, "org-managed jail")) {
|
|
12576
13364
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
12577
13365
|
}
|
|
12578
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
13366
|
+
mergedPolicy.managedJailPaths.push({ path: path26, verdict });
|
|
12579
13367
|
}
|
|
12580
13368
|
}
|
|
12581
13369
|
if (Array.isArray(mc.trustedHosts)) {
|
|
12582
13370
|
mergedPolicy.trustedHostsManaged = true;
|
|
12583
|
-
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) =>
|
|
13371
|
+
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
|
|
12584
13372
|
}
|
|
12585
13373
|
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
12586
13374
|
const coerced = {};
|
|
@@ -12598,7 +13386,7 @@ function getConfig(cwd) {
|
|
|
12598
13386
|
);
|
|
12599
13387
|
}
|
|
12600
13388
|
const on = (v) => !!v && typeof v === "object" && v.enabled === true;
|
|
12601
|
-
cloudMandatesEnforcement = cloudMandatesAppPerm || Array.isArray(mc.jailPaths) && mc.jailPaths.some((jp) => typeof jp?.path === "string" && jp.path.trim() !== "") || on(mc.egress) || on(mc.dlp) || on(mc.injectionScan) || on(mc.skillPinning) || on(mc.loopDetection) || !!mc.commandChecks && typeof mc.commandChecks === "object" && Object.values(mc.commandChecks).some(
|
|
13389
|
+
cloudMandatesEnforcement = cloudMandatesAppPerm || Array.isArray(mc.jailPaths) && mc.jailPaths.some((jp) => typeof jp?.path === "string" && jp.path.trim() !== "") || on(mc.egress) || on(mc.dlp) || on(mc.injectionScan) || on(mc.untrustedContent) || on(mc.skillPinning) || on(mc.loopDetection) || !!mc.commandChecks && typeof mc.commandChecks === "object" && Object.values(mc.commandChecks).some(
|
|
12602
13390
|
(v) => typeof v === "string" && v !== "off"
|
|
12603
13391
|
);
|
|
12604
13392
|
}
|
|
@@ -12834,6 +13622,37 @@ ${error.replace("Invalid config:\n", "")}
|
|
|
12834
13622
|
}
|
|
12835
13623
|
return sanitized;
|
|
12836
13624
|
}
|
|
13625
|
+
function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false) {
|
|
13626
|
+
if (!raw || typeof raw !== "object") return;
|
|
13627
|
+
const uc = raw;
|
|
13628
|
+
const AFTER_RANK = { "log-only": 0, "ask-once": 1, "ask-every-time": 2 };
|
|
13629
|
+
if (typeof uc.enabled === "boolean" && !(isProject && uc.enabled === false))
|
|
13630
|
+
cur.enabled = uc.enabled;
|
|
13631
|
+
if (typeof uc.spotlight === "boolean" && !(isProject && uc.spotlight === false))
|
|
13632
|
+
cur.spotlight = uc.spotlight;
|
|
13633
|
+
if ((uc.afterExposure === "ask-once" || uc.afterExposure === "ask-every-time" || uc.afterExposure === "log-only") && !(isProject && AFTER_RANK[uc.afterExposure] < AFTER_RANK[cur.afterExposure]))
|
|
13634
|
+
cur.afterExposure = uc.afterExposure;
|
|
13635
|
+
if (Array.isArray(uc.orgDomains) && !isProject) {
|
|
13636
|
+
const domains = uc.orgDomains.filter((d) => typeof d === "string").map((d) => d.trim().toLowerCase().replace(/^@/, "")).filter((d) => /^[a-z0-9.-]+\.[a-z]{2,}$/.test(d));
|
|
13637
|
+
if (replaceLists) cur.orgDomains = domains;
|
|
13638
|
+
else for (const d of domains) if (!cur.orgDomains.includes(d)) cur.orgDomains.push(d);
|
|
13639
|
+
}
|
|
13640
|
+
if (uc.roles && typeof uc.roles === "object" && !Array.isArray(uc.roles)) {
|
|
13641
|
+
const next = replaceLists ? {} : cur.roles;
|
|
13642
|
+
for (const [server, tools] of Object.entries(uc.roles)) {
|
|
13643
|
+
if (!tools || typeof tools !== "object" || Array.isArray(tools)) continue;
|
|
13644
|
+
const m = { ...next[server] ?? {} };
|
|
13645
|
+
for (const [tool, role] of Object.entries(tools)) {
|
|
13646
|
+
if (role === "source" || role === "action" || role === "read") {
|
|
13647
|
+
if (isProject && role !== "action") continue;
|
|
13648
|
+
m[tool] = role;
|
|
13649
|
+
}
|
|
13650
|
+
}
|
|
13651
|
+
if (Object.keys(m).length > 0) next[server] = m;
|
|
13652
|
+
}
|
|
13653
|
+
cur.roles = next;
|
|
13654
|
+
}
|
|
13655
|
+
}
|
|
12837
13656
|
|
|
12838
13657
|
// src/utils/regex.ts
|
|
12839
13658
|
init_dist();
|
|
@@ -12994,9 +13813,9 @@ var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
|
|
|
12994
13813
|
function findInPath(cmd) {
|
|
12995
13814
|
if (path7.posix.isAbsolute(cmd)) return cmd;
|
|
12996
13815
|
const pathEnv = process.env.PATH ?? "";
|
|
12997
|
-
for (const
|
|
12998
|
-
if (!
|
|
12999
|
-
const full = path7.join(
|
|
13816
|
+
for (const dir2 of pathEnv.split(path7.delimiter)) {
|
|
13817
|
+
if (!dir2) continue;
|
|
13818
|
+
const full = path7.join(dir2, cmd);
|
|
13000
13819
|
try {
|
|
13001
13820
|
fs7.accessSync(full, fs7.constants.X_OK);
|
|
13002
13821
|
return full;
|
|
@@ -13097,7 +13916,7 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
|
13097
13916
|
// config; changes arrive via cloud sync). Unmanaged → the local file via
|
|
13098
13917
|
// getCachedHosts (5s TTL + mtime), so a `node9 trust add/remove` still
|
|
13099
13918
|
// reaches a long-lived in-process authorizer (the gateway) within seconds.
|
|
13100
|
-
isTrustedHost: config.policy.trustedHostsManaged ? (
|
|
13919
|
+
isTrustedHost: config.policy.trustedHostsManaged ? (host2) => matchesTrustedHost(host2, config.policy.trustedHosts) : isTrustedHost
|
|
13101
13920
|
}
|
|
13102
13921
|
);
|
|
13103
13922
|
}
|
|
@@ -13115,8 +13934,8 @@ import fs8 from "fs";
|
|
|
13115
13934
|
import path8 from "path";
|
|
13116
13935
|
import { randomUUID } from "crypto";
|
|
13117
13936
|
function atomicWriteSync(filePath, data, options) {
|
|
13118
|
-
const
|
|
13119
|
-
if (!fs8.existsSync(
|
|
13937
|
+
const dir2 = path8.dirname(filePath);
|
|
13938
|
+
if (!fs8.existsSync(dir2)) fs8.mkdirSync(dir2, { recursive: true });
|
|
13120
13939
|
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
13121
13940
|
try {
|
|
13122
13941
|
fs8.writeFileSync(tmpPath, data, options);
|
|
@@ -13215,9 +14034,9 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
13215
14034
|
}
|
|
13216
14035
|
function getPersistentDecision(toolName) {
|
|
13217
14036
|
try {
|
|
13218
|
-
const
|
|
13219
|
-
if (!fs9.existsSync(
|
|
13220
|
-
const decisions = JSON.parse(fs9.readFileSync(
|
|
14037
|
+
const file2 = path9.join(os7.homedir(), ".node9", "decisions.json");
|
|
14038
|
+
if (!fs9.existsSync(file2)) return null;
|
|
14039
|
+
const decisions = JSON.parse(fs9.readFileSync(file2, "utf-8"));
|
|
13221
14040
|
const d = decisions[toolName];
|
|
13222
14041
|
if (d === "allow" || d === "deny") return d;
|
|
13223
14042
|
} catch {
|
|
@@ -13327,7 +14146,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
|
13327
14146
|
return false;
|
|
13328
14147
|
}
|
|
13329
14148
|
}
|
|
13330
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
|
|
14149
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
|
|
13331
14150
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
13332
14151
|
const ctrl = new AbortController();
|
|
13333
14152
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -13352,7 +14171,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
13352
14171
|
...recoveryCommand && { recoveryCommand },
|
|
13353
14172
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
13354
14173
|
...viewOnly && { viewOnly: true },
|
|
13355
|
-
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
14174
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true },
|
|
14175
|
+
...sessionId && { sessionId }
|
|
13356
14176
|
}),
|
|
13357
14177
|
signal: ctrl.signal
|
|
13358
14178
|
});
|
|
@@ -13451,7 +14271,7 @@ async function checkSessionTaint(sessionId) {
|
|
|
13451
14271
|
try {
|
|
13452
14272
|
const res = await fetch(`${base2}/session-taint/check`, {
|
|
13453
14273
|
method: "POST",
|
|
13454
|
-
headers:
|
|
14274
|
+
headers: exposureHeaders(),
|
|
13455
14275
|
body: JSON.stringify({ sessionId }),
|
|
13456
14276
|
signal: AbortSignal.timeout(2e3)
|
|
13457
14277
|
});
|
|
@@ -13460,6 +14280,42 @@ async function checkSessionTaint(sessionId) {
|
|
|
13460
14280
|
return { tainted: false, daemonUnavailable: true };
|
|
13461
14281
|
}
|
|
13462
14282
|
}
|
|
14283
|
+
function exposureHeaders() {
|
|
14284
|
+
return {
|
|
14285
|
+
"Content-Type": "application/json",
|
|
14286
|
+
"X-Node9-Internal": getInternalToken() ?? ""
|
|
14287
|
+
};
|
|
14288
|
+
}
|
|
14289
|
+
async function exposurePost(route, body, timeoutMs) {
|
|
14290
|
+
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
14291
|
+
const res = await fetch(`${base2}${route}`, {
|
|
14292
|
+
method: "POST",
|
|
14293
|
+
headers: exposureHeaders(),
|
|
14294
|
+
body: JSON.stringify(body),
|
|
14295
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
14296
|
+
});
|
|
14297
|
+
if (!res.ok) throw new Error(`session-exposure ${route}: HTTP ${res.status}`);
|
|
14298
|
+
return res.json();
|
|
14299
|
+
}
|
|
14300
|
+
async function checkSessionExposure(sessionId) {
|
|
14301
|
+
if (!sessionId || !isDaemonRunning()) return { exposed: false };
|
|
14302
|
+
try {
|
|
14303
|
+
return await exposurePost(
|
|
14304
|
+
"/session-exposure/check",
|
|
14305
|
+
{ sessionId },
|
|
14306
|
+
2e3
|
|
14307
|
+
);
|
|
14308
|
+
} catch {
|
|
14309
|
+
return { exposed: false, daemonUnavailable: true };
|
|
14310
|
+
}
|
|
14311
|
+
}
|
|
14312
|
+
async function approveSessionExposure(sessionId, keys) {
|
|
14313
|
+
if (!sessionId || keys.length === 0 || !isDaemonRunning()) return;
|
|
14314
|
+
try {
|
|
14315
|
+
await exposurePost("/session-exposure/approve", { sessionId, keys }, 1e3);
|
|
14316
|
+
} catch {
|
|
14317
|
+
}
|
|
14318
|
+
}
|
|
13463
14319
|
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
13464
14320
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
13465
14321
|
await fetch(`${base2}/resolve/${id}`, {
|
|
@@ -13472,6 +14328,7 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
13472
14328
|
|
|
13473
14329
|
// src/auth/orchestrator.ts
|
|
13474
14330
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
14331
|
+
import os18 from "os";
|
|
13475
14332
|
|
|
13476
14333
|
// src/ui/native.ts
|
|
13477
14334
|
import { spawn } from "child_process";
|
|
@@ -13591,6 +14448,7 @@ function resolveNativeDecision(opts) {
|
|
|
13591
14448
|
const { code, output, elapsedMs, locked } = opts;
|
|
13592
14449
|
if (locked) return "deny";
|
|
13593
14450
|
const tooFast = elapsedMs < MIN_INTERACTION_MS;
|
|
14451
|
+
if (output.includes("Allow & trust")) return tooFast ? "deny" : "trust_recipients";
|
|
13594
14452
|
if (output.includes("Always Allow")) return tooFast ? "deny" : "always_allow";
|
|
13595
14453
|
if (code === 0) return tooFast ? "deny" : "allow";
|
|
13596
14454
|
return "deny";
|
|
@@ -13613,12 +14471,12 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
13613
14471
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
13614
14472
|
const obj = parsed;
|
|
13615
14473
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
13616
|
-
const
|
|
14474
|
+
const file2 = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
|
|
13617
14475
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
13618
14476
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
13619
14477
|
return {
|
|
13620
14478
|
intent: "EDIT",
|
|
13621
|
-
message: `\u{1F4DD} EDITING: ${
|
|
14479
|
+
message: `\u{1F4DD} EDITING: ${file2}
|
|
13622
14480
|
\u{1F4C2} PATH: ${obj.file_path}
|
|
13623
14481
|
|
|
13624
14482
|
--- REPLACING ---
|
|
@@ -13676,7 +14534,7 @@ ${smartTruncate(str, 500)}`
|
|
|
13676
14534
|
function escapePango(text) {
|
|
13677
14535
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13678
14536
|
}
|
|
13679
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
14537
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription, exposure = false) {
|
|
13680
14538
|
const lines = [];
|
|
13681
14539
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
13682
14540
|
const safeAgent = safeMessage(agent ?? "AI Agent", 80);
|
|
@@ -13685,17 +14543,19 @@ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13685
14543
|
if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
|
|
13686
14544
|
lines.push("");
|
|
13687
14545
|
lines.push(formattedArgs);
|
|
13688
|
-
if (allowCount >= 3) {
|
|
14546
|
+
if (allowCount >= 3 && !exposure) {
|
|
13689
14547
|
lines.push("");
|
|
13690
14548
|
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
13691
14549
|
}
|
|
13692
14550
|
if (!locked) {
|
|
13693
14551
|
lines.push("");
|
|
13694
|
-
lines.push(
|
|
14552
|
+
lines.push(
|
|
14553
|
+
exposure ? '\u21B5 Enter = Allow once | \u238B Esc = Block | "Allow & trust" = this address is fine from now on' : '\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again'
|
|
14554
|
+
);
|
|
13695
14555
|
}
|
|
13696
14556
|
return lines.join("\n");
|
|
13697
14557
|
}
|
|
13698
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
14558
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription, exposure = false) {
|
|
13699
14559
|
const lines = [];
|
|
13700
14560
|
if (locked) {
|
|
13701
14561
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -13708,7 +14568,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13708
14568
|
if (ruleDescription) lines.push(`<i>\u2139 ${escapePango(ruleDescription)}</i>`);
|
|
13709
14569
|
lines.push("");
|
|
13710
14570
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
13711
|
-
if (allowCount >= 3) {
|
|
14571
|
+
if (allowCount >= 3 && !exposure) {
|
|
13712
14572
|
lines.push("");
|
|
13713
14573
|
lines.push(
|
|
13714
14574
|
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
@@ -13717,13 +14577,14 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13717
14577
|
if (!locked) {
|
|
13718
14578
|
lines.push("");
|
|
13719
14579
|
lines.push(
|
|
13720
|
-
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
14580
|
+
exposure ? '<small>\u21B5 Enter = <b>Allow once</b> | \u238B Esc = <b>Block</b> | "Allow & trust" = this address is fine from now on</small>' : '<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
13721
14581
|
);
|
|
13722
14582
|
}
|
|
13723
14583
|
return lines.join("\n");
|
|
13724
14584
|
}
|
|
13725
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription) {
|
|
14585
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription, exposureDestinations = []) {
|
|
13726
14586
|
if (isTestEnv()) return "deny";
|
|
14587
|
+
const thirdButton = exposureDestinations.length > 0 ? "Allow & trust" : "Always Allow";
|
|
13727
14588
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
13728
14589
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
13729
14590
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
@@ -13734,7 +14595,8 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
13734
14595
|
explainableLabel,
|
|
13735
14596
|
locked,
|
|
13736
14597
|
allowCount,
|
|
13737
|
-
ruleDescription
|
|
14598
|
+
ruleDescription,
|
|
14599
|
+
exposureDestinations.length > 0
|
|
13738
14600
|
);
|
|
13739
14601
|
return new Promise((resolve) => {
|
|
13740
14602
|
let childProcess = null;
|
|
@@ -13754,7 +14616,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
13754
14616
|
}
|
|
13755
14617
|
try {
|
|
13756
14618
|
if (process.platform === "darwin") {
|
|
13757
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "
|
|
14619
|
+
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "${thirdButton}", "Allow \u21B5"} default button "Allow \u21B5" cancel button "Block \u238B"`;
|
|
13758
14620
|
const script = `on run argv
|
|
13759
14621
|
tell application "System Events"
|
|
13760
14622
|
activate
|
|
@@ -13770,7 +14632,8 @@ end run`;
|
|
|
13770
14632
|
explainableLabel,
|
|
13771
14633
|
locked,
|
|
13772
14634
|
allowCount,
|
|
13773
|
-
ruleDescription
|
|
14635
|
+
ruleDescription,
|
|
14636
|
+
exposureDestinations.length > 0
|
|
13774
14637
|
);
|
|
13775
14638
|
const argsList = [
|
|
13776
14639
|
locked ? "--info" : "--question",
|
|
@@ -13785,7 +14648,7 @@ end run`;
|
|
|
13785
14648
|
];
|
|
13786
14649
|
if (!locked) {
|
|
13787
14650
|
argsList.push("--cancel-label", "Block \u238B");
|
|
13788
|
-
argsList.push("--extra-button",
|
|
14651
|
+
argsList.push("--extra-button", thirdButton);
|
|
13789
14652
|
}
|
|
13790
14653
|
childProcess = spawn("zenity", argsList);
|
|
13791
14654
|
} else if (process.platform === "win32") {
|
|
@@ -13894,31 +14757,452 @@ function canaryValues() {
|
|
|
13894
14757
|
init_dist();
|
|
13895
14758
|
init_dist();
|
|
13896
14759
|
init_audit();
|
|
14760
|
+
init_dist();
|
|
13897
14761
|
|
|
13898
|
-
// src/
|
|
13899
|
-
init_audit();
|
|
14762
|
+
// src/exposure/known-hosts.ts
|
|
13900
14763
|
import fs14 from "fs";
|
|
13901
|
-
import
|
|
14764
|
+
import path16 from "path";
|
|
14765
|
+
|
|
14766
|
+
// src/utils/git-remotes.ts
|
|
14767
|
+
init_dist();
|
|
14768
|
+
import fs13 from "fs";
|
|
13902
14769
|
import path15 from "path";
|
|
14770
|
+
function readSmallFile(p, max) {
|
|
14771
|
+
let fd;
|
|
14772
|
+
try {
|
|
14773
|
+
fd = fs13.openSync(p, fs13.constants.O_RDONLY | (fs13.constants.O_NOFOLLOW ?? 0));
|
|
14774
|
+
const st = fs13.fstatSync(fd);
|
|
14775
|
+
if (!st.isFile() || st.size > max) return null;
|
|
14776
|
+
const buf = Buffer.alloc(st.size);
|
|
14777
|
+
const n = fs13.readSync(fd, buf, 0, st.size, 0);
|
|
14778
|
+
return buf.subarray(0, n).toString("utf8");
|
|
14779
|
+
} catch {
|
|
14780
|
+
return null;
|
|
14781
|
+
} finally {
|
|
14782
|
+
if (fd !== void 0) fs13.closeSync(fd);
|
|
14783
|
+
}
|
|
14784
|
+
}
|
|
14785
|
+
function gitRemoteHost(url) {
|
|
14786
|
+
const u = url.trim();
|
|
14787
|
+
if (!u) return null;
|
|
14788
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
14789
|
+
try {
|
|
14790
|
+
const h = new URL(u).hostname.toLowerCase();
|
|
14791
|
+
return h || null;
|
|
14792
|
+
} catch {
|
|
14793
|
+
return null;
|
|
14794
|
+
}
|
|
14795
|
+
}
|
|
14796
|
+
const scp = /^(?:[^@/\s]+@)?([A-Za-z0-9.-]+):(?!\/\/)/.exec(u);
|
|
14797
|
+
if (scp && scp[1].includes(".")) return scp[1].toLowerCase();
|
|
14798
|
+
return null;
|
|
14799
|
+
}
|
|
14800
|
+
function findGitConfig(dir2, walkUp = true) {
|
|
14801
|
+
let cur = path15.resolve(dir2);
|
|
14802
|
+
for (let i = 0; i < (walkUp ? 64 : 1); i++) {
|
|
14803
|
+
const dotGit = path15.join(cur, ".git");
|
|
14804
|
+
const direct = readSmallFile(path15.join(dotGit, "config"), 1e6);
|
|
14805
|
+
if (direct !== null) return direct;
|
|
14806
|
+
const pointer = readSmallFile(dotGit, 4096);
|
|
14807
|
+
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
14808
|
+
if (m) {
|
|
14809
|
+
let gitDir = path15.resolve(cur, m[1]);
|
|
14810
|
+
const common = readSmallFile(path15.join(gitDir, "commondir"), 4096);
|
|
14811
|
+
if (common !== null) gitDir = path15.resolve(gitDir, common.trim());
|
|
14812
|
+
return readSmallFile(path15.join(gitDir, "config"), 1e6);
|
|
14813
|
+
}
|
|
14814
|
+
const parent = path15.dirname(cur);
|
|
14815
|
+
if (parent === cur) return null;
|
|
14816
|
+
cur = parent;
|
|
14817
|
+
}
|
|
14818
|
+
return null;
|
|
14819
|
+
}
|
|
14820
|
+
var cache = /* @__PURE__ */ new Map();
|
|
14821
|
+
function readGitRemotes(dir2, opts = {}) {
|
|
14822
|
+
if (!dir2) return [];
|
|
14823
|
+
const walkUp = opts.walkUp !== false;
|
|
14824
|
+
const key = `${walkUp ? "up" : "at"}:${path15.resolve(dir2)}`;
|
|
14825
|
+
const hit = cache.get(key);
|
|
14826
|
+
if (hit) return hit;
|
|
14827
|
+
const out = [];
|
|
14828
|
+
try {
|
|
14829
|
+
const cfg = findGitConfig(path15.resolve(dir2), walkUp);
|
|
14830
|
+
if (cfg !== null) {
|
|
14831
|
+
let current = null;
|
|
14832
|
+
for (const line of cfg.split(/\r?\n/)) {
|
|
14833
|
+
const sec = /^\s*\[\s*([^\]\s]+)(?:\s+"([^"]*)")?\s*\]/.exec(line);
|
|
14834
|
+
if (sec) {
|
|
14835
|
+
current = sec[1].toLowerCase() === "remote" && sec[2] ? { name: sec[2], urls: [], hosts: [], repos: [] } : null;
|
|
14836
|
+
if (current) out.push(current);
|
|
14837
|
+
continue;
|
|
14838
|
+
}
|
|
14839
|
+
const url = current && /^\s*(?:push)?url\s*=\s*(.+?)\s*$/i.exec(line);
|
|
14840
|
+
if (url && current) {
|
|
14841
|
+
current.urls.push(url[1]);
|
|
14842
|
+
const h = gitRemoteHost(url[1]);
|
|
14843
|
+
if (h && !current.hosts.includes(`host:${h}`)) current.hosts.push(`host:${h}`);
|
|
14844
|
+
const repo = gitRepoPath(url[1]);
|
|
14845
|
+
if (repo && !current.repos.includes(repo)) current.repos.push(repo);
|
|
14846
|
+
}
|
|
14847
|
+
}
|
|
14848
|
+
}
|
|
14849
|
+
} catch {
|
|
14850
|
+
}
|
|
14851
|
+
cache.set(key, out);
|
|
14852
|
+
return out;
|
|
14853
|
+
}
|
|
14854
|
+
|
|
14855
|
+
// src/exposure/known-hosts.ts
|
|
14856
|
+
var ECOSYSTEMS = [
|
|
14857
|
+
["package.json", "host:registry.npmjs.org"],
|
|
14858
|
+
["pyproject.toml", "host:upload.pypi.org"],
|
|
14859
|
+
["setup.py", "host:upload.pypi.org"],
|
|
14860
|
+
["Cargo.toml", "host:crates.io"],
|
|
14861
|
+
["Gemfile", "host:rubygems.org"]
|
|
14862
|
+
];
|
|
14863
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
14864
|
+
function manifestRoot(dir2) {
|
|
14865
|
+
let cur = path16.resolve(dir2);
|
|
14866
|
+
for (let i = 0; i < 64; i++) {
|
|
14867
|
+
for (const [m] of ECOSYSTEMS) {
|
|
14868
|
+
try {
|
|
14869
|
+
if (fs14.existsSync(path16.join(cur, m))) return cur;
|
|
14870
|
+
} catch {
|
|
14871
|
+
}
|
|
14872
|
+
}
|
|
14873
|
+
const parent = path16.dirname(cur);
|
|
14874
|
+
if (parent === cur) return null;
|
|
14875
|
+
cur = parent;
|
|
14876
|
+
}
|
|
14877
|
+
return null;
|
|
14878
|
+
}
|
|
14879
|
+
function knownProjectHosts(dir2) {
|
|
14880
|
+
const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
14881
|
+
if (!dir2) return empty;
|
|
14882
|
+
const key = path16.resolve(dir2);
|
|
14883
|
+
const hit = cache2.get(key);
|
|
14884
|
+
if (hit) return hit;
|
|
14885
|
+
const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
14886
|
+
try {
|
|
14887
|
+
for (const r of readGitRemotes(key)) {
|
|
14888
|
+
out.remotes.set(r.name, r.hosts);
|
|
14889
|
+
for (const h of r.hosts) out.hosts.add(h);
|
|
14890
|
+
for (const repo of r.repos) out.repos.add(repo);
|
|
14891
|
+
}
|
|
14892
|
+
const root = manifestRoot(key);
|
|
14893
|
+
if (root !== null) {
|
|
14894
|
+
for (const [m, registry] of ECOSYSTEMS) {
|
|
14895
|
+
if (fs14.existsSync(path16.join(root, m))) out.hosts.add(registry);
|
|
14896
|
+
}
|
|
14897
|
+
try {
|
|
14898
|
+
if (fs14.readdirSync(root).some((f) => f.endsWith(".gemspec")))
|
|
14899
|
+
out.hosts.add("host:rubygems.org");
|
|
14900
|
+
} catch {
|
|
14901
|
+
}
|
|
14902
|
+
}
|
|
14903
|
+
} catch {
|
|
14904
|
+
}
|
|
14905
|
+
cache2.set(key, out);
|
|
14906
|
+
return out;
|
|
14907
|
+
}
|
|
14908
|
+
|
|
14909
|
+
// src/exposure/writes.ts
|
|
14910
|
+
init_dist();
|
|
14911
|
+
import fs15 from "fs";
|
|
14912
|
+
import os11 from "os";
|
|
14913
|
+
import path17 from "path";
|
|
14914
|
+
var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
|
|
14915
|
+
var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
|
|
14916
|
+
function writtenContent(args) {
|
|
14917
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return [];
|
|
14918
|
+
const a = args;
|
|
14919
|
+
const out = [];
|
|
14920
|
+
for (const k of ["content", "new_string", "new_str", "file_text", "contents"]) {
|
|
14921
|
+
if (typeof a[k] === "string") out.push(a[k]);
|
|
14922
|
+
}
|
|
14923
|
+
if (Array.isArray(a.edits)) {
|
|
14924
|
+
for (const e of a.edits) {
|
|
14925
|
+
if (e && typeof e === "object" && typeof e.new_string === "string")
|
|
14926
|
+
out.push(e.new_string);
|
|
14927
|
+
}
|
|
14928
|
+
}
|
|
14929
|
+
return out;
|
|
14930
|
+
}
|
|
14931
|
+
function packageScriptsTouched(args, absPath, shellCommand) {
|
|
14932
|
+
if (shellCommand !== null) return true;
|
|
14933
|
+
const a = args ?? {};
|
|
14934
|
+
const texts = [
|
|
14935
|
+
...writtenContent(args),
|
|
14936
|
+
...typeof a.old_string === "string" ? [a.old_string] : [],
|
|
14937
|
+
...Array.isArray(a.edits) ? a.edits.map((e) => String(e?.old_string ?? "")) : []
|
|
14938
|
+
];
|
|
14939
|
+
if (typeof a.content === "string") {
|
|
14940
|
+
try {
|
|
14941
|
+
const next = JSON.parse(a.content);
|
|
14942
|
+
const prev = JSON.parse(fs15.readFileSync(absPath, "utf8"));
|
|
14943
|
+
return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
|
|
14944
|
+
} catch {
|
|
14945
|
+
return LIFECYCLE_RE.test(a.content);
|
|
14946
|
+
}
|
|
14947
|
+
}
|
|
14948
|
+
return texts.some((t) => LIFECYCLE_RE.test(t));
|
|
14949
|
+
}
|
|
14950
|
+
function judgeWritesAfterExposure(opts) {
|
|
14951
|
+
const holds = [];
|
|
14952
|
+
const home = opts.home ?? os11.homedir();
|
|
14953
|
+
const tmp = normalizeFilePath(os11.tmpdir());
|
|
14954
|
+
const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
|
|
14955
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14956
|
+
for (const access of opts.accesses) {
|
|
14957
|
+
if (!WRITE_OPS.has(access.operation)) continue;
|
|
14958
|
+
const p = access.candidates?.[0] ?? normalizeFilePath(access.path, access.cwd ?? opts.cwd ?? "", home);
|
|
14959
|
+
if (seen.has(p)) continue;
|
|
14960
|
+
seen.add(p);
|
|
14961
|
+
if (p === "/dev" || p.startsWith("/dev/")) continue;
|
|
14962
|
+
if (root !== null && isInside(root, p)) {
|
|
14963
|
+
if (isAgentInstructionPath(p, root) && (path17.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
|
|
14964
|
+
holds.push({ path: p, why: "agent-instruction" });
|
|
14965
|
+
continue;
|
|
14966
|
+
}
|
|
14967
|
+
if (isInside(tmp, p) && !(root !== null && isInside(tmp, root))) continue;
|
|
14968
|
+
holds.push({ path: p, why: "outside-project" });
|
|
14969
|
+
}
|
|
14970
|
+
const texts = opts.shellCommand !== null ? [opts.shellCommand] : writtenContent(opts.args);
|
|
14971
|
+
if (texts.length > 0 && opts.inContent.size > 0) {
|
|
14972
|
+
for (const d of extractContentDestinations(texts.join("\n"))) {
|
|
14973
|
+
if (opts.inContent.has(d) && !opts.isKnown(d)) {
|
|
14974
|
+
const target = [...seen][0] ?? "";
|
|
14975
|
+
if (!holds.some((h) => h.why === "content-destination" && h.destination === d))
|
|
14976
|
+
holds.push({ path: target, why: "content-destination", destination: d });
|
|
14977
|
+
}
|
|
14978
|
+
}
|
|
14979
|
+
}
|
|
14980
|
+
return holds;
|
|
14981
|
+
}
|
|
14982
|
+
|
|
14983
|
+
// src/exposure/marker.ts
|
|
14984
|
+
import crypto3 from "crypto";
|
|
14985
|
+
import fs16 from "fs";
|
|
14986
|
+
import os12 from "os";
|
|
14987
|
+
import path18 from "path";
|
|
14988
|
+
|
|
14989
|
+
// src/daemon/exposure-store.ts
|
|
14990
|
+
var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
14991
|
+
|
|
14992
|
+
// src/exposure/marker.ts
|
|
14993
|
+
function dir() {
|
|
14994
|
+
return path18.join(os12.homedir(), ".node9", "exposure-sessions");
|
|
14995
|
+
}
|
|
14996
|
+
function file(sessionId) {
|
|
14997
|
+
const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
14998
|
+
return path18.join(dir(), id);
|
|
14999
|
+
}
|
|
15000
|
+
function maybeSessionExposed(sessionId) {
|
|
15001
|
+
if (!sessionId) return false;
|
|
15002
|
+
try {
|
|
15003
|
+
const st = fs16.statSync(file(sessionId));
|
|
15004
|
+
return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
|
|
15005
|
+
} catch (e) {
|
|
15006
|
+
return e.code !== "ENOENT";
|
|
15007
|
+
}
|
|
15008
|
+
}
|
|
15009
|
+
|
|
15010
|
+
// src/exposure/roles.ts
|
|
15011
|
+
import fs18 from "fs";
|
|
15012
|
+
|
|
15013
|
+
// src/daemon/mcp-tools.ts
|
|
15014
|
+
import fs17 from "fs";
|
|
15015
|
+
import path19 from "path";
|
|
15016
|
+
import os13 from "os";
|
|
15017
|
+
function getMcpToolsFile() {
|
|
15018
|
+
return path19.join(os13.homedir(), ".node9", "mcp-tools.json");
|
|
15019
|
+
}
|
|
15020
|
+
function readMcpToolsConfig() {
|
|
15021
|
+
try {
|
|
15022
|
+
const file2 = getMcpToolsFile();
|
|
15023
|
+
if (!fs17.existsSync(file2)) return {};
|
|
15024
|
+
const raw = fs17.readFileSync(file2, "utf-8");
|
|
15025
|
+
return JSON.parse(raw);
|
|
15026
|
+
} catch {
|
|
15027
|
+
return {};
|
|
15028
|
+
}
|
|
15029
|
+
}
|
|
15030
|
+
|
|
15031
|
+
// src/exposure/roles.ts
|
|
15032
|
+
var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
|
|
15033
|
+
var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
|
|
15034
|
+
function foldServerName(name) {
|
|
15035
|
+
return name.replace(/[^A-Za-z0-9_-]/g, "_").toLowerCase();
|
|
15036
|
+
}
|
|
15037
|
+
var inventoryCache = null;
|
|
15038
|
+
var inventoryStamp = null;
|
|
15039
|
+
function inventoryFileStamp() {
|
|
15040
|
+
try {
|
|
15041
|
+
const st = fs18.statSync(getMcpToolsFile());
|
|
15042
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
15043
|
+
} catch {
|
|
15044
|
+
return "";
|
|
15045
|
+
}
|
|
15046
|
+
}
|
|
15047
|
+
function resolveToolAnnotations(server, tool) {
|
|
15048
|
+
const stamp = inventoryFileStamp();
|
|
15049
|
+
if (inventoryCache === null || stamp !== inventoryStamp) {
|
|
15050
|
+
inventoryStamp = stamp;
|
|
15051
|
+
inventoryCache = /* @__PURE__ */ new Map();
|
|
15052
|
+
try {
|
|
15053
|
+
const seen = /* @__PURE__ */ new Map();
|
|
15054
|
+
for (const cfg of Object.values(readMcpToolsConfig())) {
|
|
15055
|
+
if (!cfg?.name) continue;
|
|
15056
|
+
const key = foldServerName(cfg.name);
|
|
15057
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
15058
|
+
const tools = /* @__PURE__ */ new Map();
|
|
15059
|
+
for (const t of cfg.tools ?? []) tools.set(t.name, t.annotations ?? null);
|
|
15060
|
+
inventoryCache.set(key, tools);
|
|
15061
|
+
}
|
|
15062
|
+
for (const [key, n] of seen) if (n > 1) inventoryCache.delete(key);
|
|
15063
|
+
} catch {
|
|
15064
|
+
}
|
|
15065
|
+
}
|
|
15066
|
+
return inventoryCache.get(foldServerName(server))?.get(tool) ?? null;
|
|
15067
|
+
}
|
|
15068
|
+
var BUILTIN_SOURCES = /* @__PURE__ */ new Set([
|
|
15069
|
+
"webfetch",
|
|
15070
|
+
"websearch",
|
|
15071
|
+
"web_fetch",
|
|
15072
|
+
"web_search",
|
|
15073
|
+
"fetch",
|
|
15074
|
+
"navigate",
|
|
15075
|
+
"browser_batch",
|
|
15076
|
+
"get_page_text",
|
|
15077
|
+
"read_page",
|
|
15078
|
+
"find",
|
|
15079
|
+
"read_console_messages",
|
|
15080
|
+
"read_network_requests",
|
|
15081
|
+
// Claude Code reads MCP resources through two built-ins, not through the
|
|
15082
|
+
// server's own tools: an email read as a resource is still an email.
|
|
15083
|
+
"readmcpresourcetool",
|
|
15084
|
+
"listmcpresourcestool"
|
|
15085
|
+
]);
|
|
15086
|
+
var MCP_RE = /^mcp__([^_](?:[^_]|_(?!_))*?)__(.+)$/i;
|
|
15087
|
+
function splitMcpToolName(toolName) {
|
|
15088
|
+
const m = toolName.match(MCP_RE);
|
|
15089
|
+
return m ? { server: m[1], bare: m[2] } : null;
|
|
15090
|
+
}
|
|
15091
|
+
function toolRole(toolName, policy) {
|
|
15092
|
+
const mcp = splitMcpToolName(toolName);
|
|
15093
|
+
const bare = (mcp ? mcp.bare : toolName).toLowerCase();
|
|
15094
|
+
if (mcp) {
|
|
15095
|
+
const configured = policy.roles[mcp.server]?.[mcp.bare] ?? policy.roles[mcp.server]?.[bare];
|
|
15096
|
+
if (configured === "source") return { source: true, action: false };
|
|
15097
|
+
if (configured === "action") return { source: false, action: true };
|
|
15098
|
+
if (configured === "read") return { source: false, action: false };
|
|
15099
|
+
const serverLower = mcp.server.toLowerCase();
|
|
15100
|
+
if (AGENT_INTERNAL_PREFIXES.some((p) => serverLower.startsWith(p)))
|
|
15101
|
+
return { source: false, action: false };
|
|
15102
|
+
if (BUILTIN_SOURCES.has(bare)) return { source: true, action: false };
|
|
15103
|
+
const ann = resolveToolAnnotations(mcp.server, mcp.bare);
|
|
15104
|
+
return { source: true, action: true, destructive: ann?.destructiveHint === true };
|
|
15105
|
+
}
|
|
15106
|
+
return { source: BUILTIN_SOURCES.has(bare), action: false };
|
|
15107
|
+
}
|
|
15108
|
+
function sourceWord(toolName) {
|
|
15109
|
+
const mcp = splitMcpToolName(toolName);
|
|
15110
|
+
const bare = (mcp ? mcp.bare : toolName).toLowerCase();
|
|
15111
|
+
const server = (mcp?.server ?? "").toLowerCase();
|
|
15112
|
+
if (/mail|message|inbox|thread/.test(bare) || /mail|slack|teams|chat/.test(server))
|
|
15113
|
+
return "email";
|
|
15114
|
+
if (/issue|ticket|pull|pr\b|comment/.test(bare) || /github|gitlab|jira|linear/.test(server))
|
|
15115
|
+
return "issue";
|
|
15116
|
+
if (BUILTIN_SOURCES.has(bare) || /browser|web|http|search/.test(server)) return "web page";
|
|
15117
|
+
return "tool result";
|
|
15118
|
+
}
|
|
15119
|
+
|
|
15120
|
+
// src/auth/trusted-recipients.ts
|
|
15121
|
+
init_dist();
|
|
15122
|
+
import fs19 from "fs";
|
|
15123
|
+
import path20 from "path";
|
|
15124
|
+
import os14 from "os";
|
|
15125
|
+
function getTrustedRecipientsPath() {
|
|
15126
|
+
return path20.join(os14.homedir(), ".node9", "trusted-recipients.json");
|
|
15127
|
+
}
|
|
15128
|
+
function readTrustedRecipients() {
|
|
15129
|
+
try {
|
|
15130
|
+
const raw = fs19.readFileSync(getTrustedRecipientsPath(), "utf8");
|
|
15131
|
+
const parsed = JSON.parse(raw);
|
|
15132
|
+
return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
|
|
15133
|
+
} catch {
|
|
15134
|
+
return [];
|
|
15135
|
+
}
|
|
15136
|
+
}
|
|
15137
|
+
function writeTrustedRecipients(entries) {
|
|
15138
|
+
const filePath = getTrustedRecipientsPath();
|
|
15139
|
+
fs19.mkdirSync(path20.dirname(filePath), { recursive: true });
|
|
15140
|
+
const tmp = filePath + ".node9-tmp";
|
|
15141
|
+
fs19.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
|
|
15142
|
+
fs19.renameSync(tmp, filePath);
|
|
15143
|
+
}
|
|
15144
|
+
function normalizeRecipient(raw) {
|
|
15145
|
+
const v = raw.trim();
|
|
15146
|
+
if (!v) return null;
|
|
15147
|
+
if (/^(email|host|iban):/i.test(v)) {
|
|
15148
|
+
const i = v.indexOf(":");
|
|
15149
|
+
const kind = v.slice(0, i).toLowerCase();
|
|
15150
|
+
const inner = normalizeRecipient(v.slice(i + 1));
|
|
15151
|
+
return inner && inner.startsWith(`${kind}:`) ? inner : null;
|
|
15152
|
+
}
|
|
15153
|
+
const found = extractArgDestinations({ to: v });
|
|
15154
|
+
return found.length === 1 ? found[0] : null;
|
|
15155
|
+
}
|
|
15156
|
+
function addTrustedRecipient(raw, addedBy = "user") {
|
|
15157
|
+
const value = normalizeRecipient(raw);
|
|
15158
|
+
if (!value) return null;
|
|
15159
|
+
const entries = readTrustedRecipients();
|
|
15160
|
+
if (!entries.some((e) => e.value === value)) {
|
|
15161
|
+
entries.push({ value, addedAt: Date.now(), addedBy });
|
|
15162
|
+
writeTrustedRecipients(entries);
|
|
15163
|
+
}
|
|
15164
|
+
return value;
|
|
15165
|
+
}
|
|
15166
|
+
var cache3 = null;
|
|
15167
|
+
function mtimeOf() {
|
|
15168
|
+
try {
|
|
15169
|
+
return fs19.statSync(getTrustedRecipientsPath()).mtimeMs;
|
|
15170
|
+
} catch {
|
|
15171
|
+
return 0;
|
|
15172
|
+
}
|
|
15173
|
+
}
|
|
15174
|
+
function isTrustedRecipient(value) {
|
|
15175
|
+
const mtime = mtimeOf();
|
|
15176
|
+
if (!cache3 || cache3.mtime !== mtime) {
|
|
15177
|
+
cache3 = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
|
|
15178
|
+
}
|
|
15179
|
+
return cache3.values.has(value);
|
|
15180
|
+
}
|
|
15181
|
+
|
|
15182
|
+
// src/auth/cloud.ts
|
|
15183
|
+
init_audit();
|
|
15184
|
+
import fs21 from "fs";
|
|
15185
|
+
import os15 from "os";
|
|
15186
|
+
import path21 from "path";
|
|
13903
15187
|
|
|
13904
15188
|
// src/utils/read-capped.ts
|
|
13905
|
-
import
|
|
13906
|
-
function readCapped(
|
|
15189
|
+
import fs20 from "fs";
|
|
15190
|
+
function readCapped(file2, maxBytes) {
|
|
13907
15191
|
if (maxBytes < 0) return null;
|
|
13908
15192
|
let fd;
|
|
13909
15193
|
try {
|
|
13910
|
-
fd =
|
|
15194
|
+
fd = fs20.openSync(file2, "r");
|
|
13911
15195
|
} catch {
|
|
13912
15196
|
return null;
|
|
13913
15197
|
}
|
|
13914
15198
|
try {
|
|
13915
|
-
const st =
|
|
15199
|
+
const st = fs20.fstatSync(fd);
|
|
13916
15200
|
if (!st.isFile()) return null;
|
|
13917
15201
|
const want = Math.min(st.size, maxBytes + 1);
|
|
13918
15202
|
const buf = Buffer.alloc(want);
|
|
13919
15203
|
let read = 0;
|
|
13920
15204
|
while (read < want) {
|
|
13921
|
-
const n =
|
|
15205
|
+
const n = fs20.readSync(fd, buf, read, want - read, read);
|
|
13922
15206
|
if (n <= 0) break;
|
|
13923
15207
|
read += n;
|
|
13924
15208
|
}
|
|
@@ -13930,13 +15214,13 @@ function readCapped(file, maxBytes) {
|
|
|
13930
15214
|
return null;
|
|
13931
15215
|
} finally {
|
|
13932
15216
|
try {
|
|
13933
|
-
|
|
15217
|
+
fs20.closeSync(fd);
|
|
13934
15218
|
} catch {
|
|
13935
15219
|
}
|
|
13936
15220
|
}
|
|
13937
15221
|
}
|
|
13938
|
-
function readCappedText(
|
|
13939
|
-
const r = readCapped(
|
|
15222
|
+
function readCappedText(file2, maxBytes) {
|
|
15223
|
+
const r = readCapped(file2, maxBytes);
|
|
13940
15224
|
return r ? { text: r.bytes.toString("utf8"), truncated: r.truncated } : null;
|
|
13941
15225
|
}
|
|
13942
15226
|
|
|
@@ -13948,7 +15232,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
13948
15232
|
let ciContext;
|
|
13949
15233
|
if (process.env.CI) {
|
|
13950
15234
|
try {
|
|
13951
|
-
const ciContextPath =
|
|
15235
|
+
const ciContextPath = path21.join(os15.homedir(), ".node9", "ci-context.json");
|
|
13952
15236
|
const ci = readCappedText(ciContextPath, 1e4);
|
|
13953
15237
|
if (!ci) throw new Error("ci-context.json unreadable");
|
|
13954
15238
|
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
@@ -13985,11 +15269,17 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
13985
15269
|
context: {
|
|
13986
15270
|
agent: meta?.agent,
|
|
13987
15271
|
mcpServer: meta?.mcpServer,
|
|
13988
|
-
hostname:
|
|
15272
|
+
hostname: os15.hostname(),
|
|
13989
15273
|
cwd: process.cwd(),
|
|
13990
|
-
platform:
|
|
15274
|
+
platform: os15.platform()
|
|
15275
|
+
},
|
|
15276
|
+
// Local-only fields (fw#318 exposureDestinations) stay off the wire:
|
|
15277
|
+
// the SaaS validates riskMetadata strictly and would drop it whole.
|
|
15278
|
+
...riskMetadata && {
|
|
15279
|
+
riskMetadata: Object.fromEntries(
|
|
15280
|
+
Object.entries(riskMetadata).filter(([, v]) => !Array.isArray(v))
|
|
15281
|
+
)
|
|
13991
15282
|
},
|
|
13992
|
-
...riskMetadata && { riskMetadata },
|
|
13993
15283
|
...ciContext && { ciContext },
|
|
13994
15284
|
...agentPolicy && { policy: agentPolicy },
|
|
13995
15285
|
...forceReview && { forceReview: true }
|
|
@@ -14051,14 +15341,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
14051
15341
|
});
|
|
14052
15342
|
clearTimeout(timer);
|
|
14053
15343
|
if (!res.ok) {
|
|
14054
|
-
|
|
15344
|
+
fs21.appendFileSync(
|
|
14055
15345
|
HOOK_DEBUG_LOG,
|
|
14056
15346
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
14057
15347
|
`
|
|
14058
15348
|
);
|
|
14059
15349
|
}
|
|
14060
15350
|
} catch (err) {
|
|
14061
|
-
|
|
15351
|
+
fs21.appendFileSync(
|
|
14062
15352
|
HOOK_DEBUG_LOG,
|
|
14063
15353
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
14064
15354
|
`
|
|
@@ -14069,16 +15359,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
14069
15359
|
// src/loop-detector.ts
|
|
14070
15360
|
init_dist();
|
|
14071
15361
|
init_dist();
|
|
14072
|
-
import
|
|
14073
|
-
import
|
|
14074
|
-
import
|
|
15362
|
+
import fs22 from "fs";
|
|
15363
|
+
import path22 from "path";
|
|
15364
|
+
import os16 from "os";
|
|
14075
15365
|
function loopStateFile() {
|
|
14076
|
-
return
|
|
15366
|
+
return path22.join(os16.homedir(), ".node9", "loop-state.json");
|
|
14077
15367
|
}
|
|
14078
15368
|
function readState() {
|
|
14079
15369
|
try {
|
|
14080
|
-
if (!
|
|
14081
|
-
const raw =
|
|
15370
|
+
if (!fs22.existsSync(loopStateFile())) return [];
|
|
15371
|
+
const raw = fs22.readFileSync(loopStateFile(), "utf-8");
|
|
14082
15372
|
const parsed = JSON.parse(raw);
|
|
14083
15373
|
if (!Array.isArray(parsed)) return [];
|
|
14084
15374
|
return parsed;
|
|
@@ -14087,11 +15377,11 @@ function readState() {
|
|
|
14087
15377
|
}
|
|
14088
15378
|
}
|
|
14089
15379
|
function writeState(records) {
|
|
14090
|
-
const
|
|
14091
|
-
if (!
|
|
14092
|
-
const tmpPath = `${loopStateFile()}.${
|
|
14093
|
-
|
|
14094
|
-
|
|
15380
|
+
const dir2 = path22.dirname(loopStateFile());
|
|
15381
|
+
if (!fs22.existsSync(dir2)) fs22.mkdirSync(dir2, { recursive: true });
|
|
15382
|
+
const tmpPath = `${loopStateFile()}.${os16.hostname()}.${process.pid}.tmp`;
|
|
15383
|
+
fs22.writeFileSync(tmpPath, JSON.stringify(records));
|
|
15384
|
+
fs22.renameSync(tmpPath, loopStateFile());
|
|
14095
15385
|
}
|
|
14096
15386
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
14097
15387
|
try {
|
|
@@ -14106,20 +15396,20 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
14106
15396
|
// src/supply-chain/check.ts
|
|
14107
15397
|
init_dist();
|
|
14108
15398
|
import picomatch2 from "picomatch";
|
|
14109
|
-
import
|
|
15399
|
+
import path25 from "path";
|
|
14110
15400
|
|
|
14111
15401
|
// src/supply-chain/osv-index.ts
|
|
14112
15402
|
init_dist();
|
|
14113
|
-
import
|
|
14114
|
-
import
|
|
14115
|
-
import
|
|
15403
|
+
import fs23 from "fs";
|
|
15404
|
+
import os17 from "os";
|
|
15405
|
+
import path23 from "path";
|
|
14116
15406
|
import { createHash as createHash3 } from "crypto";
|
|
14117
15407
|
var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
|
|
14118
15408
|
function osvRoot() {
|
|
14119
|
-
return
|
|
15409
|
+
return path23.join(os17.homedir(), ".node9", "osv");
|
|
14120
15410
|
}
|
|
14121
15411
|
function ecoDir(eco) {
|
|
14122
|
-
return
|
|
15412
|
+
return path23.join(osvRoot(), eco);
|
|
14123
15413
|
}
|
|
14124
15414
|
function indexKey(eco, name) {
|
|
14125
15415
|
return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
|
|
@@ -14128,11 +15418,11 @@ function shardOf(key) {
|
|
|
14128
15418
|
return createHash3("sha1").update(key).digest("hex").slice(0, 2);
|
|
14129
15419
|
}
|
|
14130
15420
|
function shardPath(eco, shard) {
|
|
14131
|
-
return
|
|
15421
|
+
return path23.join(ecoDir(eco), "shards", `${shard}.json`);
|
|
14132
15422
|
}
|
|
14133
15423
|
function readMeta(eco) {
|
|
14134
15424
|
try {
|
|
14135
|
-
const m = JSON.parse(
|
|
15425
|
+
const m = JSON.parse(fs23.readFileSync(path23.join(ecoDir(eco), "meta.json"), "utf8"));
|
|
14136
15426
|
if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
|
|
14137
15427
|
return m;
|
|
14138
15428
|
} catch {
|
|
@@ -14141,7 +15431,7 @@ function readMeta(eco) {
|
|
|
14141
15431
|
}
|
|
14142
15432
|
function readShard(eco, shard) {
|
|
14143
15433
|
try {
|
|
14144
|
-
const s = JSON.parse(
|
|
15434
|
+
const s = JSON.parse(fs23.readFileSync(shardPath(eco, shard), "utf8"));
|
|
14145
15435
|
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
14146
15436
|
} catch {
|
|
14147
15437
|
return {};
|
|
@@ -14338,19 +15628,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
|
14338
15628
|
}
|
|
14339
15629
|
|
|
14340
15630
|
// src/supply-chain/local-resolve.ts
|
|
14341
|
-
import
|
|
14342
|
-
import
|
|
15631
|
+
import fs24 from "fs";
|
|
15632
|
+
import path24 from "path";
|
|
14343
15633
|
var MAX_LEVELS = 32;
|
|
14344
15634
|
var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
|
|
14345
15635
|
function resolveInstalled(name, cwd) {
|
|
14346
|
-
if (!cwd || !
|
|
15636
|
+
if (!cwd || !path24.isAbsolute(cwd)) return null;
|
|
14347
15637
|
if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
|
|
14348
|
-
let
|
|
15638
|
+
let dir2 = path24.resolve(cwd);
|
|
14349
15639
|
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
14350
|
-
const pkgDir =
|
|
14351
|
-
const manifest =
|
|
15640
|
+
const pkgDir = path24.join(dir2, "node_modules", ...name.split("/"));
|
|
15641
|
+
const manifest = path24.join(pkgDir, "package.json");
|
|
14352
15642
|
try {
|
|
14353
|
-
const raw =
|
|
15643
|
+
const raw = fs24.readFileSync(manifest, "utf8");
|
|
14354
15644
|
const parsed = JSON.parse(raw);
|
|
14355
15645
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
14356
15646
|
return { version: parsed.version, dir: pkgDir };
|
|
@@ -14358,9 +15648,9 @@ function resolveInstalled(name, cwd) {
|
|
|
14358
15648
|
return null;
|
|
14359
15649
|
} catch {
|
|
14360
15650
|
}
|
|
14361
|
-
const parent =
|
|
14362
|
-
if (parent ===
|
|
14363
|
-
|
|
15651
|
+
const parent = path24.dirname(dir2);
|
|
15652
|
+
if (parent === dir2) break;
|
|
15653
|
+
dir2 = parent;
|
|
14364
15654
|
}
|
|
14365
15655
|
return null;
|
|
14366
15656
|
}
|
|
@@ -14455,7 +15745,7 @@ function checkInstalled(p, version, misses) {
|
|
|
14455
15745
|
}
|
|
14456
15746
|
async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
14457
15747
|
if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
|
|
14458
|
-
const localCwd = cwd &&
|
|
15748
|
+
const localCwd = cwd && path25.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path25.resolve(dir2, step), cwd) : void 0;
|
|
14459
15749
|
const installed = resolveInstalled(p.name, localCwd);
|
|
14460
15750
|
if (installed && (!p.version || p.version === installed.version))
|
|
14461
15751
|
return checkInstalled(p, installed.version, misses);
|
|
@@ -14590,6 +15880,9 @@ function isWriteTool(toolName) {
|
|
|
14590
15880
|
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
14591
15881
|
return WRITE_TOOLS.has(t);
|
|
14592
15882
|
}
|
|
15883
|
+
function isMcpToolName(toolName) {
|
|
15884
|
+
return /^mcp__[^_](?:[^_]|_(?!_))*?__/i.test(toolName);
|
|
15885
|
+
}
|
|
14593
15886
|
function extractFilePaths(toolName, args, shellCommand) {
|
|
14594
15887
|
const paths = [];
|
|
14595
15888
|
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
@@ -14823,13 +16116,165 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
14823
16116
|
}
|
|
14824
16117
|
}
|
|
14825
16118
|
}
|
|
14826
|
-
|
|
16119
|
+
const mcpHookTool = options?.sessionTaintGated === void 0 && isMcpToolName(toolName);
|
|
16120
|
+
let daemonSaysExposed;
|
|
16121
|
+
if (!taintWarning && meta?.sessionId && (networkShell || isWriteTool(toolName) || options?.sessionTaintGated === true || mcpHookTool)) {
|
|
14827
16122
|
const sessionTaint = await checkSessionTaint(meta.sessionId);
|
|
16123
|
+
if (typeof sessionTaint.exposed === "boolean") daemonSaysExposed = sessionTaint.exposed;
|
|
14828
16124
|
if (sessionTaint.tainted && sessionTaint.record) {
|
|
14829
|
-
const kind = isWriteTool(toolName) ? "write" : networkShell ? "network" : "state-changing";
|
|
16125
|
+
const kind = isWriteTool(toolName) ? "write" : networkShell ? "network" : mcpHookTool ? "MCP" : "state-changing";
|
|
14830
16126
|
taintWarning = `\u26A0\uFE0F node9 flagged this session \u2014 earlier tool output contained ${sessionTaint.record.source}. Approve this ${kind} action before it proceeds.`;
|
|
14831
16127
|
}
|
|
14832
16128
|
}
|
|
16129
|
+
let exposureReview = null;
|
|
16130
|
+
let exposureKeys = [];
|
|
16131
|
+
let exposureDestinations = [];
|
|
16132
|
+
const ucPolicy = config.policy.untrustedContent;
|
|
16133
|
+
if (ucPolicy?.enabled && !taintWarning && meta?.sessionId && !isManual) {
|
|
16134
|
+
const role = toolRole(toolName, ucPolicy);
|
|
16135
|
+
const acts = role.action || options?.sessionTaintGated === true;
|
|
16136
|
+
const shellExp = shellInput.kind === "shell" ? classifyShellExposure(shellInput.command) : null;
|
|
16137
|
+
const bareName = splitMcpToolName(toolName)?.bare;
|
|
16138
|
+
const mcpFileWrite = bareName !== void 0 && fileToolOperation(bareName, args) === "write";
|
|
16139
|
+
const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, config.policy.toolInspection).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
|
|
16140
|
+
const sendsSomething = acts && !mcpFileWrite || !!shellExp?.sends;
|
|
16141
|
+
const mayBeExposed = daemonSaysExposed ?? (maybeSessionExposed(meta.sessionId) || writes.length > 0 && judgeWritesAfterExposure({
|
|
16142
|
+
accesses: writes,
|
|
16143
|
+
args,
|
|
16144
|
+
shellCommand: shellInput.kind === "shell" ? shellInput.command : null,
|
|
16145
|
+
cwd: options?.cwd,
|
|
16146
|
+
inContent: /* @__PURE__ */ new Set(),
|
|
16147
|
+
isKnown: () => true
|
|
16148
|
+
}).length > 0);
|
|
16149
|
+
const judged = sendsSomething || (!!shellExp?.fetches || writes.length > 0) && mayBeExposed;
|
|
16150
|
+
if (judged) {
|
|
16151
|
+
const exposure = await checkSessionExposure(meta.sessionId);
|
|
16152
|
+
if (exposure.exposed && exposure.record) {
|
|
16153
|
+
const rec = exposure.record;
|
|
16154
|
+
const dests = new Set(
|
|
16155
|
+
acts && !shellExp && !mcpFileWrite ? extractArgDestinations(args) : []
|
|
16156
|
+
);
|
|
16157
|
+
const fetchOnly = /* @__PURE__ */ new Set();
|
|
16158
|
+
const projectHosts = shellExp ? knownProjectHosts(options?.cwd) : null;
|
|
16159
|
+
let unknownRemote = false;
|
|
16160
|
+
if (shellExp) {
|
|
16161
|
+
if (shellExp.sends) {
|
|
16162
|
+
for (const d of shellExp.destinations) dests.add(d);
|
|
16163
|
+
for (const name of shellExp.gitRemotes) {
|
|
16164
|
+
const remotes = projectHosts.remotes;
|
|
16165
|
+
const hosts = name === "" ? remotes.get("origin") ?? [...remotes.values()].flat() : remotes.get(name);
|
|
16166
|
+
if (hosts) for (const h of hosts) dests.add(h);
|
|
16167
|
+
else if (projectHosts.remotes.size > 0) unknownRemote = true;
|
|
16168
|
+
}
|
|
16169
|
+
}
|
|
16170
|
+
if (shellExp.fetches) {
|
|
16171
|
+
for (const h of shellExp.fetchDestinations) if (!dests.has(h)) fetchOnly.add(h);
|
|
16172
|
+
}
|
|
16173
|
+
}
|
|
16174
|
+
const inContent = new Set(rec.destinations);
|
|
16175
|
+
const mentioned = new Set(rec.userMentioned);
|
|
16176
|
+
const approved = new Set(rec.approved);
|
|
16177
|
+
const held = [];
|
|
16178
|
+
const logged = [];
|
|
16179
|
+
const writeProjectHosts = projectHosts ?? knownProjectHosts(options?.cwd);
|
|
16180
|
+
const isKnown = (d) => {
|
|
16181
|
+
const domain = destinationDomain(d);
|
|
16182
|
+
const value = destinationValue(d);
|
|
16183
|
+
return domain !== null && ucPolicy.orgDomains.includes(domain) || mentioned.has(d) || isTrustedRecipient(d) || destinationKind(d) === "host" && (isLoopbackHost(value) || isTrustedHost(value) || config.policy.trustedHostsManaged && matchesTrustedHost(value, config.policy.trustedHosts) || config.policy.egress.allow.some((pat) => hostMatches(value, pat)) || writeProjectHosts.hosts.has(d));
|
|
16184
|
+
};
|
|
16185
|
+
const approvedOnce = (key) => ucPolicy.afterExposure === "ask-once" && approved.has(key);
|
|
16186
|
+
const repos = (shellExp?.sends ? shellExp.repos : []).filter(
|
|
16187
|
+
(r) => writeProjectHosts.hosts.has(`host:${r.split("/")[0]}`)
|
|
16188
|
+
);
|
|
16189
|
+
const repoHosts = new Set(repos.map((r) => `host:${r.split("/")[0]}`));
|
|
16190
|
+
for (const r of repos) {
|
|
16191
|
+
if (writeProjectHosts.repos.has(r) || approvedOnce(`${toolName}|repo:${r}`))
|
|
16192
|
+
logged.push(`repo:${r}`);
|
|
16193
|
+
else held.push({ dest: "", why: "repository", repo: r });
|
|
16194
|
+
}
|
|
16195
|
+
for (const d of dests) {
|
|
16196
|
+
if (repoHosts.has(d)) continue;
|
|
16197
|
+
if (approvedOnce(`${toolName}|${d}`) || isKnown(d)) logged.push(d);
|
|
16198
|
+
else if (inContent.has(d)) held.push({ dest: d, why: "appeared in what was read" });
|
|
16199
|
+
else held.push({ dest: d, why: "not known to this session" });
|
|
16200
|
+
}
|
|
16201
|
+
for (const d of fetchOnly) {
|
|
16202
|
+
if (approvedOnce(`${toolName}|${d}`) || isKnown(d) || !inContent.has(d)) logged.push(d);
|
|
16203
|
+
else held.push({ dest: d, why: "appeared in what was read" });
|
|
16204
|
+
}
|
|
16205
|
+
if (shellExp?.sends && (shellExp.unresolved || unknownRemote || shellExp.destinations.length === 0 && shellExp.gitRemotes.length === 0) && !approvedOnce(`${toolName}|unresolved`)) {
|
|
16206
|
+
held.push({ dest: "", why: "unresolved" });
|
|
16207
|
+
}
|
|
16208
|
+
const bare = splitMcpToolName(toolName)?.bare ?? toolName;
|
|
16209
|
+
if (dests.size === 0 && acts && !mcpFileWrite && (role.destructive === true || isDestructiveToolName(bare))) {
|
|
16210
|
+
held.push({ dest: "", why: "destructive" });
|
|
16211
|
+
}
|
|
16212
|
+
const writeHolds = writes.length > 0 ? judgeWritesAfterExposure({
|
|
16213
|
+
accesses: writes,
|
|
16214
|
+
args,
|
|
16215
|
+
shellCommand: shellInput.kind === "shell" ? shellInput.command : null,
|
|
16216
|
+
cwd: options?.cwd,
|
|
16217
|
+
inContent,
|
|
16218
|
+
isKnown
|
|
16219
|
+
}).filter((h) => {
|
|
16220
|
+
const key = `${toolName}|${h.why === "content-destination" ? h.destination : `path:${h.path}`}`;
|
|
16221
|
+
if (ucPolicy.afterExposure === "ask-once" && approved.has(key)) {
|
|
16222
|
+
logged.push(key);
|
|
16223
|
+
return false;
|
|
16224
|
+
}
|
|
16225
|
+
return true;
|
|
16226
|
+
}) : [];
|
|
16227
|
+
const last = rec.sources[rec.sources.length - 1];
|
|
16228
|
+
const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
|
|
16229
|
+
const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
|
|
16230
|
+
const read = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
|
|
16231
|
+
if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
|
|
16232
|
+
const noun = (d) => {
|
|
16233
|
+
const k = destinationKind(d);
|
|
16234
|
+
return k === "email" ? "an address" : k === "host" ? "a host" : "an account";
|
|
16235
|
+
};
|
|
16236
|
+
const parts = held.map(
|
|
16237
|
+
(h) => h.dest ? `sends to ${destinationValue(h.dest)}, ${h.why === "appeared in what was read" ? `${noun(h.dest)} that appeared in that ${last ? readWord : "content"}` : `${noun(h.dest)} this session did not know`}` : h.why === "repository" ? h.repo.endsWith("/*") ? `sends to a repository on ${h.repo.slice(0, -2)} it does not name (a gist, a new repository or an API call)` : `sends to ${h.repo}, a repository that is not one of this project's remotes` : h.why === "unresolved" ? "sends to a destination this session cannot see (built at run time, or a remote of another repository)" : `is ${bare}, a destructive call, with no known destination`
|
|
16238
|
+
);
|
|
16239
|
+
const home = os18.homedir();
|
|
16240
|
+
const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
16241
|
+
for (const w of writeHolds) {
|
|
16242
|
+
parts.push(
|
|
16243
|
+
w.why === "outside-project" ? `writes outside the project, to ${short(w.path)}` : w.why === "agent-instruction" ? `writes to ${short(w.path)}, a file that runs code or instructs the agent` : `writes content carrying ${destinationValue(w.destination ?? "")}, ${noun(w.destination ?? "host:x")} that appeared in that ${readWord}`
|
|
16244
|
+
);
|
|
16245
|
+
}
|
|
16246
|
+
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
|
|
16247
|
+
exposureKeys = [
|
|
16248
|
+
...held.filter((h) => h.dest || h.why === "unresolved" || h.why === "repository").map(
|
|
16249
|
+
(h) => h.why === "repository" ? `${toolName}|repo:${h.repo}` : `${toolName}|${h.dest || "unresolved"}`
|
|
16250
|
+
),
|
|
16251
|
+
...writeHolds.map(
|
|
16252
|
+
(w) => `${toolName}|${w.why === "content-destination" ? w.destination : `path:${w.path}`}`
|
|
16253
|
+
)
|
|
16254
|
+
];
|
|
16255
|
+
exposureDestinations = held.filter((h) => h.dest).map((h) => h.dest).slice(0, 10);
|
|
16256
|
+
if (exposureDestinations.length > 0 && options?.deferReview) {
|
|
16257
|
+
exposureReview += ` To trust ${exposureDestinations.length === 1 ? "this address" : "these addresses"} for good, the user runs: ` + exposureDestinations.map((d) => `node9 trust recipient add ${destinationValue(d)}`).join("; ") + ".";
|
|
16258
|
+
}
|
|
16259
|
+
} else if (held.length > 0 || writeHolds.length > 0 || logged.length > 0) {
|
|
16260
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
16261
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16262
|
+
event: "exposure-logged",
|
|
16263
|
+
tool: toolName,
|
|
16264
|
+
held: held.length + writeHolds.length,
|
|
16265
|
+
known: logged.length,
|
|
16266
|
+
sessionId: meta.sessionId
|
|
16267
|
+
});
|
|
16268
|
+
}
|
|
16269
|
+
} else if (exposure.daemonUnavailable) {
|
|
16270
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
16271
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16272
|
+
event: "exposure-unavailable",
|
|
16273
|
+
tool: toolName
|
|
16274
|
+
});
|
|
16275
|
+
}
|
|
16276
|
+
}
|
|
16277
|
+
}
|
|
14833
16278
|
const canaryVals = safeCanaryValues();
|
|
14834
16279
|
if (canaryVals.length > 0) {
|
|
14835
16280
|
const canaryHit = matchCanaryArgs(args, canaryVals);
|
|
@@ -15070,7 +16515,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
15070
16515
|
}
|
|
15071
16516
|
let appPermReview = null;
|
|
15072
16517
|
let appPermReviewTool = null;
|
|
15073
|
-
const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!packageReview;
|
|
16518
|
+
const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!exposureReview || !!packageReview;
|
|
15074
16519
|
const standingTrustApplies = () => !callSpecificReview() && !hardBlockDowngraded;
|
|
15075
16520
|
const reviewDemanded = () => callSpecificReview() || localSmartRuleMatched === true || options?.localSmartRuleMatched === true;
|
|
15076
16521
|
if (meta?.serverKey) {
|
|
@@ -15315,6 +16760,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
15315
16760
|
appPermReview ? `${taintWarning}
|
|
15316
16761
|
${appPermReview}` : taintWarning
|
|
15317
16762
|
);
|
|
16763
|
+
} else if (exposureReview) {
|
|
16764
|
+
explainableLabel = EXPOSURE_LABEL;
|
|
16765
|
+
riskMetadata = computeRiskMetadata(args, 6, explainableLabel, void 0, void 0, "exposure");
|
|
16766
|
+
riskMetadata.ruleDescription = (appPermReview ? `${exposureReview}
|
|
16767
|
+
${appPermReview}` : exposureReview).slice(0, 1024);
|
|
16768
|
+
if (exposureDestinations.length > 0) riskMetadata.exposureDestinations = exposureDestinations;
|
|
15318
16769
|
} else if (appPermReview) {
|
|
15319
16770
|
if (dlpReviewFlagged) {
|
|
15320
16771
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review) + \u{1F512} App Permission (Review)";
|
|
@@ -15357,7 +16808,8 @@ ${packageReview}`;
|
|
|
15357
16808
|
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
15358
16809
|
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
15359
16810
|
// deferReview and serverKey never co-occur in production — see above.)
|
|
15360
|
-
reason: taintWarning || packageReview || explainableLabel || "Node9 flagged this action for review.",
|
|
16811
|
+
reason: taintWarning || packageReview || exposureReview || explainableLabel || "Node9 flagged this action for review.",
|
|
16812
|
+
...exposureReview && exposureKeys.length > 0 ? { exposureKeys } : {},
|
|
15361
16813
|
ruleDescription: policyRuleDescription,
|
|
15362
16814
|
blockedByLabel: explainableLabel
|
|
15363
16815
|
};
|
|
@@ -15451,10 +16903,14 @@ ${packageReview}`;
|
|
|
15451
16903
|
// auto-allows. This process (the long-running gateway — appPermReview
|
|
15452
16904
|
// only ever happens here) stays alive to run its own racers, so the
|
|
15453
16905
|
// daemon just holds the card and waits for the human decision.
|
|
15454
|
-
|
|
16906
|
+
// fw#318 P1.1: the same for every call-specific review (exposure,
|
|
16907
|
+
// session taint, DLP, file review): the re-run carries no session id
|
|
16908
|
+
// and cannot see these reasons, so it must never answer the card.
|
|
16909
|
+
appPermReview || callSpecificReview() ? true : void 0,
|
|
15455
16910
|
void 0,
|
|
15456
16911
|
localSmartRuleMatched || options?.localSmartRuleMatched,
|
|
15457
|
-
options?.socketActivitySent
|
|
16912
|
+
options?.socketActivitySent,
|
|
16913
|
+
meta?.sessionId
|
|
15458
16914
|
);
|
|
15459
16915
|
daemonEntryId = entry.id;
|
|
15460
16916
|
daemonAllowCount = entry.allowCount;
|
|
@@ -15495,8 +16951,15 @@ ${packageReview}`;
|
|
|
15495
16951
|
policyMatchedField,
|
|
15496
16952
|
policyMatchedWord,
|
|
15497
16953
|
daemonAllowCount,
|
|
15498
|
-
riskMetadata?.ruleDescription
|
|
16954
|
+
riskMetadata?.ruleDescription,
|
|
16955
|
+
exposureDestinations
|
|
15499
16956
|
);
|
|
16957
|
+
if (decision === "trust_recipients") {
|
|
16958
|
+
if (config.policySource !== "workspace") {
|
|
16959
|
+
for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
|
|
16960
|
+
}
|
|
16961
|
+
return { approved: true, checkedBy: "daemon", decisionSource: "native" };
|
|
16962
|
+
}
|
|
15500
16963
|
if (decision === "always_allow") {
|
|
15501
16964
|
if (standingTrustApplies()) {
|
|
15502
16965
|
writeTrustSession(toolName, 36e5, args);
|
|
@@ -15597,6 +17060,9 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
15597
17060
|
});
|
|
15598
17061
|
}
|
|
15599
17062
|
});
|
|
17063
|
+
if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
|
|
17064
|
+
await approveSessionExposure(meta.sessionId, exposureKeys);
|
|
17065
|
+
}
|
|
15600
17066
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
15601
17067
|
await resolveNode9SaaS(
|
|
15602
17068
|
cloudRequestId,
|