@node9/proxy 2.29.0 → 2.30.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.
Files changed (3) hide show
  1. package/dist/cli.js +1354 -145
  2. package/dist/cli.mjs +1354 -145
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -19438,19 +19438,24 @@ async function postCostBatches(apiUrl, apiKey, machineId, entries) {
19438
19438
  } catch {
19439
19439
  }
19440
19440
  if (typeof stored === "number" && stored < batch.length) {
19441
- fs35.appendFileSync(
19442
- HOOK_DEBUG_LOG,
19443
- `[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
19444
- `
19445
- );
19441
+ debugLog(`[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
19442
+ `);
19446
19443
  }
19447
19444
  }
19448
19445
  } catch (err2) {
19449
- fs35.appendFileSync(HOOK_DEBUG_LOG, `[cost-sync] ${safeMessage(err2)}
19446
+ debugLog(`[cost-sync] ${safeMessage(err2)}
19450
19447
  `);
19451
19448
  }
19452
19449
  }
19453
19450
  }
19451
+ function debugLog(line) {
19452
+ try {
19453
+ fs35.mkdirSync(path36.dirname(HOOK_DEBUG_LOG), { recursive: true });
19454
+ fs35.appendFileSync(HOOK_DEBUG_LOG, line);
19455
+ } catch (err2) {
19456
+ console.error(`[cost-sync] could not write ${HOOK_DEBUG_LOG}: ${safeMessage(err2)}`);
19457
+ }
19458
+ }
19454
19459
  async function syncCost() {
19455
19460
  const creds = getCredentials();
19456
19461
  if (!creds?.apiKey || !creds?.apiUrl) return;
@@ -32332,7 +32337,7 @@ var require_diagnostics = __commonJS({
32332
32337
  proxyConnected: diagnosticsChannel.channel("undici:proxy:connected")
32333
32338
  };
32334
32339
  var isTrackingClientEvents = false;
32335
- function trackClientEvents(debugLog = undiciDebugLog) {
32340
+ function trackClientEvents(debugLog2 = undiciDebugLog) {
32336
32341
  if (isTrackingClientEvents) {
32337
32342
  return;
32338
32343
  }
@@ -32347,7 +32352,7 @@ var require_diagnostics = __commonJS({
32347
32352
  const {
32348
32353
  connectParams: { version: version2, protocol, port, host }
32349
32354
  } = evt;
32350
- debugLog(
32355
+ debugLog2(
32351
32356
  "connecting to %s%s using %s%s",
32352
32357
  host,
32353
32358
  port ? `:${port}` : "",
@@ -32362,7 +32367,7 @@ var require_diagnostics = __commonJS({
32362
32367
  const {
32363
32368
  connectParams: { version: version2, protocol, port, host }
32364
32369
  } = evt;
32365
- debugLog(
32370
+ debugLog2(
32366
32371
  "connected to %s%s using %s%s",
32367
32372
  host,
32368
32373
  port ? `:${port}` : "",
@@ -32378,7 +32383,7 @@ var require_diagnostics = __commonJS({
32378
32383
  connectParams: { version: version2, protocol, port, host },
32379
32384
  error
32380
32385
  } = evt;
32381
- debugLog(
32386
+ debugLog2(
32382
32387
  "connection to %s%s using %s%s errored - %s",
32383
32388
  host,
32384
32389
  port ? `:${port}` : "",
@@ -32394,12 +32399,12 @@ var require_diagnostics = __commonJS({
32394
32399
  const {
32395
32400
  request: { method, path: path91, origin }
32396
32401
  } = evt;
32397
- debugLog("sending request to %s %s%s", method, origin, path91);
32402
+ debugLog2("sending request to %s %s%s", method, origin, path91);
32398
32403
  }
32399
32404
  );
32400
32405
  }
32401
32406
  var isTrackingRequestEvents = false;
32402
- function trackRequestEvents(debugLog = undiciDebugLog) {
32407
+ function trackRequestEvents(debugLog2 = undiciDebugLog) {
32403
32408
  if (isTrackingRequestEvents) {
32404
32409
  return;
32405
32410
  }
@@ -32415,7 +32420,7 @@ var require_diagnostics = __commonJS({
32415
32420
  request: { method, path: path91, origin },
32416
32421
  response: { statusCode }
32417
32422
  } = evt;
32418
- debugLog(
32423
+ debugLog2(
32419
32424
  "received response to %s %s%s - HTTP %d",
32420
32425
  method,
32421
32426
  origin,
@@ -32430,7 +32435,7 @@ var require_diagnostics = __commonJS({
32430
32435
  const {
32431
32436
  request: { method, path: path91, origin }
32432
32437
  } = evt;
32433
- debugLog("trailers received from %s %s%s", method, origin, path91);
32438
+ debugLog2("trailers received from %s %s%s", method, origin, path91);
32434
32439
  }
32435
32440
  );
32436
32441
  diagnosticsChannel.subscribe(
@@ -32440,7 +32445,7 @@ var require_diagnostics = __commonJS({
32440
32445
  request: { method, path: path91, origin },
32441
32446
  error
32442
32447
  } = evt;
32443
- debugLog(
32448
+ debugLog2(
32444
32449
  "request to %s %s%s errored - %s",
32445
32450
  method,
32446
32451
  origin,
@@ -32451,7 +32456,7 @@ var require_diagnostics = __commonJS({
32451
32456
  );
32452
32457
  }
32453
32458
  var isTrackingWebSocketEvents = false;
32454
- function trackWebSocketEvents(debugLog = websocketDebuglog) {
32459
+ function trackWebSocketEvents(debugLog2 = websocketDebuglog) {
32455
32460
  if (isTrackingWebSocketEvents) {
32456
32461
  return;
32457
32462
  }
@@ -32465,9 +32470,9 @@ var require_diagnostics = __commonJS({
32465
32470
  (evt) => {
32466
32471
  if (evt.address != null) {
32467
32472
  const { address, port } = evt.address;
32468
- debugLog("connection opened %s%s", address, port ? `:${port}` : "");
32473
+ debugLog2("connection opened %s%s", address, port ? `:${port}` : "");
32469
32474
  } else {
32470
- debugLog("connection opened");
32475
+ debugLog2("connection opened");
32471
32476
  }
32472
32477
  }
32473
32478
  );
@@ -32475,7 +32480,7 @@ var require_diagnostics = __commonJS({
32475
32480
  "undici:websocket:close",
32476
32481
  (evt) => {
32477
32482
  const { websocket, code: code2, reason } = evt;
32478
- debugLog(
32483
+ debugLog2(
32479
32484
  "closed connection to %s - %s %s",
32480
32485
  websocket.url,
32481
32486
  code2,
@@ -32486,19 +32491,19 @@ var require_diagnostics = __commonJS({
32486
32491
  diagnosticsChannel.subscribe(
32487
32492
  "undici:websocket:socket_error",
32488
32493
  (err2) => {
32489
- debugLog("connection errored - %s", err2.message);
32494
+ debugLog2("connection errored - %s", err2.message);
32490
32495
  }
32491
32496
  );
32492
32497
  diagnosticsChannel.subscribe(
32493
32498
  "undici:websocket:ping",
32494
32499
  (evt) => {
32495
- debugLog("ping received");
32500
+ debugLog2("ping received");
32496
32501
  }
32497
32502
  );
32498
32503
  diagnosticsChannel.subscribe(
32499
32504
  "undici:websocket:pong",
32500
32505
  (evt) => {
32501
- debugLog("pong received");
32506
+ debugLog2("pong received");
32502
32507
  }
32503
32508
  );
32504
32509
  }
@@ -64547,6 +64552,97 @@ function isZwLegitScript(cp) {
64547
64552
  return cp >= 3584 && cp <= 3711 || cp >= 3712 && cp <= 3839 || cp >= 4096 && cp <= 4255 || cp >= 6016 && cp <= 6143 || cp >= 12352 && cp <= 12543 || cp >= 13312 && cp <= 40959 || cp >= 44032 && cp <= 55215;
64548
64553
  }
64549
64554
  var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
64555
+ var LOOKALIKE = {
64556
+ \u0430: "a",
64557
+ \u0435: "e",
64558
+ \u043E: "o",
64559
+ \u0440: "p",
64560
+ \u0441: "c",
64561
+ \u0443: "y",
64562
+ \u0445: "x",
64563
+ \u0456: "i",
64564
+ \u0458: "j",
64565
+ \u0455: "s",
64566
+ "\u0501": "d",
64567
+ "\u051B": "q",
64568
+ "\u051D": "w",
64569
+ \u0410: "A",
64570
+ \u0412: "B",
64571
+ \u0415: "E",
64572
+ \u041A: "K",
64573
+ \u041C: "M",
64574
+ \u041D: "H",
64575
+ \u041E: "O",
64576
+ \u0420: "P",
64577
+ \u0421: "C",
64578
+ \u0422: "T",
64579
+ \u0425: "X",
64580
+ \u0406: "I",
64581
+ \u0408: "J",
64582
+ \u0405: "S",
64583
+ "\u050C": "G",
64584
+ "\u051A": "Q",
64585
+ "\u051C": "W",
64586
+ \u03BF: "o",
64587
+ \u03B1: "a",
64588
+ \u03C1: "p",
64589
+ \u03C4: "t",
64590
+ \u03BD: "v",
64591
+ \u03C5: "u",
64592
+ \u03BA: "k",
64593
+ \u03C7: "x",
64594
+ \u03B9: "i",
64595
+ \u0391: "A",
64596
+ \u0392: "B",
64597
+ \u0395: "E",
64598
+ \u0397: "H",
64599
+ \u0399: "I",
64600
+ \u039A: "K",
64601
+ \u039C: "M",
64602
+ \u039D: "N",
64603
+ \u039F: "O",
64604
+ \u03A1: "P",
64605
+ \u03A4: "T",
64606
+ \u03A7: "X",
64607
+ \u03A5: "Y",
64608
+ \u0396: "Z",
64609
+ // Armenian (review, round 2)
64610
+ "\u0585": "o",
64611
+ "\u057D": "u",
64612
+ "\u0570": "h",
64613
+ "\u0578": "n",
64614
+ "\u0581": "g",
64615
+ "\u0566": "q"
64616
+ };
64617
+ var MIXED_WORD_RE = /[A-Za-z\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]{4,60}/g;
64618
+ var FOREIGN_LETTER_RE = /[\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]/;
64619
+ var GREEK_RE = /[\u0370-\u03ff]/;
64620
+ var EXPLAINED_LINE_RE = /cyrillic|greek|armenian|homoglyph|confusable|look-?alike/i;
64621
+ var HOMOGLYPH_MAX_LINE = 2e3;
64622
+ function homoglyphWord(content) {
64623
+ const lines = content.split("\n");
64624
+ for (let i = 0; i < lines.length; i++) {
64625
+ const text = lines[i];
64626
+ if (text.length > HOMOGLYPH_MAX_LINE) continue;
64627
+ const visible = text.replace(/<!--[\s\S]*?(?:-->|$)/g, "");
64628
+ if (EXPLAINED_LINE_RE.test(visible)) continue;
64629
+ for (const m of text.matchAll(MIXED_WORD_RE)) {
64630
+ const chars = [...m[0]];
64631
+ const latin = chars.filter((c) => /[A-Za-z]/.test(c)).length;
64632
+ const foreign = chars.filter((c) => FOREIGN_LETTER_RE.test(c));
64633
+ if (latin < 2 || !foreign.some((c) => c in LOOKALIKE) || foreign.length > latin) continue;
64634
+ const lead = chars.findIndex((c) => !FOREIGN_LETTER_RE.test(c));
64635
+ if (lead > 0 && chars.slice(0, lead).every((c) => GREEK_RE.test(c)) && lead === foreign.length)
64636
+ continue;
64637
+ return {
64638
+ folded: chars.map((c) => LOOKALIKE[c] ?? c).join(""),
64639
+ count: foreign.length,
64640
+ line: i + 1
64641
+ };
64642
+ }
64643
+ }
64644
+ return null;
64645
+ }
64550
64646
  function suspiciousZeroWidth(text) {
64551
64647
  let n = 0;
64552
64648
  let first = -1;
@@ -64790,6 +64886,21 @@ var lineAt = (text, i) => i >= 0 ? lineAtIndex(text, i) : void 0;
64790
64886
  function analyzeInstructionFile(path91, content) {
64791
64887
  const findings = [];
64792
64888
  const decoded = decodeSuspiciousBase64(content);
64889
+ const glyph = homoglyphWord(content);
64890
+ if (glyph)
64891
+ findings.push(
64892
+ mk2(
64893
+ "CI-6.homoglyph",
64894
+ "advisory",
64895
+ "A word in an agent instruction file is spelled with look-alike letters",
64896
+ [
64897
+ `\`${glyph.folded}\` is written with ${glyph.count} non-Latin look-alike letter(s): it reads the same to a reviewer, but a search, a replace or a policy check on the real word misses it`
64898
+ ],
64899
+ "Spell the word with plain Latin letters, or explain on the same line why the look-alikes are there.",
64900
+ path91,
64901
+ glyph.line
64902
+ )
64903
+ );
64793
64904
  if (TAG_CHARS.test(content))
