@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/cli.mjs CHANGED
@@ -1210,6 +1210,120 @@ function expandPlainParam(p) {
1210
1210
  if (recorded !== void 0) return recorded;
1211
1211
  return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
1212
1212
  }
1213
+ function functionBodies(f) {
1214
+ const out = /* @__PURE__ */ new Map();
1215
+ syntax.Walk(f, (node) => {
1216
+ const n = node;
1217
+ if (n && syntax.NodeType(n) === "FuncDecl" && n.Name?.Value && n.Body) {
1218
+ const list = out.get(n.Name.Value) ?? [];
1219
+ list.push(n.Body);
1220
+ out.set(n.Name.Value, list);
1221
+ }
1222
+ return true;
1223
+ });
1224
+ return out;
1225
+ }
1226
+ function rebindsPositionals(body) {
1227
+ let found = false;
1228
+ syntax.Walk(body, (node) => {
1229
+ const n = node;
1230
+ if (found || !n) return false;
1231
+ if (syntax.NodeType(n) === "CallExpr") {
1232
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1233
+ if (words[0] === "shift") found = true;
1234
+ else if (words[0] === "set" && words.slice(1).some((w) => w === null || w === "--" || !/^[-+]/.test(w)))
1235
+ found = true;
1236
+ }
1237
+ return !found;
1238
+ });
1239
+ return found;
1240
+ }
1241
+ function tableBindings() {
1242
+ if (!assignmentTable) return "";
1243
+ const out = [];
1244
+ for (const [name, rec] of assignmentTable)
1245
+ if (rec.at < currentStmtOffset) out.push(`${name}=${rec.value ?? "\0"}`);
1246
+ return out.sort().join("");
1247
+ }
1248
+ function walkWithCalls(f, visit2) {
1249
+ const bodies = functionBodies(f);
1250
+ if (bodies.size === 0) {
1251
+ syntax.Walk(f, visit2);
1252
+ return;
1253
+ }
1254
+ const seenCalls = /* @__PURE__ */ new Set();
1255
+ const inline = (call) => {
1256
+ const words = (call.Args || []).map((a) => resolveWordLiteral(a));
1257
+ const list = words[0] ? bodies.get(words[0]) : void 0;
1258
+ if (!list || callDepth >= MAX_CALL_DEPTH) return;
1259
+ const args = words.slice(1);
1260
+ const key = `${words[0]}\0${JSON.stringify(args)}\0${tableBindings()}`;
1261
+ if (seenCalls.has(key)) return;
1262
+ seenCalls.add(key);
1263
+ const outerTable = assignmentTable;
1264
+ const outerOffset = currentStmtOffset;
1265
+ for (const body of list) {
1266
+ if (callWalks >= MAX_CALL_WALKS) return;
1267
+ callWalks++;
1268
+ const table = /* @__PURE__ */ new Map();
1269
+ for (const [name, rec] of outerTable ?? [])
1270
+ if (rec.at < outerOffset) table.set(name, { value: rec.value, at: -1 });
1271
+ for (const name of ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"])
1272
+ table.delete(name);
1273
+ if (!rebindsPositionals(body)) {
1274
+ args.slice(0, 9).forEach((value, i) => table.set(String(i + 1), { value, at: -1 }));
1275
+ if (args.length === 1) {
1276
+ table.set("@", { value: args[0], at: -1 });
1277
+ table.set("*", { value: args[0], at: -1 });
1278
+ }
1279
+ table.set("#", { value: String(args.length), at: -1 });
1280
+ }
1281
+ assignmentTable = table;
1282
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1283
+ callDepth++;
1284
+ try {
1285
+ if (body.Cmd && syntax.NodeType(body.Cmd) === "Block") recordTopLevelAssignments(body.Cmd);
1286
+ syntax.Walk(body, step);
1287
+ } finally {
1288
+ callDepth--;
1289
+ assignmentTable = outerTable;
1290
+ currentStmtOffset = outerOffset;
1291
+ }
1292
+ }
1293
+ };
1294
+ const step = (node) => {
1295
+ if (!node) return false;
1296
+ const go = visit2(node);
1297
+ if (go !== false && syntax.NodeType(node) === "CallExpr") inline(node);
1298
+ return go;
1299
+ };
1300
+ syntax.Walk(f, step);
1301
+ }
1302
+ function withAssignments(f, body) {
1303
+ const outerTable = assignmentTable;
1304
+ const outerOffset = currentStmtOffset;
1305
+ const outerWalks = callWalks;
1306
+ callWalks = 0;
1307
+ assignmentTable = new Map(
1308
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
1309
+ );
1310
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1311
+ try {
1312
+ recordTopLevelAssignments(f);
1313
+ return body();
1314
+ } finally {
1315
+ assignmentTable = outerTable;
1316
+ currentStmtOffset = outerOffset;
1317
+ callWalks = outerWalks;
1318
+ }
1319
+ }
1320
+ function trackStatement(n) {
1321
+ try {
1322
+ currentStmtOffset = n.Pos().Offset();
1323
+ } catch {
1324
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1325
+ }
1326
+ }
1213
1327
  function resolveWordLiteral(w) {
1214
1328
  const parts = w?.Parts || [];
1215
1329
  let s = "";
@@ -1330,27 +1444,31 @@ function extractShellDestinations(command) {
1330
1444
  const out = [];
1331
1445
  const seen = /* @__PURE__ */ new Set();
1332
1446
  try {
1333
- syntax.Walk(f, (node) => {
1334
- if (!node) return false;
1335
- const n = node;
1336
- if (syntax.NodeType(n) !== "CallExpr") return true;
1337
- const callArgs = n.Args || [];
1338
- if (callArgs.length === 0) return true;
1339
- const words = callArgs.map((a) => resolveWordLiteral(a));
1340
- const at = unwrapCommandHead(words);
1341
- const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1342
- if (!NET_BINARIES.has(name)) return true;
1343
- const rest = words.slice(at + 1);
1344
- for (const raw of destTokensForBinary(name, rest)) {
1345
- const host2 = parseDestHost(raw);
1346
- if (!host2) continue;
1347
- const key = `${name}:${host2}`;
1348
- if (seen.has(key)) continue;
1349
- seen.add(key);
1350
- out.push({ host: host2, binary: name, raw });
1351
- }
1352
- return true;
1353
- });
1447
+ withAssignments(
1448
+ f,
1449
+ () => walkWithCalls(f, (node) => {
1450
+ if (!node) return false;
1451
+ const n = node;
1452
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1453
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1454
+ const callArgs = n.Args || [];
1455
+ if (callArgs.length === 0) return true;
1456
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1457
+ const at = unwrapCommandHead(words);
1458
+ const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1459
+ if (!NET_BINARIES.has(name)) return true;
1460
+ const rest = words.slice(at + 1);
1461
+ for (const raw of destTokensForBinary(name, rest)) {
1462
+ const host2 = parseDestHost(raw);
1463
+ if (!host2) continue;
1464
+ const key = `${name}:${host2}`;
1465
+ if (seen.has(key)) continue;
1466
+ seen.add(key);
1467
+ out.push({ host: host2, binary: name, raw });
1468
+ }
1469
+ return true;
1470
+ })
1471
+ );
1354
1472
  } catch {
1355
1473
  return out;
1356
1474
  }
@@ -1362,39 +1480,45 @@ function extractShellDestTokens(command) {
1362
1480
  const out = [];
1363
1481
  const seen = /* @__PURE__ */ new Set();
1364
1482
  try {
1365
- syntax.Walk(f, (node) => {
1366
- if (!node) return false;
1367
- const n = node;
1368
- if (syntax.NodeType(n) !== "CallExpr") return true;
1369
- const callArgs = n.Args || [];
1370
- if (callArgs.length === 0) return true;
1371
- const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1372
- if (!NET_BINARIES.has(name)) return true;
1373
- const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1374
- for (const raw of destTokensForBinary(name, rest)) {
1375
- if (!raw) continue;
1376
- let tok = raw.trim();
1377
- const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1378
- const hasScheme = scheme !== null;
1379
- if (hasScheme) tok = tok.slice(scheme[0].length);
1380
- tok = tok.split(/[/?#]/)[0];
1381
- const at = tok.lastIndexOf("@");
1382
- if (at >= 0) tok = tok.slice(at + 1);
1383
- if (tok.startsWith("[")) {
1384
- const close = tok.indexOf("]");
1385
- if (close > 0) tok = tok.slice(0, close + 1);
1386
- } else {
1387
- tok = tok.split(":")[0];
1483
+ withAssignments(
1484
+ f,
1485
+ () => walkWithCalls(f, (node) => {
1486
+ if (!node) return false;
1487
+ const n = node;
1488
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1489
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1490
+ const callArgs = n.Args || [];
1491
+ if (callArgs.length === 0) return true;
1492
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1493
+ const head = unwrapCommandHead(words);
1494
+ const name = (words[head] || "").toLowerCase().split("/").pop() ?? "";
1495
+ if (!NET_BINARIES.has(name)) return true;
1496
+ const rest = words.slice(head + 1);
1497
+ for (const raw of destTokensForBinary(name, rest)) {
1498
+ if (!raw) continue;
1499
+ let tok = raw.trim();
1500
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1501
+ const hasScheme = scheme !== null;
1502
+ if (hasScheme) tok = tok.slice(scheme[0].length);
1503
+ tok = tok.split(/[/?#]/)[0];
1504
+ const at = tok.lastIndexOf("@");
1505
+ if (at >= 0) tok = tok.slice(at + 1);
1506
+ if (tok.startsWith("[")) {
1507
+ const close = tok.indexOf("]");
1508
+ if (close > 0) tok = tok.slice(0, close + 1);
1509
+ } else {
1510
+ tok = tok.split(":")[0];
1511
+ }
1512
+ if (!tok) continue;
1513
+ if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1514
+ const key = `${name}:${tok}`;
1515
+ if (seen.has(key)) continue;
1516
+ seen.add(key);
1517
+ out.push({ token: tok, binary: name });
1388
1518
  }
1389
- if (!tok) continue;
1390
- if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1391
- const key = `${name}:${tok}`;
1392
- if (seen.has(key)) continue;
1393
- seen.add(key);
1394
- out.push({ token: tok, binary: name });
1395
- }
1396
- return true;
1397
- });
1519
+ return true;
1520
+ })
1521
+ );
1398
1522
  } catch {
1399
1523
  return out;
1400
1524
  }
@@ -1513,6 +1637,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1513
1637
  if (depth === 0) {
1514
1638
  seenPayloads = /* @__PURE__ */ new Set();
1515
1639
  payloadBudget = PAYLOAD_BUDGET;
1640
+ if (callDepth === 0) callWalks = 0;
1516
1641
  }
1517
1642
  assignmentTable = new Map(
1518
1643
  [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
@@ -1520,7 +1645,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1520
1645
  currentStmtOffset = Number.MAX_SAFE_INTEGER;
1521
1646
  try {
1522
1647
  recordTopLevelAssignments(f);
1523
- syntax.Walk(f, (node) => {
1648
+ walkWithCalls(f, (node) => {
1524
1649
  if (!node || result?.verdict === "block") return false;
1525
1650
  const n = node;
1526
1651
  const nodeType = syntax.NodeType(n);
@@ -2309,6 +2434,19 @@ function runnablePrefix(command) {
2309
2434
  }
2310
2435
  return null;
2311
2436
  }
2437
+ function uploadedFile(tool, flag, value) {
2438
+ const v = value.value;
2439
+ const file2 = (f) => f && f !== "-" && f !== "." ? { value: f } : null;
2440
+ if (tool === "curl") {
2441
+ if (["-d", "--data", "--data-ascii", "--data-binary", "--json"].includes(flag))
2442
+ return v.startsWith("@") ? file2(v.slice(1)) : null;
2443
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(v)?.[1]);
2444
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(v)?.[1]);
2445
+ if (flag === "-T" || flag === "--upload-file") return file2(v);
2446
+ }
2447
+ if (tool === "wget" && ["--post-file", "--body-file"].includes(flag)) return file2(v);
2448
+ return null;
2449
+ }
2312
2450
  function merge(a, b) {
2313
2451
  a.cwd = unique([...a.cwd, ...b.cwd]);
2314
2452
  for (const key of /* @__PURE__ */ new Set([...a.vars.keys(), ...b.vars.keys()])) {
@@ -2382,6 +2520,9 @@ function wordValues(word, state) {
2382
2520
  function collectShellFileAccesses(command, context = {}) {
2383
2521
  const out = [];
2384
2522
  let payloads = 256;
2523
+ const functions = /* @__PURE__ */ new Map();
2524
+ const seenCalls = /* @__PURE__ */ new Set();
2525
+ let callWalks2 = MAX_CALL_WALKS2;
2385
2526
  const initial = {
2386
2527
  cwd: [context.cwd ?? ""],
2387
2528
  vars: /* @__PURE__ */ new Map([["HOME", [context.homeDirs?.[0] ?? "~"]]])
@@ -2639,8 +2780,98 @@ function collectShellFileAccesses(command, context = {}) {
2639
2780
  }
2640
2781
  return;
2641
2782
  }
2783
+ if (name === "wget" || name === "curl" || name === "aria2c") {
2784
+ downloadOutputs(name, args, state);
2785
+ return;
2786
+ }
2787
+ if (["http", "https", "xh", "xhs"].includes(name)) {
2788
+ for (const token of args) {
2789
+ if (!token || token.dynamic || /^[a-z][a-z0-9+.-]*:\/\//i.test(token.value)) continue;
2790
+ const m = /^(?:@|[^=:@\s]+@|[^=:@\s]*=@)(.+)$/.exec(token.value);
2791
+ if (m && m[1] !== "-") add("read", { value: m[1] }, state, { upload: true });
2792
+ }
2793
+ return;
2794
+ }
2795
+ if (name === "scp" || name === "rsync") {
2796
+ const operands2 = strings.filter((s) => s && !s.startsWith("-"));
2797
+ const remote = operands2.slice(1).some((o) => /^[^/\s]+:/.test(o));
2798
+ for (const p of copySourcePaths(words))
2799
+ add("read", locate(p), state, remote ? { upload: true } : {});
2800
+ return;
2801
+ }
2642
2802
  for (const p of copySourcePaths(words)) add("read", locate(p), state);
2643
2803
  }
2804
+ function downloadOutputs(name, args, state) {
2805
+ const valueShort = DOWNLOAD_SHORT_VALUE[name];
2806
+ const outputs = [];
2807
+ const urls = [];
2808
+ let dir2 = null;
2809
+ let remoteName = false;
2810
+ let document = false;
2811
+ let serverNamed = false;
2812
+ const take = (flag, value) => {
2813
+ if (!value) return;
2814
+ const sent = uploadedFile(name, flag, value);
2815
+ if (sent) add("read", sent, state, { upload: true });
2816
+ if (DOWNLOAD_OUTPUT_FLAGS[name].includes(flag)) {
2817
+ if (["-O", "--output-document", "--out"].includes(flag) || name === "aria2c" && flag === "-o")
2818
+ document = true;
2819
+ if (value.value !== "-" || value.pattern || value.dynamic) outputs.push(value);
2820
+ } else if (DOWNLOAD_DIR_FLAGS[name].includes(flag)) dir2 = value;
2821
+ else if (name === "wget" && (flag === "-i" || flag === "--input-file")) serverNamed = true;
2822
+ };
2823
+ for (let i = 0; i < args.length; i++) {
2824
+ const token = args[i];
2825
+ if (!token) continue;
2826
+ const v = token.value;
2827
+ if (v.startsWith("--")) {
2828
+ const eq = v.indexOf("=");
2829
+ const flag = eq < 0 ? v : v.slice(0, eq);
2830
+ if (["--remote-name", "--remote-name-all"].includes(flag)) remoteName = true;
2831
+ if (["--remote-header-name", "--recursive", "--mirror", "--page-requisites"].includes(flag))
2832
+ serverNamed = true;
2833
+ if (DOWNLOAD_LONG_VALUE[name].includes(flag))
2834
+ take(flag, eq < 0 ? args[++i] ?? null : { ...token, value: v.slice(eq + 1) });
2835
+ continue;
2836
+ }
2837
+ if (v.startsWith("-") && v.length > 1 && !token.dynamic) {
2838
+ for (let j = 1; j < v.length; j++) {
2839
+ const c = v[j];
2840
+ if (name === "wget" && c === "n") {
2841
+ j++;
2842
+ continue;
2843
+ }
2844
+ if (name === "curl" && c === "O") remoteName = true;
2845
+ if (name === "curl" && c === "J") serverNamed = true;
2846
+ if (name === "wget" && (c === "r" || c === "m" || c === "p")) serverNamed = true;
2847
+ if (valueShort.includes(c)) {
2848
+ const rest = v.slice(j + 1);
2849
+ take(`-${c}`, rest ? { value: rest } : args[++i] ?? null);
2850
+ break;
2851
+ }
2852
+ }
2853
+ continue;
2854
+ }
2855
+ if (/^(?:https?|ftps?):\/\//i.test(v)) urls.push(v);
2856
+ }
2857
+ for (const out2 of outputs) {
2858
+ const spelled = name === "aria2c" && dir2?.value && !out2.value.startsWith("/") ? { ...out2, value: `${dir2.value.replace(/\/+$/, "")}/${out2.value}` } : out2;
2859
+ if (name === "curl" && /#\d/.test(spelled.value))
2860
+ add("write", { value: "", pattern: spelled.value.replace(/#\d+/g, "*"), dot: true }, state);
2861
+ else add("write", spelled, state);
2862
+ }
2863
+ const remote = name === "curl" ? remoteName || serverNamed : !document || serverNamed;
2864
+ if (!remote) return;
2865
+ const into = dir2?.value ? dir2.value.replace(/\/+$/, "") : ".";
2866
+ if (serverNamed || urls.length === 0) {
2867
+ add("write", { value: "", pattern: `${quoteGlob(into)}/*`, dot: true }, state);
2868
+ return;
2869
+ }
2870
+ for (const url of urls) {
2871
+ const file2 = url.replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]*/i, "").split("/").pop();
2872
+ add("write", { value: `${into}/${file2 || "index.html"}` }, state);
2873
+ }
2874
+ }
2644
2875
  function findActions2(tokens, words, head, state, nested, depth) {
2645
2876
  const starts = findStartPoints(words, head).starts;
2646
2877
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
@@ -2712,7 +2943,35 @@ function collectShellFileAccesses(command, context = {}) {
2712
2943
  const words = wordValues(node, state);
2713
2944
  alternatives = alternatives.flatMap((prefix) => (words.length ? words : [null]).map((w) => [...prefix, w])).slice(0, MAX_ALTERNATIVES);
2714
2945
  }
2715
- for (const tokens of alternatives) fromWords(tokens, state, nested, depth);
2946
+ for (const tokens of alternatives) {
2947
+ fromWords(tokens, state, nested, depth);
2948
+ inlineCall(tokens, state, depth);
2949
+ }
2950
+ }
2951
+ function inlineCall(tokens, state, depth) {
2952
+ const head = tokens[0];
2953
+ if (!head || head.dynamic || head.pattern) return;
2954
+ const bodies = functions.get(head.value);
2955
+ if (!bodies || (state.callDepth ?? 0) >= MAX_CALL_DEPTH2) return;
2956
+ const args = tokens.slice(1).map((a) => a && !a.dynamic ? a.value : null);
2957
+ const vars = JSON.stringify([...state.vars].sort(([a], [b]) => a < b ? -1 : 1));
2958
+ const key = `${head.value}\0${JSON.stringify(args)}\0${state.cwd.join("")}\0${vars}`;
2959
+ if (seenCalls.has(key)) return;
2960
+ seenCalls.add(key);
2961
+ for (const body of bodies) {
2962
+ if (callWalks2-- <= 0) return;
2963
+ const local = clone(state);
2964
+ local.callDepth = (state.callDepth ?? 0) + 1;
2965
+ for (const name of POSITIONAL) local.vars.delete(name);
2966
+ if (!rebindsPositionals(body)) {
2967
+ args.slice(0, 9).forEach((v, i) => local.vars.set(String(i + 1), v === null ? null : [v]));
2968
+ const all = args.every((v) => v !== null) && args.length ? unique(args) : null;
2969
+ local.vars.set("@", all);
2970
+ local.vars.set("*", all);
2971
+ local.vars.set("#", [String(args.length)]);
2972
+ }
2973
+ statement(body, local, depth + 1);
2974
+ }
2716
2975
  }
2717
2976
  function readsScriptFromStdin(stmt) {
2718
2977
  const cmd = stmt?.Cmd;
@@ -2749,11 +3008,13 @@ function collectShellFileAccesses(command, context = {}) {
2749
3008
  if (!stmt || depth > MAX_DEPTH) return;
2750
3009
  state.onSuccess = void 0;
2751
3010
  if (stmt.Background) state = clone(state);
3011
+ const headWord = stmt.Cmd && syntax2.NodeType(stmt.Cmd) === "CallExpr" ? base(resolveWordLiteral(stmt.Cmd.Args?.[0])) : "";
3012
+ const sends = SENDING_STDIN.has(headWord);
2752
3013
  for (const redir of stmt.Redirs ?? []) {
2753
3014
  substitutions(redir.Word, state, depth);
2754
3015
  for (const word of wordValues(redir.Word, state)) {
2755
3016
  if (WRITES.has(redir.Op)) add("write", word, state);
2756
- if (READS.has(redir.Op)) add("read", word, state);
3017
+ if (READS.has(redir.Op)) add("read", word, state, sends ? { upload: true } : {});
2757
3018
  if (redir.Op === DUP_OUT && word.value && !/^\d+$/.test(word.value))
2758
3019
  add("write", word, state);
2759
3020
  }
@@ -2862,7 +3123,10 @@ function collectShellFileAccesses(command, context = {}) {
2862
3123
  }
2863
3124
  const parsed = parseShared(command);
2864
3125
  const program2 = typeof parsed !== "symbol" ? parsed : runnablePrefix(command);
2865
- if (program2) statements(program2.Stmts ?? [], initial, 0);
3126
+ if (program2) {
3127
+ for (const [name, bodies] of functionBodies(program2)) functions.set(name, bodies);
3128
+ statements(program2.Stmts ?? [], initial, 0);
3129
+ }
2866
3130
  return out;
2867
3131
  }
2868
3132
  function isAgentInstructionPath(absPath, root) {
@@ -2978,6 +3242,33 @@ function hits(access, paths, directories, context) {
2978
3242
  }
2979
3243
  return false;
2980
3244
  }
3245
+ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3246
+ const home = context.homeDirs?.[0] ?? "~";
3247
+ const candidates2 = access.candidates ?? [
3248
+ normalizeFilePath(access.path, access.cwd ?? context.cwd, home)
3249
+ ];
3250
+ for (const p of candidates2) {
3251
+ if (dirs.some((d) => within(p, d)) && !exempt(p)) return true;
3252
+ if (access.recursive && dirs.some((d) => within(d, p))) return true;
3253
+ }
3254
+ if (!access.glob) return false;
3255
+ const pattern = normalizeFilePath(access.glob, access.cwd ?? context.cwd, home);
3256
+ const magic = pattern.search(/[*?[{]/);
3257
+ const literal = magic < 0 ? pattern : pattern.slice(0, pattern.lastIndexOf("/", magic) + 1);
3258
+ if (literal && exempt(literal.replace(/\/$/, "") || "/")) return false;
3259
+ const options = { dot: access.dotGlob === true, nonegate: true };
3260
+ const components = pattern.split("/");
3261
+ for (const root of dirs) {
3262
+ const count = root.split("/").length;
3263
+ if (components.length > count && picomatch(components.slice(0, count).join("/"), options)(root))
3264
+ return true;
3265
+ if (access.recursive && picomatch(pattern, options)(root)) return true;
3266
+ }
3267
+ return false;
3268
+ }
3269
+ function uploadDirs(context) {
3270
+ return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3271
+ }
2981
3272
  function evaluateProtectedFileAccess(accesses, context, settings) {
2982
3273
  const homes = [.../* @__PURE__ */ new Set([...context.homeDirs ?? ["~"], "/root"])];
2983
3274
  const startup = homes.flatMap(
@@ -2999,6 +3290,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2999
3290
  "files.shell-startup",
3000
3291
  "review-shell-startup-mutation",
3001
3292
  "Changing shell startup files requires approval"
3293
+ ] : mutation && hitsSystemDir(access, context) ? [
3294
+ "files.system-dirs",
3295
+ "review-system-dir-mutation",
3296
+ "Changing files in a system directory requires approval"
3297
+ ] : !mutation && access.upload && hitsSystemDir(access, context, uploadDirs(context)) ? [
3298
+ "files.system-dirs",
3299
+ "review-system-file-upload",
3300
+ "Uploading a system file requires approval"
3002
3301
  ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
3003
3302
  "files.password-hashes",
3004
3303
  "review-password-hash-read",
@@ -3326,16 +3625,21 @@ function looksGenerated(value, minLength, minEntropy, lettersOnlyAllowed = false
3326
3625
  if (EMAIL_VALUE_RE.test(value)) return false;
3327
3626
  if (looksLikeJoinedWords(value) || hasSequentialRun(value)) return false;
3328
3627
  if (PATH_VALUE_RE.test(value) || DOTTED_REFERENCE_RE.test(value)) return false;
3628
+ if (CONCATENATION_RE.test(value)) return false;
3329
3629
  if (rejectedByStructuredValidator(value)) return false;
3330
3630
  return shannonEntropy(value) >= minEntropy;
3331
3631
  }
3332
3632
  function nameHoldsCredential(name) {
3333
3633
  return !FIXTURE_NAME_RE.test(name) && !DERIVATIVE_NAME_RE.test(name);
3334
3634
  }
3635
+ function isOtpDocSecret(value) {
3636
+ return OTP_DOC_SECRETS.has(value.replace(/=+$/, "").toUpperCase());
3637
+ }
3335
3638
  function validateQueryCredential(raw) {
3336
3639
  const value = capturedValue(raw, QUERY_CREDENTIAL_VALUE_RE);
3337
3640
  if (value === null) return false;
3338
- if (OTP_SECRET_RE.test(value)) return looksGenerated(value, 16, 3, true);
3641
+ if (OTP_SECRET_RE.test(value))
3642
+ return !isOtpDocSecret(value) && looksGenerated(value, 16, 3, true);
3339
3643
  if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value)) return false;
3340
3644
  return looksGenerated(value, 12, 3);
3341
3645
  }
@@ -3349,13 +3653,40 @@ function keywordEndsAPart(name) {
3349
3653
  }
3350
3654
  function validateAssignedSecret(raw, before = "") {
3351
3655
  const value = capturedValue(raw, ASSIGNED_SECRET_VALUE_RE);
3352
- const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE);
3656
+ const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE)?.replace(/^_+/, "") ?? null;
3353
3657
  if (value === null || name === null) return false;
3354
- if (!keywordEndsAPart(name) || ARN_CONTEXT_RE.test(before)) return false;
3355
- const prefix = before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "";
3658
+ if (!keywordEndsAPart(name)) return false;
3659
+ if (ARN_CONTEXT_RE.test(before)) return false;
3660
+ const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3356
3661
  if (!nameHoldsCredential(prefix + name)) return false;
3357
3662
  return looksGenerated(value, 12, 3.5);
3358
3663
  }
3664
+ function validateBasicCredential(raw) {
3665
+ const b64 = capturedValue(raw, BASIC_VALUE_RE);
3666
+ if (b64 === null) return false;
3667
+ const bare = b64.replace(/=+$/, "");
3668
+ if (bare.length % 4 === 1) return false;
3669
+ const bytes = Buffer.from(b64, "base64");
3670
+ if (bytes.toString("base64").replace(/=+$/, "") !== bare) return false;
3671
+ const text = bytes.toString("utf8");
3672
+ if (!Buffer.from(text, "utf8").equals(bytes)) return false;
3673
+ for (let i = 0; i < text.length; i++) {
3674
+ const c = text.charCodeAt(i);
3675
+ if (c < 32 || c === 127) return false;
3676
+ }
3677
+ const pair = BASIC_PAIR_RE.exec(text);
3678
+ if (!pair) return false;
3679
+ const password = pair[2];
3680
+ if (!/[0-9]/.test(password) || !/[A-Za-z]/.test(password)) return false;
3681
+ const lower2 = password.toLowerCase();
3682
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3683
+ return shannonEntropy(password) >= 3;
3684
+ }
3685
+ function validateOtpSecret(raw) {
3686
+ const secret = capturedValue(raw, OTP_CAPTURE_RE);
3687
+ if (secret === null || isOtpDocSecret(secret)) return false;
3688
+ return looksGenerated(secret.replace(/=+$/, ""), 16, 3, true);
3689
+ }
3359
3690
  function matchSensitivePath2(resolvedPath, originalPath, options = {}) {
3360
3691
  const normalised = resolvedPath.replace(/\\/g, "/");
3361
3692
  if (!options.systemAccessHandled && isLegacySystemPath(normalised))
@@ -3664,6 +3995,156 @@ function parsePySpec(raw) {
3664
3995
  const exact = PY_EXACT_RE.exec(m[2].trim());
3665
3996
  return { name, version: exact ? exact[1] : void 0 };
3666
3997
  }
3998
+ function exactVersion(v) {
3999
+ if (!v) return void 0;
4000
+ const m = EXACT_VERSION_RE.exec(v.trim());
4001
+ return m ? m[1] : void 0;
4002
+ }
4003
+ function push(out, ecosystem, manager, name, version2, raw) {
4004
+ out.push({ ecosystem, manager, name, ...version2 ? { version: version2 } : {}, raw });
4005
+ }
4006
+ function gemInstall(words, from, out) {
4007
+ const dash = words.indexOf("--", from);
4008
+ const own = dash < 0 ? words : words.slice(0, dash);
4009
+ const pinned = exactVersion(flagValues(own, from, ["-v", "--version"])[0]);
4010
+ for (const raw of operands(own, from, GEM_VALUE_FLAGS)) {
4011
+ if (NON_REGISTRY_RE.test(raw) || /\.gem$/i.test(raw)) continue;
4012
+ const colon = raw.indexOf(":");
4013
+ const name = colon < 0 ? raw : raw.slice(0, colon);
4014
+ if (!GEM_NAME_RE.test(name)) continue;
4015
+ push(
4016
+ out,
4017
+ "RubyGems",
4018
+ "gem",
4019
+ name,
4020
+ colon < 0 ? pinned : exactVersion(raw.slice(colon + 1)),
4021
+ raw
4022
+ );
4023
+ }
4024
+ }
4025
+ function cargoCrates(words, from, manager, out) {
4026
+ const flags = manager === "cargo add" ? CARGO_ADD_VALUE_FLAGS : CARGO_INSTALL_VALUE_FLAGS;
4027
+ if (flagValues(words, from, ["--git", "--path", "--registry", "--index"]).length > 0) return;
4028
+ if (words.some((w) => w === "--git" || w === "--path")) return;
4029
+ const flagVersion = manager === "cargo install" ? exactVersion(flagValues(words, from, ["--version", "--vers"])[0]) : void 0;
4030
+ for (const raw of operands(words, from, flags)) {
4031
+ const at = raw.indexOf("@");
4032
+ const name = at < 0 ? raw : raw.slice(0, at);
4033
+ if (!CRATE_NAME_RE.test(name)) continue;
4034
+ push(
4035
+ out,
4036
+ "crates.io",
4037
+ manager,
4038
+ name,
4039
+ at < 0 ? flagVersion : exactVersion(raw.slice(at + 1)),
4040
+ raw
4041
+ );
4042
+ }
4043
+ }
4044
+ function goModules(words, from, manager, out) {
4045
+ let specs = operands(words, from, GO_VALUE_FLAGS);
4046
+ if (manager === "go run") specs = specs.slice(0, 1).filter((s) => s.includes("@"));
4047
+ for (const raw of specs) {
4048
+ const at = raw.indexOf("@");
4049
+ const pathPart = (at < 0 ? raw : raw.slice(0, at)).replace(/\/\.\.\.$/, "");
4050
+ const ver = at < 0 ? void 0 : raw.slice(at + 1);
4051
+ if (ver === "none") continue;
4052
+ if (!GO_PATH_RE.test(pathPart) || /\.go$/i.test(pathPart)) continue;
4053
+ const exact = ver && /^v\d/.test(ver) ? exactVersion(ver) : void 0;
4054
+ push(out, "Go", manager, pathPart, exact, raw);
4055
+ }
4056
+ }
4057
+ function dotnetAdd(words, from, out) {
4058
+ let k = -1;
4059
+ for (let i = from; i < Math.min(words.length, from + 2); i++) {
4060
+ if (lower(words[i]) === "package") {
4061
+ k = i;
4062
+ break;
4063
+ }
4064
+ }
4065
+ if (k < 0) return;
4066
+ const name = operands(words, k + 1, DOTNET_ADD_VALUE_FLAGS)[0];
4067
+ if (!name || !NUGET_NAME_RE.test(name)) return;
4068
+ const version2 = exactVersion(flagValues(words, k + 1, ["-v", "--version"])[0]);
4069
+ push(out, "NuGet", "dotnet add package", name, version2, name);
4070
+ }
4071
+ function dotnetTool(words, from, out) {
4072
+ const name = operands(words, from, DOTNET_TOOL_VALUE_FLAGS)[0];
4073
+ if (!name || !NUGET_NAME_RE.test(name)) return;
4074
+ const version2 = exactVersion(flagValues(words, from, ["--version"])[0]);
4075
+ push(out, "NuGet", "dotnet tool", name, version2, name);
4076
+ }
4077
+ function nugetInstall(words, from, out) {
4078
+ const lowered = words.map((w) => w !== null && w.startsWith("-") ? w.toLowerCase() : w);
4079
+ const name = operands(lowered, from, NUGET_VALUE_FLAGS)[0];
4080
+ if (!name || !NUGET_NAME_RE.test(name) || /\.config$/i.test(name)) return;
4081
+ const version2 = exactVersion(flagValues(lowered, from, ["-version"])[0]);
4082
+ push(out, "NuGet", "nuget install", name, version2, name);
4083
+ }
4084
+ function mavenGet(words, from, out) {
4085
+ const goal = words.slice(from).some(
4086
+ (w) => w !== null && /(?:^|:)dependency:get$|^org\.apache\.maven\.plugins:maven-dependency-plugin:(?:[\w.-]+:)?get$/.test(
4087
+ w
4088
+ )
4089
+ );
4090
+ if (!goal) return;
4091
+ const prop = (key) => {
4092
+ for (let i = from; i < words.length; i++) {
4093
+ const w = words[i];
4094
+ if (w === null) continue;
4095
+ if (w.startsWith(`-D${key}=`)) return w.slice(key.length + 3);
4096
+ if (w === "-D" && words[i + 1]?.startsWith(`${key}=`))
4097
+ return words[i + 1].slice(key.length + 1);
4098
+ }
4099
+ return void 0;
4100
+ };
4101
+ const artifact = prop("artifact");
4102
+ let coord;
4103
+ let ver;
4104
+ if (artifact) {
4105
+ const parts = artifact.split(":");
4106
+ if (parts.length < 2) return;
4107
+ coord = `${parts[0]}:${parts[1]}`;
4108
+ ver = parts[2];
4109
+ } else {
4110
+ const g = prop("groupId");
4111
+ const a = prop("artifactId");
4112
+ if (!g || !a) return;
4113
+ coord = `${g}:${a}`;
4114
+ ver = prop("version");
4115
+ }
4116
+ if (!MAVEN_COORD_RE.test(coord)) return;
4117
+ const exact = ver && !/^(?:LATEST|RELEASE)$/i.test(ver) ? exactVersion(ver) : void 0;
4118
+ push(out, "Maven", "mvn dependency:get", coord, exact, artifact ?? coord);
4119
+ }
4120
+ function composerPackages(words, from, manager, out) {
4121
+ const ops = operands(words, from, COMPOSER_VALUE_FLAGS);
4122
+ const specs = manager === "composer create-project" ? ops.slice(0, 1) : ops;
4123
+ for (const raw of specs) {
4124
+ const sep = raw.search(/[:=]/);
4125
+ const name = sep < 0 ? raw : raw.slice(0, sep);
4126
+ if (!COMPOSER_NAME_RE.test(name)) continue;
4127
+ let ver = sep < 0 ? void 0 : raw.slice(sep + 1);
4128
+ if (manager === "composer create-project" && ver === void 0) ver = ops[2] ?? void 0;
4129
+ push(out, "Packagist", manager, name.toLowerCase(), exactVersion(ver), raw);
4130
+ }
4131
+ }
4132
+ function editorExtensions(words, from, out) {
4133
+ for (const raw of flagValues(words, from, ["--install-extension"])) {
4134
+ if (/\.vsix$/i.test(raw) || NON_REGISTRY_RE.test(raw)) continue;
4135
+ const at = raw.lastIndexOf("@");
4136
+ const name = at <= 0 ? raw : raw.slice(0, at);
4137
+ if (!EXTENSION_ID_RE.test(name)) continue;
4138
+ push(
4139
+ out,
4140
+ "VSCode",
4141
+ "code --install-extension",
4142
+ name,
4143
+ at <= 0 ? void 0 : exactVersion(raw.slice(at + 1)),
4144
+ raw
4145
+ );
4146
+ }
4147
+ }
3667
4148
  function basename(w) {
3668
4149
  return (w ?? "").toLowerCase().split("/").pop() ?? "";
3669
4150
  }
@@ -3895,7 +4376,63 @@ function fromCall(words, out, depth) {
3895
4376
  pushPy(out, "pipenv", operands(words, v + 1, PIPENV_VALUE_FLAGS));
3896
4377
  return;
3897
4378
  }
4379
+ case "gem": {
4380
+ const v = verbIndex(words, start + 1, GEM_VALUE_FLAGS);
4381
+ if (v >= 0 && ["install", "i"].includes(lower(words[v]))) gemInstall(words, v + 1, out);
4382
+ return;
4383
+ }
4384
+ case "bundle": {
4385
+ const v = verbIndex(words, start + 1, BUNDLE_VALUE_FLAGS);
4386
+ if (v < 0 || lower(words[v]) !== "add") return;
4387
+ if (flagValues(words, v + 1, ["--git", "--github", "--path"]).length > 0) return;
4388
+ const version2 = exactVersion(flagValues(words, v + 1, ["-v", "--version"])[0]);
4389
+ for (const raw of operands(words, v + 1, BUNDLE_VALUE_FLAGS))
4390
+ if (GEM_NAME_RE.test(raw)) push(out, "RubyGems", "bundle add", raw, version2, raw);
4391
+ return;
4392
+ }
4393
+ case "cargo": {
4394
+ const from = words[start + 1]?.startsWith("+") ? start + 2 : start + 1;
4395
+ const v = verbIndex(words, from, CARGO_ADD_VALUE_FLAGS);
4396
+ const verb = lower(words[v]);
4397
+ if (verb === "add") cargoCrates(words, v + 1, "cargo add", out);
4398
+ else if (verb === "install") cargoCrates(words, v + 1, "cargo install", out);
4399
+ return;
4400
+ }
4401
+ case "go": {
4402
+ const v = verbIndex(words, start + 1, GO_VALUE_FLAGS);
4403
+ const verb = lower(words[v]);
4404
+ if (verb === "get" || verb === "install" || verb === "run")
4405
+ goModules(words, v + 1, `go ${verb}`, out);
4406
+ return;
4407
+ }
4408
+ case "dotnet": {
4409
+ const v = verbIndex(words, start + 1, DOTNET_ADD_VALUE_FLAGS);
4410
+ const verb = lower(words[v]);
4411
+ if (verb === "add") dotnetAdd(words, v + 1, out);
4412
+ else if (verb === "tool" && ["install", "update"].includes(lower(words[v + 1])))
4413
+ dotnetTool(words, v + 2, out);
4414
+ return;
4415
+ }
4416
+ case "nuget": {
4417
+ if (lower(words[start + 1]) === "install") nugetInstall(words, start + 2, out);
4418
+ return;
4419
+ }
4420
+ case "mvn":
4421
+ case "mvnw":
4422
+ mavenGet(words, start + 1, out);
4423
+ return;
4424
+ case "composer": {
4425
+ let v = verbIndex(words, start + 1, COMPOSER_VALUE_FLAGS);
4426
+ if (v >= 0 && lower(words[v]) === "global") v = verbIndex(words, v + 1, COMPOSER_VALUE_FLAGS);
4427
+ const verb = lower(words[v]);
4428
+ if (verb === "require" || verb === "req" || verb === "r")
4429
+ composerPackages(words, v + 1, "composer require", out);
4430
+ else if (verb === "create-project")
4431
+ composerPackages(words, v + 1, "composer create-project", out);
4432
+ return;
4433
+ }
3898
4434
  default:
4435
+ if (EDITOR_HEADS.includes(family)) editorExtensions(words, start + 1, out);
3899
4436
  return;
3900
4437
  }
3901
4438
  }
@@ -5459,11 +5996,11 @@ function isDecimalFraction(text, index, unseparated) {
5459
5996
  const a = text[index - 2];
5460
5997
  if (sep === ".") {
5461
5998
  if (a !== void 0 && a >= "0" && a <= "9") return true;
5462
- return a === void 0 || NUMBER_LEAD_RE.test(a);
5999
+ return a === void 0 || (a === "-" || a === "+") && index === 2;
5463
6000
  }
5464
6001
  if (sep === "," && a === "0") {
5465
6002
  const b = text[index - 3];
5466
- return b === void 0 || NUMBER_LEAD_RE.test(b);
6003
+ return b === void 0 || b === ";";
5467
6004
  }
5468
6005
  return false;
5469
6006
  }
@@ -6663,7 +7200,7 @@ function* stringValues(obj, depth = 0) {
6663
7200
  }
6664
7201
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
6665
7202
  }
6666
- 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, CHECK_GROUPS, 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, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, MAX, UNTRUSTED_TOOLS, SIGNALS, 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, DLP_SCAN_LIMITS, 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, CONTROL_CHAR_RE, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, 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, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
7203
+ 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, CHECK_GROUPS, 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, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, MAX, UNTRUSTED_TOOLS, SIGNALS, 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, DLP_SCAN_LIMITS, 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, CONTROL_CHAR_RE, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, 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, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
6667
7204
  var init_dist = __esm({
6668
7205
  "packages/policy-engine/dist/index.mjs"() {
6669
7206
  "use strict";
@@ -7464,6 +8001,11 @@ var init_dist = __esm({
7464
8001
  example: "cat /etc/shadow",
7465
8002
  advice: "Keep Review, or choose Block when agents must never read system password hashes."
7466
8003
  },
8004
+ "files.system-dirs": {
8005
+ 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.",
8006
+ example: "wget -O /usr/bin/tool https://example.com/tool",
8007
+ advice: "Keep Review on a workstation. A container image build that installs into /usr can set it to Log."
8008
+ },
7467
8009
  // ── Commands ──────────────────────────────────────────────────────────────
7468
8010
  "commands.inline-exec": {
7469
8011
  plain: "The agent runs a small program written inside the command itself.",
@@ -7660,7 +8202,7 @@ var init_dist = __esm({
7660
8202
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
7661
8203
  },
7662
8204
  "loading.malicious-package": {
7663
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago.",
8205
+ 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.",
7664
8206
  example: "npm install of a package flagged as malware",
7665
8207
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
7666
8208
  }
@@ -7706,6 +8248,15 @@ var init_dist = __esm({
7706
8248
  defaultValue: "review",
7707
8249
  legacy: ["review-password-hash-read"]
7708
8250
  },
8251
+ {
8252
+ id: "files.system-dirs",
8253
+ group: "files",
8254
+ title: "System directories",
8255
+ catches: "Writes, new files and removals under /usr, /etc, /boot, /bin, /sbin, /lib and /var/log, and uploads of system files",
8256
+ values: ALL,
8257
+ defaultValue: "review",
8258
+ legacy: ["review-system-dir-mutation", "review-system-file-upload"]
8259
+ },
7709
8260
  // ── Commands ────────────────────────────────────────────────────────────
7710
8261
  {
7711
8262
  id: "commands.inline-exec",
@@ -9003,6 +9554,10 @@ var init_dist = __esm({
9003
9554
  AND_OP = probeBinOp("a && b");
9004
9555
  OR_OP = probeBinOp("a || b");
9005
9556
  AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
9557
+ MAX_CALL_DEPTH = 4;
9558
+ MAX_CALL_WALKS = 256;
9559
+ callDepth = 0;
9560
+ callWalks = 0;
9006
9561
  FS_OP_CACHE_MAX = 5e3;
9007
9562
  fsOpCache = /* @__PURE__ */ new Map();
9008
9563
  stripDotSlash = (p) => p.replace(/^\.\//, "");
@@ -9055,10 +9610,79 @@ var init_dist = __esm({
9055
9610
  MAX_ARGV_CHARACTERS = 262144;
9056
9611
  ({ syntax: syntax2 } = mvdanSh2);
9057
9612
  MAX_ALTERNATIVES = 64;
9613
+ MAX_CALL_DEPTH2 = 4;
9614
+ MAX_CALL_WALKS2 = 256;
9615
+ POSITIONAL = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"];
9058
9616
  MAX_DEPTH = 64;
9059
9617
  MAX_PAYLOAD_DEPTH = 24;
9060
9618
  unique = (a) => [...new Set(a)].slice(0, MAX_ALTERNATIVES);
9061
- clone = (s) => ({ cwd: [...s.cwd], vars: new Map(s.vars) });
9619
+ clone = (s) => ({
9620
+ cwd: [...s.cwd],
9621
+ vars: new Map(s.vars),
9622
+ ...s.callDepth ? { callDepth: s.callDepth } : {}
9623
+ });
9624
+ SENDING_STDIN = /* @__PURE__ */ new Set(["nc", "ncat", "netcat", "socat", "telnet", "curl", "wget"]);
9625
+ DOWNLOAD_SHORT_VALUE = {
9626
+ curl: "odDeEFHKmPrtTuUwxXyYzAbcCQ",
9627
+ wget: "OoaPiBetTwQlARDXIUY",
9628
+ aria2c: "odisx"
9629
+ };
9630
+ DOWNLOAD_LONG_VALUE = {
9631
+ curl: [
9632
+ "--output",
9633
+ "--output-dir",
9634
+ "--dump-header",
9635
+ "--cookie-jar",
9636
+ "--data",
9637
+ "--data-binary",
9638
+ "--data-raw",
9639
+ "--data-ascii",
9640
+ "--data-urlencode",
9641
+ "--form",
9642
+ "--header",
9643
+ "--request",
9644
+ "--user",
9645
+ "--user-agent",
9646
+ "--referer",
9647
+ "--cookie",
9648
+ "--config",
9649
+ "--json",
9650
+ "--upload-file",
9651
+ "--max-time",
9652
+ "--proxy",
9653
+ "--write-out",
9654
+ "--url"
9655
+ ],
9656
+ wget: [
9657
+ "--output-document",
9658
+ "--output-file",
9659
+ "--append-output",
9660
+ "--directory-prefix",
9661
+ "--input-file",
9662
+ "--post-data",
9663
+ "--post-file",
9664
+ "--body-data",
9665
+ "--body-file",
9666
+ "--method",
9667
+ "--header",
9668
+ "--user-agent",
9669
+ "--user",
9670
+ "--password",
9671
+ "--tries",
9672
+ "--timeout"
9673
+ ],
9674
+ aria2c: ["--out", "--dir", "--input-file"]
9675
+ };
9676
+ DOWNLOAD_OUTPUT_FLAGS = {
9677
+ curl: ["-o", "--output", "-D", "--dump-header", "-c", "--cookie-jar"],
9678
+ wget: ["-O", "--output-document", "-o", "--output-file", "-a", "--append-output"],
9679
+ aria2c: ["-o", "--out"]
9680
+ };
9681
+ DOWNLOAD_DIR_FLAGS = {
9682
+ curl: ["--output-dir"],
9683
+ wget: ["-P", "--directory-prefix"],
9684
+ aria2c: ["-d", "--dir"]
9685
+ };
9062
9686
  base = (s) => (s ?? "").replace(/\/+$/, "").split("/").pop() ?? "";
9063
9687
  quoteGlob = (s) => s.replace(/[\\*?\[\]{}()!+@]/g, "\\$&");
9064
9688
  asWord = (p) => ({ value: p.value, ...p.glob ? { pattern: p.pattern } : {} });
@@ -9083,6 +9707,18 @@ var init_dist = __esm({
9083
9707
  SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9084
9708
  PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9085
9709
  STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9710
+ SYSTEM_DIRS = [
9711
+ "/usr",
9712
+ "/etc",
9713
+ "/boot",
9714
+ "/bin",
9715
+ "/sbin",
9716
+ "/lib",
9717
+ "/lib64",
9718
+ "/var/log"
9719
+ ];
9720
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9721
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9086
9722
  AGENT_INSTRUCTION_FILES = [
9087
9723
  ".gitlab-ci.yml",
9088
9724
  "CLAUDE.md",
@@ -9100,6 +9736,7 @@ var init_dist = __esm({
9100
9736
  ".husky"
9101
9737
  ];
9102
9738
  LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9739
+ exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
9103
9740
  RANK = { allow: 0, review: 1, block: 2 };
9104
9741
  BIP39_ENGLISH = `abandon
9105
9742
  ability
@@ -11426,15 +12063,20 @@ zoo`.split("\n");
11426
12063
  IDENTIFIER_VALUE_RE = /^[A-Za-z_.-]+$/;
11427
12064
  PATH_VALUE_RE = /^(?:\/|\.{1,2}\/|~\/)/;
11428
12065
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
12066
+ CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
11429
12067
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
11430
12068
  FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
11431
- 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;
12069
+ 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;
11432
12070
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
11433
12071
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
11434
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
12072
+ OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
12073
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
11435
12074
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
11436
- NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)/i;
12075
+ 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;
11437
12076
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
12077
+ BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
12078
+ BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
12079
+ OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
11438
12080
  DLP_PATTERNS = [
11439
12081
  // ── AWS ───────────────────────────────────────────────────────────────────
11440
12082
  {
@@ -11772,6 +12414,22 @@ zoo`.split("\n");
11772
12414
  contextBoost: true,
11773
12415
  minEntropy: 3
11774
12416
  },
12417
+ // ── HTTP Basic credential ─────────────────────────────────────────────────
12418
+ // `Authorization: Basic <base64 of user:password>`, or the bare scheme in a
12419
+ // config list. The validator decodes the value and accepts only a
12420
+ // `user:password` whose password looks generated (letters and digits, 8+
12421
+ // characters, the query-credential rule), so prose ("Basic authentication")
12422
+ // and documentation pairs (`admin:changeme`, `user:pass1234`) stay allowed.
12423
+ // review: the same weight as a bare Bearer token.
12424
+ {
12425
+ name: "HTTP Basic Credential",
12426
+ regex: /\bBasic[ \t]{1,8}([A-Za-z0-9+/]{8,512}={0,2})/i,
12427
+ severity: "review",
12428
+ keywords: ["basic"],
12429
+ validate: validateBasicCredential,
12430
+ redactGroup: 1,
12431
+ argumentsOnly: true
12432
+ },
11775
12433
  // ── Resend ────────────────────────────────────────────────────────────────
11776
12434
  {
11777
12435
  name: "Resend API Key",
@@ -11913,6 +12571,19 @@ zoo`.split("\n");
11913
12571
  validate: validateQueryCredential,
11914
12572
  argumentsOnly: true
11915
12573
  },
12574
+ // ── OTP secret in a constructor ───────────────────────────────────────────
12575
+ // `ROTP::TOTP.new('<base32>')`, `pyotp.TOTP("<base32>")`, `new HOTP('...')`:
12576
+ // the shared secret of a one-time-password generator written as a literal.
12577
+ // The otpauth URI form is `Credential in URL Query`. review.
12578
+ {
12579
+ name: "OTP Secret",
12580
+ regex: /\b(?:TOTP|HOTP|OTP)(?:\.new|\.create)?[ \t]{0,8}\(?[ \t]{0,8}["']([A-Z2-7]{16,128}=*)["']/i,
12581
+ severity: "review",
12582
+ keywords: ["otp"],
12583
+ validate: validateOtpSecret,
12584
+ redactGroup: 1,
12585
+ argumentsOnly: true
12586
+ },
11916
12587
  // ── Secret assigned to a telling name ─────────────────────────────────────
11917
12588
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11918
12589
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11920,12 +12591,14 @@ zoo`.split("\n");
11920
12591
  // generated: 12+ characters, not an identifier, Shannon entropy of 3.5
11921
12592
  // bits per character or more. review, never block (node9Firewall#348,
11922
12593
  // item 4). Tuned on CredData's public split; its look-alike lines are the
11923
- // negative control.
12594
+ // negative control. A quote may be escaped (`\"access_token\":\"...\"`):
12595
+ // JSON written inside a string literal. The two optional quotes after the
12596
+ // name are written out, not as a counted group, to keep star height 1.
11924
12597
  {
11925
12598
  name: "Assigned Secret",
11926
- 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,
12599
+ 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,
11927
12600
  severity: "review",
11928
- keywords: ["password", "passphrase", "passwd", "pwd", "secret", "token", "key"],
12601
+ keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11929
12602
  validate: validateAssignedSecret,
11930
12603
  argumentsOnly: true
11931
12604
  }
@@ -12193,7 +12866,169 @@ zoo`.split("\n");
12193
12866
  ]);
12194
12867
  PY_SPEC_RE = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?:\[[^\]]*\])?\s*(.*)$/;
12195
12868
  PY_EXACT_RE = /^===?\s*v?(\d+(?:\.\d+)*(?:[a-zA-Z0-9.+!-]*))$/;
12869
+ EXACT_VERSION_RE = /^=?v?(\d+(?:\.\d+)*(?:[-+][0-9A-Za-z][0-9A-Za-z.+-]*)?)$/;
12870
+ GEM_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12871
+ GEM_VALUE_FLAGS = /* @__PURE__ */ new Set([
12872
+ "-v",
12873
+ "--version",
12874
+ "-i",
12875
+ "--install-dir",
12876
+ "-n",
12877
+ "--bindir",
12878
+ "--document",
12879
+ "--build-root",
12880
+ "-P",
12881
+ "--trust-policy",
12882
+ "-s",
12883
+ "--source",
12884
+ "-p",
12885
+ "--http-proxy",
12886
+ "--platform",
12887
+ "-g",
12888
+ "--file",
12889
+ "--without"
12890
+ ]);
12891
+ BUNDLE_VALUE_FLAGS = /* @__PURE__ */ new Set([
12892
+ "-v",
12893
+ "--version",
12894
+ "-g",
12895
+ "--group",
12896
+ "-s",
12897
+ "--source",
12898
+ "-r",
12899
+ "--require",
12900
+ "--path",
12901
+ "--git",
12902
+ "--github",
12903
+ "--branch",
12904
+ "--ref",
12905
+ "--glob"
12906
+ ]);
12907
+ CRATE_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*$/u;
12908
+ CARGO_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12909
+ "-F",
12910
+ "--features",
12911
+ "--rename",
12912
+ "--registry",
12913
+ "--manifest-path",
12914
+ "-p",
12915
+ "--package",
12916
+ "--git",
12917
+ "--branch",
12918
+ "--tag",
12919
+ "--rev",
12920
+ "--path",
12921
+ "--base",
12922
+ "--target",
12923
+ "--color",
12924
+ "--config",
12925
+ "-Z",
12926
+ "--lockfile-path"
12927
+ ]);
12928
+ CARGO_INSTALL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12929
+ "--version",
12930
+ "--vers",
12931
+ "--git",
12932
+ "--branch",
12933
+ "--tag",
12934
+ "--rev",
12935
+ "--path",
12936
+ "--root",
12937
+ "--index",
12938
+ "--registry",
12939
+ "-F",
12940
+ "--features",
12941
+ "--profile",
12942
+ "--target",
12943
+ "--target-dir",
12944
+ "-j",
12945
+ "--jobs",
12946
+ "--bin",
12947
+ "--example",
12948
+ "--color",
12949
+ "--config",
12950
+ "-Z"
12951
+ ]);
12952
+ GO_VALUE_FLAGS = /* @__PURE__ */ new Set([
12953
+ "-C",
12954
+ "-tags",
12955
+ "-ldflags",
12956
+ "-gcflags",
12957
+ "-asmflags",
12958
+ "-mod",
12959
+ "-modfile",
12960
+ "-o",
12961
+ "-p",
12962
+ "-pkgdir",
12963
+ "-toolexec",
12964
+ "-exec",
12965
+ "-overlay",
12966
+ "-pgo",
12967
+ "-buildmode",
12968
+ "-compiler",
12969
+ "-gccgoflags",
12970
+ "-installsuffix",
12971
+ "-covermode",
12972
+ "-coverpkg"
12973
+ ]);
12974
+ GO_PATH_RE = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+(?:\/[\p{L}\p{N}_.~+-]+)*$/u;
12975
+ NUGET_NAME_RE = /^[\p{L}\p{N}_][\p{L}\p{N}_.-]*$/u;
12976
+ DOTNET_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12977
+ "-v",
12978
+ "--version",
12979
+ "-s",
12980
+ "--source",
12981
+ "-f",
12982
+ "--framework",
12983
+ "--package-directory"
12984
+ ]);
12985
+ DOTNET_TOOL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12986
+ "--version",
12987
+ "--add-source",
12988
+ "--configfile",
12989
+ "--framework",
12990
+ "--tool-path",
12991
+ "-a",
12992
+ "--arch",
12993
+ "--tool-manifest",
12994
+ "-v",
12995
+ "--verbosity"
12996
+ ]);
12997
+ NUGET_VALUE_FLAGS = /* @__PURE__ */ new Set([
12998
+ "-version",
12999
+ "-outputdirectory",
13000
+ "-o",
13001
+ "-source",
13002
+ "-configfile",
13003
+ "-fallbacksource",
13004
+ "-framework",
13005
+ "-packagesavemode",
13006
+ "-dependencyversion",
13007
+ "-solutiondirectory",
13008
+ "-verbosity"
13009
+ ]);
13010
+ MAVEN_COORD_RE = /^[\p{L}\p{N}_.-]+:[\p{L}\p{N}_.-]+$/u;
13011
+ COMPOSER_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*\/[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
13012
+ COMPOSER_VALUE_FLAGS = /* @__PURE__ */ new Set([
13013
+ "-d",
13014
+ "--working-dir",
13015
+ "--stability",
13016
+ "--repository",
13017
+ "--add-repository"
13018
+ ]);
13019
+ EDITOR_HEADS = ["code", "code-insiders", "codium", "vscodium", "cursor"];
13020
+ EXTENSION_ID_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*\.[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12196
13021
  MANAGER_HEADS = /* @__PURE__ */ new Set([
13022
+ "gem",
13023
+ "bundle",
13024
+ "cargo",
13025
+ "go",
13026
+ "dotnet",
13027
+ "nuget",
13028
+ "mvn",
13029
+ "mvnw",
13030
+ "composer",
13031
+ ...EDITOR_HEADS,
12197
13032
  "npm",
12198
13033
  "npx",
12199
13034
  "pnpm",
@@ -12272,6 +13107,7 @@ zoo`.split("\n");
12272
13107
  MAP_GOVERNED = /* @__PURE__ */ new Set([
12273
13108
  "files.shell-startup",
12274
13109
  "files.password-hashes",
13110
+ "files.system-dirs",
12275
13111
  "commands.inline-exec",
12276
13112
  "commands.eval-dynamic",
12277
13113
  "commands.curl-pipe-shell",
@@ -12540,7 +13376,6 @@ zoo`.split("\n");
12540
13376
  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/;
12541
13377
  PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
12542
13378
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
12543
- NUMBER_LEAD_RE = /[\s:=;([{"'+-]/;
12544
13379
  REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
12545
13380
  MAX_PII_SCAN_BYTES = 1e5;
12546
13381
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
@@ -12762,7 +13597,7 @@ zoo`.split("\n");
12762
13597
  { view: "separators-stripped", decoder: "separators", stripped: true }
12763
13598
  ];
12764
13599
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
12765
- CANONICAL_EXTRACTOR_VERSION = "canonical-v27";
13600
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v28";
12766
13601
  DEDUPE_PREVIEW_LEN = 120;
12767
13602
  ENGINE_VERSION = "1.4.0";
12768
13603
  }
@@ -12927,6 +13762,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
12927
13762
  ...transcriptPathField,
12928
13763
  ...taintFields,
12929
13764
  ...exposureFields,
13765
+ ...meta?.decidedBy && { decidedBy: meta.decidedBy },
13766
+ ...meta?.channel && { channel: meta.channel },
13767
+ ...meta?.cardId && { cardId: meta.cardId },
12930
13768
  ...testRun,
12931
13769
  agent: meta?.agent,
12932
13770
  mcpServer: meta?.mcpServer,
@@ -16014,8 +16852,204 @@ var init_state = __esm({
16014
16852
  }
16015
16853
  });
16016
16854
 
16855
+ // src/auth/decision-record.ts
16856
+ function personRecord(channel, decision, extra = {}, now = Date.now()) {
16857
+ return {
16858
+ decision,
16859
+ decidedBy: "person",
16860
+ channel,
16861
+ label: extra.redirect ? REDIRECT_LABEL : PERSON_LABELS[channel],
16862
+ ...extra,
16863
+ decidedAt: now
16864
+ };
16865
+ }
16866
+ function timerRecord(timeoutMs, now = Date.now()) {
16867
+ return {
16868
+ decision: "deny",
16869
+ decidedBy: "timer",
16870
+ label: TIMER_LABEL,
16871
+ reason: `No response within ${Math.round(timeoutMs / 1e3)}s: auto-denied by the approval timeout.`,
16872
+ decidedAt: now
16873
+ };
16874
+ }
16875
+ function abandonedRecord(now = Date.now()) {
16876
+ return {
16877
+ decision: "deny",
16878
+ decidedBy: "abandoned",
16879
+ label: ABANDONED_LABEL,
16880
+ reason: "No approver was left to answer: node9 tail was closed.",
16881
+ decidedAt: now
16882
+ };
16883
+ }
16884
+ function boundedString(v, max) {
16885
+ return typeof v === "string" && v.length > 0 ? v.slice(0, max) : void 0;
16886
+ }
16887
+ function parseDecisionRecord(x) {
16888
+ if (!x || typeof x !== "object") return null;
16889
+ const r = x;
16890
+ if (r.decision !== "allow" && r.decision !== "deny") return null;
16891
+ if (typeof r.decidedBy !== "string" || !DECIDED_BY.has(r.decidedBy)) return null;
16892
+ const label3 = boundedString(r.label, 200);
16893
+ if (!label3) return null;
16894
+ const channel = typeof r.channel === "string" && CHANNELS.includes(r.channel) ? r.channel : void 0;
16895
+ if (r.decidedBy === "person" && (!channel || !PERSON_CHANNELS.has(channel))) return null;
16896
+ const out = {
16897
+ decision: r.decision,
16898
+ decidedBy: r.decidedBy,
16899
+ label: label3,
16900
+ decidedAt: typeof r.decidedAt === "number" && Number.isFinite(r.decidedAt) ? r.decidedAt : 0
16901
+ };
16902
+ if (channel) out.channel = channel;
16903
+ const redirect = boundedString(r.redirect, 2e3);
16904
+ if (redirect) out.redirect = redirect;
16905
+ const reason = boundedString(r.reason, 2e3);
16906
+ if (reason) out.reason = reason;
16907
+ const ruleName = boundedString(r.ruleName, 256);
16908
+ if (ruleName) out.ruleName = ruleName;
16909
+ if (typeof r.standing === "string" && STANDING.has(r.standing))
16910
+ out.standing = r.standing;
16911
+ const trustDuration = boundedString(r.trustDuration, 8);
16912
+ if (trustDuration) out.trustDuration = trustDuration;
16913
+ return out;
16914
+ }
16915
+ function legacyWaitFields(record2) {
16916
+ switch (record2.decidedBy) {
16917
+ case "person": {
16918
+ const source = record2.redirect ? "terminal-redirect" : record2.channel === "native" ? "native" : "terminal";
16919
+ const reason = record2.redirect ?? record2.reason;
16920
+ return { decision: record2.decision, source, ...reason && { reason } };
16921
+ }
16922
+ case "timer":
16923
+ return {
16924
+ decision: record2.decision,
16925
+ source: "timeout",
16926
+ ...record2.reason && { reason: record2.reason }
16927
+ };
16928
+ case "cloud":
16929
+ case "policy":
16930
+ return {
16931
+ decision: record2.decision,
16932
+ source: record2.decidedBy,
16933
+ ...record2.reason && { reason: record2.reason }
16934
+ };
16935
+ case "abandoned":
16936
+ return { decision: "abandoned" };
16937
+ }
16938
+ }
16939
+ function recordFromLegacyWait(body, now = Date.now()) {
16940
+ if (body.decision === "abandoned") return abandonedRecord(now);
16941
+ const decision = body.decision;
16942
+ switch (body.source) {
16943
+ case "terminal":
16944
+ case "browser":
16945
+ return personRecord("terminal", decision, body.reason ? { reason: body.reason } : {}, now);
16946
+ case "native":
16947
+ return personRecord("native", decision, {}, now);
16948
+ case "terminal-redirect":
16949
+ return personRecord("terminal", decision, body.reason ? { redirect: body.reason } : {}, now);
16950
+ case "timeout":
16951
+ return { ...timerRecord(0, now), ...body.reason && { reason: body.reason } };
16952
+ case "cloud":
16953
+ return {
16954
+ decision,
16955
+ decidedBy: "cloud",
16956
+ label: CLOUD_LABEL,
16957
+ ...body.reason && { reason: body.reason },
16958
+ decidedAt: now
16959
+ };
16960
+ case "policy":
16961
+ return {
16962
+ decision,
16963
+ decidedBy: "policy",
16964
+ label: "Policy",
16965
+ ...body.reason && { reason: body.reason },
16966
+ decidedAt: now
16967
+ };
16968
+ default:
16969
+ return null;
16970
+ }
16971
+ }
16972
+ function recordFromVerdict(v, now = Date.now()) {
16973
+ const decidedBy = v.checkedBy === "cloud" || v.blockedBy === "team-policy" ? "cloud" : v.blockedBy === "timeout" ? "timer" : "policy";
16974
+ return {
16975
+ decision: v.approved ? "allow" : "deny",
16976
+ decidedBy,
16977
+ label: v.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : v.approved ? "Allowed by policy" : "Blocked by policy"),
16978
+ ...v.reason && { reason: v.reason },
16979
+ ...v.ruleHit && { ruleName: v.ruleHit },
16980
+ decidedAt: now
16981
+ };
16982
+ }
16983
+ function channelFromDecisionSource(source) {
16984
+ switch (source) {
16985
+ case void 0:
16986
+ case "terminal":
16987
+ case "browser":
16988
+ return { channel: "terminal", redirect: false };
16989
+ case "terminal-redirect":
16990
+ return { channel: "terminal", redirect: true };
16991
+ case "monitor":
16992
+ case "dashboard":
16993
+ return { channel: "monitor", redirect: false };
16994
+ case "native":
16995
+ return { channel: "native", redirect: false };
16996
+ default:
16997
+ return null;
16998
+ }
16999
+ }
17000
+ function recordFromLegacyResolve(decision, source, now = Date.now()) {
17001
+ const person = source === void 0 ? null : channelFromDecisionSource(source);
17002
+ if (person) return personRecord(person.channel, decision, {}, now);
17003
+ if (decision === "allow")
17004
+ return { decision, decidedBy: "cloud", label: CLOUD_LABEL, decidedAt: now };
17005
+ return {
17006
+ decision,
17007
+ decidedBy: "timer",
17008
+ label: "Resolved by the hook",
17009
+ reason: "An older node9 hook ended the approval without naming who decided.",
17010
+ decidedAt: now
17011
+ };
17012
+ }
17013
+ var CHANNELS, LOCAL_UI_CHANNELS, PERSON_CHANNELS, CARD_PROTOCOL, PERSON_LABELS, REDIRECT_LABEL, TIMER_LABEL, CLOUD_LABEL, ABANDONED_LABEL, DECIDED_BY, STANDING;
17014
+ var init_decision_record = __esm({
17015
+ "src/auth/decision-record.ts"() {
17016
+ "use strict";
17017
+ CHANNELS = ["terminal", "monitor", "native", "cloud"];
17018
+ LOCAL_UI_CHANNELS = /* @__PURE__ */ new Set(["terminal", "monitor"]);
17019
+ PERSON_CHANNELS = /* @__PURE__ */ new Set([
17020
+ "terminal",
17021
+ "monitor",
17022
+ "native"
17023
+ ]);
17024
+ CARD_PROTOCOL = 2;
17025
+ PERSON_LABELS = {
17026
+ terminal: "User Decision (Terminal (node9 tail))",
17027
+ monitor: "User Decision (node9 monitor)",
17028
+ native: "User Decision (Native)"
17029
+ };
17030
+ REDIRECT_LABEL = "Steered Redirect (Terminal)";
17031
+ TIMER_LABEL = "Approval Timeout";
17032
+ CLOUD_LABEL = "Organization Policy (SaaS)";
17033
+ ABANDONED_LABEL = "No Approver (node9 tail closed)";
17034
+ DECIDED_BY = /* @__PURE__ */ new Set([
17035
+ "person",
17036
+ "timer",
17037
+ "cloud",
17038
+ "policy",
17039
+ "abandoned"
17040
+ ]);
17041
+ STANDING = /* @__PURE__ */ new Set([
17042
+ "always-allow",
17043
+ "always-deny",
17044
+ "trust",
17045
+ "trust-recipients"
17046
+ ]);
17047
+ }
17048
+ });
17049
+
16017
17050
  // src/auth/daemon.ts
16018
17051
  import fs11 from "fs";
17052
+ import http from "http";
16019
17053
  import net from "net";
16020
17054
  import path11 from "path";
16021
17055
  import os9 from "os";
@@ -16134,7 +17168,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
16134
17168
  return false;
16135
17169
  }
16136
17170
  }
16137
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
17171
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId, card) {
16138
17172
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16139
17173
  const ctrl = new AbortController();
16140
17174
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -16160,19 +17194,43 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
16160
17194
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
16161
17195
  ...viewOnly && { viewOnly: true },
16162
17196
  ...localSmartRuleMatched && { localSmartRuleMatched: true },
16163
- ...sessionId && { sessionId }
17197
+ ...sessionId && { sessionId },
17198
+ ...cardFields(card)
16164
17199
  }),
16165
17200
  signal: ctrl.signal
16166
17201
  });
16167
17202
  if (!res.ok) throw new Error("Daemon fail");
16168
- const { id, allowCount } = await res.json();
16169
- return { id, allowCount: allowCount ?? 1 };
17203
+ return registeredCard(await res.json());
16170
17204
  } finally {
16171
17205
  clearTimeout(timer);
16172
17206
  }
16173
17207
  }
17208
+ function cardFields(card) {
17209
+ return {
17210
+ // A review card: the hook already decided this call needs a person; the
17211
+ // daemon never checks it again (approval-card design, defect 1).
17212
+ kind: "review",
17213
+ // Protocol 2 makes the card's /wait require the internal token: claim it
17214
+ // only when this process can read the token (a daemon under another uid
17215
+ // is running but its pid file probe fails with EPERM), or every wait
17216
+ // would get 403 and the review would be denied unseen.
17217
+ ...getInternalToken() && { protocol: CARD_PROTOCOL },
17218
+ ...card?.channels && { channels: card.channels },
17219
+ ...typeof card?.timeoutMs === "number" && { timeoutMs: card.timeoutMs }
17220
+ };
17221
+ }
17222
+ function registeredCard(body) {
17223
+ const b = body;
17224
+ return {
17225
+ id: b.id,
17226
+ allowCount: b.allowCount ?? 1,
17227
+ ...b.protocol === CARD_PROTOCOL && {
17228
+ protocol: CARD_PROTOCOL,
17229
+ deadline: typeof b.deadline === "number" ? b.deadline : null
17230
+ }
17231
+ };
17232
+ }
16174
17233
  async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
16175
- const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16176
17234
  const waitCtrl = new AbortController();
16177
17235
  let capped = false;
16178
17236
  const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
@@ -16182,14 +17240,18 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
16182
17240
  const onAbort = () => waitCtrl.abort();
16183
17241
  if (signal) signal.addEventListener("abort", onAbort);
16184
17242
  try {
16185
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
17243
+ const { status, body } = await daemonGet(`/wait/${id}`, waitCtrl.signal).catch(
16186
17244
  (err2) => {
16187
17245
  if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
16188
17246
  throw err2;
16189
17247
  }
16190
17248
  );
16191
- if (!waitRes.ok) return { decision: "deny" };
16192
- const { decision, source, reason } = await waitRes.json();
17249
+ if (status === 409) throw new Error("node9 card was withdrawn");
17250
+ if (status !== 200) return { decision: "deny" };
17251
+ const parsed = JSON.parse(body);
17252
+ const record2 = parseDecisionRecord(parsed.record);
17253
+ if (record2) return { ...legacyWaitFields(record2), record: record2 };
17254
+ const { decision, source, reason } = parsed;
16193
17255
  if (decision === "allow") return { decision: "allow", source };
16194
17256
  if (decision === "abandoned") return { decision: "abandoned", source };
16195
17257
  return { decision: "deny", source, reason };
@@ -16198,7 +17260,29 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
16198
17260
  if (signal) signal.removeEventListener("abort", onAbort);
16199
17261
  }
16200
17262
  }
16201
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
17263
+ function daemonGet(urlPath, signal, token = getInternalToken()) {
17264
+ return new Promise((resolve2, reject) => {
17265
+ const req = http.get(
17266
+ {
17267
+ host: DAEMON_HOST,
17268
+ port: DAEMON_PORT,
17269
+ path: urlPath,
17270
+ signal,
17271
+ agent: false,
17272
+ headers: token ? { "X-Node9-Internal": token } : {}
17273
+ },
17274
+ (res) => {
17275
+ let body = "";
17276
+ res.setEncoding("utf8");
17277
+ res.on("data", (chunk2) => body += chunk2);
17278
+ res.on("end", () => resolve2({ status: res.statusCode ?? 0, body }));
17279
+ res.on("error", reject);
17280
+ }
17281
+ );
17282
+ req.on("error", reject);
17283
+ });
17284
+ }
17285
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent, extra) {
16202
17286
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16203
17287
  const res = await fetch(`${base2}/check`, {
16204
17288
  method: "POST",
@@ -16214,13 +17298,16 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
16214
17298
  // event via socket. Same contract as registerDaemonEntry — without
16215
17299
  // it the daemon double-emits 'activity' for cloud-enforced flows.
16216
17300
  fromCLI: socketActivitySent !== false,
16217
- activityId
17301
+ activityId,
17302
+ ...extra?.cwd && { cwd: extra.cwd },
17303
+ ...extra?.recoveryCommand && { recoveryCommand: extra.recoveryCommand },
17304
+ ...extra?.sessionId && { sessionId: extra.sessionId },
17305
+ ...cardFields(extra?.card)
16218
17306
  }),
16219
17307
  signal: AbortSignal.timeout(3e3)
16220
17308
  });
16221
17309
  if (!res.ok) throw new Error("Daemon unreachable");
16222
- const { id, allowCount } = await res.json();
16223
- return { id, allowCount: allowCount ?? 1 };
17310
+ return registeredCard(await res.json());
16224
17311
  }
16225
17312
  async function notifyTaint(filePath, source, fromEid) {
16226
17313
  if (!isDaemonRunning()) return;
@@ -16404,19 +17491,24 @@ async function clearSessionExposure(opts) {
16404
17491
  return { ok: false, cleared: 0, daemonUnavailable: true };
16405
17492
  }
16406
17493
  }
16407
- async function resolveViaDaemon(id, decision, internalToken, source) {
17494
+ async function resolveViaDaemon(id, decision, internalToken, source, record2) {
16408
17495
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16409
- await fetch(`${base2}/resolve/${id}`, {
17496
+ const res = await fetch(`${base2}/resolve/${id}`, {
16410
17497
  method: "POST",
16411
17498
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
16412
- body: JSON.stringify({ decision, ...source && { source } }),
17499
+ body: JSON.stringify({ decision, ...source && { source }, ...record2 && { record: record2 } }),
16413
17500
  signal: AbortSignal.timeout(3e3)
16414
17501
  });
17502
+ if (!res.ok) return null;
17503
+ const body = await res.json().catch(() => null);
17504
+ const winner = parseDecisionRecord(body?.record);
17505
+ return winner && typeof body?.won === "boolean" ? { won: body.won, record: winner } : null;
16415
17506
  }
16416
17507
  var ACTIVITY_SOCKET_PATH, DEFAULT_DAEMON_PORT, DAEMON_PORT, DAEMON_HOST;
16417
17508
  var init_daemon = __esm({
16418
17509
  "src/auth/daemon.ts"() {
16419
17510
  "use strict";
17511
+ init_decision_record();
16420
17512
  ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path11.join(os9.tmpdir(), "node9-activity.sock");
16421
17513
  DEFAULT_DAEMON_PORT = 7391;
16422
17514
  DAEMON_PORT = resolveDaemonPort();
@@ -18122,7 +19214,20 @@ function ecoDir(eco) {
18122
19214
  return path24.join(osvRoot(), eco);
18123
19215
  }
18124
19216
  function indexKey(eco, name) {
18125
- return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
19217
+ switch (eco) {
19218
+ case "PyPI":
19219
+ return normalizePyPiName(name);
19220
+ case "crates.io":
19221
+ return name.toLowerCase().replace(/_/g, "-");
19222
+ case "Go":
19223
+ case "Maven":
19224
+ return name;
19225
+ default:
19226
+ return name.toLowerCase();
19227
+ }
19228
+ }
19229
+ function recordEcosystemMatches(eco, recordEco) {
19230
+ return typeof recordEco === "string" && (recordEco === eco || recordEco.startsWith(`${eco}:`));
18126
19231
  }
18127
19232
  function shardOf(key) {
18128
19233
  return createHash5("sha1").update(key).digest("hex").slice(0, 2);
@@ -18168,7 +19273,8 @@ function entriesFromRecord(eco, record2) {
18168
19273
  const modifiedMs = typeof r.modified === "string" ? Date.parse(r.modified) || 0 : 0;
18169
19274
  const rows = /* @__PURE__ */ new Map();
18170
19275
  for (const a of r.affected ?? []) {
18171
- if (a?.package?.ecosystem !== eco || typeof a.package.name !== "string") continue;
19276
+ if (!recordEcosystemMatches(eco, a?.package?.ecosystem) || typeof a?.package?.name !== "string")
19277
+ continue;
18172
19278
  const key = indexKey(eco, a.package.name);
18173
19279
  const entry = rows.get(key) ?? { id: r.id };
18174
19280
  if (Array.isArray(a.versions)) {
@@ -18408,15 +19514,18 @@ async function pypiInfo(name, version2) {
18408
19514
  };
18409
19515
  }
18410
19516
  async function registryInfo(eco, name, version2, freshWindowMs, raw) {
18411
- return eco === "npm" ? npmInfo(name, version2, freshWindowMs, raw) : pypiInfo(name, version2);
19517
+ if (eco === "npm") return npmInfo(name, version2, freshWindowMs, raw);
19518
+ if (eco === "PyPI") return pypiInfo(name, version2);
19519
+ return null;
18412
19520
  }
18413
- var TIMEOUT_MS2, MAX_BYTES;
19521
+ var TIMEOUT_MS2, MAX_BYTES, HAS_REGISTRY_CLIENT;
18414
19522
  var init_registry2 = __esm({
18415
19523
  "src/supply-chain/registry.ts"() {
18416
19524
  "use strict";
18417
19525
  init_net();
18418
19526
  TIMEOUT_MS2 = 1500;
18419
19527
  MAX_BYTES = 2 * 1024 * 1024;
19528
+ HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
18420
19529
  }
18421
19530
  });
18422
19531
 
@@ -18463,25 +19572,39 @@ function commandOf(toolName, args) {
18463
19572
  return typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : null;
18464
19573
  }
18465
19574
  function label(p, version2) {
18466
- return `${p.ecosystem === "npm" ? "npm" : "PyPI"} package "${p.name}${version2 ? `@${version2}` : ""}"`;
19575
+ return `${KIND[p.ecosystem]} "${p.name}${version2 ? `@${version2}` : ""}"`;
19576
+ }
19577
+ function osvNames(p) {
19578
+ if (p.ecosystem !== "Go") return [p.name];
19579
+ const parts = p.name.split("/");
19580
+ const out = [];
19581
+ for (let n = parts.length; n >= 2 && out.length < 6; n--) out.push(parts.slice(0, n).join("/"));
19582
+ return out;
18467
19583
  }
18468
19584
  async function maliciousFor(p, version2, cfg, misses, networkCapped) {
18469
- const local = lookupIndex(p.ecosystem, p.name);
18470
- const entries = local.status === "hit" ? local.entries : null;
18471
- if (!entries && !(local.status === "clean" && local.fresh)) {
19585
+ const names = osvNames(p);
19586
+ const local = names.map((n) => lookupIndex(p.ecosystem, n));
19587
+ const hits2 = local.flatMap((l) => l.status === "hit" ? l.entries : []);
19588
+ const entries = hits2.length > 0 ? hits2 : null;
19589
+ const freshClean = local.every((l) => l.status === "clean" && l.fresh);
19590
+ const status = local.some((l) => l.status === "unavailable") ? "unavailable" : local[0].status;
19591
+ if (!entries && !freshClean) {
18472
19592
  if (!cfg.onlineFallback || networkCapped) {
18473
19593
  misses.push(
18474
- `${label(p, version2)}: local index ${local.status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
19594
+ `${label(p, version2)}: local index ${status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
18475
19595
  );
18476
19596
  return null;
18477
19597
  }
18478
- let ids = null;
19598
+ let perName = null;
18479
19599
  try {
18480
- ids = await queryOsvMalicious([{ ecosystem: p.ecosystem, name: p.name, version: version2 }]);
19600
+ perName = await queryOsvMalicious(
19601
+ names.map((name) => ({ ecosystem: p.ecosystem, name, version: version2 }))
19602
+ );
18481
19603
  } catch (err2) {
18482
19604
  misses.push(`${label(p, version2)}: OSV online ${err2.message}`);
18483
19605
  return null;
18484
19606
  }
19607
+ const ids = perName ? [[...new Set(perName.flat())]] : null;
18485
19608
  if (!ids) {
18486
19609
  misses.push(`${label(p, version2)}: OSV online unavailable`);
18487
19610
  return null;
@@ -18551,7 +19674,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
18551
19674
  }
18552
19675
  const findings = [];
18553
19676
  let info = null;
18554
- const wantsRegistry = cfg.newPackage !== "off" || cfg.installScript !== "off";
19677
+ const wantsRegistry = (cfg.newPackage !== "off" || cfg.installScript !== "off") && HAS_REGISTRY_CLIENT.has(p.ecosystem);
18555
19678
  if (wantsRegistry && !networkCapped) {
18556
19679
  try {
18557
19680
  const windowMs = cfg.newPackage === "off" ? 0 : cfg.maxAgeHours * 36e5;
@@ -18631,7 +19754,7 @@ async function runPackageCheck(toolName, args, cfg, cwd) {
18631
19754
  return { ...ALLOW, misses: [`package check error: ${err2?.message ?? err2}`] };
18632
19755
  }
18633
19756
  }
18634
- var MAX_NETWORK_PACKAGES, ALLOW;
19757
+ var MAX_NETWORK_PACKAGES, ALLOW, KIND;
18635
19758
  var init_check = __esm({
18636
19759
  "src/supply-chain/check.ts"() {
18637
19760
  "use strict";
@@ -18642,11 +19765,29 @@ var init_check = __esm({
18642
19765
  init_local_resolve();
18643
19766
  MAX_NETWORK_PACKAGES = 10;
18644
19767
  ALLOW = { verdict: "allow", findings: [], misses: [], checked: 0 };
19768
+ KIND = {
19769
+ npm: "npm package",
19770
+ PyPI: "PyPI package",
19771
+ RubyGems: "RubyGems package",
19772
+ "crates.io": "crates.io package",
19773
+ Go: "Go module",
19774
+ NuGet: "NuGet package",
19775
+ Maven: "Maven artifact",
19776
+ Packagist: "Packagist package",
19777
+ VSCode: "VS Code extension"
19778
+ };
18645
19779
  }
18646
19780
  });
18647
19781
 
18648
19782
  // src/policy/negotiation.ts
18649
19783
  function buildNegotiationMessage(blockedByLabel, isHumanDecision2, humanReason, recoveryCommand, context) {
19784
+ if (isHumanDecision2 && context?.redirect && humanReason) {
19785
+ return `NODE9: The user stopped this action and gave you an instruction.
19786
+ INSTRUCTION FROM THE USER: ${humanReason}
19787
+ INSTRUCTIONS:
19788
+ - Follow the user's instruction above.
19789
+ - Retry the original action only if the instruction says you may.`;
19790
+ }
18650
19791
  if (isHumanDecision2) {
18651
19792
  return `NODE9: The human user rejected this action.
18652
19793
  REASON: ${humanReason || "No specific reason provided."}
@@ -18737,7 +19878,8 @@ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
18737
19878
  const sentence2 = /[.!?\u2026]$/.test(trimmed) ? trimmed : `${trimmed}.`;
18738
19879
  return `Node9 flagged this for your review: ${sentence2} Approve to proceed, or deny to cancel.`;
18739
19880
  }
18740
- function isHumanDecision(label3, blockedBy) {
19881
+ function isHumanDecision(label3, blockedBy, decidedBy) {
19882
+ if (decidedBy !== void 0) return decidedBy === "person";
18741
19883
  if (blockedBy !== void 0 && blockedBy !== "local-decision") return false;
18742
19884
  const l = label3.toLowerCase();
18743
19885
  return l.includes("user") || l.includes("daemon") || l.includes("decision");
@@ -18769,26 +19911,103 @@ var init_negotiation = __esm({
18769
19911
  }
18770
19912
  });
18771
19913
 
18772
- // src/auth/decision-sources.ts
18773
- function backgroundPassSource(result) {
18774
- if (result.checkedBy === "cloud" || result.blockedBy === "team-policy") return "cloud";
18775
- if (result.blockedBy === "timeout") return "timeout";
19914
+ // src/auth/card-result.ts
19915
+ function authResultFromRecord(r, ctx = {}) {
19916
+ const who = { decidedBy: r.decidedBy, ...r.channel && { channel: r.channel } };
19917
+ switch (r.decidedBy) {
19918
+ case "person": {
19919
+ const channel = r.channel ?? "terminal";
19920
+ const decisionSource = channel === "native" ? "native" : "terminal";
19921
+ if (r.decision === "allow") {
19922
+ return { approved: true, checkedBy: "daemon", decisionSource, ...who };
19923
+ }
19924
+ return {
19925
+ approved: false,
19926
+ reason: r.redirect || `The human user rejected this action via the Node9 ${CHANNEL_NAMES[channel]}.`,
19927
+ blockedBy: "local-decision",
19928
+ blockedByLabel: r.label,
19929
+ decisionSource,
19930
+ ...who,
19931
+ ...r.redirect && { redirect: r.redirect }
19932
+ };
19933
+ }
19934
+ case "timer":
19935
+ if (r.decision === "deny" && ctx.downgradedBlock)
19936
+ return { ...ctx.downgradedBlock, blockedBy: "timeout", ...who };
19937
+ return r.decision === "allow" ? { approved: true, ...who } : {
19938
+ approved: false,
19939
+ reason: ctx.timeoutReason || r.reason || "No response: auto-denied after the approval timeout.",
19940
+ blockedBy: "timeout",
19941
+ blockedByLabel: TIMER_LABEL,
19942
+ ...who
19943
+ };
19944
+ case "cloud":
19945
+ return r.decision === "allow" ? { approved: true, checkedBy: "cloud", decisionSource: "cloud", ...who } : {
19946
+ approved: false,
19947
+ reason: r.reason || "Action rejected by organization administrator via Slack.",
19948
+ blockedBy: "team-policy",
19949
+ blockedByLabel: CLOUD_LABEL,
19950
+ decisionSource: "cloud",
19951
+ ...who
19952
+ };
19953
+ case "policy":
19954
+ return r.decision === "allow" ? { approved: true, checkedBy: "local-policy", ...who } : {
19955
+ approved: false,
19956
+ reason: r.reason || "Action blocked by policy.",
19957
+ blockedBy: "local-config",
19958
+ blockedByLabel: r.label,
19959
+ ...r.ruleName && { ruleHit: r.ruleName },
19960
+ ...who
19961
+ };
19962
+ case "abandoned":
19963
+ return {
19964
+ approved: false,
19965
+ reason: r.reason || "No approver was left to answer.",
19966
+ blockedBy: "no-approval-mechanism",
19967
+ blockedByLabel: ABANDONED_LABEL,
19968
+ ...who
19969
+ };
19970
+ }
19971
+ }
19972
+ function approvalUnavailable(reason) {
19973
+ return {
19974
+ approved: false,
19975
+ reason: reason || "node9 could not get an approval decision for this call.",
19976
+ blockedBy: "no-approval-mechanism",
19977
+ blockedByLabel: "Approval Unavailable"
19978
+ };
19979
+ }
19980
+ function decidedByOf(res) {
19981
+ if (res.decidedBy) return res.decidedBy;
19982
+ if (res.blockedBy === "timeout") return "timer";
19983
+ if (res.checkedBy === "cloud" || res.blockedBy === "team-policy") return "cloud";
18776
19984
  return "policy";
18777
19985
  }
18778
- var HUMAN_DECISION_SOURCES, REDIRECT_SOURCE, DECISION_ROUTE_SOURCES;
18779
- var init_decision_sources = __esm({
18780
- "src/auth/decision-sources.ts"() {
19986
+ function recordFromAuthResult(res, now = Date.now()) {
19987
+ const decidedBy = decidedByOf(res);
19988
+ const channel = res.channel ?? (decidedBy === "cloud" ? "cloud" : decidedBy === "person" ? "native" : void 0);
19989
+ return {
19990
+ decision: res.approved ? "allow" : "deny",
19991
+ decidedBy,
19992
+ ...channel && { channel },
19993
+ label: res.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : decidedBy === "timer" ? TIMER_LABEL : res.approved ? "Allowed" : "Blocked"),
19994
+ ...res.reason && { reason: res.reason.slice(0, 2e3) },
19995
+ ...res.ruleHit && { ruleName: res.ruleHit },
19996
+ ...res.redirect && { redirect: res.redirect },
19997
+ decidedAt: now
19998
+ };
19999
+ }
20000
+ var CHANNEL_NAMES;
20001
+ var init_card_result = __esm({
20002
+ "src/auth/card-result.ts"() {
18781
20003
  "use strict";
18782
- HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
18783
- "terminal",
18784
- "browser",
18785
- "native"
18786
- ]);
18787
- REDIRECT_SOURCE = "terminal-redirect";
18788
- DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
18789
- ...HUMAN_DECISION_SOURCES,
18790
- REDIRECT_SOURCE
18791
- ]);
20004
+ init_decision_record();
20005
+ CHANNEL_NAMES = {
20006
+ terminal: "Terminal (node9 tail)",
20007
+ monitor: "node9 monitor",
20008
+ native: "system dialog window",
20009
+ cloud: "organization"
20010
+ };
18792
20011
  }
18793
20012
  });
18794
20013
 
@@ -19819,28 +21038,22 @@ ${packageReview}`;
19819
21038
  const { signal } = abortController;
19820
21039
  const racePromises = [];
19821
21040
  const approvalTimeoutMs = config.settings.approvalTimeoutMs ?? 0;
19822
- if (approvalTimeoutMs > 0) {
19823
- racePromises.push(
19824
- new Promise((resolve2, reject) => {
19825
- const timer = setTimeout(() => {
19826
- resolve2({
19827
- approved: false,
19828
- reason: approvalTimeoutReason(approvalTimeoutMs),
19829
- blockedBy: "timeout",
19830
- blockedByLabel: "Approval Timeout"
19831
- });
19832
- }, approvalTimeoutMs);
19833
- signal.addEventListener("abort", () => {
19834
- clearTimeout(timer);
19835
- reject(new Error("Aborted"));
19836
- });
19837
- })
19838
- );
19839
- }
19840
21041
  let viewerId = null;
19841
21042
  const internalToken = getInternalToken();
19842
21043
  let daemonEntryId = null;
19843
21044
  let daemonAllowCount = 1;
21045
+ let cardDeadline;
21046
+ const cardOptions = {
21047
+ channels: [
21048
+ "terminal",
21049
+ "monitor",
21050
+ // Only when the popup can show: a card listing a channel nobody can
21051
+ // use is never abandoned when node9 tail closes, and waits unanswered.
21052
+ ...approvers.native && !isManual && !options?.calledFromDaemon && (process.platform === "darwin" || process.platform === "win32" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) ? ["native"] : [],
21053
+ ...cloudEnforced && cloudRequestId && cloudMayVote ? ["cloud"] : []
21054
+ ],
21055
+ timeoutMs: approvalTimeoutMs
21056
+ };
19844
21057
  if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
19845
21058
  if (cloudEnforced && cloudRequestId) {
19846
21059
  const viewer = await notifyDaemonViewer(
@@ -19849,11 +21062,20 @@ ${packageReview}`;
19849
21062
  meta,
19850
21063
  riskMetadata,
19851
21064
  options?.activityId,
19852
- options?.socketActivitySent
21065
+ options?.socketActivitySent,
21066
+ {
21067
+ cwd: options?.cwd,
21068
+ recoveryCommand: statefulRecoveryCommand,
21069
+ sessionId: meta?.sessionId,
21070
+ card: cardOptions
21071
+ }
19853
21072
  ).catch(() => null);
19854
21073
  viewerId = viewer?.id ?? null;
19855
21074
  daemonEntryId = viewerId;
19856
- if (viewer) daemonAllowCount = viewer.allowCount;
21075
+ if (viewer) {
21076
+ daemonAllowCount = viewer.allowCount;
21077
+ cardDeadline = viewer.deadline;
21078
+ }
19857
21079
  } else {
19858
21080
  try {
19859
21081
  const entry = await registerDaemonEntry(
@@ -19877,14 +21099,35 @@ ${packageReview}`;
19877
21099
  void 0,
19878
21100
  localSmartRuleMatched || options?.localSmartRuleMatched,
19879
21101
  options?.socketActivitySent,
19880
- meta?.sessionId
21102
+ meta?.sessionId,
21103
+ cardOptions
19881
21104
  );
19882
21105
  daemonEntryId = entry.id;
19883
21106
  daemonAllowCount = entry.allowCount;
21107
+ cardDeadline = entry.deadline;
19884
21108
  } catch {
19885
21109
  }
19886
21110
  }
19887
21111
  }
21112
+ if (approvalTimeoutMs > 0 && cardDeadline === void 0) {
21113
+ racePromises.push(
21114
+ new Promise((resolve2, reject) => {
21115
+ const timer = setTimeout(() => {
21116
+ resolve2({
21117
+ approved: false,
21118
+ reason: approvalTimeoutReason(approvalTimeoutMs),
21119
+ blockedBy: "timeout",
21120
+ blockedByLabel: "Approval Timeout",
21121
+ decidedBy: "timer"
21122
+ });
21123
+ }, approvalTimeoutMs);
21124
+ signal.addEventListener("abort", () => {
21125
+ clearTimeout(timer);
21126
+ reject(new Error("Aborted"));
21127
+ });
21128
+ })
21129
+ );
21130
+ }
19888
21131
  if (cloudEnforced && cloudRequestId && cloudMayVote) {
19889
21132
  racePromises.push(
19890
21133
  (async () => {
@@ -19895,7 +21138,9 @@ ${packageReview}`;
19895
21138
  reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
19896
21139
  checkedBy: cloudResult.approved ? "cloud" : void 0,
19897
21140
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
19898
- blockedByLabel: "Organization Policy (SaaS)"
21141
+ blockedByLabel: "Organization Policy (SaaS)",
21142
+ decidedBy: "cloud",
21143
+ channel: "cloud"
19899
21144
  };
19900
21145
  } catch (err2) {
19901
21146
  const error = err2;
@@ -19925,14 +21170,31 @@ ${packageReview}`;
19925
21170
  if (config.policySource !== "workspace") {
19926
21171
  for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
19927
21172
  }
19928
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
21173
+ return {
21174
+ approved: true,
21175
+ checkedBy: "daemon",
21176
+ decisionSource: "native",
21177
+ decidedBy: "person",
21178
+ channel: "native"
21179
+ };
19929
21180
  }
19930
21181
  if (decision === "always_allow") {
19931
21182
  if (standingTrustApplies()) {
19932
21183
  writeTrustSession(toolName, 36e5, args);
19933
- return { approved: true, checkedBy: "trust" };
21184
+ return {
21185
+ approved: true,
21186
+ checkedBy: "trust",
21187
+ decidedBy: "person",
21188
+ channel: "native"
21189
+ };
19934
21190
  }
19935
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
21191
+ return {
21192
+ approved: true,
21193
+ checkedBy: "daemon",
21194
+ decisionSource: "native",
21195
+ decidedBy: "person",
21196
+ channel: "native"
21197
+ };
19936
21198
  }
19937
21199
  const isApproved = decision === "allow";
19938
21200
  return {
@@ -19941,75 +21203,32 @@ ${packageReview}`;
19941
21203
  checkedBy: isApproved ? "daemon" : void 0,
19942
21204
  blockedBy: isApproved ? void 0 : "local-decision",
19943
21205
  blockedByLabel: "User Decision (Native)",
19944
- decisionSource: "native"
21206
+ decisionSource: "native",
21207
+ decidedBy: "person",
21208
+ channel: "native"
19945
21209
  };
19946
21210
  })()
19947
21211
  );
19948
21212
  }
21213
+ const fromCard = /* @__PURE__ */ new WeakSet();
21214
+ const timeoutReason = approvalTimeoutMs > 0 ? approvalTimeoutReason(approvalTimeoutMs) : void 0;
19949
21215
  if (daemonEntryId && approvers.terminal) {
19950
21216
  racePromises.push(
19951
21217
  (async () => {
19952
- const {
19953
- decision: daemonDecision,
19954
- source: decisionSource,
19955
- reason: daemonReason
19956
- } = await waitForDaemonDecision(
19957
- daemonEntryId,
19958
- signal,
19959
- daemonWaitCapMs(config.settings.approvalTimeoutMs)
19960
- );
19961
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
19962
- const isApproved = daemonDecision === "allow";
19963
- const isRedirect = decisionSource === REDIRECT_SOURCE;
19964
- if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
19965
- if (decisionSource === "cloud")
19966
- return {
19967
- approved: false,
19968
- reason: daemonReason || "Action rejected by organization administrator via Slack.",
19969
- blockedBy: "team-policy",
19970
- blockedByLabel: "Organization Policy (SaaS)",
19971
- decisionSource: "cloud"
19972
- };
19973
- const timedOut = decisionSource === "timeout";
19974
- if (downgradedBlock)
19975
- return {
19976
- ...downgradedBlock,
19977
- blockedBy: timedOut ? "timeout" : "local-config"
19978
- };
19979
- if (timedOut)
19980
- return {
19981
- approved: false,
19982
- reason: daemonReason || "No response: auto-denied after the approval timeout.",
19983
- blockedBy: "timeout",
19984
- blockedByLabel: "Approval Timeout"
19985
- };
19986
- if (decisionSource === "policy")
19987
- return {
19988
- approved: false,
19989
- reason: daemonReason || "Action blocked by policy.",
19990
- blockedBy: "local-config",
19991
- blockedByLabel: explainableLabel
19992
- };
19993
- return {
19994
- approved: false,
19995
- reason: daemonReason || "node9 could not get an approval decision for this call.",
19996
- blockedBy: "no-approval-mechanism",
19997
- blockedByLabel: "Approval Unavailable"
19998
- };
19999
- }
20000
- if (isApproved && decisionSource === "cloud")
20001
- return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
20002
- if (isApproved && decisionSource === "policy")
20003
- return { approved: true, checkedBy: "local-policy" };
20004
- const via = "Terminal (node9 tail)";
20005
- return {
20006
- approved: isApproved,
20007
- reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
20008
- checkedBy: isApproved ? "daemon" : void 0,
20009
- blockedBy: isApproved ? void 0 : "local-decision",
20010
- blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
20011
- decisionSource: "terminal"
20012
- };
21218
+ const cap = cardDeadline === void 0 ? daemonWaitCapMs(config.settings.approvalTimeoutMs) : cardDeadline === null ? 0 : Math.max(cardDeadline - Date.now(), 0) + 5e3;
21219
+ const outcome = await waitForDaemonDecision(daemonEntryId, signal, cap);
21220
+ const legacy = !outcome.record;
21221
+ const record2 = outcome.record ?? recordFromLegacyWait(outcome);
21222
+ if (downgradedBlock && (!record2 || legacy && record2.decidedBy === "policy" && record2.decision === "deny")) {
21223
+ const softened = { ...downgradedBlock, blockedBy: "local-config" };
21224
+ fromCard.add(softened);
21225
+ return softened;
21226
+ }
21227
+ if (!record2) return approvalUnavailable(outcome.reason);
21228
+ if (legacy && record2.decidedBy === "policy") record2.label = explainableLabel;
21229
+ const result = authResultFromRecord(record2, { downgradedBlock, timeoutReason });
21230
+ fromCard.add(result);
21231
+ return result;
20013
21232
  })()
20014
21233
  );
20015
21234
  }
@@ -20041,7 +21260,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20041
21260
  blockedByLabel: explainableLabel
20042
21261
  };
20043
21262
  }
20044
- const finalResult = await new Promise((resolve2) => {
21263
+ const raceResult = await new Promise((resolve2) => {
20045
21264
  let resolved = false;
20046
21265
  let failures = 0;
20047
21266
  const total = racePromises.length;
@@ -20049,14 +21268,6 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20049
21268
  if (!resolved) {
20050
21269
  resolved = true;
20051
21270
  abortController.abort();
20052
- if (daemonEntryId && internalToken) {
20053
- resolveViaDaemon(
20054
- daemonEntryId,
20055
- res.approved ? "allow" : "deny",
20056
- internalToken,
20057
- res.decisionSource
20058
- ).catch(() => null);
20059
- }
20060
21271
  resolve2(res);
20061
21272
  }
20062
21273
  };
@@ -20064,26 +21275,29 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20064
21275
  p.then(finish).catch((err2) => {
20065
21276
  if (err2.name === "AbortError" || err2.message?.includes("canceled") || err2.message?.includes("Aborted"))
20066
21277
  return;
20067
- if (err2.message === "Abandoned") {
20068
- finish({
20069
- approved: false,
20070
- reason: "Browser dashboard closed without making a decision.",
20071
- blockedBy: "local-decision",
20072
- blockedByLabel: "Browser Dashboard (Abandoned)"
20073
- });
20074
- return;
20075
- }
20076
21278
  failures++;
20077
21279
  if (failures === total && !resolved) {
20078
- finish({ approved: false, reason: "All approval channels failed or disconnected." });
21280
+ finish(approvalUnavailable("All approval channels failed or disconnected."));
20079
21281
  }
20080
21282
  });
20081
21283
  }
20082
21284
  });
21285
+ let finalResult = raceResult;
21286
+ if (daemonEntryId && internalToken && !fromCard.has(raceResult)) {
21287
+ const reply = await resolveViaDaemon(
21288
+ daemonEntryId,
21289
+ raceResult.approved ? "allow" : "deny",
21290
+ internalToken,
21291
+ raceResult.decisionSource,
21292
+ recordFromAuthResult(raceResult)
21293
+ ).catch(() => null);
21294
+ if (reply && !reply.won)
21295
+ finalResult = authResultFromRecord(reply.record, { downgradedBlock, timeoutReason });
21296
+ }
20083
21297
  if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
20084
21298
  await approveSessionExposure(meta.sessionId, exposureKeys);
20085
21299
  }
20086
- if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
21300
+ if (cloudRequestId && creds && finalResult.checkedBy !== "cloud" && finalResult.decidedBy !== "cloud") {
20087
21301
  await resolveNode9SaaS(
20088
21302
  cloudRequestId,
20089
21303
  creds,
@@ -20107,7 +21321,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20107
21321
  {
20108
21322
  ...ownRowMeta(finalResult.approved),
20109
21323
  ...cloudRequestId && { cloudRequestId },
20110
- ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
21324
+ ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` },
21325
+ // Who decided, from the decision record, and the card: the daemon
21326
+ // writes its own row for the same card, and the report counts one.
21327
+ ...finalResult.decidedBy && { decidedBy: finalResult.decidedBy },
21328
+ ...finalResult.channel && { channel: finalResult.channel },
21329
+ ...daemonEntryId && { cardId: daemonEntryId }
20111
21330
  },
20112
21331
  hashAuditArgs
20113
21332
  );
@@ -20159,7 +21378,8 @@ var init_orchestrator = __esm({
20159
21378
  init_jail();
20160
21379
  init_safe_text();
20161
21380
  init_negotiation();
20162
- init_decision_sources();
21381
+ init_decision_record();
21382
+ init_card_result();
20163
21383
  SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
20164
21384
  WRITE_TOOLS = /* @__PURE__ */ new Set([
20165
21385
  "write",
@@ -29796,12 +31016,206 @@ var init_session_history = __esm({
29796
31016
  }
29797
31017
  });
29798
31018
 
31019
+ // src/daemon/cards.ts
31020
+ import { randomUUID as randomUUID5 } from "crypto";
31021
+ var CARD_RETENTION_MS, MAX_CARD_TIMEOUT_MS, CardStore;
31022
+ var init_cards = __esm({
31023
+ "src/daemon/cards.ts"() {
31024
+ "use strict";
31025
+ init_decision_record();
31026
+ CARD_RETENTION_MS = 6e4;
31027
+ MAX_CARD_TIMEOUT_MS = 24 * 60 * 6e4;
31028
+ CardStore = class {
31029
+ slots = /* @__PURE__ */ new Map();
31030
+ now;
31031
+ setTimer;
31032
+ clearTimer;
31033
+ deps;
31034
+ constructor(deps = {}) {
31035
+ this.deps = deps;
31036
+ this.now = deps.now ?? Date.now;
31037
+ this.setTimer = deps.setTimer ?? ((fn, ms) => {
31038
+ const t = setTimeout(fn, ms);
31039
+ t.unref?.();
31040
+ return t;
31041
+ });
31042
+ this.clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
31043
+ }
31044
+ register(request2, opts) {
31045
+ const id = opts.id && !this.slots.has(opts.id) ? opts.id : randomUUID5();
31046
+ const createdAt = this.now();
31047
+ const timeoutMs = opts.timeoutMs && opts.timeoutMs > 0 ? Math.min(opts.timeoutMs, MAX_CARD_TIMEOUT_MS) : null;
31048
+ const card = {
31049
+ id,
31050
+ kind: opts.kind,
31051
+ state: "open",
31052
+ request: request2,
31053
+ channels: [...opts.channels],
31054
+ createdAt,
31055
+ deadline: timeoutMs === null ? null : createdAt + timeoutMs
31056
+ };
31057
+ const slot = { card, waiters: /* @__PURE__ */ new Set(), everWaited: false };
31058
+ if (timeoutMs !== null) {
31059
+ slot.timer = this.setTimer(() => {
31060
+ slot.timer = void 0;
31061
+ this.decide(id, timerRecord(timeoutMs, this.now()));
31062
+ }, timeoutMs);
31063
+ }
31064
+ this.slots.set(id, slot);
31065
+ return card;
31066
+ }
31067
+ get(id) {
31068
+ return this.slots.get(id)?.card;
31069
+ }
31070
+ /** Cards still waiting for a decision, oldest first. */
31071
+ open() {
31072
+ return [...this.slots.values()].filter((s) => s.card.state === "open").map((s) => s.card);
31073
+ }
31074
+ /**
31075
+ * The ONLY way a card gets a decision. The first call wins; a later call
31076
+ * gets the winning record back with `won: false`. A withdrawn card can
31077
+ * still be decided (its asker, an older hook, may report its own winner
31078
+ * after closing its wait). Null when the card does not exist.
31079
+ */
31080
+ decide(id, record2) {
31081
+ const slot = this.slots.get(id);
31082
+ if (!slot) return null;
31083
+ const { card } = slot;
31084
+ if (card.state === "decided") return { record: card.record, won: false };
31085
+ if (slot.timer !== void 0) {
31086
+ this.clearTimer(slot.timer);
31087
+ slot.timer = void 0;
31088
+ }
31089
+ card.state = "decided";
31090
+ card.record = record2;
31091
+ card.closedAt = this.now();
31092
+ this.deps.onDecided?.(card);
31093
+ if (slot.waiters.size > 0) {
31094
+ const waiters = [...slot.waiters];
31095
+ slot.waiters.clear();
31096
+ for (const w of waiters) w({ kind: "decided", record: record2 });
31097
+ this.slots.delete(id);
31098
+ }
31099
+ return { record: record2, won: true };
31100
+ }
31101
+ /** The asker left before any decision. False when the card was not open. */
31102
+ withdraw(id) {
31103
+ const slot = this.slots.get(id);
31104
+ if (!slot || slot.card.state !== "open") return false;
31105
+ if (slot.timer !== void 0) {
31106
+ this.clearTimer(slot.timer);
31107
+ slot.timer = void 0;
31108
+ }
31109
+ slot.card.state = "withdrawn";
31110
+ slot.card.closedAt = this.now();
31111
+ const waiters = [...slot.waiters];
31112
+ slot.waiters.clear();
31113
+ for (const w of waiters) w({ kind: "withdrawn" });
31114
+ this.deps.onWithdrawn?.(slot.card);
31115
+ return true;
31116
+ }
31117
+ /**
31118
+ * Wait for the card's decision. Any number of waiters. A decided card
31119
+ * answers at once, and is removed once read. When the last waiter of an
31120
+ * open card goes away (its signal aborts), the card is withdrawn: nobody is
31121
+ * left to act on a decision.
31122
+ */
31123
+ wait(id, signal) {
31124
+ const slot = this.slots.get(id);
31125
+ if (!slot) return Promise.resolve({ kind: "missing" });
31126
+ const { card } = slot;
31127
+ if (card.state === "decided") {
31128
+ this.slots.delete(id);
31129
+ return Promise.resolve({ kind: "decided", record: card.record });
31130
+ }
31131
+ if (card.state === "withdrawn") return Promise.resolve({ kind: "withdrawn" });
31132
+ slot.everWaited = true;
31133
+ return new Promise((resolve2) => {
31134
+ const waiter = (o) => {
31135
+ signal?.removeEventListener("abort", onAbort);
31136
+ resolve2(o);
31137
+ };
31138
+ const onAbort = () => {
31139
+ if (!slot.waiters.delete(waiter)) return;
31140
+ resolve2({ kind: "withdrawn" });
31141
+ if (slot.waiters.size === 0 && card.state === "open") this.withdraw(id);
31142
+ };
31143
+ slot.waiters.add(waiter);
31144
+ if (signal) {
31145
+ if (signal.aborted) onAbort();
31146
+ else signal.addEventListener("abort", onAbort, { once: true });
31147
+ }
31148
+ });
31149
+ }
31150
+ /**
31151
+ * The daemon's local UIs all went away. Open cards that only they could
31152
+ * answer are decided as abandoned; a card that Slack or the native popup
31153
+ * can still answer stays open. Returns how many were abandoned.
31154
+ */
31155
+ abandonLocalOnly() {
31156
+ let n = 0;
31157
+ for (const card of this.open()) {
31158
+ const localOnly = card.channels.length > 0 && card.channels.every((c) => LOCAL_UI_CHANNELS.has(c));
31159
+ if (!localOnly) continue;
31160
+ if (this.decide(card.id, abandonedRecord(this.now()))?.won) n++;
31161
+ }
31162
+ return n;
31163
+ }
31164
+ /**
31165
+ * Housekeeping, run every few seconds:
31166
+ * - a decided card nobody read and a withdrawn card are removed after the
31167
+ * retention;
31168
+ * - an asked card that nobody ever waited on (its asker died between
31169
+ * registering and waiting) is withdrawn after the retention.
31170
+ */
31171
+ sweep() {
31172
+ const now = this.now();
31173
+ for (const [id, slot] of this.slots) {
31174
+ const { card } = slot;
31175
+ if (card.state !== "open") {
31176
+ if (now - (card.closedAt ?? card.createdAt) >= CARD_RETENTION_MS) this.slots.delete(id);
31177
+ continue;
31178
+ }
31179
+ if (card.kind !== "discovery" && !slot.everWaited && now - card.createdAt >= CARD_RETENTION_MS)
31180
+ this.withdraw(id);
31181
+ }
31182
+ }
31183
+ /** Cards in any state (tests and diagnostics). */
31184
+ size() {
31185
+ return this.slots.size;
31186
+ }
31187
+ };
31188
+ }
31189
+ });
31190
+
29799
31191
  // src/daemon/state.ts
29800
31192
  import net2 from "net";
29801
31193
  import fs49 from "fs";
29802
31194
  import path49 from "path";
29803
31195
  import os40 from "os";
29804
- import { randomUUID as randomUUID5 } from "crypto";
31196
+ import { randomUUID as randomUUID6 } from "crypto";
31197
+ function onCardDecided(card) {
31198
+ const r = card.record;
31199
+ broadcast("remove", { id: card.id, decision: r.decision, decidedBy: r.decidedBy });
31200
+ if (card.kind === "discovery") return;
31201
+ appendAuditLog({
31202
+ toolName: card.request.toolName,
31203
+ args: card.request.args,
31204
+ decision: r.decidedBy === "timer" ? "auto-deny" : r.standing === "trust" && r.trustDuration ? `trust:${r.trustDuration}` : r.decision,
31205
+ decidedBy: r.decidedBy,
31206
+ ...r.channel && { channel: r.channel },
31207
+ cardId: card.id
31208
+ });
31209
+ if (r.decidedBy !== "person") return;
31210
+ const tool = card.request.toolName;
31211
+ if (r.decision === "allow" && r.standing !== "always-allow" && r.standing !== "trust") {
31212
+ insightCounts.set(tool, (insightCounts.get(tool) ?? 0) + 1);
31213
+ saveInsightCounts();
31214
+ } else if (r.decision === "deny") {
31215
+ insightCounts.delete(tool);
31216
+ saveInsightCounts();
31217
+ }
31218
+ }
29805
31219
  function loadInsightCounts() {
29806
31220
  try {
29807
31221
  if (!fs49.existsSync(INSIGHT_COUNTS_FILE)) return;
@@ -29868,7 +31282,10 @@ function appendAuditLog(data) {
29868
31282
  tool: data.toolName,
29869
31283
  args: maskPersonalArgs(redactArgs(data.args)),
29870
31284
  decision: data.decision,
29871
- source: "daemon"
31285
+ source: "daemon",
31286
+ ...data.decidedBy && { decidedBy: data.decidedBy },
31287
+ ...data.channel && { channel: data.channel },
31288
+ ...data.cardId && { cardId: data.cardId }
29872
31289
  };
29873
31290
  const dir2 = path49.dirname(AUDIT_LOG_FILE);
29874
31291
  if (!fs49.existsSync(dir2)) fs49.mkdirSync(dir2, { recursive: true });
@@ -30001,7 +31418,7 @@ function broadcastForensic(finding) {
30001
31418
  const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
30002
31419
  const event = {
30003
31420
  type: "forensic",
30004
- id: `fnd_${randomUUID5()}`,
31421
+ id: `fnd_${randomUUID6()}`,
30005
31422
  ts: Date.now(),
30006
31423
  sessionId: finding.sessionId,
30007
31424
  category: finding.type,
@@ -30012,14 +31429,8 @@ function broadcastForensic(finding) {
30012
31429
  }
30013
31430
  function abandonPending() {
30014
31431
  setAbandonTimer(null);
30015
- pending.forEach((entry, id) => {
30016
- clearTimeout(entry.timer);
30017
- if (entry.waiter) entry.waiter("abandoned");
30018
- else entry.earlyDecision = "abandoned";
30019
- pending.delete(id);
30020
- broadcast("remove", { id });
30021
- });
30022
- if (autoStarted) {
31432
+ cards.abandonLocalOnly();
31433
+ if (autoStarted && cards.open().length === 0) {
30023
31434
  try {
30024
31435
  fs49.unlinkSync(DAEMON_PID_FILE);
30025
31436
  } catch {
@@ -30183,7 +31594,7 @@ function bindActivitySocket() {
30183
31594
  });
30184
31595
  activitySocketServer = unixServer;
30185
31596
  }
30186
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, sessionTaintStore, sessionExposureStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, CRITICAL_FORENSIC_CATEGORIES, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
31597
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, sseClients, cards, suggestionTracker, taintStore, sessionTaintStore, sessionExposureStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, CRITICAL_FORENSIC_CATEGORIES, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
30187
31598
  var init_state2 = __esm({
30188
31599
  "src/daemon/state.ts"() {
30189
31600
  "use strict";
@@ -30196,6 +31607,7 @@ var init_state2 = __esm({
30196
31607
  init_session_counters();
30197
31608
  init_session_history();
30198
31609
  init_atomic_write();
31610
+ init_cards();
30199
31611
  homeDir = os40.homedir();
30200
31612
  DAEMON_PID_FILE = path49.join(homeDir, ".node9", "daemon.pid");
30201
31613
  DECISIONS_FILE = path49.join(homeDir, ".node9", "decisions.json");
@@ -30204,8 +31616,11 @@ var init_state2 = __esm({
30204
31616
  GLOBAL_CONFIG_FILE = path49.join(homeDir, ".node9", "config.json");
30205
31617
  CREDENTIALS_FILE = path49.join(homeDir, ".node9", "credentials.json");
30206
31618
  INSIGHT_COUNTS_FILE = path49.join(homeDir, ".node9", "insight-counts.json");
30207
- pending = /* @__PURE__ */ new Map();
30208
31619
  sseClients = /* @__PURE__ */ new Set();
31620
+ cards = new CardStore({
31621
+ onDecided: onCardDecided,
31622
+ onWithdrawn: (card) => broadcast("remove", { id: card.id })
31623
+ });
30209
31624
  suggestionTracker = new SuggestionTracker(3);
30210
31625
  taintStore = new TaintStore();
30211
31626
  sessionTaintStore = new SessionTaintStore();
@@ -31518,7 +32933,7 @@ var init_posture = __esm({
31518
32933
  });
31519
32934
 
31520
32935
  // src/posture/ship.ts
31521
- import http from "http";
32936
+ import http2 from "http";
31522
32937
  import https2 from "https";
31523
32938
  import { URL as URL2 } from "url";
31524
32939
  function buildShipBody(result) {
@@ -31559,7 +32974,7 @@ async function shipPosture(result, creds) {
31559
32974
  if (!url) return false;
31560
32975
  const body = JSON.stringify(buildShipBody(result));
31561
32976
  const parsed = new URL2(url);
31562
- const transport = parsed.protocol === "http:" ? http : https2;
32977
+ const transport = parsed.protocol === "http:" ? http2 : https2;
31563
32978
  return new Promise((resolve2) => {
31564
32979
  const req = transport.request(
31565
32980
  {
@@ -31758,7 +33173,7 @@ var init_mcp_status = __esm({
31758
33173
  });
31759
33174
 
31760
33175
  // src/policy-snapshot/ship.ts
31761
- import http2 from "http";
33176
+ import http3 from "http";
31762
33177
  import https3 from "https";
31763
33178
  import { URL as URL3 } from "url";
31764
33179
  function policySnapshotUrlFrom(apiUrl) {
@@ -31769,7 +33184,7 @@ async function shipPolicySnapshot(body, creds) {
31769
33184
  if (!url) return false;
31770
33185
  const payload = JSON.stringify(body);
31771
33186
  const parsed = new URL3(url);
31772
- const transport = parsed.protocol === "http:" ? http2 : https3;
33187
+ const transport = parsed.protocol === "http:" ? http3 : https3;
31773
33188
  return new Promise((resolve2) => {
31774
33189
  const req = transport.request(
31775
33190
  {
@@ -31837,17 +33252,17 @@ var init_machine_id = __esm({
31837
33252
  // src/daemon/sync-trigger.ts
31838
33253
  function createSyncTrigger(sync) {
31839
33254
  let running2;
31840
- let pending2 = false;
33255
+ let pending = false;
31841
33256
  return (invalidate = true) => {
31842
33257
  if (running2) {
31843
- if (invalidate) pending2 = true;
33258
+ if (invalidate) pending = true;
31844
33259
  return running2;
31845
33260
  }
31846
33261
  running2 = (async () => {
31847
33262
  do {
31848
- pending2 = false;
33263
+ pending = false;
31849
33264
  await sync();
31850
- } while (pending2);
33265
+ } while (pending);
31851
33266
  })().finally(() => {
31852
33267
  running2 = void 0;
31853
33268
  });
@@ -32755,7 +34170,11 @@ function buildWireRows(chunk2) {
32755
34170
  ...typeof parsed.taintFromEid === "string" ? { taintFromEid: parsed.taintFromEid } : {},
32756
34171
  ...typeof parsed.taintSource === "string" ? { taintSource: parsed.taintSource } : {},
32757
34172
  ...exposureOf(parsed.exposure) ? { exposure: exposureOf(parsed.exposure) } : {},
32758
- ...typeof parsed.exposureFromEid === "string" ? { exposureFromEid: parsed.exposureFromEid } : {}
34173
+ ...typeof parsed.exposureFromEid === "string" ? { exposureFromEid: parsed.exposureFromEid } : {},
34174
+ // Approval cards. Only the known values: the ingest's schema is a
34175
+ // closed vocabulary and rejects the whole batch for anything else.
34176
+ ...typeof parsed.decidedBy === "string" && DECIDED_BY_VALUES.has(parsed.decidedBy) ? { decidedBy: parsed.decidedBy } : {},
34177
+ ...typeof parsed.channel === "string" && CHANNELS.includes(parsed.channel) ? { channel: parsed.channel } : {}
32759
34178
  });
32760
34179
  }
32761
34180
  return { rows, consumed: lastNl + 1 };
@@ -32907,15 +34326,23 @@ function outboxBacklog(auditLogPath = LOCAL_AUDIT_LOG, watermarkPath = AUDIT_SHI
32907
34326
  };
32908
34327
  }
32909
34328
  }
32910
- var AUDIT_SHIP_WATERMARK, DEFAULT_INTERVAL_MS, MAX_BATCH, MAX_CHUNK_BYTES, MAX_CHUNKS_PER_TICK, FETCH_TIMEOUT_MS, SKIP_CHECKED_BY, shipperStarted;
34329
+ var DECIDED_BY_VALUES, AUDIT_SHIP_WATERMARK, DEFAULT_INTERVAL_MS, MAX_BATCH, MAX_CHUNK_BYTES, MAX_CHUNKS_PER_TICK, FETCH_TIMEOUT_MS, SKIP_CHECKED_BY, shipperStarted;
32911
34330
  var init_audit_shipper = __esm({
32912
34331
  "src/daemon/audit-shipper.ts"() {
32913
34332
  "use strict";
32914
34333
  init_audit();
34334
+ init_decision_record();
32915
34335
  init_config();
32916
34336
  init_sync();
32917
34337
  init_cloud();
32918
34338
  init_dist();
34339
+ DECIDED_BY_VALUES = /* @__PURE__ */ new Set([
34340
+ "person",
34341
+ "timer",
34342
+ "cloud",
34343
+ "policy",
34344
+ "abandoned"
34345
+ ]);
32919
34346
  AUDIT_SHIP_WATERMARK = path55.join(os47.homedir(), ".node9", "audit-ship.json");
32920
34347
  DEFAULT_INTERVAL_MS = 2e4;
32921
34348
  MAX_BATCH = 500;
@@ -32940,6 +34367,18 @@ function classifyDecision(a, b) {
32940
34367
  }
32941
34368
  if (d === "dlp") return { outcome: "info", label: "Finding", raw };
32942
34369
  if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
34370
+ const decidedBy = isRow ? a.decidedBy : void 0;
34371
+ if (typeof decidedBy === "string") {
34372
+ const allowed = d === "allow" || d === "allowed" || d.startsWith("trust:");
34373
+ const denied = d === "deny" || d === "auto-deny" || d === "block";
34374
+ if (allowed || denied) {
34375
+ if (decidedBy === "person")
34376
+ return allowed ? { outcome: "allow", label: "Approved", raw } : { outcome: "deny", label: "Denied", raw };
34377
+ if (decidedBy === "timer" || decidedBy === "abandoned")
34378
+ return { outcome: "deny", label: "Timed out", raw };
34379
+ return allowed ? { outcome: "allow", label: "Auto-allowed", raw } : { outcome: "deny", label: "Blocked", raw };
34380
+ }
34381
+ }
32943
34382
  if (d === "allow" || d === "allowed") {
32944
34383
  if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
32945
34384
  if (HUMAN_SOURCES.has(src)) {
@@ -33873,7 +35312,17 @@ var init_osv_sync = __esm({
33873
35312
  init_zip();
33874
35313
  init_osv_index();
33875
35314
  init_net();
33876
- ECOSYSTEMS2 = ["npm", "PyPI"];
35315
+ ECOSYSTEMS2 = [
35316
+ "npm",
35317
+ "PyPI",
35318
+ "RubyGems",
35319
+ "crates.io",
35320
+ "Go",
35321
+ "NuGet",
35322
+ "Maven",
35323
+ "Packagist",
35324
+ "VSCode"
35325
+ ];
33877
35326
  SYNC_INTERVAL_MS2 = 6 * 60 * 60 * 1e3;
33878
35327
  FIRST_SYNC_DELAY_MS = 2 * 60 * 1e3;
33879
35328
  FULL_REBUILD_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -34356,11 +35805,11 @@ var init_heartbeat = __esm({
34356
35805
  });
34357
35806
 
34358
35807
  // src/daemon/server.ts
34359
- import http3 from "http";
35808
+ import http4 from "http";
34360
35809
  import fs63 from "fs";
34361
35810
  import path61 from "path";
34362
35811
  import os52 from "os";
34363
- import { randomUUID as randomUUID7 } from "crypto";
35812
+ import { randomUUID as randomUUID8 } from "crypto";
34364
35813
  import { spawnSync as spawnSync3 } from "child_process";
34365
35814
  import chalk7 from "chalk";
34366
35815
  function buildDaemonReport(allEntries, period, now) {
@@ -34433,14 +35882,40 @@ function buildDaemonReport(allEntries, period, now) {
34433
35882
  byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
34434
35883
  };
34435
35884
  }
34436
- function autoDenyMs() {
34437
- const ms = getConfig().settings.approvalTimeoutMs;
34438
- if (ms === 0) return null;
35885
+ function configTimeoutMs(config) {
35886
+ const ms = config.settings.approvalTimeoutMs;
34439
35887
  return ms ?? AUTO_DENY_MS;
34440
35888
  }
34441
- function armAutoDeny(fire) {
34442
- const ms = autoDenyMs();
34443
- return ms === null ? void 0 : setTimeout(fire, ms);
35889
+ function parseChannels(v) {
35890
+ if (!Array.isArray(v)) return null;
35891
+ const out = v.filter((c) => CHANNELS.includes(c));
35892
+ return out.length > 0 ? [...new Set(out)] : null;
35893
+ }
35894
+ function inferChannels(kind, slackDelegated) {
35895
+ if (kind === "evaluate") return ["terminal", "monitor"];
35896
+ return slackDelegated ? ["terminal", "monitor", "cloud"] : ["terminal", "monitor", "native", "cloud"];
35897
+ }
35898
+ function cardView(card) {
35899
+ const r = card.request;
35900
+ return {
35901
+ id: card.id,
35902
+ toolName: r.toolName,
35903
+ args: r.args,
35904
+ riskMetadata: r.riskMetadata,
35905
+ ...r.recoveryCommand && { recoveryCommand: r.recoveryCommand },
35906
+ ...r.viewOnly && { viewOnly: true },
35907
+ slackDelegated: card.channels.includes("cloud") && !card.channels.includes("native"),
35908
+ timestamp: card.createdAt,
35909
+ deadline: card.deadline,
35910
+ agent: r.agent,
35911
+ mcpServer: r.mcpServer,
35912
+ ...card.kind === "discovery" && {
35913
+ type: "mcp-discovery",
35914
+ serverKey: r.serverKey,
35915
+ mcpTools: r.mcpTools
35916
+ },
35917
+ allowCount: (insightCounts.get(r.toolName) ?? 0) + 1
35918
+ };
34444
35919
  }
34445
35920
  function startDaemon() {
34446
35921
  try {
@@ -34462,7 +35937,7 @@ function startDaemon() {
34462
35937
  recordStartupState("failed", "startup-throw", err2 instanceof Error ? err2.message : String(err2));
34463
35938
  process.exit(1);
34464
35939
  }
34465
- const internalToken = randomUUID7();
35940
+ const internalToken = randomUUID8();
34466
35941
  const applyTrustRecipients = (entry) => {
34467
35942
  const dests = entry.riskMetadata?.exposureDestinations ?? [];
34468
35943
  if (dests.length === 0) return;
@@ -34507,8 +35982,9 @@ function startDaemon() {
34507
35982
  idleTimer.unref();
34508
35983
  }
34509
35984
  resetIdleTimer();
35985
+ setInterval(() => cards.sweep(), 1e4).unref();
34510
35986
  const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
34511
- const server = http3.createServer(async (req, res) => {
35987
+ const server = http4.createServer(async (req, res) => {
34512
35988
  const host2 = req.headers.host ?? "";
34513
35989
  if (!allowedHosts.has(host2)) {
34514
35990
  res.writeHead(421, { "Content-Type": "text/plain" });
@@ -34538,21 +36014,8 @@ function startDaemon() {
34538
36014
  res.write(
34539
36015
  `event: init
34540
36016
  data: ${JSON.stringify({
34541
- requests: Array.from(pending.values()).map((e) => ({
34542
- id: e.id,
34543
- toolName: e.toolName,
34544
- args: e.args,
34545
- riskMetadata: e.riskMetadata,
34546
- ...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
34547
- ...e.viewOnly && { viewOnly: true },
34548
- slackDelegated: e.slackDelegated,
34549
- timestamp: e.timestamp,
34550
- agent: e.agent,
34551
- mcpServer: e.mcpServer,
34552
- allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
34553
- })),
34554
- orgName: getOrgName(),
34555
- autoDenyMs: autoDenyMs()
36017
+ requests: cards.open().map(cardView),
36018
+ orgName: getOrgName()
34556
36019
  })}
34557
36020
 
34558
36021
  `
@@ -34573,7 +36036,7 @@ data: ${JSON.stringify(item.data)}
34573
36036
  }
34574
36037
  return req.on("close", () => {
34575
36038
  sseClients.delete(sseClient);
34576
- if (sseClients.size === 0 && pending.size > 0) {
36039
+ if (sseClients.size === 0 && cards.open().length > 0) {
34577
36040
  setAbandonTimer(setTimeout(abandonPending, getHadBrowserClient() ? 1e4 : 15e3));
34578
36041
  }
34579
36042
  });
@@ -34584,137 +36047,81 @@ data: ${JSON.stringify(item.data)}
34584
36047
  _resetConfigCache();
34585
36048
  const body = await readBody(req);
34586
36049
  if (body.length > 65536) return res.writeHead(413).end();
34587
- const {
34588
- toolName,
34589
- args,
34590
- slackDelegated = false,
34591
- agent,
34592
- mcpServer,
34593
- riskMetadata,
34594
- recoveryCommand,
34595
- skipBackgroundAuth = false,
34596
- viewOnly = false,
34597
- fromCLI = false,
34598
- activityId,
34599
- cwd,
34600
- localSmartRuleMatched = false,
34601
- sessionId
34602
- } = JSON.parse(body);
34603
- const id = fromCLI && typeof activityId === "string" && activityId || randomUUID7();
34604
- const entry = {
34605
- id,
36050
+ const parsed = JSON.parse(body);
36051
+ const { toolName, args, agent, mcpServer, riskMetadata, recoveryCommand, activityId, cwd } = parsed;
36052
+ if (typeof toolName !== "string" || !toolName) return res.writeHead(400).end();
36053
+ const fromCLI = parsed.fromCLI === true;
36054
+ const kind = parsed.kind === "review" || parsed.kind === "evaluate" ? parsed.kind : "fromCLI" in parsed ? "review" : "evaluate";
36055
+ const projectCwd = typeof cwd === "string" && path61.isAbsolute(cwd) ? cwd : void 0;
36056
+ const projectConfig = getConfig(projectCwd);
36057
+ const timeoutMs = typeof parsed.timeoutMs === "number" && Number.isFinite(parsed.timeoutMs) && parsed.timeoutMs >= 0 ? parsed.timeoutMs : configTimeoutMs(projectConfig);
36058
+ const sessionId = parsed.sessionId;
36059
+ const request2 = {
34606
36060
  toolName,
34607
36061
  args,
34608
- riskMetadata: riskMetadata ?? void 0,
34609
- ...typeof sessionId === "string" && sessionId.length <= 256 && { sessionId },
36062
+ ...riskMetadata && typeof riskMetadata === "object" ? { riskMetadata } : {},
34610
36063
  ...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
34611
- ...viewOnly && { viewOnly: true },
34612
- agent: typeof agent === "string" ? agent : void 0,
34613
- mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
34614
- slackDelegated: !!slackDelegated,
34615
- timestamp: Date.now(),
34616
- earlyDecision: null,
34617
- waiter: null,
34618
- timer: armAutoDeny(() => {
34619
- if (pending.has(id)) {
34620
- const e = pending.get(id);
34621
- appendAuditLog({
34622
- toolName: e.toolName,
34623
- args: e.args,
34624
- decision: "auto-deny"
34625
- });
34626
- e.decisionSource = "timeout";
34627
- if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
34628
- else {
34629
- e.earlyDecision = "deny";
34630
- e.earlyReason = "No response \u2014 auto-denied after timeout";
34631
- }
34632
- pending.delete(id);
34633
- broadcast("remove", { id, decision: "deny" });
34634
- }
34635
- })
36064
+ ...parsed.viewOnly === true && { viewOnly: true },
36065
+ ...typeof agent === "string" && { agent },
36066
+ ...typeof mcpServer === "string" && { mcpServer },
36067
+ ...typeof sessionId === "string" && sessionId.length <= 256 && { sessionId },
36068
+ ...projectCwd && { cwd: projectCwd },
36069
+ ...parsed.protocol === CARD_PROTOCOL && { protocol: CARD_PROTOCOL }
34636
36070
  };
34637
- pending.set(id, entry);
36071
+ const card = cards.register(request2, {
36072
+ id: fromCLI && typeof activityId === "string" && activityId ? activityId : void 0,
36073
+ kind,
36074
+ channels: parseChannels(parsed.channels) ?? inferChannels(kind, parsed.slackDelegated === true),
36075
+ timeoutMs
36076
+ });
34638
36077
  if (!fromCLI) {
34639
36078
  broadcast("activity", {
34640
- id,
34641
- ts: entry.timestamp,
36079
+ id: card.id,
36080
+ ts: card.createdAt,
34642
36081
  tool: toolName,
34643
36082
  args: redactArgs(args),
34644
36083
  status: "pending",
34645
- agent: entry.agent,
34646
- mcpServer: entry.mcpServer
36084
+ agent: request2.agent,
36085
+ mcpServer: request2.mcpServer
34647
36086
  });
34648
36087
  }
34649
- const projectCwd = typeof cwd === "string" && path61.isAbsolute(cwd) ? cwd : void 0;
34650
- const projectConfig = getConfig(projectCwd);
34651
36088
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
34652
36089
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
34653
36090
  if (browserEnabled || terminalEnabled) {
34654
- broadcast("add", {
34655
- id,
34656
- toolName,
34657
- args,
34658
- riskMetadata: entry.riskMetadata,
34659
- ...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
34660
- ...entry.viewOnly && { viewOnly: true },
34661
- slackDelegated: entry.slackDelegated,
34662
- agent: entry.agent,
34663
- mcpServer: entry.mcpServer,
34664
- interactive: terminalEnabled,
34665
- // allowCount = what this count will be if the user allows.
34666
- // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
34667
- allowCount: (insightCounts.get(toolName) ?? 0) + 1
34668
- });
36091
+ broadcast("add", { ...cardView(card), interactive: terminalEnabled });
34669
36092
  }
34670
36093
  res.writeHead(200, { "Content-Type": "application/json" });
34671
- res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
34672
- if (slackDelegated || skipBackgroundAuth) return;
36094
+ res.end(
36095
+ JSON.stringify({
36096
+ id: card.id,
36097
+ allowCount: (insightCounts.get(toolName) ?? 0) + 1,
36098
+ protocol: CARD_PROTOCOL,
36099
+ deadline: card.deadline
36100
+ })
36101
+ );
36102
+ if (kind !== "evaluate") return;
34673
36103
  authorizeHeadless(
34674
36104
  toolName,
34675
36105
  args,
36106
+ { agent: request2.agent, mcpServer: request2.mcpServer },
34676
36107
  {
34677
- agent: typeof agent === "string" ? agent : void 0,
34678
- mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
34679
- },
34680
- { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
36108
+ calledFromDaemon: true,
36109
+ ...projectCwd && { cwd: projectCwd },
36110
+ localSmartRuleMatched: parsed.localSmartRuleMatched === true
36111
+ }
34681
36112
  ).then((result) => {
34682
- const e = pending.get(id);
34683
- if (!e) return;
34684
- if (result.noApprovalMechanism) return;
34685
- if (result.checkedBy === "audit") return;
34686
- if (e.earlyDecision !== null) return;
36113
+ if (cards.get(card.id)?.state !== "open") return;
36114
+ if (result.noApprovalMechanism || result.checkedBy === "audit") return;
34687
36115
  broadcast("activity-result", {
34688
- id,
36116
+ id: card.id,
34689
36117
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
34690
36118
  label: result.blockedByLabel,
34691
- agent: e.agent,
34692
- mcpServer: e.mcpServer
36119
+ agent: request2.agent,
36120
+ mcpServer: request2.mcpServer
34693
36121
  });
34694
- clearTimeout(e.timer);
34695
- const decision = result.approved ? "allow" : "deny";
34696
- appendAuditLog({ toolName: e.toolName, args: e.args, decision });
34697
- e.decisionSource = backgroundPassSource(result);
34698
- if (e.waiter) {
34699
- e.waiter(decision, result.reason);
34700
- pending.delete(id);
34701
- broadcast("remove", { id, decision });
34702
- } else {
34703
- e.earlyDecision = decision;
34704
- e.earlyReason = result.reason;
34705
- }
36122
+ cards.decide(card.id, recordFromVerdict(result));
34706
36123
  }).catch((err2) => {
34707
- const e = pending.get(id);
34708
- if (!e) return;
34709
- clearTimeout(e.timer);
34710
- const reason = err2?.reason || "No response \u2014 request timed out";
34711
- if (e.waiter) e.waiter("deny", reason);
34712
- else {
34713
- e.earlyDecision = "deny";
34714
- e.earlyReason = reason;
34715
- }
34716
- pending.delete(id);
34717
- broadcast("remove", { id, decision: "deny" });
36124
+ console.error(chalk7.red("[node9 daemon] check of an evaluate card failed:"), err2);
34718
36125
  });
34719
36126
  return;
34720
36127
  } catch {
@@ -34723,36 +36130,33 @@ data: ${JSON.stringify(item.data)}
34723
36130
  }
34724
36131
  if (req.method === "GET" && pathname.startsWith("/wait/")) {
34725
36132
  const id = pathname.split("/").pop();
34726
- const entry = pending.get(id);
34727
- if (!entry) return res.writeHead(404).end();
34728
- if (entry.earlyDecision) {
34729
- clearTimeout(entry.timer);
34730
- const source = entry.decisionSource;
34731
- pending.delete(id);
34732
- res.writeHead(200, { "Content-Type": "application/json" });
34733
- const body = {
34734
- decision: entry.earlyDecision
34735
- };
34736
- if (entry.earlyReason) body.reason = entry.earlyReason;
34737
- if (source) body.source = source;
34738
- return res.end(JSON.stringify(body));
34739
- }
34740
- entry.waiter = (d, reason) => {
34741
- res.writeHead(200, { "Content-Type": "application/json" });
34742
- const body = { decision: d };
34743
- if (reason) body.reason = reason;
34744
- if (entry.decisionSource) body.source = entry.decisionSource;
34745
- res.end(JSON.stringify(body));
34746
- };
34747
- req.on("close", () => {
34748
- setTimeout(() => {
34749
- const e = pending.get(id);
34750
- if (e && e.waiter && e.earlyDecision === null) {
34751
- clearTimeout(e.timer);
34752
- pending.delete(id);
34753
- broadcast("remove", { id });
34754
- }
34755
- }, 200);
36133
+ const card = cards.get(id);
36134
+ if (!card) return res.writeHead(404).end();
36135
+ if (card.request.protocol === CARD_PROTOCOL && !validToken(req))
36136
+ return res.writeHead(403).end();
36137
+ const ctrl = new AbortController();
36138
+ let answered = false;
36139
+ res.on("close", () => {
36140
+ if (!answered) ctrl.abort();
36141
+ });
36142
+ void cards.wait(id, ctrl.signal).then((outcome) => {
36143
+ answered = true;
36144
+ if (res.writableEnded || res.destroyed) return;
36145
+ if (outcome.kind === "decided") {
36146
+ res.writeHead(200, { "Content-Type": "application/json" });
36147
+ res.end(
36148
+ JSON.stringify({
36149
+ protocol: CARD_PROTOCOL,
36150
+ record: outcome.record,
36151
+ ...legacyWaitFields(outcome.record)
36152
+ })
36153
+ );
36154
+ } else if (outcome.kind === "withdrawn") {
36155
+ res.writeHead(409, { "Content-Type": "application/json" });
36156
+ res.end(JSON.stringify({ protocol: CARD_PROTOCOL, withdrawn: true }));
36157
+ } else {
36158
+ res.writeHead(404).end();
36159
+ }
34756
36160
  });
34757
36161
  return;
34758
36162
  }
@@ -34760,68 +36164,63 @@ data: ${JSON.stringify(item.data)}
34760
36164
  if (!validToken(req)) return res.writeHead(403).end();
34761
36165
  try {
34762
36166
  const id = pathname.split("/").pop();
34763
- const entry = pending.get(id);
34764
- if (!entry) return res.writeHead(404).end();
34765
- if (entry.earlyDecision !== null) {
36167
+ const card = cards.get(id);
36168
+ if (!card) return res.writeHead(404).end();
36169
+ if (card.state === "decided") {
34766
36170
  res.writeHead(409, { "Content-Type": "application/json" });
34767
- return res.end(JSON.stringify({ conflict: true, decision: entry.earlyDecision }));
36171
+ return res.end(
36172
+ JSON.stringify({ conflict: true, decision: card.record.decision, record: card.record })
36173
+ );
34768
36174
  }
36175
+ if (card.request.viewOnly) return res.writeHead(409).end();
34769
36176
  const { decision, persist, trustDuration, reason, source, trustRecipients } = JSON.parse(
34770
36177
  await readBody(req)
34771
36178
  );
34772
- if (decision === "allow" && trustRecipients === true) {
34773
- applyTrustRecipients(entry);
36179
+ const via = channelFromDecisionSource(source);
36180
+ if (!via) return res.writeHead(400).end();
36181
+ const isTrust = decision === "trust";
36182
+ if (decision !== "allow" && decision !== "deny" && !isTrust)
36183
+ return res.writeHead(400).end();
36184
+ if (isTrust && (typeof trustDuration !== "string" || !(trustDuration in TRUST_DURATIONS)))
36185
+ return res.writeHead(400).end();
36186
+ if (persist === true && isTrust) return res.writeHead(400).end();
36187
+ const verdict = isTrust ? "allow" : decision;
36188
+ const redirect = via.redirect && typeof reason === "string" && reason ? reason.slice(0, 2e3) : void 0;
36189
+ const record2 = personRecord(via.channel, verdict, {
36190
+ ...redirect && { redirect },
36191
+ ...isTrust ? { standing: "trust", trustDuration } : persist === true ? {
36192
+ standing: verdict === "allow" ? "always-allow" : "always-deny"
36193
+ } : verdict === "allow" && trustRecipients === true ? { standing: "trust-recipients" } : {}
36194
+ });
36195
+ const outcome = cards.decide(id, record2);
36196
+ if (!outcome) return res.writeHead(404).end();
36197
+ if (!outcome.won) {
36198
+ res.writeHead(409, { "Content-Type": "application/json" });
36199
+ return res.end(
36200
+ JSON.stringify({
36201
+ conflict: true,
36202
+ decision: outcome.record.decision,
36203
+ record: outcome.record
36204
+ })
36205
+ );
34774
36206
  }
34775
- if (decision === "trust" && trustDuration) {
34776
- const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
34777
- const commandPattern = extractCommandPattern(entry.toolName, entry.args);
34778
- writeTrustEntry(entry.toolName, ms, commandPattern);
34779
- appendAuditLog({
34780
- toolName: entry.toolName,
34781
- args: entry.args,
34782
- decision: `trust:${trustDuration}`
34783
- });
34784
- clearTimeout(entry.timer);
34785
- if (entry.waiter) {
34786
- entry.waiter("allow");
34787
- pending.delete(id);
34788
- broadcast("remove", { id, decision: "allow" });
34789
- } else {
34790
- entry.earlyDecision = "allow";
34791
- broadcast("remove", { id, decision: "allow" });
34792
- entry.timer = setTimeout(() => pending.delete(id), 3e4);
34793
- }
34794
- res.writeHead(200);
34795
- return res.end(JSON.stringify({ ok: true }));
36207
+ if (isTrust) {
36208
+ const ms = TRUST_DURATIONS[trustDuration];
36209
+ writeTrustEntry(
36210
+ card.request.toolName,
36211
+ ms,
36212
+ extractCommandPattern(card.request.toolName, card.request.args)
36213
+ );
36214
+ } else if (persist === true) {
36215
+ writePersistentDecision(card.request.toolName, verdict);
36216
+ } else if (verdict === "allow" && trustRecipients === true) {
36217
+ applyTrustRecipients(card.request);
34796
36218
  }
34797
- const resolvedDecision = decision === "allow" || decision === "deny" ? decision : "deny";
34798
- if (persist) writePersistentDecision(entry.toolName, resolvedDecision);
34799
- appendAuditLog({
34800
- toolName: entry.toolName,
34801
- args: entry.args,
34802
- decision: resolvedDecision
34803
- });
34804
- clearTimeout(entry.timer);
34805
- if (resolvedDecision === "allow" && !persist) {
34806
- insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
34807
- saveInsightCounts();
34808
- } else if (resolvedDecision === "deny") {
34809
- insightCounts.delete(entry.toolName);
34810
- saveInsightCounts();
34811
- }
34812
- if (source && DECISION_ROUTE_SOURCES.has(source)) entry.decisionSource = source;
34813
- if (entry.waiter) {
34814
- entry.waiter(resolvedDecision, reason);
34815
- pending.delete(id);
34816
- broadcast("remove", { id, decision: resolvedDecision });
34817
- } else {
34818
- entry.earlyDecision = resolvedDecision;
34819
- entry.earlyReason = reason;
34820
- broadcast("remove", { id, decision: resolvedDecision });
34821
- entry.timer = setTimeout(() => pending.delete(id), 3e4);
36219
+ if (card.kind === "discovery" && verdict === "allow" && card.request.serverKey) {
36220
+ approveServer(card.request.serverKey, []);
34822
36221
  }
34823
- res.writeHead(200);
34824
- return res.end(JSON.stringify({ ok: true }));
36222
+ res.writeHead(200, { "Content-Type": "application/json" });
36223
+ return res.end(JSON.stringify({ ok: true, record: outcome.record }));
34825
36224
  } catch {
34826
36225
  res.writeHead(400).end();
34827
36226
  }
@@ -34926,34 +36325,18 @@ data: ${JSON.stringify(item.data)}
34926
36325
  }
34927
36326
  }
34928
36327
  if (req.method === "POST" && pathname.startsWith("/resolve/")) {
34929
- const internalAuth = req.headers["x-node9-internal"];
34930
- if (internalAuth !== internalToken) return res.writeHead(403).end();
36328
+ if (!validToken(req)) return res.writeHead(403).end();
34931
36329
  try {
34932
36330
  const id = pathname.split("/").pop();
34933
- const entry = pending.get(id);
34934
- if (!entry) return res.writeHead(404).end();
34935
- const { decision, source } = JSON.parse(await readBody(req));
34936
- const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
34937
- appendAuditLog({
34938
- toolName: entry.toolName,
34939
- args: entry.args,
34940
- decision: resolvedResolveDecision
34941
- });
34942
- clearTimeout(entry.timer);
34943
- if (resolvedResolveDecision === "allow") {
34944
- insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
34945
- saveInsightCounts();
34946
- } else {
34947
- insightCounts.delete(entry.toolName);
34948
- saveInsightCounts();
34949
- }
34950
- if (source && HUMAN_DECISION_SOURCES.has(source)) entry.decisionSource = source;
34951
- if (entry.waiter) entry.waiter(resolvedResolveDecision);
34952
- else entry.earlyDecision = resolvedResolveDecision;
34953
- pending.delete(id);
34954
- broadcast("remove", { id, decision: resolvedResolveDecision });
34955
- res.writeHead(200);
34956
- return res.end(JSON.stringify({ ok: true }));
36331
+ if (!cards.get(id)) return res.writeHead(404).end();
36332
+ const body = JSON.parse(await readBody(req));
36333
+ const record2 = parseDecisionRecord(body.record) ?? recordFromLegacyResolve(body.decision === "allow" ? "allow" : "deny", body.source);
36334
+ const outcome = cards.decide(id, record2);
36335
+ if (!outcome) return res.writeHead(404).end();
36336
+ res.writeHead(200, { "Content-Type": "application/json" });
36337
+ return res.end(
36338
+ JSON.stringify({ protocol: CARD_PROTOCOL, won: outcome.won, record: outcome.record })
36339
+ );
34957
36340
  } catch {
34958
36341
  res.writeHead(400).end();
34959
36342
  }
@@ -35298,35 +36681,16 @@ data: ${JSON.stringify(item.data)}
35298
36681
  args: { toolCount: tools.length, status },
35299
36682
  decision: "mcp-discovered"
35300
36683
  });
35301
- const id = randomUUID7();
35302
- const entry = {
35303
- id,
35304
- type: "mcp-discovery",
35305
- serverKey,
35306
- mcpTools: tools,
35307
- toolName: "mcp-discovery",
35308
- args: { serverKey, tools: tools.length },
35309
- timestamp: Date.now(),
35310
- slackDelegated: false,
35311
- timer: setTimeout(() => {
35312
- if (pending.has(id)) {
35313
- pending.delete(id);
35314
- broadcast("remove", { id, decision: "deny" });
35315
- }
35316
- }, 6e4),
35317
- // 60s timeout for discovery
35318
- waiter: null,
35319
- earlyDecision: null
35320
- };
35321
- pending.set(id, entry);
35322
- broadcast("add", {
35323
- id,
35324
- type: "mcp-discovery",
35325
- serverKey,
35326
- mcpTools: tools,
35327
- toolName: "mcp-discovery",
35328
- args: { serverKey, toolCount: tools.length }
35329
- });
36684
+ const card = cards.register(
36685
+ {
36686
+ toolName: "mcp-discovery",
36687
+ args: { serverKey, toolCount: tools.length },
36688
+ serverKey,
36689
+ mcpTools: tools
36690
+ },
36691
+ { kind: "discovery", channels: ["terminal", "monitor"], timeoutMs: 6e4 }
36692
+ );
36693
+ broadcast("add", cardView(card));
35330
36694
  }
35331
36695
  res.writeHead(200).end();
35332
36696
  return;
@@ -35359,20 +36723,22 @@ data: ${JSON.stringify(item.data)}
35359
36723
  if (!validToken(req)) return res.writeHead(403).end();
35360
36724
  try {
35361
36725
  const { id, serverKey, disabledTools } = JSON.parse(await readBody(req));
35362
- const entry = pending.get(id);
35363
- if (!entry || entry.type !== "mcp-discovery" || entry.serverKey !== serverKey) {
36726
+ const card = typeof id === "string" ? cards.get(id) : void 0;
36727
+ if (!card || card.kind !== "discovery" || card.request.serverKey !== serverKey) {
35364
36728
  res.writeHead(404).end();
35365
36729
  return;
35366
36730
  }
35367
- clearTimeout(entry.timer);
36731
+ const outcome = cards.decide(card.id, personRecord("terminal", "allow"));
36732
+ if (!outcome?.won) {
36733
+ res.writeHead(409).end();
36734
+ return;
36735
+ }
35368
36736
  approveServer(serverKey, disabledTools);
35369
36737
  appendAuditLog({
35370
36738
  toolName: `mcp-server:${serverKey}`,
35371
36739
  args: { disabledTools },
35372
36740
  decision: "allow"
35373
36741
  });
35374
- pending.delete(id);
35375
- broadcast("remove", { id, decision: "allow" });
35376
36742
  res.writeHead(200).end();
35377
36743
  return;
35378
36744
  } catch {
@@ -35605,7 +36971,7 @@ var init_server = __esm({
35605
36971
  "use strict";
35606
36972
  init_migrate();
35607
36973
  init_trusted_recipients();
35608
- init_decision_sources();
36974
+ init_decision_record();
35609
36975
  init_core();
35610
36976
  init_keyed_guard();
35611
36977
  init_scan();
@@ -35864,14 +37230,14 @@ async function onboardMachine(apiKey, opts = {}) {
35864
37230
  detail: `${sync.rules} cloud rule${sync.rules === 1 ? "" : "s"} active`
35865
37231
  } : { name: "policy-sync", ok: false, detail: sync.reason }
35866
37232
  );
35867
- const push = await runPolicyPush();
37233
+ const push2 = await runPolicyPush();
35868
37234
  steps.push(
35869
- push.ok ? { name: "register", ok: true, detail: "machine visible in the dashboard" } : { name: "register", ok: false, detail: push.reason }
37235
+ push2.ok ? { name: "register", ok: true, detail: "machine visible in the dashboard" } : { name: "register", ok: false, detail: push2.reason }
35870
37236
  );
35871
37237
  const healed = ensureAutostartHealthy(!!getConfig().settings.autoStartDaemon);
35872
37238
  let detail = healed === "repaired" ? "autostart re-enabled (survives reboot)" : isDaemonRunning() ? "running" : healed === "unsupported" ? "no background service on this platform: starts on agent activity" : "starts on agent activity (make it survive reboots: node9 daemon install)";
35873
37239
  let daemonOk = true;
35874
- if (sync.ok && push.ok && isInteractive() && mayChangeService({ stdoutTTY: !!process.stdout.isTTY, ci: isCI() }) && autostartState() === "absent") {
37240
+ if (sync.ok && push2.ok && isInteractive() && mayChangeService({ stdoutTTY: !!process.stdout.isTTY, ci: isCI() }) && autostartState() === "absent") {
35875
37241
  try {
35876
37242
  if (await confirm2({
35877
37243
  message: "Run node9 in the background when this computer starts?",
@@ -35996,14 +37362,14 @@ var init_open_browser = __esm({
35996
37362
  });
35997
37363
 
35998
37364
  // src/utils/post-json.ts
35999
- import * as http4 from "http";
37365
+ import * as http5 from "http";
36000
37366
  import * as https5 from "https";
36001
37367
  import { URL as URL4 } from "url";
36002
37368
  function postJson2(url, body, bearer) {
36003
37369
  return new Promise((resolve2, reject) => {
36004
37370
  const payload = JSON.stringify(body);
36005
37371
  const u = new URL4(url);
36006
- const lib = u.protocol === "http:" ? http4 : https5;
37372
+ const lib = u.protocol === "http:" ? http5 : https5;
36007
37373
  const req = lib.request(
36008
37374
  {
36009
37375
  method: "POST",
@@ -45402,7 +46768,7 @@ var require_client = __commonJS({
45402
46768
  "use strict";
45403
46769
  var assert = __require("assert");
45404
46770
  var net3 = __require("net");
45405
- var http8 = __require("http");
46771
+ var http9 = __require("http");
45406
46772
  var util = require_util();
45407
46773
  var { ClientStats } = require_stats();
45408
46774
  var { channels } = require_diagnostics();
@@ -45460,7 +46826,7 @@ var require_client = __commonJS({
45460
46826
  var connectH1 = require_client_h1();
45461
46827
  var connectH2 = require_client_h2();
45462
46828
  var kClosedResolve = /* @__PURE__ */ Symbol("kClosedResolve");
45463
- var getDefaultNodeMaxHeaderSize = http8 && http8.maxHeaderSize && Number.isInteger(http8.maxHeaderSize) && http8.maxHeaderSize > 0 ? () => http8.maxHeaderSize : () => {
46829
+ var getDefaultNodeMaxHeaderSize = http9 && http9.maxHeaderSize && Number.isInteger(http9.maxHeaderSize) && http9.maxHeaderSize > 0 ? () => http9.maxHeaderSize : () => {
45464
46830
  throw new InvalidArgumentError("http module not available or http.maxHeaderSize invalid");
45465
46831
  };
45466
46832
  var noop = () => {
@@ -46077,8 +47443,8 @@ var require_pool_base = __commonJS({
46077
47443
  }
46078
47444
  get [kPending]() {
46079
47445
  let ret = this[kQueued];
46080
- for (const { [kPending]: pending2 } of this[kClients]) {
46081
- ret += pending2;
47446
+ for (const { [kPending]: pending } of this[kClients]) {
47447
+ ret += pending;
46082
47448
  }
46083
47449
  return ret;
46084
47450
  }
@@ -50507,19 +51873,19 @@ var require_mock_agent = __commonJS({
50507
51873
  }
50508
51874
  pendingInterceptors() {
50509
51875
  const mockAgentClients = this[kClients];
50510
- return Array.from(mockAgentClients.entries()).flatMap(([origin, result]) => result.dispatcher[kDispatches].map((dispatch) => ({ ...dispatch, origin }))).filter(({ pending: pending2 }) => pending2);
51876
+ return Array.from(mockAgentClients.entries()).flatMap(([origin, result]) => result.dispatcher[kDispatches].map((dispatch) => ({ ...dispatch, origin }))).filter(({ pending }) => pending);
50511
51877
  }
50512
51878
  assertNoPendingInterceptors({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
50513
- const pending2 = this.pendingInterceptors();
50514
- if (pending2.length === 0) {
51879
+ const pending = this.pendingInterceptors();
51880
+ if (pending.length === 0) {
50515
51881
  return;
50516
51882
  }
50517
51883
  throw new UndiciError(
50518
- pending2.length === 1 ? `1 interceptor is pending:
51884
+ pending.length === 1 ? `1 interceptor is pending:
50519
51885
 
50520
- ${pendingInterceptorsFormatter.format(pending2)}`.trim() : `${pending2.length} interceptors are pending:
51886
+ ${pendingInterceptorsFormatter.format(pending)}`.trim() : `${pending.length} interceptors are pending:
50521
51887
 
50522
- ${pendingInterceptorsFormatter.format(pending2)}`.trim()
51888
+ ${pendingInterceptorsFormatter.format(pending)}`.trim()
50523
51889
  );
50524
51890
  }
50525
51891
  };
@@ -62828,12 +64194,13 @@ var init_first_run = __esm({
62828
64194
  var tail_exports = {};
62829
64195
  __export(tail_exports, {
62830
64196
  agentLabel: () => agentLabel,
64197
+ deadlineLine: () => deadlineLine,
62831
64198
  eventsUrl: () => eventsUrl,
62832
64199
  sessionTag: () => sessionTag,
62833
64200
  shortenPathSummary: () => shortenPathSummary,
62834
64201
  startTail: () => startTail
62835
64202
  });
62836
- import http6 from "http";
64203
+ import http7 from "http";
62837
64204
  import chalk47 from "chalk";
62838
64205
  import fs97 from "fs";
62839
64206
  import os80 from "os";
@@ -63045,7 +64412,7 @@ function postDecisionHttp(id, decision, authToken, port, opts) {
63045
64412
  if (opts?.trustRecipients) bodyObj.trustRecipients = true;
63046
64413
  if (opts?.reason) bodyObj.reason = opts.reason;
63047
64414
  const body = JSON.stringify(bodyObj);
63048
- const req = http6.request(
64415
+ const req = http7.request(
63049
64416
  {
63050
64417
  hostname: "127.0.0.1",
63051
64418
  port,
@@ -63061,9 +64428,21 @@ function postDecisionHttp(id, decision, authToken, port, opts) {
63061
64428
  }
63062
64429
  },
63063
64430
  (res) => {
63064
- res.resume();
63065
- if (res.statusCode === 200 || res.statusCode === 409) resolve2();
63066
- else reject(new Error(`POST /decision returned ${res.statusCode}`));
64431
+ let text = "";
64432
+ res.setEncoding("utf8");
64433
+ res.on("data", (c) => text += c);
64434
+ res.on("end", () => {
64435
+ if (res.statusCode === 200) return resolve2(null);
64436
+ if (res.statusCode === 409) {
64437
+ try {
64438
+ const r = JSON.parse(text).record;
64439
+ if (r?.decision && r.label) return resolve2({ decision: r.decision, label: r.label });
64440
+ } catch {
64441
+ }
64442
+ return resolve2(null);
64443
+ }
64444
+ reject(new Error(`POST /decision returned ${res.statusCode}`));
64445
+ });
63067
64446
  }
63068
64447
  );
63069
64448
  req.on("error", reject);
@@ -63092,6 +64471,15 @@ function cleanBlockedBy(raw) {
63092
64471
  if (smartMatch) return smartMatch[1];
63093
64472
  return raw;
63094
64473
  }
64474
+ function deadlineLine(deadline, now = Date.now()) {
64475
+ if (deadline === void 0) return null;
64476
+ if (deadline === null) return "No timeout: waits for your answer";
64477
+ const left = Math.max(0, Math.round((deadline - now) / 1e3));
64478
+ const m = Math.floor(left / 60);
64479
+ const sec = left % 60;
64480
+ const at = new Date(deadline).toLocaleTimeString([], { hour12: false });
64481
+ return `Auto-denies in ${m > 0 ? `${m}m ` : ""}${sec}s (at ${at}) if nobody answers`;
64482
+ }
63095
64483
  function buildCardLines(req, localCount = 0) {
63096
64484
  if (req.recoveryCommand) {
63097
64485
  return buildRecoveryCardLines(req);
@@ -63131,8 +64519,10 @@ function buildCardLines(req, localCount = 0) {
63131
64519
  `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
63132
64520
  );
63133
64521
  }
64522
+ const timing = deadlineLine(req.deadline);
63134
64523
  lines.push(
63135
64524
  `${CYAN}\u255A${RESET}`,
64525
+ ...timing ? [` ${GRAY}${timing}${RESET}`] : [],
63136
64526
  ``,
63137
64527
  exposure.length > 0 ? ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${CYAN}[s]${RESET} Allow + trust ${exposure.length === 1 ? "this address" : "these addresses"}` : ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
63138
64528
  ``
@@ -63149,7 +64539,7 @@ function buildRecoveryCardLines(req) {
63149
64539
  ` ${BOLD}${YELLOW}[2]${RESET} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
63150
64540
  ` ${BOLD}${RED}[3]${RESET} Deny & stop ${GRAY}(hard block)${RESET}`,
63151
64541
  ``,
63152
- ` ${GRAY}[Timeout: auto-deny]${RESET}`,
64542
+ ` ${GRAY}${deadlineLine(req.deadline) ?? "Auto-denies at the approval timeout if nobody answers"}${RESET}`,
63153
64543
  ` Select [1-3]: `
63154
64544
  ];
63155
64545
  return [
@@ -63196,7 +64586,7 @@ async function startTail(options = {}) {
63196
64586
  const port = await ensureDaemon();
63197
64587
  if (options.clear) {
63198
64588
  const result = await new Promise((resolve2) => {
63199
- const req2 = http6.request(
64589
+ const req2 = http7.request(
63200
64590
  { method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
63201
64591
  (res) => {
63202
64592
  const status = res.statusCode ?? 0;
@@ -63359,7 +64749,13 @@ async function startTail(options = {}) {
63359
64749
  } else {
63360
64750
  httpDecision = action;
63361
64751
  }
63362
- postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
64752
+ postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).then((won) => {
64753
+ if (won && !cardActive)
64754
+ process.stdout.write(
64755
+ ` ${YELLOW}\u21B3 Already decided before your answer: ${won.decision.toUpperCase()} (${safeMessage(won.label, 120)})${RESET}
64756
+ `
64757
+ );
64758
+ }).catch((err2) => {
63363
64759
  try {
63364
64760
  fs97.appendFileSync(
63365
64761
  path94.join(os80.homedir(), ".node9", "hook-debug.log"),
@@ -63484,7 +64880,7 @@ async function startTail(options = {}) {
63484
64880
  }, STALL_THRESHOLD_MS / 2);
63485
64881
  stallWatchdog.unref();
63486
64882
  const sseUrl = eventsUrl(port, canApprove);
63487
- const req = http6.get(
64883
+ const req = http7.get(
63488
64884
  sseUrl,
63489
64885
  {
63490
64886
  headers: authToken ? { "X-Node9-Internal": authToken } : {}
@@ -63702,7 +65098,7 @@ __export(hud_exports, {
63702
65098
  import fs98 from "fs";
63703
65099
  import path95 from "path";
63704
65100
  import os81 from "os";
63705
- import http7 from "http";
65101
+ import http8 from "http";
63706
65102
  async function readStdin() {
63707
65103
  const chunks = [];
63708
65104
  for await (const chunk2 of process.stdin) {
@@ -63720,7 +65116,7 @@ function queryDaemon() {
63720
65116
  return new Promise((resolve2) => {
63721
65117
  const timeout = setTimeout(() => resolve2(null), 50);
63722
65118
  try {
63723
- const req = http7.get(
65119
+ const req = http8.get(
63724
65120
  `http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
63725
65121
  { timeout: 50 },
63726
65122
  (res) => {
@@ -64191,7 +65587,7 @@ async function runProxy(targetCommand) {
64191
65587
  console.error(chalk9.gray(` Reason: ${result.reason || "Security Policy"}
64192
65588
  `));
64193
65589
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
64194
- const isHuman = isHumanDecision(blockedByLabel, result.blockedBy);
65590
+ const isHuman = isHumanDecision(blockedByLabel, result.blockedBy, result.decidedBy);
64195
65591
  const aiInstruction = buildNegotiationMessage(
64196
65592
  blockedByLabel,
64197
65593
  isHuman,
@@ -64199,7 +65595,8 @@ async function runProxy(targetCommand) {
64199
65595
  void 0,
64200
65596
  {
64201
65597
  kind: result.blockKind,
64202
- path: result.blockedPath
65598
+ path: result.blockedPath,
65599
+ redirect: !!result.redirect
64203
65600
  }
64204
65601
  );
64205
65602
  const errorResponse = {
@@ -64873,7 +66270,11 @@ RAW: ${raw}
64873
66270
  const mcpServer = mcpMatch?.[1];
64874
66271
  const sendBlock = (msg, result2) => {
64875
66272
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || (isKeyedForPolicy() ? "Workspace Policy" : "Local Security Policy");
64876
- const humanDecision = isHumanDecision(blockedByContext, result2?.blockedBy);
66273
+ const humanDecision = isHumanDecision(
66274
+ blockedByContext,
66275
+ result2?.blockedBy,
66276
+ result2?.decidedBy
66277
+ );
64877
66278
  let ttyFd = null;
64878
66279
  try {
64879
66280
  ttyFd = fs70.openSync("/dev/tty", "w");
@@ -64913,7 +66314,7 @@ RAW: ${raw}
64913
66314
  humanDecision,
64914
66315
  msg,
64915
66316
  result2?.recoveryCommand,
64916
- { kind: result2?.blockKind, path: result2?.blockedPath }
66317
+ { kind: result2?.blockKind, path: result2?.blockedPath, redirect: !!result2?.redirect }
64917
66318
  );
64918
66319
  if (agent === "Antigravity") {
64919
66320
  process.stdout.write(
@@ -65318,7 +66719,7 @@ function collectToolOutputText(toolResponse, opts = {}) {
65318
66719
  let size = 0;
65319
66720
  let fields = 0;
65320
66721
  let truncated = false;
65321
- const push = (s) => {
66722
+ const push2 = (s) => {
65322
66723
  if (s.length === 0) return true;
65323
66724
  const room = maxBytes - size;
65324
66725
  if (s.length > room) {
@@ -65336,7 +66737,7 @@ function collectToolOutputText(toolResponse, opts = {}) {
65336
66737
  return true;
65337
66738
  };
65338
66739
  const walk2 = (v, depth) => {
65339
- if (typeof v === "string") return push(v);
66740
+ if (typeof v === "string") return push2(v);
65340
66741
  if (depth >= MAX_DEPTH5 || v === null || typeof v !== "object") return true;
65341
66742
  const children = Array.isArray(v) ? v : Object.values(v);
65342
66743
  for (const child of children) if (!walk2(child, depth + 1)) return false;
@@ -67356,11 +68757,15 @@ function aggregateReportFromAudit(period, opts = {}) {
67356
68757
  let dimAppsBlocked = 0;
67357
68758
  let dimPinMismatch = 0;
67358
68759
  let dimInjectionBlocked = 0;
68760
+ const hookCardIds = new Set(
68761
+ entries.filter((e) => e.source !== "daemon" && e.cardId).map((e) => e.cardId)
68762
+ );
67359
68763
  for (const e of entries) {
68764
+ if (e.source === "daemon" && e.cardId && hookCardIds.has(e.cardId)) continue;
67360
68765
  if (superseded.has(supersedeKey(e))) continue;
67361
68766
  const view = viewOf(e);
67362
68767
  const dateKey = e.ts.slice(0, 10);
67363
- const userInteracted = e.source === "daemon";
68768
+ const userInteracted = e.decidedBy ? e.decidedBy === "person" : e.source === "daemon";
67364
68769
  if (view.observed) {
67365
68770
  if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
67366
68771
  } else if (view.info) {
@@ -67368,7 +68773,8 @@ function aggregateReportFromAudit(period, opts = {}) {
67368
68773
  if (view.ran) userApproved++;
67369
68774
  else userDenied++;
67370
68775
  } else if (view.blocked) {
67371
- if (e.checkedBy === "timeout") timedOut++;
68776
+ if (e.checkedBy === "timeout" || e.decidedBy === "timer" || e.decidedBy === "abandoned")
68777
+ timedOut++;
67372
68778
  else if (isDlp(e.checkedBy)) dlpBlocked++;
67373
68779
  else if (e.checkedBy === "local-decision") userDenied++;
67374
68780
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -68358,7 +69764,7 @@ function registerHealCommand(program2) {
68358
69764
  init_onboarding();
68359
69765
  init_cloud_endpoints();
68360
69766
  init_safe_text();
68361
- import http5 from "http";
69767
+ import http6 from "http";
68362
69768
  import https8 from "https";
68363
69769
  import { URL as URL5 } from "url";
68364
69770
  import chalk22 from "chalk";
@@ -68369,7 +69775,7 @@ function postConnect(url, token) {
68369
69775
  return new Promise((resolve2, reject) => {
68370
69776
  const body = JSON.stringify({ token });
68371
69777
  const u = new URL5(url);
68372
- const lib = u.protocol === "http:" ? http5 : https8;
69778
+ const lib = u.protocol === "http:" ? http6 : https8;
68373
69779
  const req = lib.request(
68374
69780
  {
68375
69781
  method: "POST",
@@ -68508,7 +69914,7 @@ init_safe_text();
68508
69914
  import readline3 from "readline";
68509
69915
  import chalk24 from "chalk";
68510
69916
  import { spawn as spawn8 } from "child_process";
68511
- import { randomUUID as randomUUID8 } from "crypto";
69917
+ import { randomUUID as randomUUID9 } from "crypto";
68512
69918
  import { execa as execa2 } from "execa";
68513
69919
 
68514
69920
  // src/mcp-gateway/result-scan.ts
@@ -68774,7 +70180,7 @@ async function runMcpGateway(upstreamCommand, configName) {
68774
70180
  return false;
68775
70181
  }
68776
70182
  };
68777
- const gatewaySessionId = `mcp-gateway-${randomUUID8()}`;
70183
+ const gatewaySessionId = `mcp-gateway-${randomUUID9()}`;
68778
70184
  const readOnlyTools = /* @__PURE__ */ new Set();
68779
70185
  let clientName;
68780
70186
  const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
@@ -68873,13 +70279,13 @@ async function runMcpGateway(upstreamCommand, configName) {
68873
70279
  console.error(chalk24.gray(` Reason: ${result.reason ?? "Security Policy"}
68874
70280
  `));
68875
70281
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
68876
- const humanDecision = isHumanDecision(blockedByLabel, result.blockedBy);
70282
+ const humanDecision = isHumanDecision(blockedByLabel, result.blockedBy, result.decidedBy);
68877
70283
  const aiInstruction = buildNegotiationMessage(
68878
70284
  blockedByLabel,
68879
70285
  humanDecision,
68880
70286
  result.reason,
68881
70287
  void 0,
68882
- { kind: result.blockKind, path: result.blockedPath }
70288
+ { kind: result.blockKind, path: result.blockedPath, redirect: !!result.redirect }
68883
70289
  );
68884
70290
  const errorResponse = {
68885
70291
  jsonrpc: "2.0",
@@ -70146,7 +71552,7 @@ function registerMcpGatewayCommand2(mcp) {
70146
71552
  }
70147
71553
  const ungoverned = rows.filter((e) => e.connection === "ungoverned").length;
70148
71554
  const unlaunchable = rows.filter((e) => e.connection === "unlaunchable").length;
70149
- const pending2 = rows.filter((e) => e.connection === "pending-launch").length;
71555
+ const pending = rows.filter((e) => e.connection === "pending-launch").length;
70150
71556
  if (ungoverned > 0) {
70151
71557
  console.error(
70152
71558
  chalk26.yellow(
@@ -70155,9 +71561,9 @@ ${ungoverned} ungoverned \u2014 run ` + chalk26.bold("node9 mcp gateway --all")
70155
71561
  )
70156
71562
  );
70157
71563
  }
70158
- if (pending2 > 0) {
71564
+ if (pending > 0) {
70159
71565
  console.error(
70160
- chalk26.cyan(`${pending2} governed but not yet connected \u2014 restart the agent to activate.`)
71566
+ chalk26.cyan(`${pending} governed but not yet connected \u2014 restart the agent to activate.`)
70161
71567
  );
70162
71568
  }
70163
71569
  if (unlaunchable > 0) {
@@ -75963,7 +77369,7 @@ function addHost(list, host2) {
75963
77369
  if (!cliGuardPolicyWrite(`egress ${list} ${host2}`)) return false;
75964
77370
  return guard(() => addEgressHost(list, host2));
75965
77371
  }
75966
- function exempt(address) {
77372
+ function exempt2(address) {
75967
77373
  if (!cliGuardPolicyWrite(`egress exempt ${address}`)) return false;
75968
77374
  return guard(() => addSsrfExemption(address));
75969
77375
  }
@@ -76146,7 +77552,7 @@ function registerEgressCommand(program2) {
76146
77552
  process.exitCode = 1;
76147
77553
  return;
76148
77554
  }
76149
- if (!exempt(a)) return;
77555
+ if (!exempt2(a)) return;
76150
77556
  console.log(chalk34.green(`
76151
77557
  \u2713 ${a} is exempt from the floor.`));
76152
77558
  if (!m)
@@ -77935,17 +79341,18 @@ function registerSessionExposureCommand(program2) {
77935
79341
  init_osv_index();
77936
79342
  init_osv_sync();
77937
79343
  import chalk42 from "chalk";
79344
+ var ECO_WIDTH = Math.max(...ECOSYSTEMS2.map((e) => e.length));
77938
79345
  function registerPackageIndexCommand(program2) {
77939
79346
  const cmd = program2.command("package-index").description("Local OSV malicious-package index used to check installs before they run");
77940
79347
  cmd.command("status").description("Show when each ecosystem was last synced and how many records it holds").action(() => {
77941
79348
  for (const eco of ECOSYSTEMS2) {
77942
79349
  const meta = readMeta(eco);
77943
79350
  if (!meta) {
77944
- console.log(`${eco.padEnd(5)} ${chalk42.yellow("not synced")}`);
79351
+ console.log(`${eco.padEnd(ECO_WIDTH)} ${chalk42.yellow("not synced")}`);
77945
79352
  continue;
77946
79353
  }
77947
79354
  console.log(
77948
- `${eco.padEnd(5)} ${meta.records} records synced ${meta.syncedAt} (${meta.mode})`
79355
+ `${eco.padEnd(ECO_WIDTH)} ${meta.records} records synced ${meta.syncedAt} (${meta.mode})`
77949
79356
  );
77950
79357
  }
77951
79358
  });
@@ -77955,9 +79362,9 @@ function registerPackageIndexCommand(program2) {
77955
79362
  for (const r of results) {
77956
79363
  if (r.error) {
77957
79364
  failed = true;
77958
- console.error(`${r.ecosystem.padEnd(5)} ${chalk42.red("failed")}: ${r.error}`);
79365
+ console.error(`${r.ecosystem.padEnd(ECO_WIDTH)} ${chalk42.red("failed")}: ${r.error}`);
77959
79366
  } else {
77960
- console.log(`${r.ecosystem.padEnd(5)} ${r.mode}: ${r.records} records`);
79367
+ console.log(`${r.ecosystem.padEnd(ECO_WIDTH)} ${r.mode}: ${r.records} records`);
77961
79368
  }
77962
79369
  }
77963
79370
  process.exitCode = failed ? 1 : 0;