@node9/proxy 2.33.2 → 2.35.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/index.mjs CHANGED
@@ -1173,6 +1173,120 @@ function expandPlainParam(p) {
1173
1173
  if (recorded !== void 0) return recorded;
1174
1174
  return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
1175
1175
  }
1176
+ function functionBodies(f) {
1177
+ const out = /* @__PURE__ */ new Map();
1178
+ syntax.Walk(f, (node) => {
1179
+ const n = node;
1180
+ if (n && syntax.NodeType(n) === "FuncDecl" && n.Name?.Value && n.Body) {
1181
+ const list = out.get(n.Name.Value) ?? [];
1182
+ list.push(n.Body);
1183
+ out.set(n.Name.Value, list);
1184
+ }
1185
+ return true;
1186
+ });
1187
+ return out;
1188
+ }
1189
+ function rebindsPositionals(body) {
1190
+ let found = false;
1191
+ syntax.Walk(body, (node) => {
1192
+ const n = node;
1193
+ if (found || !n) return false;
1194
+ if (syntax.NodeType(n) === "CallExpr") {
1195
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1196
+ if (words[0] === "shift") found = true;
1197
+ else if (words[0] === "set" && words.slice(1).some((w) => w === null || w === "--" || !/^[-+]/.test(w)))
1198
+ found = true;
1199
+ }
1200
+ return !found;
1201
+ });
1202
+ return found;
1203
+ }
1204
+ function tableBindings() {
1205
+ if (!assignmentTable) return "";
1206
+ const out = [];
1207
+ for (const [name, rec] of assignmentTable)
1208
+ if (rec.at < currentStmtOffset) out.push(`${name}=${rec.value ?? "\0"}`);
1209
+ return out.sort().join("");
1210
+ }
1211
+ function walkWithCalls(f, visit) {
1212
+ const bodies = functionBodies(f);
1213
+ if (bodies.size === 0) {
1214
+ syntax.Walk(f, visit);
1215
+ return;
1216
+ }
1217
+ const seenCalls = /* @__PURE__ */ new Set();
1218
+ const inline = (call) => {
1219
+ const words = (call.Args || []).map((a) => resolveWordLiteral(a));
1220
+ const list = words[0] ? bodies.get(words[0]) : void 0;
1221
+ if (!list || callDepth >= MAX_CALL_DEPTH) return;
1222
+ const args = words.slice(1);
1223
+ const key = `${words[0]}\0${JSON.stringify(args)}\0${tableBindings()}`;
1224
+ if (seenCalls.has(key)) return;
1225
+ seenCalls.add(key);
1226
+ const outerTable = assignmentTable;
1227
+ const outerOffset = currentStmtOffset;
1228
+ for (const body of list) {
1229
+ if (callWalks >= MAX_CALL_WALKS) return;
1230
+ callWalks++;
1231
+ const table = /* @__PURE__ */ new Map();
1232
+ for (const [name, rec] of outerTable ?? [])
1233
+ if (rec.at < outerOffset) table.set(name, { value: rec.value, at: -1 });
1234
+ for (const name of ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"])
1235
+ table.delete(name);
1236
+ if (!rebindsPositionals(body)) {
1237
+ args.slice(0, 9).forEach((value, i) => table.set(String(i + 1), { value, at: -1 }));
1238
+ if (args.length === 1) {
1239
+ table.set("@", { value: args[0], at: -1 });
1240
+ table.set("*", { value: args[0], at: -1 });
1241
+ }
1242
+ table.set("#", { value: String(args.length), at: -1 });
1243
+ }
1244
+ assignmentTable = table;
1245
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1246
+ callDepth++;
1247
+ try {
1248
+ if (body.Cmd && syntax.NodeType(body.Cmd) === "Block") recordTopLevelAssignments(body.Cmd);
1249
+ syntax.Walk(body, step);
1250
+ } finally {
1251
+ callDepth--;
1252
+ assignmentTable = outerTable;
1253
+ currentStmtOffset = outerOffset;
1254
+ }
1255
+ }
1256
+ };
1257
+ const step = (node) => {
1258
+ if (!node) return false;
1259
+ const go = visit(node);
1260
+ if (go !== false && syntax.NodeType(node) === "CallExpr") inline(node);
1261
+ return go;
1262
+ };
1263
+ syntax.Walk(f, step);
1264
+ }
1265
+ function withAssignments(f, body) {
1266
+ const outerTable = assignmentTable;
1267
+ const outerOffset = currentStmtOffset;
1268
+ const outerWalks = callWalks;
1269
+ callWalks = 0;
1270
+ assignmentTable = new Map(
1271
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
1272
+ );
1273
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1274
+ try {
1275
+ recordTopLevelAssignments(f);
1276
+ return body();
1277
+ } finally {
1278
+ assignmentTable = outerTable;
1279
+ currentStmtOffset = outerOffset;
1280
+ callWalks = outerWalks;
1281
+ }
1282
+ }
1283
+ function trackStatement(n) {
1284
+ try {
1285
+ currentStmtOffset = n.Pos().Offset();
1286
+ } catch {
1287
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1288
+ }
1289
+ }
1176
1290
  function resolveWordLiteral(w) {
1177
1291
  const parts = w?.Parts || [];
1178
1292
  let s = "";
@@ -1293,27 +1407,31 @@ function extractShellDestinations(command) {
1293
1407
  const out = [];
1294
1408
  const seen = /* @__PURE__ */ new Set();
1295
1409
  try {
1296
- syntax.Walk(f, (node) => {
1297
- if (!node) return false;
1298
- const n = node;
1299
- if (syntax.NodeType(n) !== "CallExpr") return true;
1300
- const callArgs = n.Args || [];
1301
- if (callArgs.length === 0) return true;
1302
- const words = callArgs.map((a) => resolveWordLiteral(a));
1303
- const at = unwrapCommandHead(words);
1304
- const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1305
- if (!NET_BINARIES.has(name)) return true;
1306
- const rest = words.slice(at + 1);
1307
- for (const raw of destTokensForBinary(name, rest)) {
1308
- const host2 = parseDestHost(raw);
1309
- if (!host2) continue;
1310
- const key = `${name}:${host2}`;
1311
- if (seen.has(key)) continue;
1312
- seen.add(key);
1313
- out.push({ host: host2, binary: name, raw });
1314
- }
1315
- return true;
1316
- });
1410
+ withAssignments(
1411
+ f,
1412
+ () => walkWithCalls(f, (node) => {
1413
+ if (!node) return false;
1414
+ const n = node;
1415
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1416
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1417
+ const callArgs = n.Args || [];
1418
+ if (callArgs.length === 0) return true;
1419
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1420
+ const at = unwrapCommandHead(words);
1421
+ const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1422
+ if (!NET_BINARIES.has(name)) return true;
1423
+ const rest = words.slice(at + 1);
1424
+ for (const raw of destTokensForBinary(name, rest)) {
1425
+ const host2 = parseDestHost(raw);
1426
+ if (!host2) continue;
1427
+ const key = `${name}:${host2}`;
1428
+ if (seen.has(key)) continue;
1429
+ seen.add(key);
1430
+ out.push({ host: host2, binary: name, raw });
1431
+ }
1432
+ return true;
1433
+ })
1434
+ );
1317
1435
  } catch {
1318
1436
  return out;
1319
1437
  }
@@ -1325,39 +1443,45 @@ function extractShellDestTokens(command) {
1325
1443
  const out = [];
1326
1444
  const seen = /* @__PURE__ */ new Set();
1327
1445
  try {
1328
- syntax.Walk(f, (node) => {
1329
- if (!node) return false;
1330
- const n = node;
1331
- if (syntax.NodeType(n) !== "CallExpr") return true;
1332
- const callArgs = n.Args || [];
1333
- if (callArgs.length === 0) return true;
1334
- const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1335
- if (!NET_BINARIES.has(name)) return true;
1336
- const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1337
- for (const raw of destTokensForBinary(name, rest)) {
1338
- if (!raw) continue;
1339
- let tok = raw.trim();
1340
- const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1341
- const hasScheme = scheme !== null;
1342
- if (hasScheme) tok = tok.slice(scheme[0].length);
1343
- tok = tok.split(/[/?#]/)[0];
1344
- const at = tok.lastIndexOf("@");
1345
- if (at >= 0) tok = tok.slice(at + 1);
1346
- if (tok.startsWith("[")) {
1347
- const close = tok.indexOf("]");
1348
- if (close > 0) tok = tok.slice(0, close + 1);
1349
- } else {
1350
- tok = tok.split(":")[0];
1446
+ withAssignments(
1447
+ f,
1448
+ () => walkWithCalls(f, (node) => {
1449
+ if (!node) return false;
1450
+ const n = node;
1451
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1452
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1453
+ const callArgs = n.Args || [];
1454
+ if (callArgs.length === 0) return true;
1455
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1456
+ const head = unwrapCommandHead(words);
1457
+ const name = (words[head] || "").toLowerCase().split("/").pop() ?? "";
1458
+ if (!NET_BINARIES.has(name)) return true;
1459
+ const rest = words.slice(head + 1);
1460
+ for (const raw of destTokensForBinary(name, rest)) {
1461
+ if (!raw) continue;
1462
+ let tok = raw.trim();
1463
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1464
+ const hasScheme = scheme !== null;
1465
+ if (hasScheme) tok = tok.slice(scheme[0].length);
1466
+ tok = tok.split(/[/?#]/)[0];
1467
+ const at = tok.lastIndexOf("@");
1468
+ if (at >= 0) tok = tok.slice(at + 1);
1469
+ if (tok.startsWith("[")) {
1470
+ const close = tok.indexOf("]");
1471
+ if (close > 0) tok = tok.slice(0, close + 1);
1472
+ } else {
1473
+ tok = tok.split(":")[0];
1474
+ }
1475
+ if (!tok) continue;
1476
+ if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1477
+ const key = `${name}:${tok}`;
1478
+ if (seen.has(key)) continue;
1479
+ seen.add(key);
1480
+ out.push({ token: tok, binary: name });
1351
1481
  }
1352
- if (!tok) continue;
1353
- if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1354
- const key = `${name}:${tok}`;
1355
- if (seen.has(key)) continue;
1356
- seen.add(key);
1357
- out.push({ token: tok, binary: name });
1358
- }
1359
- return true;
1360
- });
1482
+ return true;
1483
+ })
1484
+ );
1361
1485
  } catch {
1362
1486
  return out;
1363
1487
  }
@@ -1476,6 +1600,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1476
1600
  if (depth === 0) {
1477
1601
  seenPayloads = /* @__PURE__ */ new Set();
1478
1602
  payloadBudget = PAYLOAD_BUDGET;
1603
+ if (callDepth === 0) callWalks = 0;
1479
1604
  }
1480
1605
  assignmentTable = new Map(
1481
1606
  [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
@@ -1483,7 +1608,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1483
1608
  currentStmtOffset = Number.MAX_SAFE_INTEGER;
1484
1609
  try {
1485
1610
  recordTopLevelAssignments(f);
1486
- syntax.Walk(f, (node) => {
1611
+ walkWithCalls(f, (node) => {
1487
1612
  if (!node || result?.verdict === "block") return false;
1488
1613
  const n = node;
1489
1614
  const nodeType = syntax.NodeType(n);
@@ -2272,6 +2397,19 @@ function runnablePrefix(command) {
2272
2397
  }
2273
2398
  return null;
2274
2399
  }
2400
+ function uploadedFile(tool, flag, value) {
2401
+ const v = value.value;
2402
+ const file2 = (f) => f && f !== "-" && f !== "." ? { value: f } : null;
2403
+ if (tool === "curl") {
2404
+ if (["-d", "--data", "--data-ascii", "--data-binary", "--json"].includes(flag))
2405
+ return v.startsWith("@") ? file2(v.slice(1)) : null;
2406
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(v)?.[1]);
2407
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(v)?.[1]);
2408
+ if (flag === "-T" || flag === "--upload-file") return file2(v);
2409
+ }
2410
+ if (tool === "wget" && ["--post-file", "--body-file"].includes(flag)) return file2(v);
2411
+ return null;
2412
+ }
2275
2413
  function merge(a, b) {
2276
2414
  a.cwd = unique([...a.cwd, ...b.cwd]);
2277
2415
  for (const key of /* @__PURE__ */ new Set([...a.vars.keys(), ...b.vars.keys()])) {
@@ -2345,6 +2483,9 @@ function wordValues(word, state) {
2345
2483
  function collectShellFileAccesses(command, context = {}) {
2346
2484
  const out = [];
2347
2485
  let payloads = 256;
2486
+ const functions = /* @__PURE__ */ new Map();
2487
+ const seenCalls = /* @__PURE__ */ new Set();
2488
+ let callWalks2 = MAX_CALL_WALKS2;
2348
2489
  const initial = {
2349
2490
  cwd: [context.cwd ?? ""],
2350
2491
  vars: /* @__PURE__ */ new Map([["HOME", [context.homeDirs?.[0] ?? "~"]]])
@@ -2602,8 +2743,98 @@ function collectShellFileAccesses(command, context = {}) {
2602
2743
  }
2603
2744
  return;
2604
2745
  }
2746
+ if (name === "wget" || name === "curl" || name === "aria2c") {
2747
+ downloadOutputs(name, args, state);
2748
+ return;
2749
+ }
2750
+ if (["http", "https", "xh", "xhs"].includes(name)) {
2751
+ for (const token of args) {
2752
+ if (!token || token.dynamic || /^[a-z][a-z0-9+.-]*:\/\//i.test(token.value)) continue;
2753
+ const m = /^(?:@|[^=:@\s]+@|[^=:@\s]*=@)(.+)$/.exec(token.value);
2754
+ if (m && m[1] !== "-") add("read", { value: m[1] }, state, { upload: true });
2755
+ }
2756
+ return;
2757
+ }
2758
+ if (name === "scp" || name === "rsync") {
2759
+ const operands2 = strings.filter((s) => s && !s.startsWith("-"));
2760
+ const remote = operands2.slice(1).some((o) => /^[^/\s]+:/.test(o));
2761
+ for (const p of copySourcePaths(words))
2762
+ add("read", locate(p), state, remote ? { upload: true } : {});
2763
+ return;
2764
+ }
2605
2765
  for (const p of copySourcePaths(words)) add("read", locate(p), state);
2606
2766
  }
2767
+ function downloadOutputs(name, args, state) {
2768
+ const valueShort = DOWNLOAD_SHORT_VALUE[name];
2769
+ const outputs = [];
2770
+ const urls = [];
2771
+ let dir2 = null;
2772
+ let remoteName = false;
2773
+ let document = false;
2774
+ let serverNamed = false;
2775
+ const take = (flag, value) => {
2776
+ if (!value) return;
2777
+ const sent = uploadedFile(name, flag, value);
2778
+ if (sent) add("read", sent, state, { upload: true });
2779
+ if (DOWNLOAD_OUTPUT_FLAGS[name].includes(flag)) {
2780
+ if (["-O", "--output-document", "--out"].includes(flag) || name === "aria2c" && flag === "-o")
2781
+ document = true;
2782
+ if (value.value !== "-" || value.pattern || value.dynamic) outputs.push(value);
2783
+ } else if (DOWNLOAD_DIR_FLAGS[name].includes(flag)) dir2 = value;
2784
+ else if (name === "wget" && (flag === "-i" || flag === "--input-file")) serverNamed = true;
2785
+ };
2786
+ for (let i = 0; i < args.length; i++) {
2787
+ const token = args[i];
2788
+ if (!token) continue;
2789
+ const v = token.value;
2790
+ if (v.startsWith("--")) {
2791
+ const eq = v.indexOf("=");
2792
+ const flag = eq < 0 ? v : v.slice(0, eq);
2793
+ if (["--remote-name", "--remote-name-all"].includes(flag)) remoteName = true;
2794
+ if (["--remote-header-name", "--recursive", "--mirror", "--page-requisites"].includes(flag))
2795
+ serverNamed = true;
2796
+ if (DOWNLOAD_LONG_VALUE[name].includes(flag))
2797
+ take(flag, eq < 0 ? args[++i] ?? null : { ...token, value: v.slice(eq + 1) });
2798
+ continue;
2799
+ }
2800
+ if (v.startsWith("-") && v.length > 1 && !token.dynamic) {
2801
+ for (let j = 1; j < v.length; j++) {
2802
+ const c = v[j];
2803
+ if (name === "wget" && c === "n") {
2804
+ j++;
2805
+ continue;
2806
+ }
2807
+ if (name === "curl" && c === "O") remoteName = true;
2808
+ if (name === "curl" && c === "J") serverNamed = true;
2809
+ if (name === "wget" && (c === "r" || c === "m" || c === "p")) serverNamed = true;
2810
+ if (valueShort.includes(c)) {
2811
+ const rest = v.slice(j + 1);
2812
+ take(`-${c}`, rest ? { value: rest } : args[++i] ?? null);
2813
+ break;
2814
+ }
2815
+ }
2816
+ continue;
2817
+ }
2818
+ if (/^(?:https?|ftps?):\/\//i.test(v)) urls.push(v);
2819
+ }
2820
+ for (const out2 of outputs) {
2821
+ const spelled = name === "aria2c" && dir2?.value && !out2.value.startsWith("/") ? { ...out2, value: `${dir2.value.replace(/\/+$/, "")}/${out2.value}` } : out2;
2822
+ if (name === "curl" && /#\d/.test(spelled.value))
2823
+ add("write", { value: "", pattern: spelled.value.replace(/#\d+/g, "*"), dot: true }, state);
2824
+ else add("write", spelled, state);
2825
+ }
2826
+ const remote = name === "curl" ? remoteName || serverNamed : !document || serverNamed;
2827
+ if (!remote) return;
2828
+ const into = dir2?.value ? dir2.value.replace(/\/+$/, "") : ".";
2829
+ if (serverNamed || urls.length === 0) {
2830
+ add("write", { value: "", pattern: `${quoteGlob(into)}/*`, dot: true }, state);
2831
+ return;
2832
+ }
2833
+ for (const url of urls) {
2834
+ const file2 = url.replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]*/i, "").split("/").pop();
2835
+ add("write", { value: `${into}/${file2 || "index.html"}` }, state);
2836
+ }
2837
+ }
2607
2838
  function findActions2(tokens, words, head, state, nested, depth) {
2608
2839
  const starts = findStartPoints(words, head).starts;
2609
2840
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
@@ -2675,7 +2906,35 @@ function collectShellFileAccesses(command, context = {}) {
2675
2906
  const words = wordValues(node, state);
2676
2907
  alternatives = alternatives.flatMap((prefix) => (words.length ? words : [null]).map((w) => [...prefix, w])).slice(0, MAX_ALTERNATIVES);
2677
2908
  }
2678
- for (const tokens of alternatives) fromWords(tokens, state, nested, depth);
2909
+ for (const tokens of alternatives) {
2910
+ fromWords(tokens, state, nested, depth);
2911
+ inlineCall(tokens, state, depth);
2912
+ }
2913
+ }
2914
+ function inlineCall(tokens, state, depth) {
2915
+ const head = tokens[0];
2916
+ if (!head || head.dynamic || head.pattern) return;
2917
+ const bodies = functions.get(head.value);
2918
+ if (!bodies || (state.callDepth ?? 0) >= MAX_CALL_DEPTH2) return;
2919
+ const args = tokens.slice(1).map((a) => a && !a.dynamic ? a.value : null);
2920
+ const vars = JSON.stringify([...state.vars].sort(([a], [b]) => a < b ? -1 : 1));
2921
+ const key = `${head.value}\0${JSON.stringify(args)}\0${state.cwd.join("")}\0${vars}`;
2922
+ if (seenCalls.has(key)) return;
2923
+ seenCalls.add(key);
2924
+ for (const body of bodies) {
2925
+ if (callWalks2-- <= 0) return;
2926
+ const local = clone(state);
2927
+ local.callDepth = (state.callDepth ?? 0) + 1;
2928
+ for (const name of POSITIONAL) local.vars.delete(name);
2929
+ if (!rebindsPositionals(body)) {
2930
+ args.slice(0, 9).forEach((v, i) => local.vars.set(String(i + 1), v === null ? null : [v]));
2931
+ const all = args.every((v) => v !== null) && args.length ? unique(args) : null;
2932
+ local.vars.set("@", all);
2933
+ local.vars.set("*", all);
2934
+ local.vars.set("#", [String(args.length)]);
2935
+ }
2936
+ statement(body, local, depth + 1);
2937
+ }
2679
2938
  }
2680
2939
  function readsScriptFromStdin(stmt) {
2681
2940
  const cmd = stmt?.Cmd;
@@ -2712,11 +2971,13 @@ function collectShellFileAccesses(command, context = {}) {
2712
2971
  if (!stmt || depth > MAX_DEPTH) return;
2713
2972
  state.onSuccess = void 0;
2714
2973
  if (stmt.Background) state = clone(state);
2974
+ const headWord = stmt.Cmd && syntax2.NodeType(stmt.Cmd) === "CallExpr" ? base(resolveWordLiteral(stmt.Cmd.Args?.[0])) : "";
2975
+ const sends = SENDING_STDIN.has(headWord);
2715
2976
  for (const redir of stmt.Redirs ?? []) {
2716
2977
  substitutions(redir.Word, state, depth);
2717
2978
  for (const word of wordValues(redir.Word, state)) {
2718
2979
  if (WRITES.has(redir.Op)) add("write", word, state);
2719
- if (READS.has(redir.Op)) add("read", word, state);
2980
+ if (READS.has(redir.Op)) add("read", word, state, sends ? { upload: true } : {});
2720
2981
  if (redir.Op === DUP_OUT && word.value && !/^\d+$/.test(word.value))
2721
2982
  add("write", word, state);
2722
2983
  }
@@ -2825,7 +3086,10 @@ function collectShellFileAccesses(command, context = {}) {
2825
3086
  }
2826
3087
  const parsed = parseShared(command);
2827
3088
  const program = typeof parsed !== "symbol" ? parsed : runnablePrefix(command);
2828
- if (program) statements(program.Stmts ?? [], initial, 0);
3089
+ if (program) {
3090
+ for (const [name, bodies] of functionBodies(program)) functions.set(name, bodies);
3091
+ statements(program.Stmts ?? [], initial, 0);
3092
+ }
2829
3093
  return out;
2830
3094
  }
2831
3095
  function isAgentInstructionPath(absPath, root) {
@@ -2941,6 +3205,33 @@ function hits(access, paths, directories, context) {
2941
3205
  }
2942
3206
  return false;
2943
3207
  }
3208
+ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3209
+ const home = context.homeDirs?.[0] ?? "~";
3210
+ const candidates = access.candidates ?? [
3211
+ normalizeFilePath(access.path, access.cwd ?? context.cwd, home)
3212
+ ];
3213
+ for (const p of candidates) {
3214
+ if (dirs.some((d) => within(p, d)) && !exempt(p)) return true;
3215
+ if (access.recursive && dirs.some((d) => within(d, p))) return true;
3216
+ }
3217
+ if (!access.glob) return false;
3218
+ const pattern = normalizeFilePath(access.glob, access.cwd ?? context.cwd, home);
3219
+ const magic = pattern.search(/[*?[{]/);
3220
+ const literal = magic < 0 ? pattern : pattern.slice(0, pattern.lastIndexOf("/", magic) + 1);
3221
+ if (literal && exempt(literal.replace(/\/$/, "") || "/")) return false;
3222
+ const options = { dot: access.dotGlob === true, nonegate: true };
3223
+ const components = pattern.split("/");
3224
+ for (const root of dirs) {
3225
+ const count = root.split("/").length;
3226
+ if (components.length > count && picomatch(components.slice(0, count).join("/"), options)(root))
3227
+ return true;
3228
+ if (access.recursive && picomatch(pattern, options)(root)) return true;
3229
+ }
3230
+ return false;
3231
+ }
3232
+ function uploadDirs(context) {
3233
+ return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3234
+ }
2944
3235
  function evaluateProtectedFileAccess(accesses, context, settings) {
2945
3236
  const homes = [.../* @__PURE__ */ new Set([...context.homeDirs ?? ["~"], "/root"])];
2946
3237
  const startup = homes.flatMap(
@@ -2962,6 +3253,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2962
3253
  "files.shell-startup",
2963
3254
  "review-shell-startup-mutation",
2964
3255
  "Changing shell startup files requires approval"
3256
+ ] : mutation && hitsSystemDir(access, context) ? [
3257
+ "files.system-dirs",
3258
+ "review-system-dir-mutation",
3259
+ "Changing files in a system directory requires approval"
3260
+ ] : !mutation && access.upload && hitsSystemDir(access, context, uploadDirs(context)) ? [
3261
+ "files.system-dirs",
3262
+ "review-system-file-upload",
3263
+ "Uploading a system file requires approval"
2965
3264
  ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
2966
3265
  "files.password-hashes",
2967
3266
  "review-password-hash-read",
@@ -3222,16 +3521,21 @@ function looksGenerated(value, minLength, minEntropy, lettersOnlyAllowed = false
3222
3521
  if (EMAIL_VALUE_RE.test(value)) return false;
3223
3522
  if (looksLikeJoinedWords(value) || hasSequentialRun(value)) return false;
3224
3523
  if (PATH_VALUE_RE.test(value) || DOTTED_REFERENCE_RE.test(value)) return false;
3524
+ if (CONCATENATION_RE.test(value)) return false;
3225
3525
  if (rejectedByStructuredValidator(value)) return false;
3226
3526
  return shannonEntropy(value) >= minEntropy;
3227
3527
  }
3228
3528
  function nameHoldsCredential(name) {
3229
3529
  return !FIXTURE_NAME_RE.test(name) && !DERIVATIVE_NAME_RE.test(name);
3230
3530
  }
3531
+ function isOtpDocSecret(value) {
3532
+ return OTP_DOC_SECRETS.has(value.replace(/=+$/, "").toUpperCase());
3533
+ }
3231
3534
  function validateQueryCredential(raw) {
3232
3535
  const value = capturedValue(raw, QUERY_CREDENTIAL_VALUE_RE);
3233
3536
  if (value === null) return false;
3234
- if (OTP_SECRET_RE.test(value)) return looksGenerated(value, 16, 3, true);
3537
+ if (OTP_SECRET_RE.test(value))
3538
+ return !isOtpDocSecret(value) && looksGenerated(value, 16, 3, true);
3235
3539
  if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value)) return false;
3236
3540
  return looksGenerated(value, 12, 3);
3237
3541
  }
@@ -3245,13 +3549,40 @@ function keywordEndsAPart(name) {
3245
3549
  }
3246
3550
  function validateAssignedSecret(raw, before = "") {
3247
3551
  const value = capturedValue(raw, ASSIGNED_SECRET_VALUE_RE);
3248
- const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE);
3552
+ const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE)?.replace(/^_+/, "") ?? null;
3249
3553
  if (value === null || name === null) return false;
3250
- if (!keywordEndsAPart(name) || ARN_CONTEXT_RE.test(before)) return false;
3251
- const prefix = before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "";
3554
+ if (!keywordEndsAPart(name)) return false;
3555
+ if (ARN_CONTEXT_RE.test(before)) return false;
3556
+ const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3252
3557
  if (!nameHoldsCredential(prefix + name)) return false;
3253
3558
  return looksGenerated(value, 12, 3.5);
3254
3559
  }
3560
+ function validateBasicCredential(raw) {
3561
+ const b64 = capturedValue(raw, BASIC_VALUE_RE);
3562
+ if (b64 === null) return false;
3563
+ const bare = b64.replace(/=+$/, "");
3564
+ if (bare.length % 4 === 1) return false;
3565
+ const bytes = Buffer.from(b64, "base64");
3566
+ if (bytes.toString("base64").replace(/=+$/, "") !== bare) return false;
3567
+ const text = bytes.toString("utf8");
3568
+ if (!Buffer.from(text, "utf8").equals(bytes)) return false;
3569
+ for (let i = 0; i < text.length; i++) {
3570
+ const c = text.charCodeAt(i);
3571
+ if (c < 32 || c === 127) return false;
3572
+ }
3573
+ const pair = BASIC_PAIR_RE.exec(text);
3574
+ if (!pair) return false;
3575
+ const password = pair[2];
3576
+ if (!/[0-9]/.test(password) || !/[A-Za-z]/.test(password)) return false;
3577
+ const lower2 = password.toLowerCase();
3578
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3579
+ return shannonEntropy(password) >= 3;
3580
+ }
3581
+ function validateOtpSecret(raw) {
3582
+ const secret = capturedValue(raw, OTP_CAPTURE_RE);
3583
+ if (secret === null || isOtpDocSecret(secret)) return false;
3584
+ return looksGenerated(secret.replace(/=+$/, ""), 16, 3, true);
3585
+ }
3255
3586
  function matchSensitivePath2(resolvedPath, originalPath, options = {}) {
3256
3587
  const normalised = resolvedPath.replace(/\\/g, "/");
3257
3588
  if (!options.systemAccessHandled && isLegacySystemPath(normalised))
@@ -3522,6 +3853,156 @@ function parsePySpec(raw) {
3522
3853
  const exact = PY_EXACT_RE.exec(m[2].trim());
3523
3854
  return { name, version: exact ? exact[1] : void 0 };
3524
3855
  }
3856
+ function exactVersion(v) {
3857
+ if (!v) return void 0;
3858
+ const m = EXACT_VERSION_RE.exec(v.trim());
3859
+ return m ? m[1] : void 0;
3860
+ }
3861
+ function push(out, ecosystem, manager, name, version, raw) {
3862
+ out.push({ ecosystem, manager, name, ...version ? { version } : {}, raw });
3863
+ }
3864
+ function gemInstall(words, from, out) {
3865
+ const dash = words.indexOf("--", from);
3866
+ const own = dash < 0 ? words : words.slice(0, dash);
3867
+ const pinned = exactVersion(flagValues(own, from, ["-v", "--version"])[0]);
3868
+ for (const raw of operands(own, from, GEM_VALUE_FLAGS)) {
3869
+ if (NON_REGISTRY_RE.test(raw) || /\.gem$/i.test(raw)) continue;
3870
+ const colon = raw.indexOf(":");
3871
+ const name = colon < 0 ? raw : raw.slice(0, colon);
3872
+ if (!GEM_NAME_RE.test(name)) continue;
3873
+ push(
3874
+ out,
3875
+ "RubyGems",
3876
+ "gem",
3877
+ name,
3878
+ colon < 0 ? pinned : exactVersion(raw.slice(colon + 1)),
3879
+ raw
3880
+ );
3881
+ }
3882
+ }
3883
+ function cargoCrates(words, from, manager, out) {
3884
+ const flags = manager === "cargo add" ? CARGO_ADD_VALUE_FLAGS : CARGO_INSTALL_VALUE_FLAGS;
3885
+ if (flagValues(words, from, ["--git", "--path", "--registry", "--index"]).length > 0) return;
3886
+ if (words.some((w) => w === "--git" || w === "--path")) return;
3887
+ const flagVersion = manager === "cargo install" ? exactVersion(flagValues(words, from, ["--version", "--vers"])[0]) : void 0;
3888
+ for (const raw of operands(words, from, flags)) {
3889
+ const at = raw.indexOf("@");
3890
+ const name = at < 0 ? raw : raw.slice(0, at);
3891
+ if (!CRATE_NAME_RE.test(name)) continue;
3892
+ push(
3893
+ out,
3894
+ "crates.io",
3895
+ manager,
3896
+ name,
3897
+ at < 0 ? flagVersion : exactVersion(raw.slice(at + 1)),
3898
+ raw
3899
+ );
3900
+ }
3901
+ }
3902
+ function goModules(words, from, manager, out) {
3903
+ let specs = operands(words, from, GO_VALUE_FLAGS);
3904
+ if (manager === "go run") specs = specs.slice(0, 1).filter((s) => s.includes("@"));
3905
+ for (const raw of specs) {
3906
+ const at = raw.indexOf("@");
3907
+ const pathPart = (at < 0 ? raw : raw.slice(0, at)).replace(/\/\.\.\.$/, "");
3908
+ const ver = at < 0 ? void 0 : raw.slice(at + 1);
3909
+ if (ver === "none") continue;
3910
+ if (!GO_PATH_RE.test(pathPart) || /\.go$/i.test(pathPart)) continue;
3911
+ const exact = ver && /^v\d/.test(ver) ? exactVersion(ver) : void 0;
3912
+ push(out, "Go", manager, pathPart, exact, raw);
3913
+ }
3914
+ }
3915
+ function dotnetAdd(words, from, out) {
3916
+ let k = -1;
3917
+ for (let i = from; i < Math.min(words.length, from + 2); i++) {
3918
+ if (lower(words[i]) === "package") {
3919
+ k = i;
3920
+ break;
3921
+ }
3922
+ }
3923
+ if (k < 0) return;
3924
+ const name = operands(words, k + 1, DOTNET_ADD_VALUE_FLAGS)[0];
3925
+ if (!name || !NUGET_NAME_RE.test(name)) return;
3926
+ const version = exactVersion(flagValues(words, k + 1, ["-v", "--version"])[0]);
3927
+ push(out, "NuGet", "dotnet add package", name, version, name);
3928
+ }
3929
+ function dotnetTool(words, from, out) {
3930
+ const name = operands(words, from, DOTNET_TOOL_VALUE_FLAGS)[0];
3931
+ if (!name || !NUGET_NAME_RE.test(name)) return;
3932
+ const version = exactVersion(flagValues(words, from, ["--version"])[0]);
3933
+ push(out, "NuGet", "dotnet tool", name, version, name);
3934
+ }
3935
+ function nugetInstall(words, from, out) {
3936
+ const lowered = words.map((w) => w !== null && w.startsWith("-") ? w.toLowerCase() : w);
3937
+ const name = operands(lowered, from, NUGET_VALUE_FLAGS)[0];
3938
+ if (!name || !NUGET_NAME_RE.test(name) || /\.config$/i.test(name)) return;
3939
+ const version = exactVersion(flagValues(lowered, from, ["-version"])[0]);
3940
+ push(out, "NuGet", "nuget install", name, version, name);
3941
+ }
3942
+ function mavenGet(words, from, out) {
3943
+ const goal = words.slice(from).some(
3944
+ (w) => w !== null && /(?:^|:)dependency:get$|^org\.apache\.maven\.plugins:maven-dependency-plugin:(?:[\w.-]+:)?get$/.test(
3945
+ w
3946
+ )
3947
+ );
3948
+ if (!goal) return;
3949
+ const prop = (key) => {
3950
+ for (let i = from; i < words.length; i++) {
3951
+ const w = words[i];
3952
+ if (w === null) continue;
3953
+ if (w.startsWith(`-D${key}=`)) return w.slice(key.length + 3);
3954
+ if (w === "-D" && words[i + 1]?.startsWith(`${key}=`))
3955
+ return words[i + 1].slice(key.length + 1);
3956
+ }
3957
+ return void 0;
3958
+ };
3959
+ const artifact = prop("artifact");
3960
+ let coord;
3961
+ let ver;
3962
+ if (artifact) {
3963
+ const parts = artifact.split(":");
3964
+ if (parts.length < 2) return;
3965
+ coord = `${parts[0]}:${parts[1]}`;
3966
+ ver = parts[2];
3967
+ } else {
3968
+ const g = prop("groupId");
3969
+ const a = prop("artifactId");
3970
+ if (!g || !a) return;
3971
+ coord = `${g}:${a}`;
3972
+ ver = prop("version");
3973
+ }
3974
+ if (!MAVEN_COORD_RE.test(coord)) return;
3975
+ const exact = ver && !/^(?:LATEST|RELEASE)$/i.test(ver) ? exactVersion(ver) : void 0;
3976
+ push(out, "Maven", "mvn dependency:get", coord, exact, artifact ?? coord);
3977
+ }
3978
+ function composerPackages(words, from, manager, out) {
3979
+ const ops = operands(words, from, COMPOSER_VALUE_FLAGS);
3980
+ const specs = manager === "composer create-project" ? ops.slice(0, 1) : ops;
3981
+ for (const raw of specs) {
3982
+ const sep = raw.search(/[:=]/);
3983
+ const name = sep < 0 ? raw : raw.slice(0, sep);
3984
+ if (!COMPOSER_NAME_RE.test(name)) continue;
3985
+ let ver = sep < 0 ? void 0 : raw.slice(sep + 1);
3986
+ if (manager === "composer create-project" && ver === void 0) ver = ops[2] ?? void 0;
3987
+ push(out, "Packagist", manager, name.toLowerCase(), exactVersion(ver), raw);
3988
+ }
3989
+ }
3990
+ function editorExtensions(words, from, out) {
3991
+ for (const raw of flagValues(words, from, ["--install-extension"])) {
3992
+ if (/\.vsix$/i.test(raw) || NON_REGISTRY_RE.test(raw)) continue;
3993
+ const at = raw.lastIndexOf("@");
3994
+ const name = at <= 0 ? raw : raw.slice(0, at);
3995
+ if (!EXTENSION_ID_RE.test(name)) continue;
3996
+ push(
3997
+ out,
3998
+ "VSCode",
3999
+ "code --install-extension",
4000
+ name,
4001
+ at <= 0 ? void 0 : exactVersion(raw.slice(at + 1)),
4002
+ raw
4003
+ );
4004
+ }
4005
+ }
3525
4006
  function basename(w) {
3526
4007
  return (w ?? "").toLowerCase().split("/").pop() ?? "";
3527
4008
  }
@@ -3753,7 +4234,63 @@ function fromCall(words, out, depth) {
3753
4234
  pushPy(out, "pipenv", operands(words, v + 1, PIPENV_VALUE_FLAGS));
3754
4235
  return;
3755
4236
  }
4237
+ case "gem": {
4238
+ const v = verbIndex(words, start + 1, GEM_VALUE_FLAGS);
4239
+ if (v >= 0 && ["install", "i"].includes(lower(words[v]))) gemInstall(words, v + 1, out);
4240
+ return;
4241
+ }
4242
+ case "bundle": {
4243
+ const v = verbIndex(words, start + 1, BUNDLE_VALUE_FLAGS);
4244
+ if (v < 0 || lower(words[v]) !== "add") return;
4245
+ if (flagValues(words, v + 1, ["--git", "--github", "--path"]).length > 0) return;
4246
+ const version = exactVersion(flagValues(words, v + 1, ["-v", "--version"])[0]);
4247
+ for (const raw of operands(words, v + 1, BUNDLE_VALUE_FLAGS))
4248
+ if (GEM_NAME_RE.test(raw)) push(out, "RubyGems", "bundle add", raw, version, raw);
4249
+ return;
4250
+ }
4251
+ case "cargo": {
4252
+ const from = words[start + 1]?.startsWith("+") ? start + 2 : start + 1;
4253
+ const v = verbIndex(words, from, CARGO_ADD_VALUE_FLAGS);
4254
+ const verb = lower(words[v]);
4255
+ if (verb === "add") cargoCrates(words, v + 1, "cargo add", out);
4256
+ else if (verb === "install") cargoCrates(words, v + 1, "cargo install", out);
4257
+ return;
4258
+ }
4259
+ case "go": {
4260
+ const v = verbIndex(words, start + 1, GO_VALUE_FLAGS);
4261
+ const verb = lower(words[v]);
4262
+ if (verb === "get" || verb === "install" || verb === "run")
4263
+ goModules(words, v + 1, `go ${verb}`, out);
4264
+ return;
4265
+ }
4266
+ case "dotnet": {
4267
+ const v = verbIndex(words, start + 1, DOTNET_ADD_VALUE_FLAGS);
4268
+ const verb = lower(words[v]);
4269
+ if (verb === "add") dotnetAdd(words, v + 1, out);
4270
+ else if (verb === "tool" && ["install", "update"].includes(lower(words[v + 1])))
4271
+ dotnetTool(words, v + 2, out);
4272
+ return;
4273
+ }
4274
+ case "nuget": {
4275
+ if (lower(words[start + 1]) === "install") nugetInstall(words, start + 2, out);
4276
+ return;
4277
+ }
4278
+ case "mvn":
4279
+ case "mvnw":
4280
+ mavenGet(words, start + 1, out);
4281
+ return;
4282
+ case "composer": {
4283
+ let v = verbIndex(words, start + 1, COMPOSER_VALUE_FLAGS);
4284
+ if (v >= 0 && lower(words[v]) === "global") v = verbIndex(words, v + 1, COMPOSER_VALUE_FLAGS);
4285
+ const verb = lower(words[v]);
4286
+ if (verb === "require" || verb === "req" || verb === "r")
4287
+ composerPackages(words, v + 1, "composer require", out);
4288
+ else if (verb === "create-project")
4289
+ composerPackages(words, v + 1, "composer create-project", out);
4290
+ return;
4291
+ }
3756
4292
  default:
4293
+ if (EDITOR_HEADS.includes(family)) editorExtensions(words, start + 1, out);
3757
4294
  return;
3758
4295
  }
3759
4296
  }
@@ -5258,11 +5795,11 @@ function isDecimalFraction(text, index, unseparated) {
5258
5795
  const a = text[index - 2];
5259
5796
  if (sep === ".") {
5260
5797
  if (a !== void 0 && a >= "0" && a <= "9") return true;
5261
- return a === void 0 || NUMBER_LEAD_RE.test(a);
5798
+ return a === void 0 || (a === "-" || a === "+") && index === 2;
5262
5799
  }
5263
5800
  if (sep === "," && a === "0") {
5264
5801
  const b = text[index - 3];
5265
- return b === void 0 || NUMBER_LEAD_RE.test(b);
5802
+ return b === void 0 || b === ";";
5266
5803
  }
5267
5804
  return false;
5268
5805
  }
@@ -5936,7 +6473,7 @@ function matchCanaryArgs(args, values) {
5936
6473
  return null;
5937
6474
  }
5938
6475
  }
5939
- 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, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, 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, MAX_MNEMONIC_HASHES, 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, INVISIBLE_ONE, 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, BATCH_SEPARATOR, 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, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, NUMBER_LEAD_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, 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, 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;
6476
+ 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, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, 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, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, 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, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, BATCH_SEPARATOR, 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, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_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, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, 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, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, 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, 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;
5940
6477
  var init_dist = __esm({
5941
6478
  "packages/policy-engine/dist/index.mjs"() {
5942
6479
  "use strict";
@@ -6737,6 +7274,11 @@ var init_dist = __esm({
6737
7274
  example: "cat /etc/shadow",
6738
7275
  advice: "Keep Review, or choose Block when agents must never read system password hashes."
6739
7276
  },
7277
+ "files.system-dirs": {
7278
+ plain: "The agent writes, creates or deletes a file in a system directory, or sends a system file in a request. /usr/local and a container working directory under /usr/src are ordinary project space and are not included.",
7279
+ example: "wget -O /usr/bin/tool https://example.com/tool",
7280
+ advice: "Keep Review on a workstation. A container image build that installs into /usr can set it to Log."
7281
+ },
6740
7282
  // ── Commands ──────────────────────────────────────────────────────────────
6741
7283
  "commands.inline-exec": {
6742
7284
  plain: "The agent runs a small program written inside the command itself.",
@@ -6933,7 +7475,7 @@ var init_dist = __esm({
6933
7475
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
6934
7476
  },
6935
7477
  "loading.malicious-package": {
6936
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago.",
7478
+ plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only.",
6937
7479
  example: "npm install of a package flagged as malware",
6938
7480
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
6939
7481
  }
@@ -6971,6 +7513,15 @@ var init_dist = __esm({
6971
7513
  defaultValue: "review",
6972
7514
  legacy: ["review-password-hash-read"]
6973
7515
  },
7516
+ {
7517
+ id: "files.system-dirs",
7518
+ group: "files",
7519
+ title: "System directories",
7520
+ catches: "Writes, new files and removals under /usr, /etc, /boot, /bin, /sbin, /lib and /var/log, and uploads of system files",
7521
+ values: ALL,
7522
+ defaultValue: "review",
7523
+ legacy: ["review-system-dir-mutation", "review-system-file-upload"]
7524
+ },
6974
7525
  // ── Commands ────────────────────────────────────────────────────────────
6975
7526
  {
6976
7527
  id: "commands.inline-exec",
@@ -8268,6 +8819,10 @@ var init_dist = __esm({
8268
8819
  AND_OP = probeBinOp("a && b");
8269
8820
  OR_OP = probeBinOp("a || b");
8270
8821
  AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
8822
+ MAX_CALL_DEPTH = 4;
8823
+ MAX_CALL_WALKS = 256;
8824
+ callDepth = 0;
8825
+ callWalks = 0;
8271
8826
  FS_OP_CACHE_MAX = 5e3;
8272
8827
  fsOpCache = /* @__PURE__ */ new Map();
8273
8828
  stripDotSlash = (p) => p.replace(/^\.\//, "");
@@ -8320,10 +8875,79 @@ var init_dist = __esm({
8320
8875
  MAX_ARGV_CHARACTERS = 262144;
8321
8876
  ({ syntax: syntax2 } = mvdanSh2);
8322
8877
  MAX_ALTERNATIVES = 64;
8878
+ MAX_CALL_DEPTH2 = 4;
8879
+ MAX_CALL_WALKS2 = 256;
8880
+ POSITIONAL = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"];
8323
8881
  MAX_DEPTH = 64;
8324
8882
  MAX_PAYLOAD_DEPTH = 24;
8325
8883
  unique = (a) => [...new Set(a)].slice(0, MAX_ALTERNATIVES);
8326
- clone = (s) => ({ cwd: [...s.cwd], vars: new Map(s.vars) });
8884
+ clone = (s) => ({
8885
+ cwd: [...s.cwd],
8886
+ vars: new Map(s.vars),
8887
+ ...s.callDepth ? { callDepth: s.callDepth } : {}
8888
+ });
8889
+ SENDING_STDIN = /* @__PURE__ */ new Set(["nc", "ncat", "netcat", "socat", "telnet", "curl", "wget"]);
8890
+ DOWNLOAD_SHORT_VALUE = {
8891
+ curl: "odDeEFHKmPrtTuUwxXyYzAbcCQ",
8892
+ wget: "OoaPiBetTwQlARDXIUY",
8893
+ aria2c: "odisx"
8894
+ };
8895
+ DOWNLOAD_LONG_VALUE = {
8896
+ curl: [
8897
+ "--output",
8898
+ "--output-dir",
8899
+ "--dump-header",
8900
+ "--cookie-jar",
8901
+ "--data",
8902
+ "--data-binary",
8903
+ "--data-raw",
8904
+ "--data-ascii",
8905
+ "--data-urlencode",
8906
+ "--form",
8907
+ "--header",
8908
+ "--request",
8909
+ "--user",
8910
+ "--user-agent",
8911
+ "--referer",
8912
+ "--cookie",
8913
+ "--config",
8914
+ "--json",
8915
+ "--upload-file",
8916
+ "--max-time",
8917
+ "--proxy",
8918
+ "--write-out",
8919
+ "--url"
8920
+ ],
8921
+ wget: [
8922
+ "--output-document",
8923
+ "--output-file",
8924
+ "--append-output",
8925
+ "--directory-prefix",
8926
+ "--input-file",
8927
+ "--post-data",
8928
+ "--post-file",
8929
+ "--body-data",
8930
+ "--body-file",
8931
+ "--method",
8932
+ "--header",
8933
+ "--user-agent",
8934
+ "--user",
8935
+ "--password",
8936
+ "--tries",
8937
+ "--timeout"
8938
+ ],
8939
+ aria2c: ["--out", "--dir", "--input-file"]
8940
+ };
8941
+ DOWNLOAD_OUTPUT_FLAGS = {
8942
+ curl: ["-o", "--output", "-D", "--dump-header", "-c", "--cookie-jar"],
8943
+ wget: ["-O", "--output-document", "-o", "--output-file", "-a", "--append-output"],
8944
+ aria2c: ["-o", "--out"]
8945
+ };
8946
+ DOWNLOAD_DIR_FLAGS = {
8947
+ curl: ["--output-dir"],
8948
+ wget: ["-P", "--directory-prefix"],
8949
+ aria2c: ["-d", "--dir"]
8950
+ };
8327
8951
  base = (s) => (s ?? "").replace(/\/+$/, "").split("/").pop() ?? "";
8328
8952
  quoteGlob = (s) => s.replace(/[\\*?\[\]{}()!+@]/g, "\\$&");
8329
8953
  asWord = (p) => ({ value: p.value, ...p.glob ? { pattern: p.pattern } : {} });
@@ -8348,6 +8972,18 @@ var init_dist = __esm({
8348
8972
  SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
8349
8973
  PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
8350
8974
  STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
8975
+ SYSTEM_DIRS = [
8976
+ "/usr",
8977
+ "/etc",
8978
+ "/boot",
8979
+ "/bin",
8980
+ "/sbin",
8981
+ "/lib",
8982
+ "/lib64",
8983
+ "/var/log"
8984
+ ];
8985
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
8986
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
8351
8987
  AGENT_INSTRUCTION_FILES = [
8352
8988
  ".gitlab-ci.yml",
8353
8989
  "CLAUDE.md",
@@ -8365,6 +9001,7 @@ var init_dist = __esm({
8365
9001
  ".husky"
8366
9002
  ];
8367
9003
  LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9004
+ exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
8368
9005
  RANK = { allow: 0, review: 1, block: 2 };
8369
9006
  BIP39_ENGLISH = `abandon
8370
9007
  ability
@@ -10650,15 +11287,20 @@ zoo`.split("\n");
10650
11287
  IDENTIFIER_VALUE_RE = /^[A-Za-z_.-]+$/;
10651
11288
  PATH_VALUE_RE = /^(?:\/|\.{1,2}\/|~\/)/;
10652
11289
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
11290
+ CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
10653
11291
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
10654
11292
  FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
10655
- DERIVATIVE_NAME_RE = /(?:hash|digest|id|type|name|url|uri|path|file|len|length|count|size|ttl|expires?|expiry|age|date|time|format|field|header|param)$/i;
11293
+ DERIVATIVE_NAME_RE = /(?:hash|digest|fingerprint|thumbprint|checksum|id|type|name|url|uri|path|file|len|length|count|size|ttl|expires?|expiry|age|date|time|format|field|header|param)$/i;
10656
11294
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
10657
11295
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
10658
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
11296
+ OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
11297
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
10659
11298
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
10660
- NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)/i;
11299
+ NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|pass)/i;
10661
11300
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
11301
+ BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
11302
+ BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
11303
+ OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
10662
11304
  DLP_PATTERNS = [
10663
11305
  // ── AWS ───────────────────────────────────────────────────────────────────
10664
11306
  {
@@ -10996,6 +11638,22 @@ zoo`.split("\n");
10996
11638
  contextBoost: true,
10997
11639
  minEntropy: 3
10998
11640
  },
11641
+ // ── HTTP Basic credential ─────────────────────────────────────────────────
11642
+ // `Authorization: Basic <base64 of user:password>`, or the bare scheme in a
11643
+ // config list. The validator decodes the value and accepts only a
11644
+ // `user:password` whose password looks generated (letters and digits, 8+
11645
+ // characters, the query-credential rule), so prose ("Basic authentication")
11646
+ // and documentation pairs (`admin:changeme`, `user:pass1234`) stay allowed.
11647
+ // review: the same weight as a bare Bearer token.
11648
+ {
11649
+ name: "HTTP Basic Credential",
11650
+ regex: /\bBasic[ \t]{1,8}([A-Za-z0-9+/]{8,512}={0,2})/i,
11651
+ severity: "review",
11652
+ keywords: ["basic"],
11653
+ validate: validateBasicCredential,
11654
+ redactGroup: 1,
11655
+ argumentsOnly: true
11656
+ },
10999
11657
  // ── Resend ────────────────────────────────────────────────────────────────
11000
11658
  {
11001
11659
  name: "Resend API Key",
@@ -11137,6 +11795,19 @@ zoo`.split("\n");
11137
11795
  validate: validateQueryCredential,
11138
11796
  argumentsOnly: true
11139
11797
  },
11798
+ // ── OTP secret in a constructor ───────────────────────────────────────────
11799
+ // `ROTP::TOTP.new('<base32>')`, `pyotp.TOTP("<base32>")`, `new HOTP('...')`:
11800
+ // the shared secret of a one-time-password generator written as a literal.
11801
+ // The otpauth URI form is `Credential in URL Query`. review.
11802
+ {
11803
+ name: "OTP Secret",
11804
+ regex: /\b(?:TOTP|HOTP|OTP)(?:\.new|\.create)?[ \t]{0,8}\(?[ \t]{0,8}["']([A-Z2-7]{16,128}=*)["']/i,
11805
+ severity: "review",
11806
+ keywords: ["otp"],
11807
+ validate: validateOtpSecret,
11808
+ redactGroup: 1,
11809
+ argumentsOnly: true
11810
+ },
11140
11811
  // ── Secret assigned to a telling name ─────────────────────────────────────
11141
11812
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11142
11813
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11144,12 +11815,14 @@ zoo`.split("\n");
11144
11815
  // generated: 12+ characters, not an identifier, Shannon entropy of 3.5
11145
11816
  // bits per character or more. review, never block (node9Firewall#348,
11146
11817
  // item 4). Tuned on CredData's public split; its look-alike lines are the
11147
- // negative control.
11818
+ // negative control. A quote may be escaped (`\"access_token\":\"...\"`):
11819
+ // JSON written inside a string literal. The two optional quotes after the
11820
+ // name are written out, not as a counted group, to keep star height 1.
11148
11821
  {
11149
11822
  name: "Assigned Secret",
11150
- regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)[A-Za-z0-9_]{0,40}["'\]]{0,2}[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})/i,
11823
+ regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|\bpass(?![A-Za-z0-9])|_pass(?![A-Za-z0-9]))[A-Za-z0-9_]{0,40}\\?["'\]]?\\?["'\]]?[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})/i,
11151
11824
  severity: "review",
11152
- keywords: ["password", "passphrase", "passwd", "pwd", "secret", "token", "key"],
11825
+ keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11153
11826
  validate: validateAssignedSecret,
11154
11827
  argumentsOnly: true
11155
11828
  }
@@ -11410,7 +12083,169 @@ zoo`.split("\n");
11410
12083
  ]);
11411
12084
  PY_SPEC_RE = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?:\[[^\]]*\])?\s*(.*)$/;
11412
12085
  PY_EXACT_RE = /^===?\s*v?(\d+(?:\.\d+)*(?:[a-zA-Z0-9.+!-]*))$/;
12086
+ EXACT_VERSION_RE = /^=?v?(\d+(?:\.\d+)*(?:[-+][0-9A-Za-z][0-9A-Za-z.+-]*)?)$/;
12087
+ GEM_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12088
+ GEM_VALUE_FLAGS = /* @__PURE__ */ new Set([
12089
+ "-v",
12090
+ "--version",
12091
+ "-i",
12092
+ "--install-dir",
12093
+ "-n",
12094
+ "--bindir",
12095
+ "--document",
12096
+ "--build-root",
12097
+ "-P",
12098
+ "--trust-policy",
12099
+ "-s",
12100
+ "--source",
12101
+ "-p",
12102
+ "--http-proxy",
12103
+ "--platform",
12104
+ "-g",
12105
+ "--file",
12106
+ "--without"
12107
+ ]);
12108
+ BUNDLE_VALUE_FLAGS = /* @__PURE__ */ new Set([
12109
+ "-v",
12110
+ "--version",
12111
+ "-g",
12112
+ "--group",
12113
+ "-s",
12114
+ "--source",
12115
+ "-r",
12116
+ "--require",
12117
+ "--path",
12118
+ "--git",
12119
+ "--github",
12120
+ "--branch",
12121
+ "--ref",
12122
+ "--glob"
12123
+ ]);
12124
+ CRATE_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*$/u;
12125
+ CARGO_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12126
+ "-F",
12127
+ "--features",
12128
+ "--rename",
12129
+ "--registry",
12130
+ "--manifest-path",
12131
+ "-p",
12132
+ "--package",
12133
+ "--git",
12134
+ "--branch",
12135
+ "--tag",
12136
+ "--rev",
12137
+ "--path",
12138
+ "--base",
12139
+ "--target",
12140
+ "--color",
12141
+ "--config",
12142
+ "-Z",
12143
+ "--lockfile-path"
12144
+ ]);
12145
+ CARGO_INSTALL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12146
+ "--version",
12147
+ "--vers",
12148
+ "--git",
12149
+ "--branch",
12150
+ "--tag",
12151
+ "--rev",
12152
+ "--path",
12153
+ "--root",
12154
+ "--index",
12155
+ "--registry",
12156
+ "-F",
12157
+ "--features",
12158
+ "--profile",
12159
+ "--target",
12160
+ "--target-dir",
12161
+ "-j",
12162
+ "--jobs",
12163
+ "--bin",
12164
+ "--example",
12165
+ "--color",
12166
+ "--config",
12167
+ "-Z"
12168
+ ]);
12169
+ GO_VALUE_FLAGS = /* @__PURE__ */ new Set([
12170
+ "-C",
12171
+ "-tags",
12172
+ "-ldflags",
12173
+ "-gcflags",
12174
+ "-asmflags",
12175
+ "-mod",
12176
+ "-modfile",
12177
+ "-o",
12178
+ "-p",
12179
+ "-pkgdir",
12180
+ "-toolexec",
12181
+ "-exec",
12182
+ "-overlay",
12183
+ "-pgo",
12184
+ "-buildmode",
12185
+ "-compiler",
12186
+ "-gccgoflags",
12187
+ "-installsuffix",
12188
+ "-covermode",
12189
+ "-coverpkg"
12190
+ ]);
12191
+ GO_PATH_RE = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+(?:\/[\p{L}\p{N}_.~+-]+)*$/u;
12192
+ NUGET_NAME_RE = /^[\p{L}\p{N}_][\p{L}\p{N}_.-]*$/u;
12193
+ DOTNET_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12194
+ "-v",
12195
+ "--version",
12196
+ "-s",
12197
+ "--source",
12198
+ "-f",
12199
+ "--framework",
12200
+ "--package-directory"
12201
+ ]);
12202
+ DOTNET_TOOL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12203
+ "--version",
12204
+ "--add-source",
12205
+ "--configfile",
12206
+ "--framework",
12207
+ "--tool-path",
12208
+ "-a",
12209
+ "--arch",
12210
+ "--tool-manifest",
12211
+ "-v",
12212
+ "--verbosity"
12213
+ ]);
12214
+ NUGET_VALUE_FLAGS = /* @__PURE__ */ new Set([
12215
+ "-version",
12216
+ "-outputdirectory",
12217
+ "-o",
12218
+ "-source",
12219
+ "-configfile",
12220
+ "-fallbacksource",
12221
+ "-framework",
12222
+ "-packagesavemode",
12223
+ "-dependencyversion",
12224
+ "-solutiondirectory",
12225
+ "-verbosity"
12226
+ ]);
12227
+ MAVEN_COORD_RE = /^[\p{L}\p{N}_.-]+:[\p{L}\p{N}_.-]+$/u;
12228
+ COMPOSER_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*\/[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12229
+ COMPOSER_VALUE_FLAGS = /* @__PURE__ */ new Set([
12230
+ "-d",
12231
+ "--working-dir",
12232
+ "--stability",
12233
+ "--repository",
12234
+ "--add-repository"
12235
+ ]);
12236
+ EDITOR_HEADS = ["code", "code-insiders", "codium", "vscodium", "cursor"];
12237
+ EXTENSION_ID_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*\.[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
11413
12238
  MANAGER_HEADS = /* @__PURE__ */ new Set([
12239
+ "gem",
12240
+ "bundle",
12241
+ "cargo",
12242
+ "go",
12243
+ "dotnet",
12244
+ "nuget",
12245
+ "mvn",
12246
+ "mvnw",
12247
+ "composer",
12248
+ ...EDITOR_HEADS,
11414
12249
  "npm",
11415
12250
  "npx",
11416
12251
  "pnpm",
@@ -11489,6 +12324,7 @@ zoo`.split("\n");
11489
12324
  MAP_GOVERNED = /* @__PURE__ */ new Set([
11490
12325
  "files.shell-startup",
11491
12326
  "files.password-hashes",
12327
+ "files.system-dirs",
11492
12328
  "commands.inline-exec",
11493
12329
  "commands.eval-dynamic",
11494
12330
  "commands.curl-pipe-shell",
@@ -11757,7 +12593,6 @@ zoo`.split("\n");
11757
12593
  PII_CC16_RE = /\b(?:4\d{3}|5[1-5]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
11758
12594
  PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
11759
12595
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
11760
- NUMBER_LEAD_RE = /[\s:=;([{"'+-]/;
11761
12596
  REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
11762
12597
  MAX_PII_SCAN_BYTES = 1e5;
11763
12598
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
@@ -12099,6 +12934,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
12099
12934
  ...transcriptPathField,
12100
12935
  ...taintFields,
12101
12936
  ...exposureFields,
12937
+ ...meta?.decidedBy && { decidedBy: meta.decidedBy },
12938
+ ...meta?.channel && { channel: meta.channel },
12939
+ ...meta?.cardId && { cardId: meta.cardId },
12102
12940
  ...testRun,
12103
12941
  agent: meta?.agent,
12104
12942
  mcpServer: meta?.mcpServer,
@@ -14475,9 +15313,159 @@ function getPersistentDecision(toolName) {
14475
15313
 
14476
15314
  // src/auth/daemon.ts
14477
15315
  import fs10 from "fs";
15316
+ import http from "http";
14478
15317
  import net from "net";
14479
15318
  import path10 from "path";
14480
15319
  import os8 from "os";
15320
+
15321
+ // src/auth/decision-record.ts
15322
+ var CHANNELS = ["terminal", "monitor", "native", "cloud"];
15323
+ var PERSON_CHANNELS = /* @__PURE__ */ new Set([
15324
+ "terminal",
15325
+ "monitor",
15326
+ "native"
15327
+ ]);
15328
+ var CARD_PROTOCOL = 2;
15329
+ var PERSON_LABELS = {
15330
+ terminal: "User Decision (Terminal (node9 tail))",
15331
+ monitor: "User Decision (node9 monitor)",
15332
+ native: "User Decision (Native)"
15333
+ };
15334
+ var REDIRECT_LABEL = "Steered Redirect (Terminal)";
15335
+ var TIMER_LABEL = "Approval Timeout";
15336
+ var CLOUD_LABEL = "Organization Policy (SaaS)";
15337
+ var ABANDONED_LABEL = "No Approver (node9 tail closed)";
15338
+ function personRecord(channel, decision, extra = {}, now = Date.now()) {
15339
+ return {
15340
+ decision,
15341
+ decidedBy: "person",
15342
+ channel,
15343
+ label: extra.redirect ? REDIRECT_LABEL : PERSON_LABELS[channel],
15344
+ ...extra,
15345
+ decidedAt: now
15346
+ };
15347
+ }
15348
+ function timerRecord(timeoutMs, now = Date.now()) {
15349
+ return {
15350
+ decision: "deny",
15351
+ decidedBy: "timer",
15352
+ label: TIMER_LABEL,
15353
+ reason: `No response within ${Math.round(timeoutMs / 1e3)}s: auto-denied by the approval timeout.`,
15354
+ decidedAt: now
15355
+ };
15356
+ }
15357
+ function abandonedRecord(now = Date.now()) {
15358
+ return {
15359
+ decision: "deny",
15360
+ decidedBy: "abandoned",
15361
+ label: ABANDONED_LABEL,
15362
+ reason: "No approver was left to answer: node9 tail was closed.",
15363
+ decidedAt: now
15364
+ };
15365
+ }
15366
+ var DECIDED_BY = /* @__PURE__ */ new Set([
15367
+ "person",
15368
+ "timer",
15369
+ "cloud",
15370
+ "policy",
15371
+ "abandoned"
15372
+ ]);
15373
+ var STANDING = /* @__PURE__ */ new Set([
15374
+ "always-allow",
15375
+ "always-deny",
15376
+ "trust",
15377
+ "trust-recipients"
15378
+ ]);
15379
+ function boundedString(v, max) {
15380
+ return typeof v === "string" && v.length > 0 ? v.slice(0, max) : void 0;
15381
+ }
15382
+ function parseDecisionRecord(x) {
15383
+ if (!x || typeof x !== "object") return null;
15384
+ const r = x;
15385
+ if (r.decision !== "allow" && r.decision !== "deny") return null;
15386
+ if (typeof r.decidedBy !== "string" || !DECIDED_BY.has(r.decidedBy)) return null;
15387
+ const label2 = boundedString(r.label, 200);
15388
+ if (!label2) return null;
15389
+ const channel = typeof r.channel === "string" && CHANNELS.includes(r.channel) ? r.channel : void 0;
15390
+ if (r.decidedBy === "person" && (!channel || !PERSON_CHANNELS.has(channel))) return null;
15391
+ const out = {
15392
+ decision: r.decision,
15393
+ decidedBy: r.decidedBy,
15394
+ label: label2,
15395
+ decidedAt: typeof r.decidedAt === "number" && Number.isFinite(r.decidedAt) ? r.decidedAt : 0
15396
+ };
15397
+ if (channel) out.channel = channel;
15398
+ const redirect = boundedString(r.redirect, 2e3);
15399
+ if (redirect) out.redirect = redirect;
15400
+ const reason = boundedString(r.reason, 2e3);
15401
+ if (reason) out.reason = reason;
15402
+ const ruleName = boundedString(r.ruleName, 256);
15403
+ if (ruleName) out.ruleName = ruleName;
15404
+ if (typeof r.standing === "string" && STANDING.has(r.standing))
15405
+ out.standing = r.standing;
15406
+ const trustDuration = boundedString(r.trustDuration, 8);
15407
+ if (trustDuration) out.trustDuration = trustDuration;
15408
+ return out;
15409
+ }
15410
+ function legacyWaitFields(record) {
15411
+ switch (record.decidedBy) {
15412
+ case "person": {
15413
+ const source = record.redirect ? "terminal-redirect" : record.channel === "native" ? "native" : "terminal";
15414
+ const reason = record.redirect ?? record.reason;
15415
+ return { decision: record.decision, source, ...reason && { reason } };
15416
+ }
15417
+ case "timer":
15418
+ return {
15419
+ decision: record.decision,
15420
+ source: "timeout",
15421
+ ...record.reason && { reason: record.reason }
15422
+ };
15423
+ case "cloud":
15424
+ case "policy":
15425
+ return {
15426
+ decision: record.decision,
15427
+ source: record.decidedBy,
15428
+ ...record.reason && { reason: record.reason }
15429
+ };
15430
+ case "abandoned":
15431
+ return { decision: "abandoned" };
15432
+ }
15433
+ }
15434
+ function recordFromLegacyWait(body, now = Date.now()) {
15435
+ if (body.decision === "abandoned") return abandonedRecord(now);
15436
+ const decision = body.decision;
15437
+ switch (body.source) {
15438
+ case "terminal":
15439
+ case "browser":
15440
+ return personRecord("terminal", decision, body.reason ? { reason: body.reason } : {}, now);
15441
+ case "native":
15442
+ return personRecord("native", decision, {}, now);
15443
+ case "terminal-redirect":
15444
+ return personRecord("terminal", decision, body.reason ? { redirect: body.reason } : {}, now);
15445
+ case "timeout":
15446
+ return { ...timerRecord(0, now), ...body.reason && { reason: body.reason } };
15447
+ case "cloud":
15448
+ return {
15449
+ decision,
15450
+ decidedBy: "cloud",
15451
+ label: CLOUD_LABEL,
15452
+ ...body.reason && { reason: body.reason },
15453
+ decidedAt: now
15454
+ };
15455
+ case "policy":
15456
+ return {
15457
+ decision,
15458
+ decidedBy: "policy",
15459
+ label: "Policy",
15460
+ ...body.reason && { reason: body.reason },
15461
+ decidedAt: now
15462
+ };
15463
+ default:
15464
+ return null;
15465
+ }
15466
+ }
15467
+
15468
+ // src/auth/daemon.ts
14481
15469
  var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
14482
15470
  function notifyActivitySocket(data) {
14483
15471
  return new Promise((resolve) => {
@@ -14575,7 +15563,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
14575
15563
  return false;
14576
15564
  }
14577
15565
  }
14578
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
15566
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId, card) {
14579
15567
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
14580
15568
  const ctrl = new AbortController();
14581
15569
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -14601,19 +15589,43 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
14601
15589
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
14602
15590
  ...viewOnly && { viewOnly: true },
14603
15591
  ...localSmartRuleMatched && { localSmartRuleMatched: true },
14604
- ...sessionId && { sessionId }
15592
+ ...sessionId && { sessionId },
15593
+ ...cardFields(card)
14605
15594
  }),
14606
15595
  signal: ctrl.signal
14607
15596
  });
14608
15597
  if (!res.ok) throw new Error("Daemon fail");
14609
- const { id, allowCount } = await res.json();
14610
- return { id, allowCount: allowCount ?? 1 };
15598
+ return registeredCard(await res.json());
14611
15599
  } finally {
14612
15600
  clearTimeout(timer);
14613
15601
  }
14614
15602
  }
15603
+ function cardFields(card) {
15604
+ return {
15605
+ // A review card: the hook already decided this call needs a person; the
15606
+ // daemon never checks it again (approval-card design, defect 1).
15607
+ kind: "review",
15608
+ // Protocol 2 makes the card's /wait require the internal token: claim it
15609
+ // only when this process can read the token (a daemon under another uid
15610
+ // is running but its pid file probe fails with EPERM), or every wait
15611
+ // would get 403 and the review would be denied unseen.
15612
+ ...getInternalToken() && { protocol: CARD_PROTOCOL },
15613
+ ...card?.channels && { channels: card.channels },
15614
+ ...typeof card?.timeoutMs === "number" && { timeoutMs: card.timeoutMs }
15615
+ };
15616
+ }
15617
+ function registeredCard(body) {
15618
+ const b = body;
15619
+ return {
15620
+ id: b.id,
15621
+ allowCount: b.allowCount ?? 1,
15622
+ ...b.protocol === CARD_PROTOCOL && {
15623
+ protocol: CARD_PROTOCOL,
15624
+ deadline: typeof b.deadline === "number" ? b.deadline : null
15625
+ }
15626
+ };
15627
+ }
14615
15628
  async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
14616
- const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
14617
15629
  const waitCtrl = new AbortController();
14618
15630
  let capped = false;
14619
15631
  const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
@@ -14623,14 +15635,18 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
14623
15635
  const onAbort = () => waitCtrl.abort();
14624
15636
  if (signal) signal.addEventListener("abort", onAbort);
14625
15637
  try {
14626
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
15638
+ const { status, body } = await daemonGet(`/wait/${id}`, waitCtrl.signal).catch(
14627
15639
  (err) => {
14628
15640
  if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
14629
15641
  throw err;
14630
15642
  }
14631
15643
  );
14632
- if (!waitRes.ok) return { decision: "deny" };
14633
- const { decision, source, reason } = await waitRes.json();
15644
+ if (status === 409) throw new Error("node9 card was withdrawn");
15645
+ if (status !== 200) return { decision: "deny" };
15646
+ const parsed = JSON.parse(body);
15647
+ const record = parseDecisionRecord(parsed.record);
15648
+ if (record) return { ...legacyWaitFields(record), record };
15649
+ const { decision, source, reason } = parsed;
14634
15650
  if (decision === "allow") return { decision: "allow", source };
14635
15651
  if (decision === "abandoned") return { decision: "abandoned", source };
14636
15652
  return { decision: "deny", source, reason };
@@ -14639,7 +15655,29 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
14639
15655
  if (signal) signal.removeEventListener("abort", onAbort);
14640
15656
  }
14641
15657
  }
14642
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
15658
+ function daemonGet(urlPath, signal, token = getInternalToken()) {
15659
+ return new Promise((resolve, reject) => {
15660
+ const req = http.get(
15661
+ {
15662
+ host: DAEMON_HOST,
15663
+ port: DAEMON_PORT,
15664
+ path: urlPath,
15665
+ signal,
15666
+ agent: false,
15667
+ headers: token ? { "X-Node9-Internal": token } : {}
15668
+ },
15669
+ (res) => {
15670
+ let body = "";
15671
+ res.setEncoding("utf8");
15672
+ res.on("data", (chunk) => body += chunk);
15673
+ res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
15674
+ res.on("error", reject);
15675
+ }
15676
+ );
15677
+ req.on("error", reject);
15678
+ });
15679
+ }
15680
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent, extra) {
14643
15681
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
14644
15682
  const res = await fetch(`${base2}/check`, {
14645
15683
  method: "POST",
@@ -14655,13 +15693,16 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
14655
15693
  // event via socket. Same contract as registerDaemonEntry — without
14656
15694
  // it the daemon double-emits 'activity' for cloud-enforced flows.
14657
15695
  fromCLI: socketActivitySent !== false,
14658
- activityId
15696
+ activityId,
15697
+ ...extra?.cwd && { cwd: extra.cwd },
15698
+ ...extra?.recoveryCommand && { recoveryCommand: extra.recoveryCommand },
15699
+ ...extra?.sessionId && { sessionId: extra.sessionId },
15700
+ ...cardFields(extra?.card)
14659
15701
  }),
14660
15702
  signal: AbortSignal.timeout(3e3)
14661
15703
  });
14662
15704
  if (!res.ok) throw new Error("Daemon unreachable");
14663
- const { id, allowCount } = await res.json();
14664
- return { id, allowCount: allowCount ?? 1 };
15705
+ return registeredCard(await res.json());
14665
15706
  }
14666
15707
  async function notifyTaint(filePath, source, fromEid) {
14667
15708
  if (!isDaemonRunning()) return;
@@ -14754,14 +15795,18 @@ async function approveSessionExposure(sessionId, keys) {
14754
15795
  } catch {
14755
15796
  }
14756
15797
  }
14757
- async function resolveViaDaemon(id, decision, internalToken, source) {
15798
+ async function resolveViaDaemon(id, decision, internalToken, source, record) {
14758
15799
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
14759
- await fetch(`${base2}/resolve/${id}`, {
15800
+ const res = await fetch(`${base2}/resolve/${id}`, {
14760
15801
  method: "POST",
14761
15802
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
14762
- body: JSON.stringify({ decision, ...source && { source } }),
15803
+ body: JSON.stringify({ decision, ...source && { source }, ...record && { record } }),
14763
15804
  signal: AbortSignal.timeout(3e3)
14764
15805
  });
15806
+ if (!res.ok) return null;
15807
+ const body = await res.json().catch(() => null);
15808
+ const winner = parseDecisionRecord(body?.record);
15809
+ return winner && typeof body?.won === "boolean" ? { won: body.won, record: winner } : null;
14765
15810
  }
14766
15811
 
14767
15812
  // src/auth/orchestrator.ts
@@ -15878,7 +16923,17 @@ function ecoDir(eco) {
15878
16923
  return path23.join(osvRoot(), eco);
15879
16924
  }
15880
16925
  function indexKey(eco, name) {
15881
- return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
16926
+ switch (eco) {
16927
+ case "PyPI":
16928
+ return normalizePyPiName(name);
16929
+ case "crates.io":
16930
+ return name.toLowerCase().replace(/_/g, "-");
16931
+ case "Go":
16932
+ case "Maven":
16933
+ return name;
16934
+ default:
16935
+ return name.toLowerCase();
16936
+ }
15882
16937
  }
15883
16938
  function shardOf(key) {
15884
16939
  return createHash4("sha1").update(key).digest("hex").slice(0, 2);
@@ -16089,8 +17144,11 @@ async function pypiInfo(name, version) {
16089
17144
  publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
16090
17145
  };
16091
17146
  }
17147
+ var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
16092
17148
  async function registryInfo(eco, name, version, freshWindowMs, raw) {
16093
- return eco === "npm" ? npmInfo(name, version, freshWindowMs, raw) : pypiInfo(name, version);
17149
+ if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
17150
+ if (eco === "PyPI") return pypiInfo(name, version);
17151
+ return null;
16094
17152
  }
16095
17153
 
16096
17154
  // src/supply-chain/local-resolve.ts
@@ -16129,26 +17187,51 @@ function commandOf(toolName, args) {
16129
17187
  const a = args;
16130
17188
  return typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : null;
16131
17189
  }
17190
+ var KIND = {
17191
+ npm: "npm package",
17192
+ PyPI: "PyPI package",
17193
+ RubyGems: "RubyGems package",
17194
+ "crates.io": "crates.io package",
17195
+ Go: "Go module",
17196
+ NuGet: "NuGet package",
17197
+ Maven: "Maven artifact",
17198
+ Packagist: "Packagist package",
17199
+ VSCode: "VS Code extension"
17200
+ };
16132
17201
  function label(p, version) {
16133
- return `${p.ecosystem === "npm" ? "npm" : "PyPI"} package "${p.name}${version ? `@${version}` : ""}"`;
17202
+ return `${KIND[p.ecosystem]} "${p.name}${version ? `@${version}` : ""}"`;
17203
+ }
17204
+ function osvNames(p) {
17205
+ if (p.ecosystem !== "Go") return [p.name];
17206
+ const parts = p.name.split("/");
17207
+ const out = [];
17208
+ for (let n = parts.length; n >= 2 && out.length < 6; n--) out.push(parts.slice(0, n).join("/"));
17209
+ return out;
16134
17210
  }
16135
17211
  async function maliciousFor(p, version, cfg, misses, networkCapped) {
16136
- const local = lookupIndex(p.ecosystem, p.name);
16137
- const entries = local.status === "hit" ? local.entries : null;
16138
- if (!entries && !(local.status === "clean" && local.fresh)) {
17212
+ const names = osvNames(p);
17213
+ const local = names.map((n) => lookupIndex(p.ecosystem, n));
17214
+ const hits2 = local.flatMap((l) => l.status === "hit" ? l.entries : []);
17215
+ const entries = hits2.length > 0 ? hits2 : null;
17216
+ const freshClean = local.every((l) => l.status === "clean" && l.fresh);
17217
+ const status = local.some((l) => l.status === "unavailable") ? "unavailable" : local[0].status;
17218
+ if (!entries && !freshClean) {
16139
17219
  if (!cfg.onlineFallback || networkCapped) {
16140
17220
  misses.push(
16141
- `${label(p, version)}: local index ${local.status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
17221
+ `${label(p, version)}: local index ${status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
16142
17222
  );
16143
17223
  return null;
16144
17224
  }
16145
- let ids = null;
17225
+ let perName = null;
16146
17226
  try {
16147
- ids = await queryOsvMalicious([{ ecosystem: p.ecosystem, name: p.name, version }]);
17227
+ perName = await queryOsvMalicious(
17228
+ names.map((name) => ({ ecosystem: p.ecosystem, name, version }))
17229
+ );
16148
17230
  } catch (err) {
16149
17231
  misses.push(`${label(p, version)}: OSV online ${err.message}`);
16150
17232
  return null;
16151
17233
  }
17234
+ const ids = perName ? [[...new Set(perName.flat())]] : null;
16152
17235
  if (!ids) {
16153
17236
  misses.push(`${label(p, version)}: OSV online unavailable`);
16154
17237
  return null;
@@ -16218,7 +17301,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
16218
17301
  }
16219
17302
  const findings = [];
16220
17303
  let info = null;
16221
- const wantsRegistry = cfg.newPackage !== "off" || cfg.installScript !== "off";
17304
+ const wantsRegistry = (cfg.newPackage !== "off" || cfg.installScript !== "off") && HAS_REGISTRY_CLIENT.has(p.ecosystem);
16222
17305
  if (wantsRegistry && !networkCapped) {
16223
17306
  try {
16224
17307
  const windowMs = cfg.newPackage === "off" ? 0 : cfg.maxAgeHours * 36e5;
@@ -16317,17 +17400,98 @@ function asApprovalRequest(description) {
16317
17400
  return description;
16318
17401
  }
16319
17402
 
16320
- // src/auth/decision-sources.ts
16321
- var HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
16322
- "terminal",
16323
- "browser",
16324
- "native"
16325
- ]);
16326
- var REDIRECT_SOURCE = "terminal-redirect";
16327
- var DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
16328
- ...HUMAN_DECISION_SOURCES,
16329
- REDIRECT_SOURCE
16330
- ]);
17403
+ // src/auth/card-result.ts
17404
+ var CHANNEL_NAMES = {
17405
+ terminal: "Terminal (node9 tail)",
17406
+ monitor: "node9 monitor",
17407
+ native: "system dialog window",
17408
+ cloud: "organization"
17409
+ };
17410
+ function authResultFromRecord(r, ctx = {}) {
17411
+ const who = { decidedBy: r.decidedBy, ...r.channel && { channel: r.channel } };
17412
+ switch (r.decidedBy) {
17413
+ case "person": {
17414
+ const channel = r.channel ?? "terminal";
17415
+ const decisionSource = channel === "native" ? "native" : "terminal";
17416
+ if (r.decision === "allow") {
17417
+ return { approved: true, checkedBy: "daemon", decisionSource, ...who };
17418
+ }
17419
+ return {
17420
+ approved: false,
17421
+ reason: r.redirect || `The human user rejected this action via the Node9 ${CHANNEL_NAMES[channel]}.`,
17422
+ blockedBy: "local-decision",
17423
+ blockedByLabel: r.label,
17424
+ decisionSource,
17425
+ ...who,
17426
+ ...r.redirect && { redirect: r.redirect }
17427
+ };
17428
+ }
17429
+ case "timer":
17430
+ if (r.decision === "deny" && ctx.downgradedBlock)
17431
+ return { ...ctx.downgradedBlock, blockedBy: "timeout", ...who };
17432
+ return r.decision === "allow" ? { approved: true, ...who } : {
17433
+ approved: false,
17434
+ reason: ctx.timeoutReason || r.reason || "No response: auto-denied after the approval timeout.",
17435
+ blockedBy: "timeout",
17436
+ blockedByLabel: TIMER_LABEL,
17437
+ ...who
17438
+ };
17439
+ case "cloud":
17440
+ return r.decision === "allow" ? { approved: true, checkedBy: "cloud", decisionSource: "cloud", ...who } : {
17441
+ approved: false,
17442
+ reason: r.reason || "Action rejected by organization administrator via Slack.",
17443
+ blockedBy: "team-policy",
17444
+ blockedByLabel: CLOUD_LABEL,
17445
+ decisionSource: "cloud",
17446
+ ...who
17447
+ };
17448
+ case "policy":
17449
+ return r.decision === "allow" ? { approved: true, checkedBy: "local-policy", ...who } : {
17450
+ approved: false,
17451
+ reason: r.reason || "Action blocked by policy.",
17452
+ blockedBy: "local-config",
17453
+ blockedByLabel: r.label,
17454
+ ...r.ruleName && { ruleHit: r.ruleName },
17455
+ ...who
17456
+ };
17457
+ case "abandoned":
17458
+ return {
17459
+ approved: false,
17460
+ reason: r.reason || "No approver was left to answer.",
17461
+ blockedBy: "no-approval-mechanism",
17462
+ blockedByLabel: ABANDONED_LABEL,
17463
+ ...who
17464
+ };
17465
+ }
17466
+ }
17467
+ function approvalUnavailable(reason) {
17468
+ return {
17469
+ approved: false,
17470
+ reason: reason || "node9 could not get an approval decision for this call.",
17471
+ blockedBy: "no-approval-mechanism",
17472
+ blockedByLabel: "Approval Unavailable"
17473
+ };
17474
+ }
17475
+ function decidedByOf(res) {
17476
+ if (res.decidedBy) return res.decidedBy;
17477
+ if (res.blockedBy === "timeout") return "timer";
17478
+ if (res.checkedBy === "cloud" || res.blockedBy === "team-policy") return "cloud";
17479
+ return "policy";
17480
+ }
17481
+ function recordFromAuthResult(res, now = Date.now()) {
17482
+ const decidedBy = decidedByOf(res);
17483
+ const channel = res.channel ?? (decidedBy === "cloud" ? "cloud" : decidedBy === "person" ? "native" : void 0);
17484
+ return {
17485
+ decision: res.approved ? "allow" : "deny",
17486
+ decidedBy,
17487
+ ...channel && { channel },
17488
+ label: res.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : decidedBy === "timer" ? TIMER_LABEL : res.approved ? "Allowed" : "Blocked"),
17489
+ ...res.reason && { reason: res.reason.slice(0, 2e3) },
17490
+ ...res.ruleHit && { ruleName: res.ruleHit },
17491
+ ...res.redirect && { redirect: res.redirect },
17492
+ decidedAt: now
17493
+ };
17494
+ }
16331
17495
 
16332
17496
  // src/auth/orchestrator.ts
16333
17497
  var SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
@@ -17367,28 +18531,22 @@ ${packageReview}`;
17367
18531
  const { signal } = abortController;
17368
18532
  const racePromises = [];
17369
18533
  const approvalTimeoutMs = config.settings.approvalTimeoutMs ?? 0;
17370
- if (approvalTimeoutMs > 0) {
17371
- racePromises.push(
17372
- new Promise((resolve, reject) => {
17373
- const timer = setTimeout(() => {
17374
- resolve({
17375
- approved: false,
17376
- reason: approvalTimeoutReason(approvalTimeoutMs),
17377
- blockedBy: "timeout",
17378
- blockedByLabel: "Approval Timeout"
17379
- });
17380
- }, approvalTimeoutMs);
17381
- signal.addEventListener("abort", () => {
17382
- clearTimeout(timer);
17383
- reject(new Error("Aborted"));
17384
- });
17385
- })
17386
- );
17387
- }
17388
18534
  let viewerId = null;
17389
18535
  const internalToken = getInternalToken();
17390
18536
  let daemonEntryId = null;
17391
18537
  let daemonAllowCount = 1;
18538
+ let cardDeadline;
18539
+ const cardOptions = {
18540
+ channels: [
18541
+ "terminal",
18542
+ "monitor",
18543
+ // Only when the popup can show: a card listing a channel nobody can
18544
+ // use is never abandoned when node9 tail closes, and waits unanswered.
18545
+ ...approvers.native && !isManual && !options?.calledFromDaemon && (process.platform === "darwin" || process.platform === "win32" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) ? ["native"] : [],
18546
+ ...cloudEnforced && cloudRequestId && cloudMayVote ? ["cloud"] : []
18547
+ ],
18548
+ timeoutMs: approvalTimeoutMs
18549
+ };
17392
18550
  if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
17393
18551
  if (cloudEnforced && cloudRequestId) {
17394
18552
  const viewer = await notifyDaemonViewer(
@@ -17397,11 +18555,20 @@ ${packageReview}`;
17397
18555
  meta,
17398
18556
  riskMetadata,
17399
18557
  options?.activityId,
17400
- options?.socketActivitySent
18558
+ options?.socketActivitySent,
18559
+ {
18560
+ cwd: options?.cwd,
18561
+ recoveryCommand: statefulRecoveryCommand,
18562
+ sessionId: meta?.sessionId,
18563
+ card: cardOptions
18564
+ }
17401
18565
  ).catch(() => null);
17402
18566
  viewerId = viewer?.id ?? null;
17403
18567
  daemonEntryId = viewerId;
17404
- if (viewer) daemonAllowCount = viewer.allowCount;
18568
+ if (viewer) {
18569
+ daemonAllowCount = viewer.allowCount;
18570
+ cardDeadline = viewer.deadline;
18571
+ }
17405
18572
  } else {
17406
18573
  try {
17407
18574
  const entry = await registerDaemonEntry(
@@ -17425,14 +18592,35 @@ ${packageReview}`;
17425
18592
  void 0,
17426
18593
  localSmartRuleMatched || options?.localSmartRuleMatched,
17427
18594
  options?.socketActivitySent,
17428
- meta?.sessionId
18595
+ meta?.sessionId,
18596
+ cardOptions
17429
18597
  );
17430
18598
  daemonEntryId = entry.id;
17431
18599
  daemonAllowCount = entry.allowCount;
18600
+ cardDeadline = entry.deadline;
17432
18601
  } catch {
17433
18602
  }
17434
18603
  }
17435
18604
  }
18605
+ if (approvalTimeoutMs > 0 && cardDeadline === void 0) {
18606
+ racePromises.push(
18607
+ new Promise((resolve, reject) => {
18608
+ const timer = setTimeout(() => {
18609
+ resolve({
18610
+ approved: false,
18611
+ reason: approvalTimeoutReason(approvalTimeoutMs),
18612
+ blockedBy: "timeout",
18613
+ blockedByLabel: "Approval Timeout",
18614
+ decidedBy: "timer"
18615
+ });
18616
+ }, approvalTimeoutMs);
18617
+ signal.addEventListener("abort", () => {
18618
+ clearTimeout(timer);
18619
+ reject(new Error("Aborted"));
18620
+ });
18621
+ })
18622
+ );
18623
+ }
17436
18624
  if (cloudEnforced && cloudRequestId && cloudMayVote) {
17437
18625
  racePromises.push(
17438
18626
  (async () => {
@@ -17443,7 +18631,9 @@ ${packageReview}`;
17443
18631
  reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
17444
18632
  checkedBy: cloudResult.approved ? "cloud" : void 0,
17445
18633
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
17446
- blockedByLabel: "Organization Policy (SaaS)"
18634
+ blockedByLabel: "Organization Policy (SaaS)",
18635
+ decidedBy: "cloud",
18636
+ channel: "cloud"
17447
18637
  };
17448
18638
  } catch (err) {
17449
18639
  const error = err;
@@ -17473,14 +18663,31 @@ ${packageReview}`;
17473
18663
  if (config.policySource !== "workspace") {
17474
18664
  for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
17475
18665
  }
17476
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
18666
+ return {
18667
+ approved: true,
18668
+ checkedBy: "daemon",
18669
+ decisionSource: "native",
18670
+ decidedBy: "person",
18671
+ channel: "native"
18672
+ };
17477
18673
  }
17478
18674
  if (decision === "always_allow") {
17479
18675
  if (standingTrustApplies()) {
17480
18676
  writeTrustSession(toolName, 36e5, args);
17481
- return { approved: true, checkedBy: "trust" };
18677
+ return {
18678
+ approved: true,
18679
+ checkedBy: "trust",
18680
+ decidedBy: "person",
18681
+ channel: "native"
18682
+ };
17482
18683
  }
17483
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
18684
+ return {
18685
+ approved: true,
18686
+ checkedBy: "daemon",
18687
+ decisionSource: "native",
18688
+ decidedBy: "person",
18689
+ channel: "native"
18690
+ };
17484
18691
  }
17485
18692
  const isApproved = decision === "allow";
17486
18693
  return {
@@ -17489,75 +18696,32 @@ ${packageReview}`;
17489
18696
  checkedBy: isApproved ? "daemon" : void 0,
17490
18697
  blockedBy: isApproved ? void 0 : "local-decision",
17491
18698
  blockedByLabel: "User Decision (Native)",
17492
- decisionSource: "native"
18699
+ decisionSource: "native",
18700
+ decidedBy: "person",
18701
+ channel: "native"
17493
18702
  };
17494
18703
  })()
17495
18704
  );
17496
18705
  }
18706
+ const fromCard = /* @__PURE__ */ new WeakSet();
18707
+ const timeoutReason = approvalTimeoutMs > 0 ? approvalTimeoutReason(approvalTimeoutMs) : void 0;
17497
18708
  if (daemonEntryId && approvers.terminal) {
17498
18709
  racePromises.push(
17499
18710
  (async () => {
17500
- const {
17501
- decision: daemonDecision,
17502
- source: decisionSource,
17503
- reason: daemonReason
17504
- } = await waitForDaemonDecision(
17505
- daemonEntryId,
17506
- signal,
17507
- daemonWaitCapMs(config.settings.approvalTimeoutMs)
17508
- );
17509
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
17510
- const isApproved = daemonDecision === "allow";
17511
- const isRedirect = decisionSource === REDIRECT_SOURCE;
17512
- if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
17513
- if (decisionSource === "cloud")
17514
- return {
17515
- approved: false,
17516
- reason: daemonReason || "Action rejected by organization administrator via Slack.",
17517
- blockedBy: "team-policy",
17518
- blockedByLabel: "Organization Policy (SaaS)",
17519
- decisionSource: "cloud"
17520
- };
17521
- const timedOut = decisionSource === "timeout";
17522
- if (downgradedBlock)
17523
- return {
17524
- ...downgradedBlock,
17525
- blockedBy: timedOut ? "timeout" : "local-config"
17526
- };
17527
- if (timedOut)
17528
- return {
17529
- approved: false,
17530
- reason: daemonReason || "No response: auto-denied after the approval timeout.",
17531
- blockedBy: "timeout",
17532
- blockedByLabel: "Approval Timeout"
17533
- };
17534
- if (decisionSource === "policy")
17535
- return {
17536
- approved: false,
17537
- reason: daemonReason || "Action blocked by policy.",
17538
- blockedBy: "local-config",
17539
- blockedByLabel: explainableLabel
17540
- };
17541
- return {
17542
- approved: false,
17543
- reason: daemonReason || "node9 could not get an approval decision for this call.",
17544
- blockedBy: "no-approval-mechanism",
17545
- blockedByLabel: "Approval Unavailable"
17546
- };
18711
+ const cap = cardDeadline === void 0 ? daemonWaitCapMs(config.settings.approvalTimeoutMs) : cardDeadline === null ? 0 : Math.max(cardDeadline - Date.now(), 0) + 5e3;
18712
+ const outcome = await waitForDaemonDecision(daemonEntryId, signal, cap);
18713
+ const legacy = !outcome.record;
18714
+ const record = outcome.record ?? recordFromLegacyWait(outcome);
18715
+ if (downgradedBlock && (!record || legacy && record.decidedBy === "policy" && record.decision === "deny")) {
18716
+ const softened = { ...downgradedBlock, blockedBy: "local-config" };
18717
+ fromCard.add(softened);
18718
+ return softened;
17547
18719
  }
17548
- if (isApproved && decisionSource === "cloud")
17549
- return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
17550
- if (isApproved && decisionSource === "policy")
17551
- return { approved: true, checkedBy: "local-policy" };
17552
- const via = "Terminal (node9 tail)";
17553
- return {
17554
- approved: isApproved,
17555
- reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
17556
- checkedBy: isApproved ? "daemon" : void 0,
17557
- blockedBy: isApproved ? void 0 : "local-decision",
17558
- blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
17559
- decisionSource: "terminal"
17560
- };
18720
+ if (!record) return approvalUnavailable(outcome.reason);
18721
+ if (legacy && record.decidedBy === "policy") record.label = explainableLabel;
18722
+ const result = authResultFromRecord(record, { downgradedBlock, timeoutReason });
18723
+ fromCard.add(result);
18724
+ return result;
17561
18725
  })()
17562
18726
  );
17563
18727
  }
@@ -17589,7 +18753,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
17589
18753
  blockedByLabel: explainableLabel
17590
18754
  };
17591
18755
  }
17592
- const finalResult = await new Promise((resolve) => {
18756
+ const raceResult = await new Promise((resolve) => {
17593
18757
  let resolved = false;
17594
18758
  let failures = 0;
17595
18759
  const total = racePromises.length;
@@ -17597,14 +18761,6 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
17597
18761
  if (!resolved) {
17598
18762
  resolved = true;
17599
18763
  abortController.abort();
17600
- if (daemonEntryId && internalToken) {
17601
- resolveViaDaemon(
17602
- daemonEntryId,
17603
- res.approved ? "allow" : "deny",
17604
- internalToken,
17605
- res.decisionSource
17606
- ).catch(() => null);
17607
- }
17608
18764
  resolve(res);
17609
18765
  }
17610
18766
  };
@@ -17612,26 +18768,29 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
17612
18768
  p.then(finish).catch((err) => {
17613
18769
  if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
17614
18770
  return;
17615
- if (err.message === "Abandoned") {
17616
- finish({
17617
- approved: false,
17618
- reason: "Browser dashboard closed without making a decision.",
17619
- blockedBy: "local-decision",
17620
- blockedByLabel: "Browser Dashboard (Abandoned)"
17621
- });
17622
- return;
17623
- }
17624
18771
  failures++;
17625
18772
  if (failures === total && !resolved) {
17626
- finish({ approved: false, reason: "All approval channels failed or disconnected." });
18773
+ finish(approvalUnavailable("All approval channels failed or disconnected."));
17627
18774
  }
17628
18775
  });
17629
18776
  }
17630
18777
  });
18778
+ let finalResult = raceResult;
18779
+ if (daemonEntryId && internalToken && !fromCard.has(raceResult)) {
18780
+ const reply = await resolveViaDaemon(
18781
+ daemonEntryId,
18782
+ raceResult.approved ? "allow" : "deny",
18783
+ internalToken,
18784
+ raceResult.decisionSource,
18785
+ recordFromAuthResult(raceResult)
18786
+ ).catch(() => null);
18787
+ if (reply && !reply.won)
18788
+ finalResult = authResultFromRecord(reply.record, { downgradedBlock, timeoutReason });
18789
+ }
17631
18790
  if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
17632
18791
  await approveSessionExposure(meta.sessionId, exposureKeys);
17633
18792
  }
17634
- if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
18793
+ if (cloudRequestId && creds && finalResult.checkedBy !== "cloud" && finalResult.decidedBy !== "cloud") {
17635
18794
  await resolveNode9SaaS(
17636
18795
  cloudRequestId,
17637
18796
  creds,
@@ -17655,7 +18814,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
17655
18814
  {
17656
18815
  ...ownRowMeta(finalResult.approved),
17657
18816
  ...cloudRequestId && { cloudRequestId },
17658
- ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
18817
+ ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` },
18818
+ // Who decided, from the decision record, and the card: the daemon
18819
+ // writes its own row for the same card, and the report counts one.
18820
+ ...finalResult.decidedBy && { decidedBy: finalResult.decidedBy },
18821
+ ...finalResult.channel && { channel: finalResult.channel },
18822
+ ...daemonEntryId && { cardId: daemonEntryId }
17659
18823
  },
17660
18824
  hashAuditArgs
17661
18825
  );