64794
64905
  findings.push(
64795
64906
  mk2(
@@ -64928,13 +65039,17 @@ function analyzeInstructionFile(path91, content) {
64928
65039
  }
64929
65040
 
64930
65041
  // src/ci-check/route.ts
65042
+ var MCP_CONFIG_RE = /\.mcp\.json$|\.cursor\/mcp\.json$|(^|\/)\.vscode\/mcp\.json$|(^|\/)\.kiro\/settings\/mcp\.json$|(^|\/)\.gemini\/settings\.json$|(^|\/)opencode\.json$|(^|\/)\.opencode\/config\.json$/;
65043
+ var ISSUE_FORM_RE = /^\.github\/ISSUE_TEMPLATE\/[^/]+\.(ya?ml|md)$/i;
64931
65044
  function routeOf(path91, skillDirs, appDirs = /* @__PURE__ */ new Set()) {
64932
65045
  if (path91 === SUPPRESSIONS_FILE) return "suppressions";
64933
65046
  if (/\.github\/workflows\/.+\.ya?ml$/.test(path91)) return "workflow";
65047
+ if (ISSUE_FORM_RE.test(path91)) return "issue-form";
65048
+ if (path91 === "package.json" || path91 === "pyproject.toml") return "manifest";
64934
65049
  if (/\.claude\/settings(\.local)?\.json$/.test(path91)) return "agent-config";
64935
65050
  if (isHookScript(path91)) return "hook-script";
64936
65051
  if (isSkillScript(path91, skillDirs)) return "skill-script";
64937
- if (/\.mcp\.json$|\.cursor\/mcp\.json$/.test(path91)) return "mcp";
65052
+ if (MCP_CONFIG_RE.test(path91)) return "mcp";
64938
65053
  if (/(^|\/)\.codex\/config\.toml$/.test(path91)) return "codex";
64939
65054
  if (isInstructionFile(path91, skillDirs, appDirs)) return "instruction";
64940
65055
  if (isAppDoc(path91, skillDirs, appDirs)) return "app-doc";
@@ -64966,6 +65081,11 @@ var SURFACE_FILES = [
64966
65081
  ".claude/settings.local.json",
64967
65082
  ".mcp.json",
64968
65083
  ".cursor/mcp.json",
65084
+ ".vscode/mcp.json",
65085
+ ".kiro/settings/mcp.json",
65086
+ ".gemini/settings.json",
65087
+ "opencode.json",
65088
+ ".opencode/config.json",
64969
65089
  ".codex/config.toml",
64970
65090
  // CI-6: agent instruction files (auto-loaded into the agent's system prompt).
64971
65091
  "CLAUDE.md",
@@ -64977,7 +65097,7 @@ var SURFACE_FILES = [
64977
65097
  ".clinerules"
64978
65098
  ];
64979
65099
  var WORKFLOW_DIR = ".github/workflows";
64980
- var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$/;
65100
+ var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$|(^|\/)\.vscode\/mcp\.json$|(^|\/)\.kiro\/settings\/mcp\.json$|(^|\/)\.gemini\/settings\.json$|(^|\/)opencode\.json$|(^|\/)\.opencode\/config\.json$/;
64981
65101
  function selectSurface(paths) {
64982
65102
  const skillDirs = skillDirsOf(paths);
64983
65103
  const appDirs = appSkillDirsOf(paths, skillDirs);
@@ -65126,6 +65246,7 @@ async function listWorkflowPaths(owner, repo, notes) {
65126
65246
  return json.filter((e) => e.type === "file" && /\.ya?ml$/.test(e.name)).map((e) => e.path);
65127
65247
  }
65128
65248
  var ROOT_WORKFLOW_RE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
65249
+ var MAX_ISSUE_FORMS = 40;
65129
65250
  function kindOfMode(mode) {
65130
65251
  if (mode === "120000") return "link";
65131
65252
  if (/^100[0-7]{3}$/.test(mode)) return "file";
@@ -65335,8 +65456,15 @@ function planSurface(paths, notes, truncated) {
65335
65456
  const present = new Set(paths);
65336
65457
  const root = SURFACE_FILES.filter((p) => present.has(p));
65337
65458
  const workflows = paths.filter((p) => ROOT_WORKFLOW_RE.test(p));
65459
+ const allForms = paths.filter((p) => ISSUE_FORM_RE.test(p));
65460
+ const forms = allForms.slice(0, MAX_ISSUE_FORMS);
65461
+ if (allForms.length > MAX_ISSUE_FORMS)
65462
+ notes.push(
65463
+ `${allForms.length} issue forms; read the first ${MAX_ISSUE_FORMS} (a label an unread form applies is not known): results ${INCOMPLETE}.`
65464
+ );
65465
+ forms.push(...["package.json", "pyproject.toml"].filter((p) => present.has(p)));
65338
65466
  const nested = pickSurfacePaths(paths, truncated, notes, { files: Number.POSITIVE_INFINITY });
65339
- return [.../* @__PURE__ */ new Set([...root, ...workflows, ...nested])];
65467
+ return [.../* @__PURE__ */ new Set([...root, ...workflows, ...forms, ...nested])];
65340
65468
  }
65341
65469
  function planReads(l, R, opts = {}) {
65342
65470
  const notes = [];
@@ -65402,7 +65530,7 @@ function planReads(l, R, opts = {}) {
65402
65530
  function capNested(reads, cap, notes) {
65403
65531
  if (cap === void 0) return reads;
65404
65532
  const rootSet = new Set(SURFACE_FILES);
65405
- const isRoot = (r) => rootSet.has(r.rel) || ROOT_WORKFLOW_RE.test(r.rel);
65533
+ const isRoot = (r) => rootSet.has(r.rel) || ROOT_WORKFLOW_RE.test(r.rel) || ISSUE_FORM_RE.test(r.rel) || r.rel === "package.json" || r.rel === "pyproject.toml";
65406
65534
  const nested = reads.filter((r) => !isRoot(r));
65407
65535
  if (nested.length < cap) return reads;
65408
65536
  const keep = new Set(nested.slice(0, cap));
@@ -65498,6 +65626,45 @@ function readLocalFile(abs, rel) {
65498
65626
  if (fd !== void 0) fs78.closeSync(fd);
65499
65627
  }
65500
65628
  }
65629
+ function readSmallFile(p, max) {
65630
+ let fd;
65631
+ try {
65632
+ fd = fs78.openSync(p, fs78.constants.O_RDONLY | (fs78.constants.O_NOFOLLOW ?? 0));
65633
+ const st = fs78.fstatSync(fd);
65634
+ if (!st.isFile() || st.size > max) return null;
65635
+ const buf = Buffer.alloc(st.size);
65636
+ const n = fs78.readSync(fd, buf, 0, st.size, 0);
65637
+ return buf.subarray(0, n).toString("utf8");
65638
+ } catch {
65639
+ return null;
65640
+ } finally {
65641
+ if (fd !== void 0) fs78.closeSync(fd);
65642
+ }
65643
+ }
65644
+ function localGitHubOwner(root) {
65645
+ let cfg = readSmallFile(path75.join(root, ".git", "config"), 1e6);
65646
+ if (cfg === null) {
65647
+ const pointer = readSmallFile(path75.join(root, ".git"), 4096);
65648
+ const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
65649
+ if (!m) return void 0;
65650
+ let gitDir = path75.resolve(root, m[1]);
65651
+ const common = readSmallFile(path75.join(gitDir, "commondir"), 4096);
65652
+ if (common !== null) gitDir = path75.resolve(gitDir, common.trim());
65653
+ cfg = readSmallFile(path75.join(gitDir, "config"), 1e6);
65654
+ if (cfg === null) return void 0;
65655
+ }
65656
+ let inOrigin = false;
65657
+ for (const line of cfg.split(/\r?\n/)) {
65658
+ const sec = /^\s*\[\s*([^\]\s]+)(?:\s+"([^"]*)")?\s*\]/.exec(line);
65659
+ if (sec) {
65660
+ inOrigin = sec[1].toLowerCase() === "remote" && sec[2] === "origin";
65661
+ continue;
65662
+ }
65663
+ const url = inOrigin && /^\s*url\s*=\s*(.+?)\s*$/i.exec(line);
65664
+ if (url) return /github\.com[:/]([\w.-]{1,100})\//i.exec(url[1])?.[1];
65665
+ }
65666
+ return void 0;
65667
+ }
65501
65668
  function readLocalTree(dir) {
65502
65669
  const root = dir.replace(/^~/, process.env.HOME ?? "~");
65503
65670
  const notes = [];
@@ -65527,8 +65694,10 @@ function readLocalTree(dir) {
65527
65694
  }
65528
65695
  });
65529
65696
  }
65697
+ const owner = localGitHubOwner(root);
65530
65698
  return {
65531
65699
  source: root,
65700
+ ...owner ? { owner } : {},
65532
65701
  files,
65533
65702
  notes,
65534
65703
  paths: plan.paths,
@@ -65641,8 +65810,10 @@ function readGitRefTree(dir, ref) {
65641
65810
  }
65642
65811
  files.push({ path: rel, content, route });
65643
65812
  }
65813
+ const owner = localGitHubOwner(root);
65644
65814
  return {
65645
65815
  source: `${root}@${ref}`,
65816
+ ...owner ? { owner } : {},
65646
65817
  files,
65647
65818
  notes,
65648
65819
  paths: plan.paths,
@@ -65742,7 +65913,13 @@ async function fetchGitHubTree(owner, repo, onProgress) {
65742
65913
  const listed2 = await listTree(owner, repo, notes);
65743
65914
  if (!listed2) {
65744
65915
  const workflowPaths = await listWorkflowPaths(owner, repo, notes);
65745
- const allPaths = [.../* @__PURE__ */ new Set([...SURFACE_FILES, ...workflowPaths])];
65916
+ const allPaths = [
65917
+ .../* @__PURE__ */ new Set([...SURFACE_FILES, ...workflowPaths, "package.json", "pyproject.toml"])
65918
+ ];
65919
+ if (!notes.includes(RATE_LIMIT_NOTE) && !notes.includes(NETWORK_NOTE))
65920
+ notes.push(
65921
+ `the repository tree could not be listed; only the root files and the workflows were read: nested agent files ${INCOMPLETE}.`
65922
+ );
65746
65923
  const fetched2 = await pooled(
65747
65924
  allPaths,
65748
65925
  FETCH_CONCURRENCY,
@@ -65750,6 +65927,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
65750
65927
  );
65751
65928
  return {
65752
65929
  source: `${owner}/${repo}`,
65930
+ owner,
65753
65931
  files: fetched2.filter((f) => !!f),
65754
65932
  notes
65755
65933
  };
@@ -65805,6 +65983,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
65805
65983
  notes.push(...session.closingNotes());
65806
65984
  return {
65807
65985
  source: `${owner}/${repo}`,
65986
+ owner,
65808
65987
  files: fetched.filter((f) => !!f),
65809
65988
  notes,
65810
65989
  paths: plan.paths,
@@ -65815,7 +65994,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
65815
65994
  notes.push(
65816
65995
  `fetch degraded (${err2?.message ?? "network error"}): results ${INCOMPLETE} (the repo could not be fetched).`
65817
65996
  );
65818
- return { source: `${owner}/${repo}`, files: [], notes };
65997
+ return { source: `${owner}/${repo}`, owner, files: [], notes };
65819
65998
  }
65820
65999
  }
65821
66000
  async function fetchTree(input, onProgress) {
@@ -65978,7 +66157,15 @@ function explainGrant(f) {
65978
66157
  const git = f.broad.some((a) => /^Bash\(git:/.test(a));
65979
66158
  const files = f.broad.some((a) => /^Write|^Edit$/.test(a));
65980
66159
  const template = TEMPLATE_PATH_RE.test(f.file);
65981
- const what = f.bareShell ? "run commands and change files" : sentence([git ? "run git commands" : "", files ? "change files" : ""].filter(Boolean), true);
66160
+ const runner = f.broad.some((a) => !/^Bash\(git:|^Write|^Edit$/.test(a));
66161
+ const what = f.bareShell ? "run commands and change files" : sentence(
66162
+ [
66163
+ runner ? "run programs" : "",
66164
+ git ? "run git commands" : "",
66165
+ files ? "change files" : ""
66166
+ ].filter(Boolean),
66167
+ true
66168
+ );
65982
66169
  const saw = [];
65983
66170
  if (template) saw.push("This is a template: every project made from it gets these permissions.");
65984
66171
  saw.push(
@@ -66018,14 +66205,17 @@ function npxPackage(argv) {
66018
66205
  return null;
66019
66206
  }
66020
66207
  function explainMcp(f) {
66021
- const pkg = npxPackage(f.argv);
66022
- const what = pkg ? code(pkg) : "the package";
66208
+ const pkg = f.pkg ?? npxPackage(f.argv);
66209
+ const registry = f.registry ?? "npm";
66210
+ const image = registry === "the image registry";
66211
+ const what = pkg ? `the newest ${code(pkg)}` : image ? "the newest image" : "the newest version of the package";
66212
+ const example = !pkg ? null : image ? `${pkg}@sha256:\u2026` : registry === "PyPI" ? `${pkg}==x.y.z` : registry === "the Go module proxy" ? `${pkg}@vX.Y.Z` : `${pkg}@x.y.z`;
66023
66213
  return {
66024
66214
  headline: `MCP server ${code(f.name, 60)} installs whatever version is newest, every time`,
66025
- happens: `Each run downloads the latest ${what} from npm. If that package is ever taken over, the next version runs on every developer's computer.`,
66215
+ happens: `Each run downloads ${what} from ${registry}. If it is ever taken over, the next version runs on every developer's computer.`,
66026
66216
  saw: [`It starts with ${code(f.argv, 120)}.`],
66027
66217
  fix: [
66028
- pkg ? `Pin an exact version, for example ${code(`${pkg}@x.y.z`)}.` : "Pin an exact version."
66218
+ example ? `Pin an exact version, for example ${code(example)}.` : "Pin an exact version."
66029
66219
  ]
66030
66220
  };
66031
66221
  }
@@ -66049,6 +66239,65 @@ function parseYamlStrict(text) {
66049
66239
  return doc.toJS();
66050
66240
  }
66051
66241
 
66242
+ // src/ci-check/frontmatter.ts
66243
+ function parseFrontmatter(content) {
66244
+ const text = content.replace(/^/, "").replace(/^\s*\n/, "");
66245
+ if (!/^---[ \t]*\r?\n/.test(text)) return null;
66246
+ const close = /\r?\n---[ \t]*(\r?\n|$)/.exec(text.slice(3));
66247
+ if (!close) return null;
66248
+ const block = text.slice(3, 3 + close.index);
66249
+ try {
66250
+ const parsed = parseYamlStrict(block);
66251
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
66252
+ } catch {
66253
+ return salvageTopLevel(block);
66254
+ }
66255
+ }
66256
+ function salvageTopLevel(block) {
66257
+ const out = {};
66258
+ const lines = block.split(/\r?\n/);
66259
+ for (let i = 0; i < lines.length; i++) {
66260
+ const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]);
66261
+ if (!m) continue;
66262
+ const [, key, rest] = m;
66263
+ if (rest.trim()) {
66264
+ out[key] = rest.trim();
66265
+ continue;
66266
+ }
66267
+ const items = [];
66268
+ while (i + 1 < lines.length && /^[ \t]+-[ \t]+/.test(lines[i + 1])) {
66269
+ items.push(lines[++i].replace(/^[ \t]+-[ \t]+/, "").trim());
66270
+ }
66271
+ if (items.length) out[key] = items;
66272
+ }
66273
+ return Object.keys(out).length ? out : null;
66274
+ }
66275
+ function splitGrants(value) {
66276
+ const out = [];
66277
+ let depth = 0;
66278
+ let cur = "";
66279
+ for (const ch of value) {
66280
+ if (ch === "(") depth++;
66281
+ else if (ch === ")") depth = Math.max(0, depth - 1);
66282
+ if (depth === 0 && (ch === "," || /\s/.test(ch))) {
66283
+ if (cur) out.push(cur);
66284
+ cur = "";
66285
+ continue;
66286
+ }
66287
+ cur += ch;
66288
+ }
66289
+ if (cur) out.push(cur);
66290
+ return out;
66291
+ }
66292
+ function allowedToolsOf(fm) {
66293
+ const raw = fm?.["allowed-tools"];
66294
+ if (typeof raw === "string") return splitGrants(raw);
66295
+ if (Array.isArray(raw)) {
66296
+ return raw.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean);
66297
+ }
66298
+ return [];
66299
+ }
66300
+
66052
66301
  // src/ci-check/types.ts
66053
66302
  var SEVERITY_RANK2 = {
66054
66303
  critical: 4,
@@ -66057,11 +66306,28 @@ var SEVERITY_RANK2 = {
66057
66306
  advisory: 1
66058
66307
  };
66059
66308
 
66309
+ // src/ci-check/listing.ts
66310
+ var SAFE_LIST_FIELD_RE = /^(?:number|url|html_url|id|node_id|sha|state|createdat|updatedat|closedat|mergedat|headrefname|baserefname|isdraft|headrefoid|login|user|author_association)$/i;
66311
+ function sanitizedListing(t) {
66312
+ if (t.length > 5e4) return false;
66313
+ if (/\|\s*grep\s+-[a-zA-Z]*o/.test(t)) return true;
66314
+ const fieldsSafe = (expr, fieldRe) => {
66315
+ const fields = [...expr.matchAll(fieldRe)].map((m) => m[1]);
66316
+ return fields.length > 0 && fields.every((f) => SAFE_LIST_FIELD_RE.test(f));
66317
+ };
66318
+ const jq = [
66319
+ ...t.matchAll(/(?:--jq|\s-q)[\s=]+(?:'([^']{0,500})'|"([^"]{0,500})"|([^\s'"]{1,200}))/g)
66320
+ ].map((m) => m[1] ?? m[2] ?? m[3]);
66321
+ if (jq.length) return jq.every((e) => fieldsSafe(e, /\.([A-Za-z_]\w{0,60})/g));
66322
+ const json = [...t.matchAll(/--json[\s=]+['"]?([\w,]{1,500})/g)].map((m) => m[1]);
66323
+ return json.length > 0 && json.every((e) => fieldsSafe(e, /([A-Za-z_]\w{0,60})/g));
66324
+ }
66325
+
66060
66326
  // src/ci-check/workflows.ts
66061
66327
  var AGENT_ACTION_RE = /(anthropics\/claude-code(-base)?-action|anthropics\/claude-code|^\s*[\w.-]{1,100}\/claude-code-action@|openai\/codex|codex-action|run-?aider|aider-?action|google-github-actions\/run-gemini)/i;
66062
66328
  var BROAD_TOOL_RE = /(^|["\s,])(Bash|Write|Edit)(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|git push|git commit|git config|git:|rm|sh|bash|eval|npx|pip)/i;
66063
66329
  var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
66064
- var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|git push|git:)/i;
66330
+ var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|gh (pr|issue)\s*[:*)]|git push|git:)/i;
66065
66331
  var EGRESS_TOOL_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\(\s*(curl|wget|python|python3|node|deno|bun|ruby|perl|php|pip|pipx|npx|nc|ncat|ssh|scp|http|gh api)|WebFetch|WebSearch/i;
66066
66332
  var MEMBERSHIP_CHECK_RE = /orgs\/[^/'"\s]+\/memberships\/|teams\/[^/'"\s]+\/memberships\/|getCollaboratorPermissionLevel|collaborators\/[^/'"\s]+\/permission|checkMembershipForUser|getMembershipForUser/i;
66067
66333
  var STRANGER_ISSUE_TYPES = /* @__PURE__ */ new Set(["opened", "edited", "reopened", "closed"]);
@@ -66117,12 +66383,69 @@ function keyStrangerFirable(on, key) {
66117
66383
  return false;
66118
66384
  }
66119
66385
  }
66386
+ var AUTO_LABELS = /* @__PURE__ */ new Set();
66387
+ function issueFormLabels(content) {
66388
+ const text = content.length > 2e5 ? content.slice(0, 2e5) : content;
66389
+ let value;
66390
+ let parsed = false;
66391
+ if (/^\uFEFF?\s*---[ \t]*\r?\n/.test(text)) {
66392
+ const fm = parseFrontmatter(text);
66393
+ parsed = fm !== null;
66394
+ value = fm?.["labels"];
66395
+ } else {
66396
+ try {
66397
+ const doc = parseYamlStrict(text);
66398
+ parsed = true;
66399
+ if (doc && typeof doc === "object" && !Array.isArray(doc))
66400
+ value = doc["labels"];
66401
+ } catch {
66402
+ }
66403
+ }
66404
+ const raw = parsed ? Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [] : labelLines(text);
66405
+ return raw.filter((l) => typeof l === "string").map(
66406
+ (l) => l.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase()
66407
+ ).filter((l) => l && l.length <= 100);
66408
+ }
66409
+ function labelLines(text) {
66410
+ const out = [];
66411
+ const lines = text.split(/\r?\n/);
66412
+ for (let i = 0; i < lines.length; i++) {
66413
+ const m = /^labels:[ \t]*([^#]*)/.exec(lines[i]);
66414
+ if (!m) continue;
66415
+ const inline = m[1].trim();
66416
+ if (inline) {
66417
+ out.push(...inline.replace(/^\[|\]$/g, "").split(","));
66418
+ } else {
66419
+ for (let j = i + 1; j < lines.length && /^\s*-\s/.test(lines[j]); j++)
66420
+ out.push(lines[j].replace(/^\s*-\s/, "").replace(/\s#.*$/, ""));
66421
+ }
66422
+ }
66423
+ return out;
66424
+ }
66425
+ var CALLERS = /* @__PURE__ */ new WeakMap();
66120
66426
  function triggerReach(wf, raw) {
66121
66427
  const on = onObject(wf, raw) ?? {};
66122
66428
  const keys = triggerKeys(wf, raw);
66123
- const firable = keys.filter((k) => keyStrangerFirable(on, k));
66429
+ const firable = keys.filter(
66430
+ (k) => keyStrangerFirable(on, k) || /^issues$/i.test(k) && testsAutoLabel(allIfText(wf))
66431
+ );
66124
66432
  const untrusted = firable.length > 0;
66125
66433
  const reusable = !untrusted && keys.some((k) => /^workflow_call$/i.test(k));
66434
+ const callers = CALLERS.get(wf);
66435
+ const open = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
66436
+ const unresolved = (callers ?? []).some(
66437
+ (c) => c.untrusted && !c.gated && c.dispatched || c.reusable && !c.gated
66438
+ );
66439
+ if (reusable && callers?.length && (open.length || !unresolved)) {
66440
+ return {
66441
+ keys: [...new Set(open.flatMap((c) => c.keys))],
66442
+ untrusted: open.length > 0,
66443
+ reusable: false,
66444
+ secretExposed: open.some((c) => c.secretExposed),
66445
+ privileged: open.some((c) => c.privileged),
66446
+ workflowRun: open.some((c) => c.workflowRun)
66447
+ };
66448
+ }
66126
66449
  const secretExposed = firable.some((t) => /pull_request_target|workflow_run/i.test(t));
66127
66450
  const privileged = untrusted && firable.some(
66128
66451
  (t) => /pull_request_target|pull_request_review|workflow_run|issue|discussion/i.test(t)
@@ -66133,6 +66456,160 @@ function triggerReach(wf, raw) {
66133
66456
  function jobList(wf) {
66134
66457
  return Object.values(wf.jobs ?? {}).filter((j) => j != null);
66135
66458
  }
66459
+ var ENV_REF_RE = /\$\{\{\s*env(?:\.([A-Za-z_][A-Za-z0-9_]{0,100})|\[\s*['"]([A-Za-z_][A-Za-z0-9_]{0,100})['"]\s*\])\s*\}\}/g;
66460
+ var ENV_REF_HOPS = 3;
66461
+ var RESOLVED_MAX_LEN = 1e5;
66462
+ function resolveEnvRefs(v, envs) {
66463
+ const holderOf = (name) => envs.find((e) => e != null && typeof e === "object" && name in e);
66464
+ for (let hop = 0; hop < ENV_REF_HOPS && v.includes("env"); hop++) {
66465
+ let projected = v.length;
66466
+ let any = false;
66467
+ for (const m of v.matchAll(ENV_REF_RE)) {
66468
+ const name = m[1] ?? m[2];
66469
+ const holder = holderOf(name);
66470
+ if (holder) {
66471
+ any = true;
66472
+ projected += str(holder[name]).length - m[0].length;
66473
+ }
66474
+ if (projected > RESOLVED_MAX_LEN) return v;
66475
+ }
66476
+ if (!any) break;
66477
+ v = v.replace(ENV_REF_RE, (whole, a, b) => {
66478
+ const name = a ?? b;
66479
+ const holder = holderOf(name);
66480
+ return holder ? str(holder[name]) : whole;
66481
+ });
66482
+ }
66483
+ return v;
66484
+ }
66485
+ function ifNeverTrue(v) {
66486
+ if (v === false) return true;
66487
+ if (typeof v !== "string") return false;
66488
+ const never = (x2) => {
66489
+ switch (x2.k) {
66490
+ case "atom":
66491
+ return /^false$/i.test(x2.t.trim());
66492
+ case "not":
66493
+ return false;
66494
+ case "and":
66495
+ return x2.c.some(never);
66496
+ case "or":
66497
+ return x2.c.every(never);
66498
+ }
66499
+ };
66500
+ const x = parseIf(v);
66501
+ return x !== null && never(x);
66502
+ }
66503
+ function normalizeWorkflow(raw) {
66504
+ const wf = raw;
66505
+ const wfEnv = raw.env;
66506
+ const jobs = wf.jobs;
66507
+ if (!jobs || typeof jobs !== "object") return wf;
66508
+ const wfEnvEvent = EVENT_CONTEXT_RE.test(str(wfEnv));
66509
+ for (const [name, job] of Object.entries(jobs)) {
66510
+ if (job == null || typeof job !== "object") continue;
66511
+ if (ifNeverTrue(job.if)) {
66512
+ delete jobs[name];
66513
+ continue;
66514
+ }
66515
+ if (!Array.isArray(job.steps)) continue;
66516
+ job.steps = job.steps.map(
66517
+ (st) => st != null && typeof st === "object" && !Array.isArray(st) ? { ...st } : st
66518
+ );
66519
+ const scopeEnv = {
66520
+ ...wfEnv && typeof wfEnv === "object" ? wfEnv : {},
66521
+ ...job.env && typeof job.env === "object" ? job.env : {}
66522
+ };
66523
+ const scopeEnvEvent = wfEnvEvent || EVENT_CONTEXT_RE.test(str(job.env));
66524
+ for (const step of job.steps) {
66525
+ if (step == null || typeof step !== "object") continue;
66526
+ step.scopeEnv = scopeEnv;
66527
+ step.scopeEnvEvent = scopeEnvEvent;
66528
+ if (step.with == null || typeof step.with !== "object") continue;
66529
+ const out = {};
66530
+ for (const [k, v] of Object.entries(step.with)) {
66531
+ const key = k.toLowerCase();
66532
+ if (key in out) continue;
66533
+ out[key] = typeof v === "string" ? resolveEnvRefs(v, [step.env, job.env, wfEnv]) : v;
66534
+ }
66535
+ step.with = out;
66536
+ }
66537
+ }
66538
+ return wf;
66539
+ }
66540
+ var LOADED = null;
66541
+ function loadWorkflow(content) {
66542
+ if (LOADED !== null && LOADED.content === content) return LOADED;
66543
+ const raw = parseYamlStrict(content) ?? {};
66544
+ const wf = raw && typeof raw === "object" ? normalizeWorkflow(raw) : raw;
66545
+ LOADED = { content, raw, wf };
66546
+ return LOADED;
66547
+ }
66548
+ var LOCAL_WORKFLOW_USES_RE = /^\s*\.\/\.github\/workflows\/([\w.-]{1,200}\.ya?ml)\s*$/i;
66549
+ function localCallsOf(path91, content, autoLabels) {
66550
+ AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
66551
+ try {
66552
+ return localCallsOfInner(path91, content);
66553
+ } finally {
66554
+ AUTO_LABELS = /* @__PURE__ */ new Set();
66555
+ }
66556
+ }
66557
+ function localCallsOfInner(path91, content) {
66558
+ let raw;
66559
+ try {
66560
+ raw = loadWorkflow(content).raw;
66561
+ } catch {
66562
+ return [];
66563
+ }
66564
+ if (!raw || typeof raw !== "object") return [];
66565
+ const wf = raw;
66566
+ CALLERS.delete(wf);
66567
+ const reach = triggerReach(wf, raw);
66568
+ const out = [];
66569
+ for (const job of jobList(wf)) {
66570
+ const m = LOCAL_WORKFLOW_USES_RE.exec(str(job.uses));
66571
+ if (!m) continue;
66572
+ out.push({
66573
+ target: `.github/workflows/${m[1]}`,
66574
+ caller: {
66575
+ file: path91,
66576
+ keys: reach.keys,
66577
+ untrusted: reach.untrusted,
66578
+ secretExposed: reach.secretExposed,
66579
+ privileged: reach.privileged,
66580
+ workflowRun: reach.workflowRun,
66581
+ gated: jobGated(job, wf, raw),
66582
+ dispatched: dispatchedByGatedJob(job, wf, raw),
66583
+ reusable: reach.reusable
66584
+ }
66585
+ });
66586
+ }
66587
+ return out;
66588
+ }
66589
+ function dispatchedByGatedJob(job, wf, raw) {
66590
+ const names = [...str(job.if).matchAll(/needs\.([\w-]{1,100})\.outputs\./g)].map((m) => m[1]);
66591
+ if (!names.length) return false;
66592
+ const jobs = wf.jobs ?? {};
66593
+ const label3 = labelTypeConfigured(wf, raw);
66594
+ const someBranchGated = (x) => {
66595
+ switch (x.k) {
66596
+ case "atom":
66597
+ return exprGated(x, label3);
66598
+ case "not":
66599
+ return false;
66600
+ case "and":
66601
+ case "or":
66602
+ return x.c.some(someBranchGated);
66603
+ }
66604
+ };
66605
+ return names.every((n) => {
66606
+ const up = jobs[n];
66607
+ if (up == null) return false;
66608
+ if (jobGated(up, wf, raw)) return true;
66609
+ const x = parseIf(str(up.if));
66610
+ return x !== null && someBranchGated(x);
66611
+ });
66612
+ }
66136
66613
  function allSteps(wf) {
66137
66614
  const out = [];
66138
66615
  for (const job of jobList(wf)) {
@@ -66140,11 +66617,47 @@ function allSteps(wf) {
66140
66617
  }
66141
66618
  return out;
66142
66619
  }
66620
+ var AGENT_CLI_RE = new RegExp(
66621
+ [
66622
+ // The binary at a command start, then its "do work" flag anywhere on the same command
66623
+ // (`claude --model x -p`, `claude "$Q" --print`, `npx @anthropic-ai/claude-code -p`): the
66624
+ // flag had to come first (review, round 2). Bounded: at most 300 characters per command.
66625
+ String.raw`(?:^|[\s|;&(])(?:claude|@anthropic-ai/claude-code(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--print|--continue|--resume)(?=[\s"'=]|$)`,
66626
+ String.raw`(?:^|[\s|;&(])(?:codex|@openai/codex(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\sexec\b`,
66627
+ String.raw`(?:^|[\s|;&(])(?:gemini|@google/gemini-cli(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt)(?=[\s"'=]|$)`,
66628
+ String.raw`(?:^|[\s|;&(])copilot(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt|--allow-all-tools)(?=[\s"'=]|$)`,
66629
+ String.raw`\b(?:aider\s|opencode\s+run\b|goose\s+run\b|kilo\s+run\b|pi\s+--print\b|llmproc\s|acpx\s|harbor\s+(?:check|run)\b)`,
66630
+ String.raw`api\.devin\.ai/|software-agent-sdk|\bagent_script\.py\b`
66631
+ ].join("|"),
66632
+ "i"
66633
+ );
66634
+ var RUN_FUEL_ENV_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|GEMINI_API_KEY|GOOGLE_API_KEY|KILO_API_KEY|OPENCODE_API_KEY|DEVIN_API_KEY|LLM_API_KEY|AWS_BEARER_TOKEN_BEDROCK)$/i;
66635
+ var EVENT_CONTEXT_RE = /github\.event|context\.payload/;
66636
+ function isRunAgent(step) {
66637
+ return typeof step.run === "string" && !step.uses && AGENT_CLI_RE.test(step.run);
66638
+ }
66639
+ var FUEL_WITH_KEY_RE = /^(anthropic[_-]api[_-]key|anthropic[_-]auth[_-]token|claude[_-]code[_-]oauth[_-]token|claude[_-]oauth[_-]token|openai[_-]api[_-]key|gemini[_-]api[_-]key|google[_-]api[_-]key)$/i;
66640
+ var AGENT_PATH_RE = /(?:^|[/_.-])(claude|codex|gemini|opencode|goose|aider|kilo(?:code)?)(?:$|[/_.-])/i;
66641
+ var PROMPT_INPUT_RE = /^(prompt|direct_prompt|prompt_file|prompt-file|agent)$/i;
66642
+ function isWrapperAgent(step) {
66643
+ const uses = str(step.uses);
66644
+ const w = step.with ?? {};
66645
+ if (Object.keys(w).some((k) => FUEL_WITH_KEY_RE.test(k))) return true;
66646
+ const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
66647
+ const fuelEnv = Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
66648
+ if (/^\s*\.\//.test(uses)) return fuelEnv;
66649
+ const pathPart = uses.replace(/^\s*[^/@\s]{1,100}\//, "").replace(/@.*$/, "");
66650
+ return AGENT_PATH_RE.test(pathPart) && (Object.keys(w).some((k) => PROMPT_INPUT_RE.test(k)) || fuelEnv);
66651
+ }
66143
66652
  function isAgentStep(step) {
66144
66653
  if (step.uses && AGENT_ACTION_RE.test(step.uses)) return true;
66145
- if (step.run && /\b(claude|aider|codex|gemini)\b/i.test(step.run) && /github\.event/.test(step.run))
66654
+ if (step.uses && isWrapperAgent(step)) return true;
66655
+ if (typeof step.run !== "string" || !step.run) return false;
66656
+ if (/\b(claude|aider|codex|gemini)\b/i.test(step.run) && /github\.event/.test(step.run))
66146
66657
  return true;
66147
- return false;
66658
+ if (!AGENT_CLI_RE.test(step.run)) return false;
66659
+ const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
66660
+ return !!step.scopeEnvEvent || EVENT_CONTEXT_RE.test(str(step.env)) || Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
66148
66661
  }
66149
66662
  function allowedToolsFromArgs(claudeArgs) {
66150
66663
  let out = "";
@@ -66164,12 +66677,47 @@ function collectTools(steps) {
66164
66677
  let s = "";
66165
66678
  for (const st of steps) {
66166
66679
  const w = st.with ?? {};
66167
- s += " " + allowedToolsFromArgs(str(w["claude_args"]));
66168
- s += " " + str(w["allowed_tools"]) + " " + str(w["allowedTools"]);
66680
+ const args = str(w["claude_args"]) + " " + str(w["codex-args"]) + " " + str(w["codex_args"]);
66681
+ s += " " + allowedToolsFromArgs(args);
66682
+ s += " " + str(w["allowed_tools"]) + " " + str(w["allowedtools"]);
66169
66683
  s += " " + allowedToolsFromSettings(str(w["settings"]));
66684
+ if (PERMISSION_BYPASS_RE.test(args) || /^danger-full-access$/i.test(str(w["sandbox"])))
66685
+ s += " Bash";
66686
+ if (st.run && isRunAgent(st)) {
66687
+ s += " " + allowedToolsFromArgs(st.run);
66688
+ if (PERMISSION_BYPASS_RE.test(st.run)) s += " Bash";
66689
+ s += " " + permissionEnvGrants({ ...st.scopeEnv ?? {}, ...st.env ?? {} });
66690
+ }
66170
66691
  }
66171
66692
  return s;
66172
66693
  }
66694
+ var PERMISSION_BYPASS_RE = /--dangerously-skip-permissions|--permission-mode[=\s]+["']?bypassPermissions|--dangerously-bypass-approvals-and-sandbox|--allow-all-tools|--yolo\b/i;
66695
+ function permissionEnvGrants(env) {
66696
+ let out = "";
66697
+ for (const key of ["OPENCODE_PERMISSION", "KILO_PERMISSION"]) {
66698
+ const raw = str(env[key]);
66699
+ if (!raw || raw.length > 2e4) continue;
66700
+ let cfg;
66701
+ try {
66702
+ cfg = JSON.parse(raw);
66703
+ } catch {
66704
+ continue;
66705
+ }
66706
+ if (!cfg || typeof cfg !== "object") continue;
66707
+ const c = cfg;
66708
+ const bash = c["bash"];
66709
+ if (bash === "allow") out += " Bash";
66710
+ else if (bash && typeof bash === "object") {
66711
+ for (const [pat, v] of Object.entries(bash)) {
66712
+ if (v !== "allow") continue;
66713
+ const cmd = pat.replace(/\*+\s*$/, "").trim();
66714
+ out += cmd ? ` Bash(${cmd}:*)` : " Bash";
66715
+ }
66716
+ }
66717
+ if (c["webfetch"] === "allow") out += " WebFetch";
66718
+ }
66719
+ return out;
66720
+ }
66173
66721
  function toolTokens(blob) {
66174
66722
  const out = [];
66175
66723
  let buf = "";
@@ -66202,11 +66750,21 @@ function matchedBroadTools(blob) {
66202
66750
  }
66203
66751
  return out;
66204
66752
  }
66753
+ var GH_PR_CHECKOUT_RE = /\bgh\s+pr\s+checkout\b/;
66754
+ var FETCH_PULL_RE = /\bgit\s+fetch\b[^\n]{0,200}\bpull\/[^\s/]{1,80}\/(?:head|merge)\b/;
66755
+ var GIT_CHECKOUT_RE = /\bgit\s+(?:checkout|switch)\b/;
66756
+ function runChecksOutHead(run) {
66757
+ if (GH_PR_CHECKOUT_RE.test(run)) return true;
66758
+ const m = FETCH_PULL_RE.exec(run);
66759
+ return m !== null && GIT_CHECKOUT_RE.test(run.slice(m.index));
66760
+ }
66205
66761
  function untrustedHeadCheckout(steps) {
66206
66762
  for (const step of steps) {
66763
+ if (!step.uses && typeof step.run === "string" && runChecksOutHead(step.run)) return "root";
66207
66764
  if (!step.uses || !/actions\/checkout/.test(step.uses)) continue;
66208
66765
  const ref = str(step.with?.["ref"]);
66209
- if (/pull_request\.head|head[._]sha|head_ref|expected_head|workflow_run\.head|inputs\.[\w]*head/i.test(
66766
+ if (/refs\/pull\//.test(ref)) return step.with?.["path"] ? "subdir" : "root";
66767
+ if (/pull_request\.head|head[._-]sha|head_ref|expected_head|workflow_run\.head|inputs(?:\.|\[\s*['"])[\w-]*head/i.test(
66210
66768
  ref
66211
66769
  )) {
66212
66770
  return step.with?.["path"] ? "subdir" : "root";
@@ -66216,22 +66774,61 @@ function untrustedHeadCheckout(steps) {
66216
66774
  }
66217
66775
  function promptTakesUntrusted(steps) {
66218
66776
  for (const st of steps) {
66219
- const blob = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + collectTools([st]);
66777
+ const blob = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + collectTools([st]) + // T.2a: a run-step agent's prompt is its command line and the env it reads.
66778
+ (isRunAgent(st) ? " " + st.run + " " + str(st.env) + " " + str(st.scopeEnv) : "");
66220
66779
  if (/github\.event\.(issue|comment|pull_request|review)\.(body|title)/i.test(blob)) return true;
66221
- if (/pull_request\.number|\/code-review|\/review/i.test(blob)) return true;
66780
+ if (/pull_request\.number|(?<![\w./-])\/(?:code-)?review(?![\w-])/i.test(blob)) return true;
66222
66781
  }
66223
66782
  return false;
66224
66783
  }
66225
66784
  var ITEM_READ_TOOL_RE = /mcp__github__(get_issue|get_issue_comments|get_pull_request|get_pull_request_diff|get_pull_request_files|get_pull_request_comments|get_pull_request_reviews|search_issues|list_issues)\b|Bash\(\s*gh\s+(issue|pr)\s+(view|diff|list)\b|Bash\(\s*gh\s+(api|search)\b|Bash\(\s*gh\s*:/i;
66226
66785
  var ITEM_HANDLE_RE = /github\.event\.(issue|pull_request|discussion)\.number|github\.event\.number\b|github\.event\.comment\.id/i;
66227
66786
  var PAYLOAD_TEXT_RE = /github\.event\.(issue|comment|pull_request|review|discussion)\.(body|title)|context\.payload\.(issue|comment|pull_request|review|discussion)\b|GITHUB_EVENT_PATH|github\.event_path/i;
66787
+ var RUN_ARTIFACT_RE = /gh\s+run\s+download\b|\/runs\/\$\{\{\s*github\.event\.workflow_run\.id\s*\}\}\/(?:artifacts|logs)/i;
66788
+ var TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
66789
+ var UPSTREAM_TRUSTED = false;
66790
+ function upstreamRunTrusted(wf, raw) {
66791
+ const on = onObject(wf, raw);
66792
+ const run = on?.["workflow_run"];
66793
+ const names = Array.isArray(run?.workflows) ? run.workflows.map(String) : [];
66794
+ return names.length > 0 && names.every((n) => TRUSTED_RUN_SOURCES.has(n));
66795
+ }
66796
+ function runSourceOf(path91, content, autoLabels) {
66797
+ AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
66798
+ try {
66799
+ let raw;
66800
+ try {
66801
+ raw = loadWorkflow(content).raw;
66802
+ } catch {
66803
+ return null;
66804
+ }
66805
+ if (!raw || typeof raw !== "object") return null;
66806
+ const wf = raw;
66807
+ CALLERS.delete(wf);
66808
+ const reach = triggerReach(wf, raw);
66809
+ const name = typeof raw["name"] === "string" && raw["name"] ? raw["name"] : path91;
66810
+ return { name, trusted: !reach.untrusted && !reach.reusable };
66811
+ } finally {
66812
+ AUTO_LABELS = /* @__PURE__ */ new Set();
66813
+ }
66814
+ }
66815
+ function downloadsRunArtifacts(st) {
66816
+ if (UPSTREAM_TRUSTED) return false;
66817
+ if (/actions\/download-artifact/i.test(str(st.uses)))
66818
+ return /workflow_run\.id/.test(str(st.with?.["run-id"]) + " " + str(st.with?.["run_id"]));
66819
+ return RUN_ARTIFACT_RE.test(str(st.run) + " " + str(st.with?.["script"]));
66820
+ }
66821
+ var STEP_OUTPUT_REF_RE = /steps\.([\w-]{1,100})\.outputs\./g;
66228
66822
  function jobReadsUntrustedItem(job) {
66229
66823
  const steps = job.steps ?? [];
66230
66824
  const agent = steps.filter(isAgentStep);
66231
66825
  if (!agent.length) return null;
66232
66826
  if (ITEM_READ_TOOL_RE.test(collectTools(agent))) return "tool";
66233
66827
  for (const st of agent)
66234
- if (ITEM_HANDLE_RE.test(str(st.with) + " " + str(st.env) + " " + str(job.env))) return "handle";
66828
+ if (ITEM_HANDLE_RE.test(
66829
+ str(st.with) + " " + str(st.env) + " " + str(job.env) + " " + (isRunAgent(st) ? str(st.run) : "")
66830
+ ))
66831
+ return "handle";
66235
66832
  for (let i = 0; i < steps.length; i++) {
66236
66833
  const st = steps[i];
66237
66834
  if (!isAgentStep(st) || !str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"])) continue;
@@ -66239,6 +66836,20 @@ function jobReadsUntrustedItem(job) {
66239
66836
  if (earlier.some((p) => PAYLOAD_TEXT_RE.test(str(p.run) + " " + str(p.with) + " " + str(p.env))))
66240
66837
  return "prompt-file";
66241
66838
  }
66839
+ const firstDownload = steps.findIndex(downloadsRunArtifacts);
66840
+ if (firstDownload >= 0 && steps.slice(firstDownload + 1).some(isAgentStep)) return "artifact";
66841
+ for (let i = 0; i < steps.length; i++) {
66842
+ const st = steps[i];
66843
+ if (!isAgentStep(st)) continue;
66844
+ const input = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"]);
66845
+ const ids = /* @__PURE__ */ new Set();
66846
+ for (const m of input.matchAll(STEP_OUTPUT_REF_RE)) ids.add(m[1]);
66847
+ for (const id of ids) {
66848
+ const src = steps.slice(0, i).find((p) => p.id === id);
66849
+ if (src && PAYLOAD_TEXT_RE.test(str(src.run) + " " + str(src.with) + " " + str(src.env)))
66850
+ return "step-output";
66851
+ }
66852
+ }
66242
66853
  return null;
66243
66854
  }
66244
66855
  var NONCONTAINS_GATE_RE = new RegExp(
@@ -66251,19 +66862,46 @@ var NONCONTAINS_GATE_RE = new RegExp(
66251
66862
  ].join("|"),
66252
66863
  "i"
66253
66864
  );
66254
- var PERMISSION_OUTPUT_GATE_RE = /steps\.[\w-]+\.outputs\.[\w-]*(permission|allowed|authoriz|is[_-]?(admin|member|maintainer|collaborator))[\w-]*\s*==\s*['"]?(true|admin|write|maintain)/i;
66865
+ var PERMISSION_OUTPUT_RE = /steps\.[\w-]{1,100}\.outputs\.([\w-]{1,200})\s*==\s*['"]?(?:true|admin|write|maintain)/gi;
66866
+ var PERMISSION_OUTPUT_NAME_RE = /permission|allowed|authoriz|is[_-]?(?:admin|member|maintainer|collaborator)/i;
66867
+ var PERMISSION_OUTPUT_GATE_RE = {
66868
+ test(ifs) {
66869
+ for (const m of ifs.matchAll(PERMISSION_OUTPUT_RE))
66870
+ if (PERMISSION_OUTPUT_NAME_RE.test(m[1])) return true;
66871
+ return false;
66872
+ }
66873
+ };
66255
66874
  var CONTAINS_GATE_RE = /contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
66256
66875
  var NEGATED_CONTAINS_RE = /!\s*\(?\s*contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
66257
66876
  function labelTypeConfigured(wf, raw) {
66258
66877
  const on = wf.on ?? raw["on"] ?? raw[true];
66259
66878
  const prTypes = (t) => t && Array.isArray(t.types) ? t.types.map(String) : [];
66260
- return prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
66879
+ const pr = prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
66880
+ const onObj = onObject(wf, raw) ?? {};
66881
+ const issues = on != null && typeof on === "object" && "issues" in on && triggerKeys(wf, raw).every((k) => /^issues$/i.test(k) || !keyStrangerFirable(onObj, k));
66882
+ return { pr, issues };
66883
+ }
66884
+ var EVENT_LABEL_GATE_RE = /\blabel\.name\s*==\s*'|\b(?:contains|startsWith)\(\s*github\.event\.label\.name\b/i;
66885
+ var ISSUE_LABEL_GATE_RE = /(?<!!\s{0,5}\(?\s{0,5})contains\(\s*github\.event\.issue\.labels\.\*\.name\s*,\s*'[^']{1,100}'\s*\)(?!\s*(?:==\s*false|!=\s*true))/i;
66886
+ var PR_LABEL_GATE_RE = /(?<!!\s{0,5}\(?\s{0,5})contains\(\s*github\.event\.(?:pull_request|issue)\.labels\.\*\.name\s*,\s*'[^']{1,100}'\s*\)(?!\s*(?:==\s*false|!=\s*true))/i;
66887
+ var NONWRITE_SET_RE = /contains\(\s*(?:fromjson\(\s*['"]\[[^\]]{0,400}|'[^']{0,400})\b(CONTRIBUTOR|FIRST_TIME_CONTRIBUTOR|FIRST_TIMER|NONE|MANNEQUIN)\b/i;
66888
+ function allIfText(wf) {
66889
+ const s = jobList(wf).map((j) => str(j.if) + " " + (j.steps ?? []).map((st) => str(st.if)).join(" ")).join(" ");
66890
+ return s.length > IF_MAX_LEN ? "" : s;
66891
+ }
66892
+ function testsAutoLabel(ifs) {
66893
+ if (AUTO_LABELS.size === 0) return false;
66894
+ for (const m of ifs.matchAll(
66895
+ /labels\.\*\.name\s*,\s*'([^']{1,100})'|label\.name\s*==\s*'([^']{1,100})'/gi
66896
+ ))
66897
+ if (AUTO_LABELS.has((m[1] ?? m[2]).toLowerCase())) return true;
66898
+ return false;
66261
66899
  }
66262
- var PR_LABEL_GATE_RE = /(?<!!\s{0,5}\(?\s{0,5})contains\(\s*github\.event\.pull_request\.labels\.\*\.name\s*,\s*'[^']{1,100}'\s*\)(?!\s*(?:==\s*false|!=\s*true))/i;
66263
- function ifsAreGated(ifs, labelConfigured) {
66264
- const containsGate = CONTAINS_GATE_RE.test(ifs) && !NEGATED_CONTAINS_RE.test(ifs);
66265
- const gated = NONCONTAINS_GATE_RE.test(ifs) || containsGate;
66266
- const labelGated = labelConfigured && (/event\.label|label\.name/i.test(ifs) || PR_LABEL_GATE_RE.test(ifs));
66900
+ function ifsAreGated(ifs, label3) {
66901
+ const m = maskLiterals(ifs);
66902
+ const containsGate = CONTAINS_GATE_RE.test(m) && !NEGATED_CONTAINS_RE.test(m) && !NONWRITE_SET_RE.test(ifs);
66903
+ const gated = NONCONTAINS_GATE_RE.test(m) || containsGate;
66904
+ const labelGated = ((label3.pr || label3.issues) && EVENT_LABEL_GATE_RE.test(m) || label3.pr && PR_LABEL_GATE_RE.test(m) || label3.issues && ISSUE_LABEL_GATE_RE.test(m)) && !testsAutoLabel(ifs);
66267
66905
  return gated || labelGated;
66268
66906
  }
66269
66907
  function hasStepMembershipGate(job) {
@@ -66284,6 +66922,7 @@ function parseIf(src) {
66284
66922
  let e = src.trim();
66285
66923
  const wrapped = /^\$\{\{([\s\S]*)\}\}$/.exec(e);
66286
66924
  if (wrapped) e = wrapped[1].trim();
66925
+ if (e.includes("${{")) return null;
66287
66926
  if (!e || e.length > IF_MAX_LEN) return null;
66288
66927
  let i = 0;
66289
66928
  let depth = 0;
@@ -66369,12 +67008,21 @@ function stepIfText(steps) {
66369
67008
  const s = steps.map((st) => str(st.if)).join(" ");
66370
67009
  return s.length > IF_MAX_LEN ? "" : s;
66371
67010
  }
67011
+ function maskLiterals(ifs) {
67012
+ return ifs.replace(
67013
+ /(==|!=)?(\s*)'(?:[^']|'')*'/g,
67014
+ (m, op, sp) => op ? m : `${sp}'_'`
67015
+ );
67016
+ }
67017
+ var CHAINED_COMPARE_RE = /(?:==|!=)[^=!]*(?:==|!=)/;
66372
67018
  function gatedText(ifs, labelConfigured) {
66373
- return ifsAreGated(ifs, labelConfigured) || ASSIGNEE_GATE_RE.test(ifs) || PERMISSION_OUTPUT_GATE_RE.test(ifs);
67019
+ const masked = maskLiterals(ifs);
67020
+ return ifsAreGated(ifs, labelConfigured) || ASSIGNEE_GATE_RE.test(masked) || PERMISSION_OUTPUT_GATE_RE.test(masked);
66374
67021
  }
66375
67022
  function exprGated(x, labelConfigured) {
66376
67023
  switch (x.k) {
66377
67024
  case "atom":
67025
+ if (CHAINED_COMPARE_RE.test(maskLiterals(x.t))) return false;
66378
67026
  return gatedText(x.t, labelConfigured) || TRUSTED_EVENT_ATOM_RE.test(x.t);
66379
67027
  case "not":
66380
67028
  return false;
@@ -66410,11 +67058,43 @@ function jobOnWorkflowRun(job, workflowRun) {
66410
67058
  }
66411
67059
  function jobActorGate(job, wf, raw) {
66412
67060
  const labelConfigured = labelTypeConfigured(wf, raw);
66413
- const stepIfs = stepIfText(job.steps ?? []);
66414
- return jobIfGated(str(job.if), labelConfigured) || gatedText(stepIfs, labelConfigured) || hasStepMembershipGate(job);
67061
+ const agent = (job.steps ?? []).filter(isAgentStep);
67062
+ const ifTotal = agent.reduce((n, s) => n + str(s.if).length, 0);
67063
+ const agentStepsGated = agent.length > 0 && ifTotal <= IF_MAX_LEN && agent.every((s) => {
67064
+ const x = parseIf(str(s.if));
67065
+ return x !== null && exprGated(x, labelConfigured);
67066
+ });
67067
+ return jobIfGated(str(job.if), labelConfigured) || agentStepsGated || hasStepMembershipGate(job);
67068
+ }
67069
+ var ACTOR_ALLOWLIST_RE = /\.includes\(\s*(?:context\.actor|github\.actor|context\.payload\.sender\.login|[\w.]{0,40}user\.login|login)\b|\b(?:context\.actor|github\.actor)\s*===?\s*['"]/;
67070
+ var ACTOR_ALLOWLIST_G = new RegExp(ACTOR_ALLOWLIST_RE.source, "g");
67071
+ var BLOCK_LIST_NAME_RE = /block|deny|ban|spam|abuse|exclu|ignore/i;
67072
+ function scriptAllowsByActor(code2) {
67073
+ if (code2.length > 2e5) return false;
67074
+ for (const m of code2.matchAll(ACTOR_ALLOWLIST_G)) {
67075
+ const at = m.index ?? 0;
67076
+ const before = code2.slice(Math.max(0, at - 80), at);
67077
+ if (m[0].startsWith(".includes")) {
67078
+ const list = /([\w$.]{1,60})$/.exec(before)?.[1] ?? "";
67079
+ if (BLOCK_LIST_NAME_RE.test(list)) continue;
67080
+ }
67081
+ const negated = /!\s{0,3}\(?\s{0,3}[\w$.]{0,60}$/.test(before);
67082
+ const ternary = /^[^?;\n]{0,150}\?\s{0,5}['"`]?(true|false)\b/.exec(code2.slice(at, at + 200));
67083
+ if (!ternary || ternary[1] === "true" !== negated) return true;
67084
+ }
67085
+ return false;
67086
+ }
67087
+ function readsOutputPositively(readerIf, name) {
67088
+ const ref = `needs\\.${escapeRe(name)}\\.outputs\\.[\\w-]{1,100}`;
67089
+ if (!new RegExp(ref).test(readerIf)) return false;
67090
+ const no = `'(?:false|no|0|)'(?![\\w'])`;
67091
+ return !new RegExp(
67092
+ `!\\s{0,5}\\(?\\s{0,5}(?:fromjson\\(\\s{0,5})?${ref}|${ref}\\s{0,5}\\)?\\s{0,5}(?:!=\\s{0,5}(?!${no})|==\\s{0,5}${no})`,
67093
+ "i"
67094
+ ).test(readerIf);
66415
67095
  }
66416
67096
  var STATUS_FN_RE = /\b(success|failure|always|cancelled)\s*\(/i;
66417
- var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]/;
67097
+ var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]|actions\.cancelWorkflowRun\b/;
66418
67098
  function needsOf(job) {
66419
67099
  return (Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []).map(String);
66420
67100
  }
@@ -66426,13 +67106,18 @@ function upstreamJobGates(name, dep, wf, raw, readerIf) {
66426
67106
  const label3 = labelTypeConfigured(wf, raw);
66427
67107
  if (depIf && jobIfGated(depIf, label3)) return true;
66428
67108
  const steps = dep.steps ?? [];
66429
- const stepGated = ifsAreGated(stepIfText(steps), label3) || hasStepMembershipGate(dep);
67109
+ if (readerIf !== null && readsOutputPositively(readerIf, name) && steps.some((st) => {
67110
+ const code2 = str(st.run) + " " + str(st.with?.["script"]);
67111
+ return MEMBERSHIP_CHECK_RE.test(code2) || scriptAllowsByActor(code2);
67112
+ }))
67113
+ return true;
67114
+ const stepGated = ifsAreGated(stepIfText(steps), label3) || hasStepMembershipGate(dep) || steps.some((st) => MEMBERSHIP_CHECK_RE.test(str(st.with?.["script"])));
66430
67115
  if (!stepGated) return false;
66431
67116
  if (steps.some(
66432
67117
  (st) => FAILING_STEP_RE.test(str(st.run)) || FAILING_STEP_RE.test(str(st.with?.["script"]))
66433
67118
  ))
66434
67119
  return true;
66435
- return readerIf !== null && new RegExp(`needs\\.${escapeRe(name)}\\.outputs\\.`).test(readerIf);
67120
+ return readerIf !== null && readsOutputPositively(readerIf, name);
66436
67121
  }
66437
67122
  function gatingAncestors(job, wf, raw) {
66438
67123
  const out = /* @__PURE__ */ new Set();
@@ -66462,9 +67147,15 @@ function jobGated(job, wf, raw) {
66462
67147
  }
66463
67148
  function bypassArmed(steps) {
66464
67149
  return steps.some(
66465
- (s) => str(s.with?.["allowed_non_write_users"]) === "*" && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
67150
+ (s) => nonWriteOpen(str(s.with?.["allowed_non_write_users"])) && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
66466
67151
  );
66467
67152
  }
67153
+ var ACTOR_BOUND_RE = /\$\{\{[^}]{0,200}\b(github\.event\.[\w.]{0,80}\b(?:user|sender)\.login|github\.event\.sender\.login|github\.actor|github\.triggering_actor)\b/i;
67154
+ function nonWriteOpen(v) {
67155
+ const t = v.trim();
67156
+ if (t === "*") return true;
67157
+ return t.length <= 1e3 && ACTOR_BOUND_RE.test(t);
67158
+ }
66468
67159
  function hasImplicitActorGate(agentSteps, bypassActive, workflowRun = false) {
66469
67160
  if (bypassActive) return false;
66470
67161
  return agentSteps.some((s) => {
@@ -66583,14 +67274,28 @@ function severityFromScore(score) {
66583
67274
  if (score >= 1) return "advisory";
66584
67275
  return null;
66585
67276
  }
66586
- function analyzeWorkflow(path91, content) {
67277
+ function analyzeWorkflow(path91, content, callers, autoLabels, trustedRunSources) {
67278
+ AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
67279
+ TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
67280
+ try {
67281
+ return analyzeWorkflowInner(path91, content, callers);
67282
+ } finally {
67283
+ AUTO_LABELS = /* @__PURE__ */ new Set();
67284
+ TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
67285
+ UPSTREAM_TRUSTED = false;
67286
+ }
67287
+ }
67288
+ function analyzeWorkflowInner(path91, content, callers) {
66587
67289
  let raw;
66588
67290
  try {
66589
- raw = parseYamlStrict(content) ?? {};
67291
+ raw = loadWorkflow(content).raw;
66590
67292
  } catch {
66591
67293
  return null;
66592
67294
  }
66593
67295
  const wf = raw;
67296
+ if (callers?.length) CALLERS.set(wf, callers);
67297
+ else CALLERS.delete(wf);
67298
+ UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
66594
67299
  const steps = allSteps(wf).map((s) => s.step);
66595
67300
  const agentSteps = steps.filter(isAgentStep);
66596
67301
  const agentJobs = [
@@ -66609,17 +67314,19 @@ function analyzeWorkflow(path91, content) {
66609
67314
  } = triggerReach(wf, raw);
66610
67315
  const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
66611
67316
  const nonWriteStar = bypassArmed(agentSteps);
66612
- const starWithoutToken = nonWrite === "*" && !nonWriteStar;
66613
- const nonWriteList = !!nonWrite && nonWrite !== "*";
67317
+ const starWithoutToken = nonWriteOpen(nonWrite) && !nonWriteStar;
67318
+ const nonWriteList = !!nonWrite && !nonWriteOpen(nonWrite);
67319
+ const nonWriteActor = nonWrite.trim() !== "*" && nonWriteOpen(nonWrite);
66614
67320
  const untrustedTrigger = forkInput || reusable;
66615
67321
  const bypassActive = nonWriteStar && untrustedTrigger;
66616
- const head = untrustedHeadCheckout(steps);
67322
+ const head = agentSteps.length && agentSteps.every(readOnlySandbox) ? null : untrustedHeadCheckout(steps);
66617
67323
  const promptUntrusted = promptTakesUntrusted(agentSteps);
66618
67324
  const injJobs = injectableJobs(wf, raw, untrustedTrigger);
66619
67325
  const powerJobs = injJobs.length ? injJobs : agentJobs;
66620
67326
  const powerSteps = injJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep));
66621
67327
  const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
66622
- const toolsBlob = collectTools(scopedSteps);
67328
+ const headActionRuns = head === "root" && powerSteps.some((st) => /^\s*\.\//.test(str(st.uses)));
67329
+ const toolsBlob = collectTools(scopedSteps) + (headActionRuns ? " Bash" : "");
66623
67330
  const itemRead = forkInput ? injJobs.map(jobReadsUntrustedItem).find(Boolean) ?? null : null;
66624
67331
  const reach = untrustedTrigger && injJobs.length ? Math.max(
66625
67332
  head === "root" ? 3 : head === "subdir" ? 1 : 0,
@@ -66667,20 +67374,32 @@ function analyzeWorkflow(path91, content) {
66667
67374
  severity = "medium";
66668
67375
  if (!severity) severity = "advisory";
66669
67376
  const signals = [];
67377
+ const openCallers = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
67378
+ if (openCallers.length)
67379
+ signals.push(
67380
+ `called by ${openCallers.map((c) => `${safeText(c.file, 120)} (${c.keys.map((k) => safeText(k, 40)).join(", ")})`).join("; ")}`
67381
+ );
66670
67382
  if (secretExposed)
66671
67383
  signals.push(
66672
- `runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).join(", ")})`
67384
+ `runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).map((t) => safeText(t, 40)).join(", ")})`
67385
+ );
67386
+ else if (forkInput)
67387
+ signals.push(
67388
+ `triggered by untrusted input (${triggers.map((t) => safeText(t, 40)).join(", ")})`
66673
67389
  );
66674
- else if (forkInput) signals.push(`triggered by untrusted input (${triggers.join(", ")})`);
66675
67390
  else if (reusable)
66676
67391
  signals.push(
66677
- reusableLoadedGun ? `reusable workflow (${triggers.join(", ")}) that checks out an untrusted head / ingests untrusted input: exploitable the moment a caller wires a fork trigger (reachability depends on the caller, but this workflow is built to process attacker input)` : `reusable workflow (${triggers.join(", ")}): no untrusted trigger of its own; reachability depends on the caller's trigger + actor gate`
67392
+ reusableLoadedGun ? `reusable workflow (${triggers.map((t) => safeText(t, 40)).join(", ")}) that checks out an untrusted head / ingests untrusted input: exploitable the moment a caller wires a fork trigger (reachability depends on the caller, but this workflow is built to process attacker input)` : `reusable workflow (${triggers.map((t) => safeText(t, 40)).join(", ")}): no untrusted trigger of its own; reachability depends on the caller's trigger + actor gate`
66678
67393
  );
66679
67394
  if (forkInput && !privileged && !reusable)
66680
67395
  signals.push(
66681
67396
  "triggered by `pull_request`: fork PRs run with a read-only token (lower risk than pull_request_target)"
66682
67397
  );
66683
67398
  if (head === "root") signals.push("checks out the untrusted PR head into the workspace root");
67399
+ if (headActionRuns)
67400
+ signals.push(
67401
+ "runs the checked-out head's own action (`uses: ./\u2026`): the PR author's code runs with the job's secrets"
67402
+ );
66684
67403
  if (head === "subdir") signals.push("checks out the untrusted PR head into an isolated subdir");
66685
67404
  if (workflowRunJobs.length) {
66686
67405
  const builds = workflowRunJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep)).map((st) => claudeActionBuild(st.uses)).filter((b) => b !== null);
@@ -66702,7 +67421,7 @@ function analyzeWorkflow(path91, content) {
66702
67421
  if (promptUntrusted) signals.push("feeds untrusted PR/issue text to the agent");
66703
67422
  else if (itemRead)
66704
67423
  signals.push(
66705
- itemRead === "prompt-file" ? "an earlier step writes untrusted PR/issue text into the prompt file the agent reads" : itemRead === "tool" ? "the agent fetches PR/issue text itself with a read tool (the text never appears in the prompt)" : "the agent is given the PR/issue number and reads the text itself (the text never appears in the prompt)"
67424
+ itemRead === "prompt-file" ? "an earlier step writes untrusted PR/issue text into the prompt file the agent reads" : itemRead === "artifact" ? "the job downloads the artifacts of the triggering run (produced by the PR) before the agent runs" : itemRead === "step-output" ? "the agent's prompt is an earlier step's output, built from untrusted PR/issue text" : itemRead === "tool" ? "the agent fetches PR/issue text itself with a read tool (the text never appears in the prompt)" : "the agent is given the PR/issue number and reads the text itself (the text never appears in the prompt)"
66706
67425
  );
66707
67426
  if (broadTools) {
66708
67427
  const names = matchedBroadTools(toolsBlob);
@@ -66712,7 +67431,7 @@ function analyzeWorkflow(path91, content) {
66712
67431
  }
66713
67432
  if (bypassActive)
66714
67433
  signals.push(
66715
- 'allowed_non_write_users: "*" with github_token, so any user can trigger the agent'
67434
+ nonWriteActor ? "allowed_non_write_users is the triggering user's own login, with github_token, so any user can trigger the agent" : 'allowed_non_write_users: "*" with github_token, so any user can trigger the agent'
66716
67435
  );
66717
67436
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
66718
67437
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
@@ -66806,7 +67525,7 @@ function analyzeWorkflow(path91, content) {
66806
67525
  return finding;
66807
67526
  }
66808
67527
  var AGENT_FUEL_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|OPENAI_BASE_URL|GEMINI_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN)$/i;
66809
- var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_url|claude_code_oauth_token|openai_api_key|openai_base_url|gemini_api_key|google_api_key)$/i;
67528
+ var FUEL_INPUT_KEYS = /^(anthropic[_-]api[_-]key|anthropic[_-]auth[_-]token|anthropic[_-]base[_-]url|claude[_-]code[_-]oauth[_-]token|claude[_-]oauth[_-]token|openai[_-]api[_-]key|openai[_-]base[_-]url|gemini[_-]api[_-]key|google[_-]api[_-]key)$/i;
66810
67529
  function fuelSecretNames(agentSteps) {
66811
67530
  const out = /* @__PURE__ */ new Set();
66812
67531
  const add = (v) => {
@@ -66879,14 +67598,28 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
66879
67598
  if (reusable && !loadedGun && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
66880
67599
  return { severity, secrets, injectable, canReadEnv };
66881
67600
  }
66882
- function analyzeWorkflowSecrets(path91, content) {
67601
+ function analyzeWorkflowSecrets(path91, content, callers, autoLabels, trustedRunSources) {
67602
+ AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
67603
+ TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
67604
+ try {
67605
+ return analyzeWorkflowSecretsInner(path91, content, callers);
67606
+ } finally {
67607
+ AUTO_LABELS = /* @__PURE__ */ new Set();
67608
+ TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
67609
+ UPSTREAM_TRUSTED = false;
67610
+ }
67611
+ }
67612
+ function analyzeWorkflowSecretsInner(path91, content, callers) {
66883
67613
  let raw;
66884
67614
  try {
66885
- raw = parseYamlStrict(content) ?? {};
67615
+ raw = loadWorkflow(content).raw;
66886
67616
  } catch {
66887
67617
  return null;
66888
67618
  }
66889
67619
  const wf = raw;
67620
+ if (callers?.length) CALLERS.set(wf, callers);
67621
+ else CALLERS.delete(wf);
67622
+ UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
66890
67623
  const all = allSteps(wf);
66891
67624
  if (!all.some((s) => isAgentStep(s.step))) return null;
66892
67625
  const { untrusted: forkInput, reusable } = triggerReach(wf, raw);
@@ -66918,70 +67651,281 @@ function analyzeWorkflowSecrets(path91, content) {
66918
67651
  })
66919
67652
  };
66920
67653
  }
66921
-
66922
- // src/ci-check/agent-config.ts
66923
- import path76 from "path";
66924
-
66925
- // src/ci-check/frontmatter.ts
66926
- function parseFrontmatter(content) {
66927
- const text = content.replace(/^/, "").replace(/^\s*\n/, "");
66928
- if (!/^---[ \t]*\r?\n/.test(text)) return null;
66929
- const close = /\r?\n---[ \t]*(\r?\n|$)/.exec(text.slice(3));
66930
- if (!close) return null;
66931
- const block = text.slice(3, 3 + close.index);
67654
+ var COMMIT_PIN_RE = /[#@][0-9a-f]{40}\b/i;
67655
+ function marketplaceOwner(market) {
67656
+ if (/(^|[/\\])\.\.([/\\]|$)/.test(market)) return void 0;
67657
+ return /^(?:https?:\/\/(?:www\.)?|git@|ssh:\/\/git@)?github\.com[:/]([\w.-]{1,100})\/[\w.-]{1,100}(?:\.git)?\/?(?:[#@].*)?$/i.exec(
67658
+ market
67659
+ )?.[1] ?? /^([\w.-]{1,100})\/[\w.-]{1,100}(?:[#@].*)?$/.exec(market)?.[1];
67660
+ }
67661
+ function analyzeWorkflowPlugins(path91, content, owner) {
67662
+ let raw;
66932
67663
  try {
66933
- const parsed = parseYamlStrict(block);
66934
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
67664
+ raw = loadWorkflow(content).raw;
66935
67665
  } catch {
66936
- return salvageTopLevel(block);
67666
+ return [];
67667
+ }
67668
+ if (!raw || typeof raw !== "object") return [];
67669
+ const wf = raw;
67670
+ const out = [];
67671
+ const seen = /* @__PURE__ */ new Set();
67672
+ for (const { step } of allSteps(wf)) {
67673
+ if (!claudeActionBuild(step.uses)) continue;
67674
+ if (!str(step.with?.["plugins"]).trim()) continue;
67675
+ for (const m of str(step.with?.["plugin_marketplaces"]).split(/[\s,]+/)) {
67676
+ const market = m.trim();
67677
+ if (!market || COMMIT_PIN_RE.test(market)) continue;
67678
+ if (/^\.{1,2}(\/|$)/.test(market)) continue;
67679
+ const mo = marketplaceOwner(market);
67680
+ if (mo && mo.toLowerCase() === "anthropics") continue;
67681
+ if (owner && mo && mo.toLowerCase() === owner.toLowerCase()) continue;
67682
+ if (seen.has(market)) continue;
67683
+ seen.add(market);
67684
+ out.push({
67685
+ check: "CI-3",
67686
+ rule: "CI-3.plugin-unpinned",
67687
+ locator: safeText(market, 200),
67688
+ dimension: "mcp",
67689
+ severity: "medium",
67690
+ title: "Agent installs plugins from a third-party marketplace on every run",
67691
+ file: path91,
67692
+ signals: [
67693
+ `\`plugin_marketplaces: ${safeText(market, 120)}\` is not pinned to a commit: whoever controls it changes the code and prompts the agent runs`
67694
+ ],
67695
+ fix: "Pin the marketplace to a commit (`<url>#<sha>`), or vendor the plugin into this repository."
67696
+ });
67697
+ }
66937
67698
  }
67699
+ return out;
66938
67700
  }
66939
- function salvageTopLevel(block) {
66940
- const out = {};
66941
- const lines = block.split(/\r?\n/);
66942
- for (let i = 0; i < lines.length; i++) {
66943
- const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]);
66944
- if (!m) continue;
66945
- const [, key, rest] = m;
66946
- if (rest.trim()) {
66947
- out[key] = rest.trim();
66948
- continue;
67701
+ var ITEM_LIST_RE = /gh\s+(?:issue|pr)\s+(?:list|view)\b|gh\s+search\b|gh\s+api\s+[^\n]{0,80}\/(?:issues|pulls)\b|\blist_issues\b|\bsearch_issues\b|issues\.listForRepo|pulls\.list\b|(?<!\b(?:and|to|then|or|will|should|must|may|can|please|you|also)\s)\bopen (?:issues|pull requests)\b/i;
67702
+ var WEB_READ_RE = /(^|["\s,])(WebFetch|WebSearch)(\(|\s|,|"|$)/;
67703
+ function readsItemList(text) {
67704
+ if (text.length > 2e5) return ITEM_LIST_RE.test(text);
67705
+ return text.replace(/\\\r?\n/g, " ").split("\n").some((l) => ITEM_LIST_RE.test(l) && !sanitizedListing(l));
67706
+ }
67707
+ function analyzeTrustedReaders(path91, content, autoLabels, callers) {
67708
+ AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
67709
+ try {
67710
+ return analyzeTrustedReadersInner(path91, content, callers);
67711
+ } finally {
67712
+ AUTO_LABELS = /* @__PURE__ */ new Set();
67713
+ }
67714
+ }
67715
+ function keysWithCallers(keys, callers) {
67716
+ return [.../* @__PURE__ */ new Set([...keys, ...(callers ?? []).flatMap((c) => c.keys)])];
67717
+ }
67718
+ function analyzeTrustedReadersInner(path91, content, callers) {
67719
+ let raw;
67720
+ try {
67721
+ raw = loadWorkflow(content).raw;
67722
+ } catch {
67723
+ return [];
67724
+ }
67725
+ if (!raw || typeof raw !== "object") return [];
67726
+ const wf = raw;
67727
+ if (callers?.length) CALLERS.set(wf, callers);
67728
+ else CALLERS.delete(wf);
67729
+ const reach = triggerReach(wf, raw);
67730
+ if (reach.untrusted || reach.reusable) return [];
67731
+ const keys = keysWithCallers(reach.keys, callers);
67732
+ if (!keys.some((k) => /^schedule$/i.test(k))) return [];
67733
+ for (const job of jobList(wf)) {
67734
+ const jobIf = parseIf(str(job.if));
67735
+ if (jobIf !== null && !exprAllowsEvent(jobIf, "schedule")) continue;
67736
+ const steps = job.steps ?? [];
67737
+ const agentIdx = steps.findIndex(isAgentStep);
67738
+ if (agentIdx < 0) continue;
67739
+ const agent = steps.filter(isAgentStep);
67740
+ const tools = collectTools(agent);
67741
+ const promptText = agent.map(
67742
+ (st) => str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + (isRunAgent(st) ? str(st.run) : "")
67743
+ ).join(" ");
67744
+ const earlier = steps.slice(0, agentIdx).filter((st) => !isAgentStep(st)).map((st) => str(st.run) + " " + str(st.with?.["script"]));
67745
+ const reads = [];
67746
+ if (ITEM_READ_TOOL_RE.test(tools) || readsItemList(promptText))
67747
+ reads.push("reads open issues or pull requests");
67748
+ else if (earlier.some(readsItemList))
67749
+ reads.push("an earlier step lists open issues or pull requests for it");
67750
+ if (WEB_READ_RE.test(tools)) reads.push("fetches third-party web content");
67751
+ if (!reads.length) continue;
67752
+ const withheld = agent.every(
67753
+ (st) => str(st.with?.["github_token"]).trim() === "''" || "github_token" in (st.with ?? {}) && str(st.with?.["github_token"]).trim() === "" || "GITHUB_TOKEN" in (st.env ?? {}) && str(st.env?.["GITHUB_TOKEN"]).trim() === ""
67754
+ );
67755
+ const checkouts = steps.slice(0, agentIdx).filter((st) => /actions\/checkout/i.test(str(st.uses)));
67756
+ const persisted = checkouts.some((st) => str(st.with?.["persist-credentials"]) !== "false");
67757
+ const checkoutPat = checkouts.some(
67758
+ (st) => /secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(str(st.with?.["token"]))
67759
+ );
67760
+ const pat = usesPatIn([job]) || checkoutPat;
67761
+ const permWrite = jobsHaveWrite(
67762
+ wf,
67763
+ [job],
67764
+ /["']?(contents|issues|pull-requests)["']?\s*:\s*["']?write/i
67765
+ );
67766
+ const handsToken = agent.some(
67767
+ (st) => !!claudeActionBuild(st.uses) || !!str(st.with?.["github_token"]).trim()
67768
+ );
67769
+ const write2 = pat || permWrite && !withheld && (handsToken || persisted);
67770
+ if (!write2) continue;
67771
+ const writes = pat ? "a static PAT" : "a write token";
67772
+ return [
67773
+ {
67774
+ check: "CI-2",
67775
+ rule: "CI-2.trusted-trigger-reader",
67776
+ dimension: "workflows",
67777
+ severity: "advisory",
67778
+ title: "Agent on a trusted trigger reads content strangers write, with a write token",
67779
+ file: path91,
67780
+ signals: [
67781
+ `runs on ${keys.map((k) => safeText(k, 40)).join(", ")}: no stranger starts it`,
67782
+ ...reads.map((r) => `the agent ${r}`),
67783
+ `it holds ${writes}: injected text in that content can act with it`
67784
+ ],
67785
+ fix: "Give the agent a read-only token and apply its output in a separate step, or filter the items to trusted authors first."
67786
+ }
67787
+ ];
67788
+ }
67789
+ return [];
67790
+ }
67791
+ var FORK_PR_TRIGGER_RE = /^(pull_request_target|issue_comment|workflow_run)$/i;
67792
+ var SAME_REPO_GATE_RE = new RegExp(
67793
+ [
67794
+ String.raw`head(?:\.repo|_repository)\.full_name\s*==\s*github\.(?:repository\b|event\.(?:pull_request\.base\.repo|repository)\.full_name)`,
67795
+ String.raw`github\.repository\s*==\s*github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.full_name`,
67796
+ String.raw`head(?:\.repo|_repository)\.fork\s*==\s*false`,
67797
+ String.raw`head(?:\.repo|_repository)\.owner\.login\s*==\s*github\.repository_owner`,
67798
+ String.raw`workflow_run\.pull_requests\[0\]`
67799
+ ].join("|"),
67800
+ "i"
67801
+ );
67802
+ var NOT_FORK_ATOM_RE = /^github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.fork$/i;
67803
+ var TRUSTED_AUTHOR_GATE_RE = /\b(?:pull_request|issue)\.user\.login\s*==|github\.event\.(?:pull_request|issue)\.author_association\s*==\s*['"](?:OWNER|MEMBER|COLLABORATOR)|contains\(\s*fromjson\(\s*['"][^'"]{0,200}\b(?:OWNER|MEMBER|COLLABORATOR)\b[^'"]{0,200}['"]\s*\)\s*,\s*github\.event\.(?:pull_request|issue)\.author_association/i;
67804
+ function requiresAtom(src, re, events, notRe = NOT_FORK_ATOM_RE) {
67805
+ const x = parseIf(src);
67806
+ if (!x) return false;
67807
+ const req = (e) => {
67808
+ if (events?.length && events.every((ev) => !exprAllowsEvent(e, ev))) return true;
67809
+ switch (e.k) {
67810
+ case "atom": {
67811
+ const m = maskLiterals(e.t);
67812
+ return re.test(m) && !CHAINED_COMPARE_RE.test(m);
67813
+ }
67814
+ case "not":
67815
+ return e.c.k === "atom" && notRe.test(e.c.t.trim());
67816
+ case "and":
67817
+ return e.c.some(req);
67818
+ case "or":
67819
+ return e.c.every(req);
66949
67820
  }
66950
- const items = [];
66951
- while (i + 1 < lines.length && /^[ \t]+-[ \t]+/.test(lines[i + 1])) {
66952
- items.push(lines[++i].replace(/^[ \t]+-[ \t]+/, "").trim());
67821
+ };
67822
+ return req(x);
67823
+ }
67824
+ var PR_EVENTS = [
67825
+ "pull_request_target",
67826
+ "pull_request",
67827
+ "pull_request_review",
67828
+ "pull_request_review_comment"
67829
+ ];
67830
+ function headRefEvents(heads) {
67831
+ const evs = /* @__PURE__ */ new Set();
67832
+ for (const st of heads) {
67833
+ if (st.uses == null) return null;
67834
+ const m = /^\s*\$\{\{([\s\S]{1,500})\}\}\s*$/.exec(str(st.with?.["ref"]));
67835
+ if (!m) return null;
67836
+ for (const part of m[1].split("||").map((x) => x.trim())) {
67837
+ if (/^github\.event\.workflow_run\.[\w.]+$/.test(part)) evs.add("workflow_run");
67838
+ else if (/^github\.event\.pull_request\.[\w.]+$/.test(part))
67839
+ PR_EVENTS.forEach((e) => evs.add(e));
67840
+ else if (!/^(?:github\.(?:sha|ref|ref_name|event\.repository\.default_branch)|'[^']*')$/.test(part))
67841
+ return null;
66953
67842
  }
66954
- if (items.length) out[key] = items;
66955
67843
  }
66956
- return Object.keys(out).length ? out : null;
67844
+ return evs.size ? [...evs] : null;
66957
67845
  }
66958
- function splitGrants(value) {
66959
- const out = [];
66960
- let depth = 0;
66961
- let cur = "";
66962
- for (const ch of value) {
66963
- if (ch === "(") depth++;
66964
- else if (ch === ")") depth = Math.max(0, depth - 1);
66965
- if (depth === 0 && (ch === "," || /\s/.test(ch))) {
66966
- if (cur) out.push(cur);
66967
- cur = "";
66968
- continue;
67846
+ function readOnlySandbox(st) {
67847
+ const args = str(st.with?.["codex-args"]) + " " + str(st.with?.["codex_args"]);
67848
+ return /^read-only$/i.test(str(st.with?.["sandbox"])) && !PERMISSION_BYPASS_RE.test(args);
67849
+ }
67850
+ function requiresTrustedAuthor(src) {
67851
+ const x = parseIf(src);
67852
+ if (!x) return false;
67853
+ const req = (e) => {
67854
+ switch (e.k) {
67855
+ case "atom":
67856
+ return TRUSTED_AUTHOR_GATE_RE.test(e.t) && !NONWRITE_SET_RE.test(e.t) && !CHAINED_COMPARE_RE.test(maskLiterals(e.t));
67857
+ case "not":
67858
+ return false;
67859
+ case "and":
67860
+ return e.c.some(req);
67861
+ case "or":
67862
+ return e.c.every(req);
66969
67863
  }
66970
- cur += ch;
67864
+ };
67865
+ return req(x);
67866
+ }
67867
+ function analyzeForkCodeOnTrust(path91, content, callers, autoLabels) {
67868
+ AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
67869
+ try {
67870
+ return analyzeForkCodeOnTrustInner(path91, content, callers);
67871
+ } finally {
67872
+ AUTO_LABELS = /* @__PURE__ */ new Set();
66971
67873
  }
66972
- if (cur) out.push(cur);
66973
- return out;
66974
67874
  }
66975
- function allowedToolsOf(fm) {
66976
- const raw = fm?.["allowed-tools"];
66977
- if (typeof raw === "string") return splitGrants(raw);
66978
- if (Array.isArray(raw)) {
66979
- return raw.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean);
67875
+ function analyzeForkCodeOnTrustInner(path91, content, callers) {
67876
+ let raw;
67877
+ try {
67878
+ raw = loadWorkflow(content).raw;
67879
+ } catch {
67880
+ return [];
67881
+ }
67882
+ if (!raw || typeof raw !== "object") return [];
67883
+ const wf = raw;
67884
+ if (callers?.length) CALLERS.set(wf, callers);
67885
+ else CALLERS.delete(wf);
67886
+ const reach = triggerReach(wf, raw);
67887
+ const forkKeys = keysWithCallers(reach.keys, callers).filter((k) => FORK_PR_TRIGGER_RE.test(k));
67888
+ if (!forkKeys.length) return [];
67889
+ const inj = new Set(injectableJobs(wf, raw, reach.untrusted || reach.reusable));
67890
+ for (const job of jobList(wf)) {
67891
+ if (inj.has(job)) continue;
67892
+ const steps = job.steps ?? [];
67893
+ const agent = steps.filter(isAgentStep);
67894
+ if (!agent.length) continue;
67895
+ if (untrustedHeadCheckout(steps) !== "root") continue;
67896
+ if (agent.every(readOnlySandbox)) continue;
67897
+ if (requiresAtom(str(job.if), SAME_REPO_GATE_RE)) continue;
67898
+ if (agent.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
67899
+ const heads = steps.filter((st) => untrustedHeadCheckout([st]) === "root");
67900
+ if (heads.length && heads.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
67901
+ const evs = headRefEvents(heads);
67902
+ if (evs && !forkKeys.some((k) => evs.includes(k.toLowerCase()))) continue;
67903
+ if (evs && requiresAtom(str(job.if), SAME_REPO_GATE_RE, evs)) continue;
67904
+ if (requiresTrustedAuthor(str(job.if))) continue;
67905
+ const holdsSecret = /secrets\./i.test(str(job.steps) + " " + str(job.env));
67906
+ if (!holdsSecret && !jobsHaveWrite(wf, [job], /:\s*["']?write/i)) continue;
67907
+ return [
67908
+ {
67909
+ check: "CI-2",
67910
+ rule: "CI-2.fork-code-on-trust",
67911
+ dimension: "workflows",
67912
+ severity: "advisory",
67913
+ title: "A maintainer's action runs the agent inside the fork's code",
67914
+ file: path91,
67915
+ signals: [
67916
+ `triggered by ${forkKeys.map((k) => safeText(k, 40)).join(", ")}, behind a gate a maintainer passes (a comment, a label, a push to the PR branch, a reopen)`,
67917
+ "checks out the untrusted PR head into the workspace root",
67918
+ "the agent loads the fork's CLAUDE.md, .claude/settings.json hooks, .mcp.json and build scripts next to the job's secrets"
67919
+ ],
67920
+ fix: "Check out the base branch and give the agent the diff, or check the PR head out into a subdirectory the agent does not start in; do not let it run the fork's build or hooks."
67921
+ }
67922
+ ];
66980
67923
  }
66981
67924
  return [];
66982
67925
  }
66983
67926
 
66984
67927
  // src/ci-check/agent-config.ts
67928
+ import path76 from "path";
66985
67929
  function asStrings(v) {
66986
67930
  return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
66987
67931
  }
@@ -66999,11 +67943,22 @@ function hookCommands(hooks) {
66999
67943
  return out;
67000
67944
  }
67001
67945
  var OPEN_RUNNER_RE = new RegExp(
67002
- String.raw`^Bash\((?:[A-Z_][A-Z0-9_]{0,40}=\S{0,200} )*(?:/[\w./-]{0,80}/)?` + String.raw`(python(?:3(?:\.\d{1,2})?)?|node|deno(?: run)?|bun(?: x)?|ruby|perl|php|uv run|uvx|npx|npm exec|pnpm dlx|yarn dlx|pipx|docker run|` + String.raw`curl|wget|nc|ncat|ssh|scp|rsync|sh|bash|zsh|eval|source|xargs|env|sudo|gh)` + String.raw`(?: --?[A-Za-z][\w-]{0,30}){0,4} ?['"]?(?::\*| \*|\*)\)$`,
67946
+ String.raw`^Bash\((?:[A-Z_][A-Z0-9_]{0,40}=\S{0,200} )*(?:/[\w./-]{0,80}/)?` + String.raw`(python(?:3(?:\.\d{1,2})?)?|node|deno(?: run)?|bun(?: x)?|ruby|perl|php|uv run|uvx|npx|npm exec|pnpm dlx|yarn dlx|pipx|docker run|` + String.raw`curl|wget|nc|ncat|ssh|scp|rsync|sh|bash|zsh|eval|source|xargs|env|sudo|gh|` + // PowerShell, and the cmdlets that run a string or a program (a PowerShell grant is read
67947
+ // as Bash, review round 2).
67948
+ String.raw`pwsh|powershell|invoke-expression|iex|invoke-command|icm|start-process|invoke-webrequest|iwr|invoke-restmethod|irm)` + String.raw`(?: --?[A-Za-z][\w-]{0,30}){0,4} ?['"]?(?::\*| \*|\*)\)$`,
67003
67949
  "i"
67004
67950
  );
67005
67951
  var OPEN_RUNNER_MAX = 300;
67006
67952
  var INFO_FLAG_RE = /\s--(version|help)\b/;
67953
+ var EXTRA_RUNNER_RE = new RegExp(
67954
+ String.raw`^Bash\((?:[A-Z_][A-Z0-9_]{0,40}=\S{0,200} )*(?:/[\w./-]{0,80}/)?` + String.raw`(awk|gawk|mawk|sed|tee|tar|make(?! -n\b)|vim|vi|nvim|less|man|timeout|dd|install|` + String.raw`dotnet (?:run|script)|cargo run|go run|mvn exec|gradle run|npm start|tmux|` + String.raw`rm|git (?:push|config|submodule|filter-branch)|gh (?:repo|secret|auth|release))` + String.raw`(?: --?[A-Za-z][\w-]{0,30}){0,4} ?['"]?(?::\*| \*|\*)\)$`,
67955
+ "i"
67956
+ );
67957
+ var MCP_EXEC_RE = /^mcp__ide__executeCode$|^mcp__[\w-]{1,80}__(?:execute|exec|run)_(?:code|command|shell|sql|query)$|^mcp__[\w-]{1,80}__(?:ctx_)?(?:batch_)?execute$|^mcp__[\w-]{1,80}__(?:bash|shell|terminal)$/;
67958
+ function extraRunner(grant) {
67959
+ if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
67960
+ return EXTRA_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "(")) || MCP_EXEC_RE.test(grant);
67961
+ }
67007
67962
  function openRunner(grant) {
67008
67963
  if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
67009
67964
  return OPEN_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "("));
@@ -67011,13 +67966,15 @@ function openRunner(grant) {
67011
67966
  function gradeBroadGrant(allow, deny, opts = {}) {
67012
67967
  const denySupported = opts.denySupported ?? true;
67013
67968
  const scope = opts.scope ?? "for everyone who opens this repo with the agent";
67014
- const broad = allow.filter(
67015
- (a) => /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a) || openRunner(a)
67016
- );
67969
+ const asBash = (a) => a.replace(/^PowerShell(?=$|\()/, "Bash");
67970
+ const broad = allow.filter((raw) => {
67971
+ const a = asBash(raw);
67972
+ return /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a) || openRunner(a) || opts.settings === true && extraRunner(a);
67973
+ });
67017
67974
  if (broad.length === 0) return null;
67018
- const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
67019
- const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
67020
- const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
67975
+ const hasBackstop = deny.some((d) => /Bash|PowerShell|Write|Edit/.test(d));
67976
+ const bashBackstop = deny.some((d) => /^(?:Bash|PowerShell)\b/.test(d));
67977
+ const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(asBash(a)));
67021
67978
  const high = bareShell && !bashBackstop;
67022
67979
  const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
67023
67980
  if (high)
@@ -67034,7 +67991,7 @@ function gradeBroadGrant(allow, deny, opts = {}) {
67034
67991
  );
67035
67992
  if (broad.some((a) => /^Write|^Edit$/.test(a)))
67036
67993
  signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
67037
- if (broad.some(openRunner))
67994
+ if (broad.some((a) => openRunner(asBash(a))))
67038
67995
  signals.push(
67039
67996
  "an interpreter, network tool, shell runner or `gh` grant ending in a wildcard runs any code or request an injected instruction writes, without a prompt"
67040
67997
  );
@@ -67147,9 +68104,12 @@ function analyzeAgentConfig(path91, content, tree) {
67147
68104
  }
67148
68105
  }
67149
68106
  }
67150
- const allow = asStrings(cfg.permissions?.allow);
68107
+ const allow = [
68108
+ ...asStrings(cfg.permissions?.allow),
68109
+ ...asStrings(cfg.permissions?.allowedTools)
68110
+ ];
67151
68111
  const deny = asStrings(cfg.permissions?.deny);
67152
- const grade = gradeBroadGrant(allow, deny);
68112
+ const grade = gradeBroadGrant(allow, deny, { settings: true });
67153
68113
  if (grade) {
67154
68114
  const { high, signals } = grade;
67155
68115
  const at = jsonValueIndex(content, grade.broad[0]);
@@ -67177,6 +68137,26 @@ function analyzeAgentConfig(path91, content, tree) {
67177
68137
  return findings;
67178
68138
  }
67179
68139
  var GRANT_CARRIER_RE = /(^|\/)[Ss][Kk][Ii][Ll][Ll]\.md$|(^|\/)\.claude\/commands\/.+\.md$/;
68140
+ var ROOT_SURFACE_RE = /^\.(claude|agents|github|codex|cursor|gemini)\//;
68141
+ var ITEM_TEXT_RE = /gh\s+(?:pr|issue)\s+(?:view|diff|list)\b|gh\s+pr\s+checkout\b|git\s+fetch\s+[^\n]{0,80}\bpull\/|gh\s+api\s+graphql\b|gh\s+api\s+[^\n]{0,120}\/(?:pulls|issues)\b|mcp__github__(?:get_issue|get_pull_request|list_issues|search_issues)|--json\s+[^\n]{0,120}\b(?:body|title)\b/;
68142
+ var PR_TEXT_RE = /gh\s+pr\s+(?:view|diff)\b|gh\s+api\s+[^\n]{0,120}\/pulls\b/;
68143
+ var WEB_TEXT_RE = /\bsearch the web\b|\bfetch (?:the )?(?:url|page)\b/i;
68144
+ function skillReadsUntrusted(path91, content, allow, broad) {
68145
+ if (!ROOT_SURFACE_RE.test(path91)) return null;
68146
+ const body = content.replace(/^---[\s\S]*?\n---/, "");
68147
+ const runsCode = broad.map((a) => a.replace(/^PowerShell(?=$|\()/, "Bash")).some((a) => /^Bash$|^Bash\(\s*\*/.test(a) || openRunner(a) && !/^Bash\(\s*gh\b/.test(a));
68148
+ const webGrant = allow.some((a) => /^(WebFetch|WebSearch)\b/.test(a));
68149
+ const reads = (re) => body.replace(/\\\r?\n/g, " ").split("\n").some((l) => re.test(l) && !sanitizedListing(l));
68150
+ if (runsCode && reads(ITEM_TEXT_RE))
68151
+ return "it reads pull request or issue text that strangers write, with a grant that runs code";
68152
+ if (runsCode && (webGrant || WEB_TEXT_RE.test(body)))
68153
+ return "it reads web content anyone can publish, with a grant that runs code";
68154
+ const writes = allow.some((a) => /^(Write|Edit|MultiEdit)$/.test(a));
68155
+ const ghOrGit = allow.some((a) => /^Bash\(\s*(gh|git)\b/.test(a));
68156
+ if (writes && ghOrGit && reads(PR_TEXT_RE))
68157
+ return "it reads pull request text that strangers write, and may edit files and run git/gh on it";
68158
+ return null;
68159
+ }
67180
68160
  function analyzeSkillGrants(path91, content) {
67181
68161
  if (!GRANT_CARRIER_RE.test(path91)) return [];
67182
68162
  const allow = allowedToolsOf(parseFrontmatter(content));
@@ -67188,6 +68168,21 @@ function analyzeSkillGrants(path91, content) {
67188
68168
  if (!grade) return [];
67189
68169
  const kind = /commands\//.test(path91) ? "slash command" : "skill";
67190
68170
  const at = content.search(/^allowed-tools\s*:/m);
68171
+ const untrusted = skillReadsUntrusted(path91, content, allow, grade.broad);
68172
+ if (untrusted)
68173
+ return [
68174
+ {
68175
+ check: "CI-1",
68176
+ rule: "CI-1.skill-untrusted-input",
68177
+ ...at >= 0 ? { line: lineAtIndex(content, at) } : {},
68178
+ dimension: "toolRules",
68179
+ severity: "advisory",
68180
+ title: `Committed ${kind} runs code or writes with text strangers control`,
68181
+ file: path91,
68182
+ signals: [...grade.signals, untrusted],
68183
+ fix: "Drop the shell or interpreter grant from a skill that reads PRs, issues or the web, or have it treat that text as data in a read-only step first."
68184
+ }
68185
+ ];
67191
68186
  return [
67192
68187
  {
67193
68188
  check: "CI-1",
@@ -67209,27 +68204,178 @@ function analyzeSkillGrants(path91, content) {
67209
68204
 
67210
68205
  // src/ci-check/mcp.ts
67211
68206
  init_dist();
67212
- var LOCAL_RUNNER_RE = /^npx\s+(?:-y\s+|--yes\s+)?(?:tsx|ts-node|node)\s+(?:\.{1,2}\/|\$\{CLAUDE_PLUGIN_ROOT\}\/)?[\w@./-]+\.(?:m?[jt]s|cjs)(?:\s+[^\s;&|`$<>()]+)*$/;
67213
- function analyzeMcp(path91, content) {
68207
+ var LOCAL_RUNNER_RE = /^(?:npx|pnpx|bunx)\s+(?:-y\s+|--yes\s+)?(?:tsx|ts-node|node)\s+(?:\.{1,2}\/|\$\{CLAUDE_PLUGIN_ROOT\}\/)?[\w@./-]+\.(?:m?[jt]s|cjs)(?:\s+[^\s;&|`$<>()]+)*$/;
68208
+ var LAUNCHERS = [
68209
+ { re: /\bnpx\b/, name: "npx", registry: "npm" },
68210
+ { re: /\bpnpx\b|\bpnpm\s+dlx\b/, name: "pnpm dlx", registry: "npm" },
68211
+ { re: /\byarn\s+dlx\b/, name: "yarn dlx", registry: "npm" },
68212
+ { re: /\bnpm\s+(?:exec|x)\b/, name: "npm exec", registry: "npm" },
68213
+ { re: /\bbunx\b|\bbun\s+x\b/, name: "bunx", registry: "npm" },
68214
+ { re: /\buvx\b|\buv\s+tool\s+run\b/, name: "uvx", registry: "PyPI" },
68215
+ { re: /\bpipx\s+run\b/, name: "pipx run", registry: "PyPI" }
68216
+ ];
68217
+ var PINNED_RE = /@v?\d|@[0-9a-f]{40}\b|==\s*\d/i;
68218
+ var LOCAL_FROM_RE = /(?:^|\s)--from(?:\s+|=)(?:\.{1,2}(?:\/\S*)?|\/\S*|~\/\S*)(?:\s|$)/;
68219
+ var RUN_VALUE_FLAGS = new Set(
68220
+ [
68221
+ "-a --attach --add-host --annotation --blkio-weight --blkio-weight-device --cap-add",
68222
+ "--cap-drop --cgroup-parent --cgroupns --cgroups --cgroup-conf --cidfile --cpu-period",
68223
+ "--cpu-quota --cpu-rt-period --cpu-rt-runtime -c --cpu-shares --cpus --cpuset-cpus",
68224
+ "--cpuset-mems --device --device-cgroup-rule --device-read-bps --device-read-iops",
68225
+ "--device-write-bps --device-write-iops --dns --dns-option --dns-search --domainname",
68226
+ "--entrypoint -e --env --env-file --expose --gpus --group-add --health-cmd",
68227
+ "--health-interval --health-retries --health-start-interval --health-start-period",
68228
+ "--health-timeout -h --hostname --ip --ip6 --ipc --isolation --kernel-memory -l --label",
68229
+ "--label-file --link --link-local-ip --log-driver --log-opt --mac-address -m --memory",
68230
+ "--memory-reservation --memory-swap --memory-swappiness --mount --name --network --net",
68231
+ "--network-alias --oom-score-adj --pid --pids-limit --platform -p --publish --pull",
68232
+ "--restart --runtime --security-opt --shm-size --stop-signal --stop-timeout",
68233
+ "--storage-opt --sysctl --tmpfs --ulimit -u --user --userns --uts -v --volume",
68234
+ "--volume-driver --volumes-from -w --workdir --secret --pod --arch --os --variant",
68235
+ "--timezone --umask --requires --uidmap --gidmap --subuidname --subgidname --hooks-dir",
68236
+ "--init-path --seccomp-policy --sdnotify --personality --preserve-fds --chrootdirs"
68237
+ ].join(" ").split(" ")
68238
+ );
68239
+ function runImage(tokens) {
68240
+ const i = tokens.findIndex(
68241
+ (t, k) => /(^|\/)(docker|podman|nerdctl)$/.test(t) && (tokens[k + 1] === "run" || tokens[k + 1] === "container" && tokens[k + 2] === "run")
68242
+ );
68243
+ if (i < 0) return null;
68244
+ const from = tokens[i + 1] === "run" ? i + 2 : i + 3;
68245
+ for (let k = from; k < tokens.length; k++) {
68246
+ const t = tokens[k];
68247
+ if (t.startsWith("-")) {
68248
+ if (!t.includes("=") && RUN_VALUE_FLAGS.has(t)) k++;
68249
+ continue;
68250
+ }
68251
+ return t;
68252
+ }
68253
+ return null;
68254
+ }
68255
+ function launchedPackages(argv) {
68256
+ const t = argv.split(/\s+/).filter(Boolean);
68257
+ const i = t.findIndex((x) => /(^|\/)(npx|pnpx|bunx|uvx|dlx|exec|x|run)$/.test(x));
68258
+ if (i < 0) return [];
68259
+ const bare = (v) => v.startsWith("@") ? v.replace(/^(@[^/]+\/[^@]+).*$/, "$1") : v.replace(/(==|@).*$/, "");
68260
+ const flagged = [];
68261
+ let positional = null;
68262
+ for (let k = i + 1; k < t.length; k++) {
68263
+ const tok = t[k];
68264
+ const eq = /^(--from|-p|--package)=(.*)$/.exec(tok);
68265
+ if (eq) {
68266
+ flagged.push(eq[2]);
68267
+ continue;
68268
+ }
68269
+ if (/^(--from|-p|--package)$/.test(tok)) {
68270
+ if (t[k + 1]) flagged.push(t[++k]);
68271
+ continue;
68272
+ }
68273
+ if (tok.startsWith("-")) continue;
68274
+ positional ??= tok;
68275
+ }
68276
+ const names = flagged.length ? flagged : positional ? [positional] : [];
68277
+ return names.map((n) => bare(n).toLowerCase()).filter(Boolean);
68278
+ }
68279
+ var SHELL_RE = /(?:^|\/)(?:sh|bash|zsh|dash|cmd|pwsh|powershell)(?:\.exe)?$/i;
68280
+ var SHELL_OP_RE = /[;&|`$<>()]/;
68281
+ function launchesOwnPackage(argv, own) {
68282
+ if (own.size === 0) return false;
68283
+ if (SHELL_OP_RE.test(argv) || SHELL_RE.test(argv.split(/\s+/)[0] ?? "")) return false;
68284
+ const l = LAUNCHERS.find((x) => x.re.test(argv));
68285
+ if (!l) return false;
68286
+ const reg = l.registry === "PyPI" ? "pypi" : "npm";
68287
+ const pkgs = launchedPackages(argv);
68288
+ return pkgs.length > 0 && pkgs.every((p) => own.has(`${reg}:${p}`) || own.has(p));
68289
+ }
68290
+ function unpinnedLauncher(argv, own) {
68291
+ if (LOCAL_RUNNER_RE.test(argv)) return null;
68292
+ if (!SHELL_OP_RE.test(argv) && LOCAL_FROM_RE.test(argv)) return null;
68293
+ if (launchesOwnPackage(argv, own)) return null;
68294
+ const image = runImage(argv.split(/\s+/));
68295
+ if (image) {
68296
+ if (image.includes("@sha256:")) return null;
68297
+ const tail = image.slice(image.lastIndexOf("/") + 1);
68298
+ const tag = tail.includes(":") ? tail.slice(tail.indexOf(":") + 1) : "";
68299
+ return !tag || tag === "latest" ? { launcher: "docker run", registry: "the image registry" } : null;
68300
+ }
68301
+ const goMod = /\bgo\s+run\s+(?:-\S+\s+)*([^\s@]+)@(\S+)/.exec(argv);
68302
+ if (goMod)
68303
+ return /^v\d/.test(goMod[2]) || /^[0-9a-f]{40}$/.test(goMod[2]) ? null : { launcher: "go run", registry: "the Go module proxy" };
68304
+ const l = LAUNCHERS.find((x) => x.re.test(argv));
68305
+ if (!l) return null;
68306
+ return /@latest\b/.test(argv) || !PINNED_RE.test(argv) ? { launcher: l.name, registry: l.registry } : null;
68307
+ }
68308
+ function ownPackageNames(manifests) {
68309
+ const out = /* @__PURE__ */ new Set();
68310
+ for (const m of manifests) {
68311
+ if (m.path === "package.json") {
68312
+ try {
68313
+ const pkg = JSON.parse(m.content);
68314
+ if (typeof pkg.name === "string" && pkg.name && pkg.private !== true)
68315
+ out.add(`npm:${pkg.name.toLowerCase()}`);
68316
+ } catch {
68317
+ }
68318
+ } else if (m.path === "pyproject.toml") {
68319
+ const sec = /^\[(project|tool\.poetry)\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m.exec(m.content);
68320
+ const name = sec && /^name\s*=\s*["']([^"']{1,200})["']/m.exec(sec[2]);
68321
+ if (name) {
68322
+ out.add(`pypi:${name[1].toLowerCase()}`);
68323
+ out.add(`pypi:${name[1].toLowerCase().replace(/_/g, "-")}`);
68324
+ }
68325
+ }
68326
+ }
68327
+ return out;
68328
+ }
68329
+ function analyzeMcp(path91, content, own = /* @__PURE__ */ new Set()) {
67214
68330
  let cfg;
67215
68331
  try {
67216
68332
  cfg = JSON.parse(content);
67217
68333
  } catch {
67218
68334
  return [];
67219
68335
  }
67220
- return analyzeMcpServers(cfg.mcpServers ?? {}, path91, content);
68336
+ if (!cfg || typeof cfg !== "object") return [];
68337
+ const sections = [];
68338
+ const obj = (x) => x && typeof x === "object" && !Array.isArray(x) ? x : {};
68339
+ sections.push(["mcpServers", obj(cfg.mcpServers)]);
68340
+ sections.push(["servers", obj(cfg.servers)]);
68341
+ const opencode = {};
68342
+ for (const [name, s] of Object.entries(obj(cfg.mcp))) {
68343
+ if (!s || typeof s !== "object") continue;
68344
+ const o = s;
68345
+ const cmd = Array.isArray(o.command) ? o.command.map(String) : [];
68346
+ opencode[name] = {
68347
+ command: cmd[0],
68348
+ args: cmd.slice(1),
68349
+ url: o.url,
68350
+ env: obj(o.environment),
68351
+ disabled: o.enabled === false
68352
+ };
68353
+ }
68354
+ sections.push(["mcp", opencode]);
68355
+ const count = /* @__PURE__ */ new Map();
68356
+ for (const [, sec] of sections)
68357
+ for (const name of Object.keys(sec)) count.set(name, (count.get(name) ?? 0) + 1);
68358
+ const servers = {};
68359
+ for (const [section, sec] of sections)
68360
+ for (const [name, spec] of Object.entries(sec)) {
68361
+ if (!spec || typeof spec !== "object") continue;
68362
+ const key = (count.get(name) ?? 0) > 1 ? `${section}.${name}` : name;
68363
+ servers[key] = { ...spec, anchor: name };
68364
+ }
68365
+ return analyzeMcpServers(servers, path91, content, own);
67221
68366
  }
67222
- function analyzeMcpServers(servers, path91, content = "") {
68367
+ function analyzeMcpServers(servers, path91, content = "", own = /* @__PURE__ */ new Set()) {
67223
68368
  const findings = [];
67224
68369
  for (const [name, srv] of Object.entries(servers ?? {})) {
67225
68370
  if (!srv || srv.disabled) continue;
67226
68371
  const argv = [srv.command, ...Array.isArray(srv.args) ? srv.args.map(String) : []].join(" ");
67227
- const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68372
+ const esc = (srv.anchor ?? name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
67228
68373
  const at = content.search(
67229
68374
  new RegExp(`"${esc}"\\s*:|^[ \\t]*\\[mcp_servers\\.(?:"${esc}"|${esc})\\]`, "m")
67230
68375
  );
67231
68376
  const line = at >= 0 ? lineAtIndex(content, at) : void 0;
67232
- if (/\bnpx\b/.test(argv) && (/@latest\b/.test(argv) || !/@\d/.test(argv)) && !LOCAL_RUNNER_RE.test(argv)) {
68377
+ const why = unpinnedLauncher(argv, own);
68378
+ if (why) {
67233
68379
  findings.push({
67234
68380
  check: "CI-3",
67235
68381
  rule: "CI-3.mcp-unpinned",
@@ -67239,9 +68385,16 @@ function analyzeMcpServers(servers, path91, content = "") {
67239
68385
  title: `MCP server "${name}" runs an unpinned executable`,
67240
68386
  file: path91,
67241
68387
  ...line ? { line } : {},
67242
- signals: [`\`${safeText(argv, 120)}\`: unversioned/@latest npx`],
68388
+ signals: [
68389
+ why.launcher === "npx" ? `\`${safeText(argv, 120)}\`: unversioned/@latest npx` : `\`${safeText(argv, 120)}\`: unversioned/latest ${why.launcher}`
68390
+ ],
67243
68391
  fix: "Pin the MCP server package to an exact version so a PR (or a registry compromise) can\u2019t swap the toolchain.",
67244
- explain: explainMcp({ name, argv })
68392
+ explain: explainMcp({
68393
+ name,
68394
+ argv,
68395
+ registry: why.registry,
68396
+ pkg: why.launcher === "docker run" ? runImage(argv.split(/\s+/))?.replace(/:[^/:]*$/, "") ?? null : why.launcher === "go run" ? /\bgo\s+run\s+(?:-\S+\s+)*([^\s@]+)@/.exec(argv)?.[1] ?? null : launchedPackages(argv)[0] ?? null
68397
+ })
67245
68398
  });
67246
68399
  }
67247
68400
  for (const [k, v] of Object.entries(srv.env ?? {})) {
@@ -67270,7 +68423,7 @@ function analyzeMcpServers(servers, path91, content = "") {
67270
68423
 
67271
68424
  // src/ci-check/codex.ts
67272
68425
  import { parse as parseToml7 } from "smol-toml";
67273
- function analyzeCodexConfig(path91, content) {
68426
+ function analyzeCodexConfig(path91, content, own = /* @__PURE__ */ new Set()) {
67274
68427
  let cfg;
67275
68428
  try {
67276
68429
  cfg = parseToml7(content);
@@ -67278,7 +68431,7 @@ function analyzeCodexConfig(path91, content) {
67278
68431
  return [];
67279
68432
  }
67280
68433
  const findings = [];
67281
- findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content));
68434
+ findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content, own));
67282
68435
  const sandbox = typeof cfg.sandbox_mode === "string" ? cfg.sandbox_mode : "";
67283
68436
  const approval = typeof cfg.approval_policy === "string" ? cfg.approval_policy : "";
67284
68437
  const fullAccess = /danger-full-access/i.test(sandbox);
@@ -67315,10 +68468,12 @@ var UNREAD_RULES = /* @__PURE__ */ new Set([
67315
68468
  "CI-1.hook-script.unscanned-size",
67316
68469
  "CI-6.skill-script.unscanned-size"
67317
68470
  ]);
67318
- var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override"]);
68471
+ var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override", "CI-6.homoglyph"]);
67319
68472
  function tierOf(f) {
67320
68473
  const { rule, severity } = f;
67321
68474
  if (rule === "CI-1.skill-allowed-tools") return "review";
68475
+ if (rule === "CI-2.trusted-trigger-reader" || rule === "CI-2.fork-code-on-trust" || rule === "CI-1.skill-untrusted-input")
68476
+ return "alert";
67322
68477
  if (UNREAD_RULES.has(rule)) return "review";
67323
68478
  if (rule.startsWith("CI-2.") || rule.startsWith("CI-4."))
67324
68479
  return severity === "advisory" ? "note" : "alert";
@@ -67683,12 +68838,54 @@ function scanTree(tree) {
67683
68838
  }
67684
68839
  return named.get(dir).has(docPath);
67685
68840
  };
68841
+ const callersOf = /* @__PURE__ */ new Map();
68842
+ const autoLabels = /* @__PURE__ */ new Set();
68843
+ const routeOfFile = (file) => file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
68844
+ const manifests = [];
68845
+ for (const file of tree.files) {
68846
+ if (routeOfFile(file) !== "manifest") continue;
68847
+ try {
68848
+ manifests.push({ path: file.path, content: file.content });
68849
+ } catch {
68850
+ }
68851
+ }
68852
+ const ownPackages = ownPackageNames(manifests);
68853
+ for (const file of tree.files) {
68854
+ if (routeOfFile(file) !== "issue-form") continue;
68855
+ try {
68856
+ for (const l of issueFormLabels(file.content)) autoLabels.add(l);
68857
+ } catch {
68858
+ }
68859
+ }
68860
+ const runSources = /* @__PURE__ */ new Map();
68861
+ for (const file of tree.files) {
68862
+ if (routeOfFile(file) !== "workflow") continue;
68863
+ try {
68864
+ const src = runSourceOf(file.path, file.content, autoLabels);
68865
+ if (src) runSources.set(src.name, (runSources.get(src.name) ?? true) && src.trusted);
68866
+ } catch {
68867
+ }
68868
+ }
68869
+ const trustedRunSources = new Set([...runSources].filter(([, t]) => t).map(([n]) => n));
68870
+ for (const file of tree.files) {
68871
+ if (routeOfFile(file) !== "workflow") continue;
68872
+ try {
68873
+ const content = file.content;
68874
+ for (const { target, caller } of localCallsOf(file.path, content, autoLabels)) {
68875
+ const list = callersOf.get(target) ?? [];
68876
+ list.push(caller);
68877
+ callersOf.set(target, list);
68878
+ }
68879
+ } catch {
68880
+ }
68881
+ }
67686
68882
  for (const file of tree.files) {
67687
68883
  let route = file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
67688
68884
  if (route === "app-doc") {
67689
68885
  if (!namedBy(file.path)) continue;
67690
68886
  route = "instruction";
67691
68887
  }
68888
+ if (route === "issue-form" || route === "manifest") continue;
67692
68889
  inspected.push(file.path);
67693
68890
  if (file.path === SUPPRESSIONS_FILE) continue;
67694
68891
  const testKind = route && CONTENT_ROUTES.has(route) ? testPathKind(file.path, route) : "none";
@@ -67698,10 +68895,20 @@ function scanTree(tree) {
67698
68895
  const content = file.content;
67699
68896
  switch (route) {
67700
68897
  case "workflow": {
67701
- const f = analyzeWorkflow(file.path, content);
68898
+ const callers = callersOf.get(file.path);
68899
+ const f = analyzeWorkflow(file.path, content, callers, autoLabels, trustedRunSources);
67702
68900
  if (f) findings.push(f);
67703
- const s = analyzeWorkflowSecrets(file.path, content);
68901
+ const s = analyzeWorkflowSecrets(
68902
+ file.path,
68903
+ content,
68904
+ callers,
68905
+ autoLabels,
68906
+ trustedRunSources
68907
+ );
67704
68908
  if (s) findings.push(s);
68909
+ findings.push(...analyzeWorkflowPlugins(file.path, content, tree.owner));
68910
+ findings.push(...analyzeTrustedReaders(file.path, content, autoLabels, callers));
68911
+ findings.push(...analyzeForkCodeOnTrust(file.path, content, callers, autoLabels));
67705
68912
  break;
67706
68913
  }
67707
68914
  case "agent-config":
@@ -67714,10 +68921,10 @@ function scanTree(tree) {
67714
68921
  findings.push(...analyzeScript(file.path, content, "CI-6.skill-script"));
67715
68922
  break;
67716
68923
  case "mcp":
67717
- findings.push(...analyzeMcp(file.path, content));
68924
+ findings.push(...analyzeMcp(file.path, content, ownPackages));
67718
68925
  break;
67719
68926
  case "codex":
67720
- findings.push(...analyzeCodexConfig(file.path, content));
68927
+ findings.push(...analyzeCodexConfig(file.path, content, ownPackages));
67721
68928
  break;
67722
68929
  case "instruction":
67723
68930
  findings.push(...analyzeInstructionFile(file.path, content));
@@ -67725,7 +68932,9 @@ function scanTree(tree) {
67725
68932
  break;
67726
68933
  }
67727
68934
  } catch (err2) {
67728
- notes.push(`checker degraded on ${file.path}: ${err2?.message ?? "error"}`);
68935
+ notes.push(
68936
+ `checker degraded on ${file.path}: ${err2?.message ?? "error"}; results may be INCOMPLETE`
68937
+ );
67729
68938
  }
67730
68939
  if (testKind === "alerts-only") {
67731
68940
  const kept = findings.splice(before).filter((f) => tierOf(f) === "alert");