@edgehero/pi-dispatch-admin 4.1.0 → 4.2.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/README.md +3 -0
  2. package/dist/index.mjs +596 -200
  3. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -9218,6 +9218,7 @@ var init_reserved_env = __esm({
9218
9218
  "PI_FLOW",
9219
9219
  "PI_COMMAND",
9220
9220
  "PI_EXCLUDE_TOOLS",
9221
+ "PI_MCP_SERVERS",
9221
9222
  // Issues #501, #502: the per-job dollar cap (written by the worker since #501, from the trigger's and the
9222
9223
  // deployment's caps) and the allowed-model list (reserved until #502 writes it). Neither can be bound through
9223
9224
  // `run.secrets`: a trigger setting its own cap, or its own list, would be the policy choosing itself.
@@ -10488,7 +10489,7 @@ function sanitizeJobId(id) {
10488
10489
  if (s2 === "") return "unknown-job";
10489
10490
  return s2.replace(/[^A-Za-z0-9._-]/g, "_");
10490
10491
  }
10491
- var COST_CAP_WHYS, RESOURCE_KEYS, EXIT_OOM_KILLED, TOKEN_KEYS, PLAN_RECORD_OUTCOMES, PLAN_RECORD_REASONS, TAIL_CAP_BYTES, UNREADABLE_RECORD, RECORD_CLOCK_SKEW_MS;
10492
+ var MCP_RUNNER_REASONS, RUNNER_POLICY_REASONS, COST_CAP_WHYS, RESOURCE_KEYS, EXIT_OOM_KILLED, TOKEN_KEYS, PLAN_RECORD_OUTCOMES, PLAN_RECORD_REASONS, TAIL_CAP_BYTES, UNREADABLE_RECORD, RECORD_CLOCK_SKEW_MS;
10492
10493
  var init_run_history = __esm({
10493
10494
  "../worker/src/run-history.mjs"() {
10494
10495
  init_redact();
@@ -10500,6 +10501,8 @@ var init_run_history = __esm({
10500
10501
  init_wait_for();
10501
10502
  init_repeat_slot();
10502
10503
  init_run_earlier();
10504
+ MCP_RUNNER_REASONS = Object.freeze(["mcp-unavailable", "mcp-config-invalid", "mcp-config-mismatch", "mcp-secret-missing", "mcp-command-missing", "mcp-shadowed"]);
10505
+ RUNNER_POLICY_REASONS = /* @__PURE__ */ new Set(["provider-auth-refused", "cost-cap", "model-not-allowed", "cost-cap-unenforceable", "model-policy-unenforceable", ...MCP_RUNNER_REASONS]);
10503
10506
  COST_CAP_WHYS = Object.freeze(["unboundable", "external", "over-cap"]);
10504
10507
  RESOURCE_KEYS = Object.freeze(["memPeak", "swapPeak", "oomKills", "memSomeUsec", "memFullUsec", "cpuUsec", "throttledUsec", "throttled", "pidsPeak"]);
10505
10508
  EXIT_OOM_KILLED = "oom-killed";
@@ -10529,6 +10532,219 @@ var init_run_history = __esm({
10529
10532
  }
10530
10533
  });
10531
10534
 
10535
+ // ../worker/src/mcp-config.mjs
10536
+ import { lstatSync as fsLstatSync, readFileSync as fsReadFileSync } from "node:fs";
10537
+ import { basename, isAbsolute as isAbsolute4, join, posix as posix2 } from "node:path";
10538
+ function isRecord(value) {
10539
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10540
+ }
10541
+ function refValue(value, { header = false } = {}) {
10542
+ if (typeof value !== "string") return { problem: "is not a string" };
10543
+ const match2 = (header ? HEADER_REF_RE : ENV_REF_RE).exec(value);
10544
+ if (match2) return { name: match2[1] };
10545
+ const shape = header ? `\`\${NAME}\`, or an auth scheme and a space before it (\`Bearer \${NAME}\`; ${MCP_HEADER_SCHEMES.join(", ")})` : "exactly `${NAME}`";
10546
+ return { problem: `must be ${shape}, NAME in capitals, digits and \`_\`: a value is never written in the overlay, which every job can read` };
10547
+ }
10548
+ function mixedProblem(value, refs) {
10549
+ if (value.startsWith("!")) return "starts with `!`";
10550
+ const rest = value.replace(REF_RE, (_, name) => {
10551
+ refs.add(name);
10552
+ return "";
10553
+ });
10554
+ if (rest.includes("${")) return "has a `${` that is not a whole `${NAME}` reference (NAME in capitals, digits and `_`)";
10555
+ return null;
10556
+ }
10557
+ function checkServer(name, raw) {
10558
+ const refuse2 = (problem) => ({ name, problem });
10559
+ if (!MCP_SERVER_NAME_RE.test(name)) return refuse2("the name must be lowercase letters and digits, words joined by single `-`, up to 32");
10560
+ if (name.includes("dispatch")) return refuse2("a name holding `dispatch` is reserved for the dispatch admin");
10561
+ if (name === FORGE_MCP_SERVER_NAME) return refuse2("`forge` is reserved for the worker's own forge server (the job's GitHub or GitLab, as its own token), which a trigger names in run.mcp without an overlay entry");
10562
+ if (!isRecord(raw)) return refuse2("the entry is not an object");
10563
+ for (const key of Object.keys(raw)) {
10564
+ if (Object.hasOwn(MCP_REFUSED_KEYS, key)) return refuse2(`\`${key}\` is not accepted: ${MCP_REFUSED_KEYS[key]}`);
10565
+ }
10566
+ const { type } = raw;
10567
+ if (type === "sse") return refuse2("the legacy SSE transport is not supported; use the server's streamable HTTP URL");
10568
+ if (type !== void 0 && !["http", "streamable-http", "stdio"].includes(type)) return refuse2("`type` must be http, streamable-http or stdio");
10569
+ const hasUrl = raw.url !== void 0;
10570
+ const hasCommand = raw.command !== void 0;
10571
+ if (hasUrl === hasCommand) return refuse2("the entry needs exactly one of `url` (streamable HTTP) or `command` (stdio)");
10572
+ const transport3 = hasUrl ? "http" : "stdio";
10573
+ if (type !== void 0 && type === "stdio" !== (transport3 === "stdio")) return refuse2(`\`type\` ${type} does not match a server with \`${hasUrl ? "url" : "command"}\``);
10574
+ const allowed = transport3 === "http" ? MCP_HTTP_KEYS : MCP_STDIO_KEYS;
10575
+ for (const key of Object.keys(raw)) {
10576
+ if (!allowed.includes(key)) return refuse2(`${shown(key)} is not a key a ${transport3 === "http" ? "streamable HTTP" : "stdio"} server takes here`);
10577
+ }
10578
+ if (typeof raw.description !== "string" || raw.description.trim() === "") return refuse2("`description` is required: one sentence on what the server offers, which pi puts in the job's prompt");
10579
+ if (raw.description.length > 300 || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(raw.description)) return refuse2("`description` must be one line of at most 300 characters, with no control or format characters");
10580
+ if (raw.exposure !== void 0 && !MCP_EXPOSURES.includes(raw.exposure)) return refuse2(`\`exposure\` must be one of ${MCP_EXPOSURES.join(", ")}`);
10581
+ if (raw.toolExposure !== void 0) {
10582
+ if (!isRecord(raw.toolExposure)) return refuse2("`toolExposure` must map tool names or `*` patterns to exposures");
10583
+ for (const [tool, exposure] of Object.entries(raw.toolExposure)) {
10584
+ if (!MCP_EXPOSURES.includes(exposure)) return refuse2(`\`toolExposure\` ${shown(tool)} must be one of ${MCP_EXPOSURES.join(", ")}`);
10585
+ }
10586
+ }
10587
+ if (raw.enabled !== void 0 && typeof raw.enabled !== "boolean") return refuse2("`enabled` must be true or false");
10588
+ if (raw.timeout !== void 0 && !(Number.isInteger(raw.timeout) && raw.timeout > 0 && raw.timeout <= 600)) return refuse2("`timeout` must be a whole number of seconds from 1 to 600");
10589
+ const entry = { name, transport: transport3 };
10590
+ for (const key of ["description", "exposure", "toolExposure", "timeout", "enabled"]) if (raw[key] !== void 0) entry[key] = raw[key];
10591
+ const refs = /* @__PURE__ */ new Set();
10592
+ if (transport3 === "http") {
10593
+ if (typeof raw.url !== "string") return refuse2("`url` must be a string");
10594
+ if (raw.url.includes("#")) return refuse2("`url` must not carry a fragment");
10595
+ const urlProblem = mixedProblem(raw.url, refs);
10596
+ if (urlProblem) return refuse2(`\`url\` ${urlProblem}`);
10597
+ const stand = (word) => raw.url.replace(REF_RE, word);
10598
+ if (!URL.canParse(stand("refa"))) return refuse2("`url` is not a URL");
10599
+ const url = new URL(stand("refa"));
10600
+ if (url.host !== new URL(stand("refb")).host) return refuse2("`url` holds a reference in its host; the host is written as is, since the egress check reads it before the job starts");
10601
+ if (url.href !== stand("refa") && url.href !== `${stand("refa")}/`) return refuse2(`\`url\` must be written in its plain form, as a URL parser writes it back (${JSON.stringify(url.href.replaceAll("refa", "${...}"))})`);
10602
+ if (url.protocol !== "https:" && url.protocol !== "http:") return refuse2("`url` must be http or https");
10603
+ if (url.username !== "" || url.password !== "") return refuse2("`url` must not carry a user name or password; send a credential in a header, as a `${NAME}` reference");
10604
+ const query = raw.url.includes("?") ? raw.url.slice(raw.url.indexOf("?") + 1) : void 0;
10605
+ if (query !== void 0) {
10606
+ for (const pair of query.split("&")) {
10607
+ const eq = pair.indexOf("=");
10608
+ const value = eq === -1 ? "" : pair.slice(eq + 1);
10609
+ if (!ENV_REF_RE.test(value)) return refuse2(`\`url\` query value ${shown(eq === -1 ? pair : pair.slice(0, eq))} must be exactly \`\${NAME}\`: a query is where hosted servers put a key, so no value there is written in the overlay`);
10610
+ }
10611
+ }
10612
+ entry.url = raw.url;
10613
+ if (raw.headers !== void 0) {
10614
+ if (!isRecord(raw.headers)) return refuse2("`headers` must map header names to strings");
10615
+ for (const [header, value] of Object.entries(raw.headers)) {
10616
+ if (!HEADER_NAME_RE.test(header) || RESERVED_NAMES.includes(header)) return refuse2(`header name ${shown(header)} is not a valid HTTP header name`);
10617
+ const ref = refValue(value, { header: true });
10618
+ if (ref.problem) return refuse2(`header ${shown(header)} ${ref.problem}`);
10619
+ refs.add(ref.name);
10620
+ }
10621
+ entry.headers = { ...raw.headers };
10622
+ }
10623
+ } else {
10624
+ if (typeof raw.command !== "string" || raw.command === "") return refuse2("`command` must be a non-empty string");
10625
+ if (raw.command.startsWith("!")) return refuse2("`command` starts with `!`");
10626
+ if (raw.command.includes("$")) return refuse2("`command` holds a `$`; it is used as written, and a reference goes in an argument or `env`");
10627
+ if (raw.command.startsWith("~")) return refuse2("`command` must not start with `~`, which pi reads as the job's home folder");
10628
+ if (!isAbsolute4(raw.command) && raw.command.includes("/")) return refuse2("`command` must be an absolute path or a bare name found on the job image's PATH");
10629
+ if (isAbsolute4(raw.command)) {
10630
+ const normalised = posix2.normalize(raw.command);
10631
+ if (!MCP_COMMAND_ROOTS.some((r2) => normalised.startsWith(`${r2}/`))) return refuse2(`\`command\` must sit under ${MCP_COMMAND_ROOTS.join(", ")} (the job image's own folders and the overlay at /opt/pi-global); the job writes the others`);
10632
+ }
10633
+ if (raw.allowPackageRunner !== void 0 && typeof raw.allowPackageRunner !== "boolean") return refuse2("`allowPackageRunner` must be true or false");
10634
+ const runner = MCP_PACKAGE_RUNNERS.includes(basename(raw.command));
10635
+ if (runner && raw.allowPackageRunner !== true) return refuse2(`\`command\` ${basename(raw.command)} fetches the server from a package registry at start; install it in the job image, or set \`allowPackageRunner: true\` and allowlist the registry`);
10636
+ entry.command = raw.command;
10637
+ if (raw.args !== void 0) {
10638
+ if (!Array.isArray(raw.args) || !raw.args.every((arg) => typeof arg === "string")) return refuse2("`args` must be a list of strings");
10639
+ for (const arg of raw.args) {
10640
+ const problem = mixedProblem(arg, refs);
10641
+ if (problem) return refuse2(`an argument ${problem}`);
10642
+ if (arg.startsWith("~")) return refuse2("an argument starts with `~`, which pi reads as the job's home folder, a folder the job writes");
10643
+ }
10644
+ entry.args = [...raw.args];
10645
+ }
10646
+ if (raw.env !== void 0) {
10647
+ if (!isRecord(raw.env)) return refuse2("`env` must map variable names to strings");
10648
+ for (const [key, value] of Object.entries(raw.env)) {
10649
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || RESERVED_NAMES.includes(key)) return refuse2(`env name ${shown(key)} is not a variable name`);
10650
+ const ref = refValue(value);
10651
+ if (ref.problem) return refuse2(`env ${key} ${ref.problem}`);
10652
+ refs.add(ref.name);
10653
+ }
10654
+ entry.env = { ...raw.env };
10655
+ }
10656
+ entry.packageRunner = runner;
10657
+ }
10658
+ entry.refs = [...refs].sort();
10659
+ return entry;
10660
+ }
10661
+ function parseOverlayMcp(text, path6 = MCP_CONFIG_FILE_NAME) {
10662
+ const fail2 = (why) => {
10663
+ const error = new Error(`${path6}: ${why}`);
10664
+ error.mcpConfig = true;
10665
+ return error;
10666
+ };
10667
+ let value;
10668
+ try {
10669
+ value = JSON.parse(text);
10670
+ } catch {
10671
+ throw fail2("not valid JSON");
10672
+ }
10673
+ const duplicate = findDuplicateKey(text);
10674
+ if (duplicate) throw fail2(`the key ${JSON.stringify(duplicate.at)} appears twice; JSON keeps only the last, so the file would not say what it reads as`);
10675
+ if (!isRecord(value)) throw fail2("expected an object with an `mcpServers` object");
10676
+ for (const key of Object.keys(value)) {
10677
+ if (!MCP_TOP_KEYS.includes(key)) throw fail2(`${shown(key)} is not a top-level key here (accepted: ${MCP_TOP_KEYS.join(", ")})`);
10678
+ }
10679
+ if (value.autoEnableCodemode !== void 0 && typeof value.autoEnableCodemode !== "boolean") throw fail2("`autoEnableCodemode` must be true or false");
10680
+ if (value.codemodeModels !== void 0 && typeof value.codemodeModels !== "boolean") throw fail2("`codemodeModels` must be true or false");
10681
+ if (value.mcpServers !== void 0 && !isRecord(value.mcpServers)) throw fail2("`mcpServers` must be an object of servers by name");
10682
+ const servers = {};
10683
+ const refused = [];
10684
+ for (const [name, raw] of Object.entries(value.mcpServers ?? {})) {
10685
+ const entry = checkServer(name, raw);
10686
+ if (entry.problem) refused.push(entry);
10687
+ else servers[name] = entry;
10688
+ }
10689
+ return { autoEnableCodemode: value.autoEnableCodemode ?? true, codemodeModels: value.codemodeModels ?? true, servers, refused };
10690
+ }
10691
+ function mcpServerWhere(server) {
10692
+ if (server.transport === "http") return URL.canParse(server.url) ? new URL(server.url).host : "an unreadable url";
10693
+ return basename(server.command);
10694
+ }
10695
+ function readOverlayMcp(globalPiDir, { readFileSync: readFileSync11 = fsReadFileSync, lstatSync: lstatSync5 = fsLstatSync } = {}) {
10696
+ if (typeof globalPiDir !== "string" || globalPiDir === "") return null;
10697
+ const path6 = join(globalPiDir, MCP_CONFIG_FILE_NAME);
10698
+ const fail2 = (why) => {
10699
+ const error = new Error(`${path6}: ${why}`);
10700
+ error.mcpConfig = true;
10701
+ return error;
10702
+ };
10703
+ let text;
10704
+ try {
10705
+ const entry = lstatSync5(path6);
10706
+ if (entry.isSymbolicLink()) throw fail2("is a link; replace it with the file itself (the job's read-only mount cannot follow links reliably)");
10707
+ if (!entry.isFile()) throw fail2("is not a regular file; replace it with the file itself");
10708
+ if (entry.size > MCP_CONFIG_MAX_BYTES) throw fail2(`is larger than ${MCP_CONFIG_MAX_BYTES} bytes`);
10709
+ text = String(readFileSync11(path6, "utf8"));
10710
+ } catch (error) {
10711
+ if (ABSENT_CODES.has(error?.code)) return null;
10712
+ throw error;
10713
+ }
10714
+ return parseOverlayMcp(text, path6);
10715
+ }
10716
+ var MCP_CONFIG_FILE_NAME, MCP_CONFIG_MAX_BYTES, MCP_SERVER_NAME_RE, MCP_EXPOSURES, MCP_TOP_KEYS, COMMON_KEYS, MCP_HTTP_KEYS, MCP_STDIO_KEYS, MCP_REFUSED_KEYS, MCP_PACKAGE_RUNNERS, MCP_COMMAND_ROOTS, MCP_HEADER_SCHEMES, RESERVED_NAMES, HEADER_NAME_RE, ENV_REF_RE, HEADER_REF_RE, REF_RE, shown, FORGE_MCP_SERVER_NAME, FORGE_MCP_KINDS, ABSENT_CODES;
10717
+ var init_mcp_config = __esm({
10718
+ "../worker/src/mcp-config.mjs"() {
10719
+ init_json_duplicates();
10720
+ MCP_CONFIG_FILE_NAME = "mcp.json";
10721
+ MCP_CONFIG_MAX_BYTES = 64 * 1024;
10722
+ MCP_SERVER_NAME_RE = /^(?=[a-z0-9-]{1,32}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
10723
+ MCP_EXPOSURES = Object.freeze(["codemode", "deferred", "direct", "hidden"]);
10724
+ MCP_TOP_KEYS = Object.freeze(["autoEnableCodemode", "codemodeModels", "mcpServers"]);
10725
+ COMMON_KEYS = ["type", "description", "exposure", "toolExposure", "timeout", "enabled"];
10726
+ MCP_HTTP_KEYS = Object.freeze([...COMMON_KEYS, "url", "headers"]);
10727
+ MCP_STDIO_KEYS = Object.freeze([...COMMON_KEYS, "command", "args", "env", "allowPackageRunner"]);
10728
+ MCP_REFUSED_KEYS = Object.freeze({
10729
+ auth: "it sends a pi login's token, which a job does not hold",
10730
+ oauth: "OAuth needs a sign-in in a browser and a token file, and a job has neither",
10731
+ cwd: "a job runs its servers in the checkout"
10732
+ });
10733
+ MCP_PACKAGE_RUNNERS = Object.freeze(["npx", "npm", "pnpm", "pnpx", "yarn", "bunx", "bun", "uvx", "uv", "pipx"]);
10734
+ MCP_COMMAND_ROOTS = Object.freeze(["/usr", "/bin", "/sbin", "/opt", "/app"]);
10735
+ MCP_HEADER_SCHEMES = Object.freeze(["Bearer", "Basic", "Token", "Bot", "ApiKey", "Key", "Splunk", "SSWS", "Digest"]);
10736
+ RESERVED_NAMES = ["__proto__", "constructor", "prototype"];
10737
+ HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
10738
+ ENV_REF_RE = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;
10739
+ HEADER_REF_RE = new RegExp(`^(?:(?:${MCP_HEADER_SCHEMES.join("|")}) )?\\$\\{([A-Z_][A-Z0-9_]*)\\}$`, "i");
10740
+ REF_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
10741
+ shown = (name) => JSON.stringify(name);
10742
+ FORGE_MCP_SERVER_NAME = "forge";
10743
+ FORGE_MCP_KINDS = Object.freeze(["github", "gitlab"]);
10744
+ ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ELOOP", "ENOTDIR", "ENAMETOOLONG"]);
10745
+ }
10746
+ });
10747
+
10532
10748
  // ../worker/src/budget.mjs
10533
10749
  function dayKey(now = /* @__PURE__ */ new Date(), prefix = "budget") {
10534
10750
  return `${prefix}:${now.toISOString().slice(0, 10)}`;
@@ -10560,7 +10776,7 @@ var init_budget = __esm({
10560
10776
  });
10561
10777
 
10562
10778
  // ../worker/src/pause-windows.mjs
10563
- import { isAbsolute as isAbsolute4 } from "node:path";
10779
+ import { isAbsolute as isAbsolute5 } from "node:path";
10564
10780
  function isNonEmptyString2(value) {
10565
10781
  return typeof value === "string" && value.trim() !== "";
10566
10782
  }
@@ -10668,8 +10884,8 @@ function zonedParts(tz, ms) {
10668
10884
  }
10669
10885
  function wallToMs(tz, y, mo, d, hh, mi) {
10670
10886
  const guess = Date.UTC(y, mo - 1, d, hh, mi);
10671
- const shown = zonedParts(tz, guess);
10672
- const shownAsUtc = Date.UTC(shown.y, shown.mo - 1, shown.d, shown.hh, shown.mi);
10887
+ const shown2 = zonedParts(tz, guess);
10888
+ const shownAsUtc = Date.UTC(shown2.y, shown2.mo - 1, shown2.d, shown2.hh, shown2.mi);
10673
10889
  return guess - (shownAsUtc - guess);
10674
10890
  }
10675
10891
  function shiftDay(tz, parts, delta) {
@@ -10700,7 +10916,7 @@ function windowEndAt(w, nowMs2) {
10700
10916
  }
10701
10917
  function parseScopeString(text) {
10702
10918
  const scope = String(text).trim().normalize("NFC");
10703
- if (isAbsolute4(scope) || /^[A-Za-z]:/.test(scope)) return { type: "local", kind: null, repo: null };
10919
+ if (isAbsolute5(scope) || /^[A-Za-z]:/.test(scope)) return { type: "local", kind: null, repo: null };
10704
10920
  const m2 = /^([^/\\:]+):(.*)$/s.exec(scope);
10705
10921
  if (m2 === null) return { type: "bare", kind: null, repo: null };
10706
10922
  if (!isForgeKind(m2[1])) throw configError4(`scope ${JSON.stringify(scope)} starts with an unknown prefix "${m2[1]}:" (a forge-qualified scope starts with one of ${FORGE_KINDS.join(", ")}, such as github:owner/name)`);
@@ -10746,7 +10962,7 @@ var init_fleet_lease = __esm({
10746
10962
  });
10747
10963
 
10748
10964
  // ../worker/src/scoped-limits.mjs
10749
- import { isAbsolute as isAbsolute5, resolve } from "node:path";
10965
+ import { isAbsolute as isAbsolute6, resolve } from "node:path";
10750
10966
  function isModelScope(scope) {
10751
10967
  return typeof scope === "string" && scope.startsWith(MODEL_SCOPE_PREFIX);
10752
10968
  }
@@ -10869,7 +11085,7 @@ function normalizeLimit(row, index, path6, version = SCOPED_LIMITS_VERSION) {
10869
11085
  // foreign-platform row (a windows drive path on a POSIX worker) stays verbatim and is inert here;
10870
11086
  // the doctor's unreferenced-scope advisory names it. Resolving it instead would "work" only by
10871
11087
  // both sides mangling into the same cwd-prefixed string -- a match by accident, not by contract.
10872
- scope: isProjectScope(trimmed) ? trimmed : form.type === "qualified" ? `${form.kind}:${form.repo}` : isAbsolute5(trimmed) ? resolve(trimmed) : trimmed,
11088
+ scope: isProjectScope(trimmed) ? trimmed : form.type === "qualified" ? `${form.kind}:${form.repo}` : isAbsolute6(trimmed) ? resolve(trimmed) : trimmed,
10873
11089
  day: null,
10874
11090
  week: null,
10875
11091
  month: null,
@@ -11015,7 +11231,7 @@ var init_fingerprint = __esm({
11015
11231
  });
11016
11232
 
11017
11233
  // ../worker/src/projects.mjs
11018
- import { isAbsolute as isAbsolute6, resolve as resolve2 } from "node:path";
11234
+ import { isAbsolute as isAbsolute7, resolve as resolve2 } from "node:path";
11019
11235
  function parseProjects(text, path6) {
11020
11236
  try {
11021
11237
  return parseProjectsText(text, path6);
@@ -11103,7 +11319,7 @@ function normalizeMember(raw, at, path6) {
11103
11319
  if (form.type === "bare") {
11104
11320
  throw configError4(`${at}: ${JSON.stringify(trimmed)} is a bare repo, which names that repo on every forge; write it with its forge, such as github:${trimmed}, or give an absolute folder: ${path6}`);
11105
11321
  }
11106
- if (!isAbsolute6(trimmed)) throw configError4(`${at}: ${JSON.stringify(trimmed)} is not an absolute path on this host: ${path6}`);
11322
+ if (!isAbsolute7(trimmed)) throw configError4(`${at}: ${JSON.stringify(trimmed)} is not an absolute path on this host: ${path6}`);
11107
11323
  return resolve2(trimmed);
11108
11324
  }
11109
11325
  function projectsFingerprintInput(projects) {
@@ -11252,7 +11468,7 @@ function parseRepos(repos, projectId, members) {
11252
11468
  if (!isPlainObject(repo)) return refuse(PLAN_INVALID, "projects.repos", "shape");
11253
11469
  if (unknownKey(repo, REPO_KEYS)) return refuse(PLAN_INVALID, "projects.repos", "unknown-key");
11254
11470
  if (repo.ref === void 0) return refuse(PLAN_INVALID, "projects.repos.ref", "missing");
11255
- if (typeof repo.ref !== "string" || !REF_RE.test(repo.ref)) return refuse(PLAN_INVALID, "projects.repos.ref", "format");
11471
+ if (typeof repo.ref !== "string" || !REF_RE2.test(repo.ref)) return refuse(PLAN_INVALID, "projects.repos.ref", "format");
11256
11472
  if (out.some((r2) => r2.ref === repo.ref)) return refuse(PLAN_INVALID, "projects.repos.ref", "duplicate");
11257
11473
  if (known && !known.includes(repo.ref)) return refuse(PLAN_INVALID, "projects.repos.ref", "unknown-ref");
11258
11474
  if (repo.weight === void 0) return refuse(PLAN_INVALID, "projects.repos.weight", "missing");
@@ -11448,7 +11664,7 @@ function rebase(vector, envelope) {
11448
11664
  for (const id of entries) alloc[id] = floors[id] + shares[id];
11449
11665
  return toNumbers(alloc, anyWeight ? shares[UNALLOCATED] : remainder);
11450
11666
  }
11451
- var PLAN_VERSION, WEIGHT_MAX, REASON_MAX, PLAN_MAX_BYTES, DEFAULT_MAX_PLAN_DAYS, OTHER, PLAN_WRITERS, PLAN_INVALID, PLAN_INCOMPLETE, PLAN_FIELDS, PLAN_RULES, PLAN_LADDER, PLAN_KEYS, PROJECT_KEYS, REPO_KEYS, BASIS_RE, REF_RE, INSTANT_RE, REASON_REFUSED, LIST_MAX, DAY_MS, MAX_PLAN_DAYS, MAX_DATE_MS, UNALLOCATED;
11667
+ var PLAN_VERSION, WEIGHT_MAX, REASON_MAX, PLAN_MAX_BYTES, DEFAULT_MAX_PLAN_DAYS, OTHER, PLAN_WRITERS, PLAN_INVALID, PLAN_INCOMPLETE, PLAN_FIELDS, PLAN_RULES, PLAN_LADDER, PLAN_KEYS, PROJECT_KEYS, REPO_KEYS, BASIS_RE, REF_RE2, INSTANT_RE, REASON_REFUSED, LIST_MAX, DAY_MS, MAX_PLAN_DAYS, MAX_DATE_MS, UNALLOCATED;
11452
11668
  var init_priorities = __esm({
11453
11669
  "../worker/src/priorities.mjs"() {
11454
11670
  init_project_id();
@@ -11481,7 +11697,7 @@ var init_priorities = __esm({
11481
11697
  PROJECT_KEYS = /* @__PURE__ */ new Set(["id", "weight", "reason", "repos"]);
11482
11698
  REPO_KEYS = /* @__PURE__ */ new Set(["ref", "weight"]);
11483
11699
  BASIS_RE = /^[0-9a-f]{16}$/;
11484
- REF_RE = /^[0-9a-f]{8}$/;
11700
+ REF_RE2 = /^[0-9a-f]{8}$/;
11485
11701
  INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
11486
11702
  REASON_REFUSED = /[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}\p{Zl}\p{Zp}]/u;
11487
11703
  LIST_MAX = 256;
@@ -11634,7 +11850,7 @@ var init_envelope = __esm({
11634
11850
  // ../worker/src/allocation.mjs
11635
11851
  import { randomUUID } from "node:crypto";
11636
11852
  import * as nodeFs2 from "node:fs";
11637
- import { isAbsolute as isAbsolute7, join } from "node:path";
11853
+ import { isAbsolute as isAbsolute8, join as join2 } from "node:path";
11638
11854
  function iso(now) {
11639
11855
  return new Date(now instanceof Date ? now.getTime() : now).toISOString();
11640
11856
  }
@@ -11713,12 +11929,12 @@ function auditRow({ at, host, writer, outcome, reason = null, field = null, rule
11713
11929
  };
11714
11930
  }
11715
11931
  function makeAllocationAudit({ logsDir, fs: fs7 = nodeFs2 }) {
11716
- const dir = join(logsDir, "allocations");
11932
+ const dir = join2(logsDir, "allocations");
11717
11933
  return {
11718
11934
  dir,
11719
11935
  append(row) {
11720
11936
  fs7.mkdirSync(dir, { recursive: true, mode: 448 });
11721
- fs7.appendFileSync(join(dir, `${String(row.at).slice(0, 7)}.jsonl`), `${JSON.stringify(row)}
11937
+ fs7.appendFileSync(join2(dir, `${String(row.at).slice(0, 7)}.jsonl`), `${JSON.stringify(row)}
11722
11938
  `, { flag: "a", mode: 384 });
11723
11939
  }
11724
11940
  };
@@ -12306,7 +12522,7 @@ __export(valkey_auth_exports, {
12306
12522
  volumeRecordText: () => volumeRecordText
12307
12523
  });
12308
12524
  import { randomBytes } from "node:crypto";
12309
- import { basename, join as join2 } from "node:path";
12525
+ import { basename as basename2, join as join3 } from "node:path";
12310
12526
  function newValkeyPassword(random = randomBytes) {
12311
12527
  return random(32).toString("hex");
12312
12528
  }
@@ -12411,7 +12627,7 @@ function valkeyPasswordDecision(keys, { envPath = ".env" } = {}) {
12411
12627
  return { password: null, generate: true, note: null };
12412
12628
  }
12413
12629
  function composeProjectName(dir) {
12414
- const name = basename(String(dir)).toLowerCase().replace(/[^a-z0-9_-]/g, "").replace(/^[_-]+/, "");
12630
+ const name = basename2(String(dir)).toLowerCase().replace(/[^a-z0-9_-]/g, "").replace(/^[_-]+/, "");
12415
12631
  return name === "" ? "pi-dispatch" : name;
12416
12632
  }
12417
12633
  function composeArgs({ project = null, override = false } = {}) {
@@ -12419,7 +12635,7 @@ function composeArgs({ project = null, override = false } = {}) {
12419
12635
  }
12420
12636
  function valkeyContainerIsOurs(info, { dirs, port }) {
12421
12637
  const labels = info?.Config?.Labels ?? {};
12422
- const mine = new Set(dirs.filter(Boolean).flatMap((d) => [d, join2(d, "deploy")]));
12638
+ const mine = new Set(dirs.filter(Boolean).flatMap((d) => [d, join3(d, "deploy")]));
12423
12639
  if (typeof labels[DEPLOYMENT_LABEL] === "string") return mine.has(labels[DEPLOYMENT_LABEL]);
12424
12640
  const wd = labels["com.docker.compose.project.working_dir"];
12425
12641
  if (typeof wd === "string") return mine.has(wd);
@@ -12528,7 +12744,7 @@ function volumeRecordText(volume) {
12528
12744
  }
12529
12745
  function readVolumeRecord(dir, fs7) {
12530
12746
  try {
12531
- const path6 = join2(dir, VALKEY_VOLUME_RECORD);
12747
+ const path6 = join3(dir, VALKEY_VOLUME_RECORD);
12532
12748
  if (!fs7.existsSync(path6)) return null;
12533
12749
  const r2 = JSON.parse(String(fs7.readFileSync(path6, "utf8")));
12534
12750
  return r2 && typeof r2 === "object" ? r2 : null;
@@ -14188,8 +14404,8 @@ function unplainCause(value) {
14188
14404
  if (v.startsWith("=")) return "an = at the start of an unquoted value, which zsh expands as a command name. Quote the whole value";
14189
14405
  if (v.includes(":=")) return "a := in an unquoted value, which zsh expands as a command name. Quote the whole value";
14190
14406
  const bad = /[^A-Za-z0-9_@+=:,./-]/u.exec(v)?.[0];
14191
- const shown = bad === void 0 ? "" : /^[\x21-\x7e]$/.test(bad) ? `\`${bad}\` ` : `U+${bad.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")} `;
14192
- return `a character (${shown.trim() || "one"}) outside A-Z, a-z, 0-9 and _@+=:,./- in an unquoted value. Quote the whole value`;
14407
+ const shown2 = bad === void 0 ? "" : /^[\x21-\x7e]$/.test(bad) ? `\`${bad}\` ` : `U+${bad.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")} `;
14408
+ return `a character (${shown2.trim() || "one"}) outside A-Z, a-z, 0-9 and _@+=:,./- in an unquoted value. Quote the whole value`;
14193
14409
  }
14194
14410
  var NETNS_KEEPER_START, QUADLET_FILES, ALL_QUADLET_FILES, DEFAULT_VALKEY_PORT, VALKEY_SHARED_KEY, STACK_KEYS, STACK_VALUE_MAX;
14195
14411
  var init_podman_stack = __esm({
@@ -14220,7 +14436,7 @@ var init_podman_stack = __esm({
14220
14436
  import { lookup as dnsLookup } from "node:dns/promises";
14221
14437
  import { readFileSync as readFileSync2 } from "node:fs";
14222
14438
  import { networkInterfaces, userInfo } from "node:os";
14223
- import { join as join3 } from "node:path";
14439
+ import { join as join4 } from "node:path";
14224
14440
  function valkeyRefusal(message) {
14225
14441
  return Object.assign(new Error(message), { piDispatchConfig: true, valkeyRefused: true });
14226
14442
  }
@@ -14231,7 +14447,7 @@ function defaultValkeyContext() {
14231
14447
  return contextProvider ? contextProvider() : valkeyClientContext();
14232
14448
  }
14233
14449
  function valkeyClientContext({ env: env2 = process.env, cwd = process.cwd(), platform = process.platform, rootRefused, readEnv: readEnv3 = (p) => readFileSync2(p) } = {}) {
14234
- const envPath = join3(cwd, ".env");
14450
+ const envPath = join4(cwd, ".env");
14235
14451
  let content = null;
14236
14452
  let error = null;
14237
14453
  try {
@@ -14576,9 +14792,9 @@ var init_connection = __esm({
14576
14792
  // src/deployment-pointer.mjs
14577
14793
  import * as nodeFs3 from "node:fs";
14578
14794
  import { homedir as homedir2 } from "node:os";
14579
- import { join as join4, isAbsolute as isAbsolute8 } from "node:path";
14795
+ import { join as join5, isAbsolute as isAbsolute9 } from "node:path";
14580
14796
  function pointerPath(env2 = process.env) {
14581
- return env2.PI_DISPATCH_DEPLOYMENT_FILE || join4(env2.PI_CODING_AGENT_DIR || join4(homedir2(), ".pi", "agent"), POINTER_BASENAME);
14797
+ return env2.PI_DISPATCH_DEPLOYMENT_FILE || join5(env2.PI_CODING_AGENT_DIR || join5(homedir2(), ".pi", "agent"), POINTER_BASENAME);
14582
14798
  }
14583
14799
  function normalizePointer(raw) {
14584
14800
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
@@ -14590,7 +14806,7 @@ function normalizePointer(raw) {
14590
14806
  if (raw.version > POINTER_VERSION) {
14591
14807
  return { ignored: `version ${raw.version} is newer than the supported version ${POINTER_VERSION}` };
14592
14808
  }
14593
- if (typeof raw.deploymentDir !== "string" || !isAbsolute8(raw.deploymentDir)) {
14809
+ if (typeof raw.deploymentDir !== "string" || !isAbsolute9(raw.deploymentDir)) {
14594
14810
  return { ignored: "deploymentDir must be an absolute path" };
14595
14811
  }
14596
14812
  const rawEnv = raw.env === void 0 ? {} : raw.env;
@@ -14601,7 +14817,7 @@ function normalizePointer(raw) {
14601
14817
  for (const key of POINTER_ENV_ALLOWLIST) {
14602
14818
  const value = rawEnv[key];
14603
14819
  if (typeof value !== "string" || value === "") continue;
14604
- if (key !== "VALKEY_URL" && !isAbsolute8(value)) continue;
14820
+ if (key !== "VALKEY_URL" && !isAbsolute9(value)) continue;
14605
14821
  env2[key] = value;
14606
14822
  }
14607
14823
  return { pointer: { version: raw.version, deploymentDir: raw.deploymentDir, env: env2 } };
@@ -15663,7 +15879,7 @@ return {size, oldest[2] or false, horizon, at, sinceHeld() and 1 or 0, range}
15663
15879
 
15664
15880
  // ../worker/src/size-records.mjs
15665
15881
  import { readdirSync, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
15666
- import { join as join5 } from "node:path";
15882
+ import { join as join6 } from "node:path";
15667
15883
  function readSizingRecords(logsDir, { nowMs: nowMs2, fs: fs7 = { readdirSync, readFileSync: readFileSync3, statSync: statSync4 } } = {}) {
15668
15884
  const since = nowMs2 - SIZING_MTIME_DAYS * DAY_MS5;
15669
15885
  let names;
@@ -15677,7 +15893,7 @@ function readSizingRecords(logsDir, { nowMs: nowMs2, fs: fs7 = { readdirSync, re
15677
15893
  for (const name of names) {
15678
15894
  if (typeof name !== "string" || !name.endsWith(".json")) continue;
15679
15895
  try {
15680
- const path6 = join5(logsDir, name);
15896
+ const path6 = join6(logsDir, name);
15681
15897
  const st = fs7.statSync(path6);
15682
15898
  if (!st.isFile() || st.mtimeMs < since) continue;
15683
15899
  if (st.size > SIZING_RECORD_MAX_BYTES) {
@@ -15718,7 +15934,7 @@ var init_size_records = __esm({
15718
15934
 
15719
15935
  // ../worker/src/capacity-records.mjs
15720
15936
  import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync5 } from "node:fs";
15721
- import { join as join6 } from "node:path";
15937
+ import { join as join7 } from "node:path";
15722
15938
  function bounded3(promise, ms) {
15723
15939
  return new Promise((resolve5, reject) => {
15724
15940
  const t2 = setTimeout(() => reject(new Error("timeout")), ms);
@@ -15792,7 +16008,7 @@ function readLocalWindow(logsDir, { sinceMs, fs: fs7 = { readdirSync: readdirSyn
15792
16008
  for (const name of names) {
15793
16009
  if (typeof name !== "string" || !name.endsWith(".json")) continue;
15794
16010
  try {
15795
- const path6 = join6(logsDir, name);
16011
+ const path6 = join7(logsDir, name);
15796
16012
  const st = fs7.statSync(path6);
15797
16013
  if (!st.isFile() || st.mtimeMs < sinceMs - DAY_MS6) continue;
15798
16014
  if (st.size > SIZING_RECORD_MAX_BYTES) {
@@ -15863,9 +16079,9 @@ var init_capacity_records = __esm({
15863
16079
  function onlyHost(report, host) {
15864
16080
  const known = report.hosts.map((h2) => h2.name);
15865
16081
  if (!known.includes(host)) return { unknown: known };
15866
- const shown = report.hosts.find((h2) => h2.name === host);
15867
- const { fromMs, source: _source, truncated, ...counts } = shown.coverage;
15868
- return { report: { ...report, hosts: [shown], coverage: { ...report.coverage, fromMs, truncated, ...counts, historyNotShared: report.coverage.historyNotShared.filter((n) => n === host) } } };
16082
+ const shown2 = report.hosts.find((h2) => h2.name === host);
16083
+ const { fromMs, source: _source, truncated, ...counts } = shown2.coverage;
16084
+ return { report: { ...report, hosts: [shown2], coverage: { ...report.coverage, fromMs, truncated, ...counts, historyNotShared: report.coverage.historyNotShared.filter((n) => n === host) } } };
15869
16085
  }
15870
16086
  async function deploymentFacts(env2) {
15871
16087
  const raw = env2.PI_LOG_RETENTION_DAYS;
@@ -16245,6 +16461,9 @@ var init_provider_steering = __esm({
16245
16461
  });
16246
16462
 
16247
16463
  // ../worker/src/triggers.mjs
16464
+ function mcpServerToolPattern(name) {
16465
+ return `mcp__${String(name).replace(/-/g, "_")}__*`;
16466
+ }
16248
16467
  function isNonEmptyString5(value) {
16249
16468
  return typeof value === "string" && value.trim() !== "";
16250
16469
  }
@@ -16357,11 +16576,12 @@ function normalizeCron(on, run, index, path6, state2) {
16357
16576
  const backend = validateBackend(on, run, `cron trigger "${id}"`, path6, { localWorkspace: true });
16358
16577
  validateWaitFor(on, run, `cron trigger "${id}"`, path6, { onType: "cron" });
16359
16578
  const excludeTools = validateExcludeTools(on, run, `cron trigger "${id}"`, path6);
16579
+ const mcp = validateMcp(on, run, `cron trigger "${id}"`, path6, { kind: "local" });
16360
16580
  const ref = validateRunModel(on, run, `cron trigger "${id}"`, path6);
16361
16581
  const portfolio = validatePortfolio(run, command, `cron trigger "${id}"`, path6);
16362
16582
  return {
16363
16583
  on: { type: "cron", id, pattern },
16364
- run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: ref.provider, model: ref.model, maxTurns: ref.maxTurns, github: run.github, packages, image, resume, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...ref.models !== void 0 && { models: ref.models }, ...ref.maxCostUsd !== void 0 && { maxCostUsd: ref.maxCostUsd }, ...portfolio !== void 0 && { portfolio } }
16584
+ run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: ref.provider, model: ref.model, maxTurns: ref.maxTurns, github: run.github, packages, image, resume, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...mcp !== void 0 && { mcp }, ...ref.models !== void 0 && { models: ref.models }, ...ref.maxCostUsd !== void 0 && { maxCostUsd: ref.maxCostUsd }, ...portfolio !== void 0 && { portfolio } }
16365
16585
  };
16366
16586
  }
16367
16587
  function validatePortfolio(run, command, at, path6) {
@@ -16728,14 +16948,20 @@ function validateExcludeTools(on, run, at, path6) {
16728
16948
  if (!Array.isArray(excludeTools) || excludeTools.length === 0) {
16729
16949
  throw configError4(`${at}: run.excludeTools must be a non-empty array of tool names when present (got ${JSON.stringify(excludeTools)}) -- an empty exclusion is a field that does nothing, and accepted-and-ignored is how an operator comes to trust one that does: ${path6}`);
16730
16950
  }
16951
+ const known = new Set(EXCLUDABLE_TOOL_NAMES);
16952
+ if (Array.isArray(run?.mcp) && run.mcp.length > 0) {
16953
+ for (const name of MCP_EXCLUDABLE_TOOL_NAMES) known.add(name);
16954
+ for (const name of run.mcp) if (typeof name === "string" && MCP_SERVER_NAME_RE.test(name)) known.add(mcpServerToolPattern(name));
16955
+ }
16731
16956
  const seen = /* @__PURE__ */ new Set();
16732
16957
  for (let i2 = 0; i2 < excludeTools.length; i2++) {
16733
16958
  const name = excludeTools[i2];
16734
16959
  if (!isNonEmptyString5(name)) {
16735
16960
  throw configError4(`${at}: run.excludeTools[${i2}] must be a non-empty string naming a pi tool (got ${JSON.stringify(name)}): ${path6}`);
16736
16961
  }
16737
- if (!EXCLUDABLE_TOOL_NAMES.has(name)) {
16738
- throw configError4(`${at}: run.excludeTools[${i2}] ${JSON.stringify(name)} is not a tool the pinned pi knows (known: ${[...EXCLUDABLE_TOOL_NAMES].join(", ")}) -- pi ignores unknown names silently, so a misspelled exclusion would exclude nothing while the file reads as though it did: ${path6}`);
16962
+ if (!known.has(name)) {
16963
+ const mcpHint = MCP_EXCLUDABLE_TOOL_NAMES.has(name) || name.startsWith("mcp__") ? " (an MCP tool is excludable only by a trigger whose run.mcp names its server, and a server's tools only all at once, as mcp__<server>__*)" : "";
16964
+ throw configError4(`${at}: run.excludeTools[${i2}] ${JSON.stringify(name)} is not a tool the pinned pi knows (known: ${[...known].join(", ")})${mcpHint} -- pi ignores unknown names silently, so a misspelled exclusion would exclude nothing while the file reads as though it did: ${path6}`);
16739
16965
  }
16740
16966
  if (seen.has(name)) {
16741
16967
  throw configError4(`${at}: run.excludeTools names ${JSON.stringify(name)} twice -- the entries are a removal set, so the second can never change the answer: ${path6}`);
@@ -16744,6 +16970,51 @@ function validateExcludeTools(on, run, at, path6) {
16744
16970
  }
16745
16971
  return [...excludeTools];
16746
16972
  }
16973
+ function mcpKeySkeleton(key) {
16974
+ return String(key).toLowerCase().replace(/[^a-z0-9]/g, "");
16975
+ }
16976
+ function isMcpNearMiss(key) {
16977
+ if (key === "mcp") return false;
16978
+ if (/^[\x20-\x7E]*$/.test(key)) return MCP_NEAR_MISSES.includes(mcpKeySkeleton(key));
16979
+ return [...key.matchAll(/[a-z]/gi)].every(([letter]) => "mcpservers".includes(letter.toLowerCase()));
16980
+ }
16981
+ function validateMcp(on, run, at, path6, { kind }) {
16982
+ for (const [label2, source, exactIsLegal] of [
16983
+ ["run", run, true],
16984
+ ["on", on, false]
16985
+ ]) {
16986
+ for (const key of Object.keys(source ?? {})) {
16987
+ if (exactIsLegal ? isMcpNearMiss(key) : key === "mcp" || isMcpNearMiss(key)) {
16988
+ if (!/^[\x20-\x7E]*$/.test(key)) {
16989
+ throw configError4(`${at}: ${label2}.${JSON.stringify(key)} is not a field: a key outside plain ASCII with no plain letter other than run.mcp's can be drawn exactly like run.mcp, which the loader would drop while the file reads as though it named servers. Write field names in plain ASCII: ${path6}`);
16990
+ }
16991
+ throw configError4(`${at}: ${label2}.${key} is not a field -- did you mean run.mcp? A list of MCP servers the loader drops runs the job without them while the file reads as though it named them, so a near miss is refused rather than dropped: ${path6}`);
16992
+ }
16993
+ }
16994
+ }
16995
+ const mcp = run?.mcp;
16996
+ if (mcp === void 0) return void 0;
16997
+ if (!Array.isArray(mcp) || mcp.length === 0) {
16998
+ throw configError4(`${at}: run.mcp must be a non-empty array of server names from the overlay's mcp.json when present (got ${JSON.stringify(mcp)}); leave it out for none: ${path6}`);
16999
+ }
17000
+ if (mcp.length > MCP_SERVERS_MAX) {
17001
+ throw configError4(`${at}: run.mcp names ${mcp.length} servers, more than ${MCP_SERVERS_MAX}: ${path6}`);
17002
+ }
17003
+ const seen = /* @__PURE__ */ new Set();
17004
+ for (let i2 = 0; i2 < mcp.length; i2++) {
17005
+ const name = mcp[i2];
17006
+ if (typeof name !== "string" || !MCP_SERVER_NAME_RE.test(name)) {
17007
+ throw configError4(`${at}: run.mcp[${i2}] must name a server of the overlay's mcp.json: lowercase letters and digits, words joined by single -, up to 32 (got ${JSON.stringify(name)}): ${path6}`);
17008
+ }
17009
+ if (seen.has(name)) throw configError4(`${at}: run.mcp names ${JSON.stringify(name)} twice: ${path6}`);
17010
+ seen.add(name);
17011
+ if (name === FORGE_MCP_SERVER_NAME && !FORGE_MCP_KINDS.includes(kind)) {
17012
+ const why = kind === "local" ? "a cron job holds no forge token of its own for the server to use" : `${kind} has no forge MCP server this deployment has verified yet`;
17013
+ throw configError4(`${at}: run.mcp names "forge", the job's own forge server, which a ${FORGE_MCP_KINDS.join(" or ")} trigger may name and this one may not: ${why}: ${path6}`);
17014
+ }
17015
+ }
17016
+ return [...mcp];
17017
+ }
16747
17018
  function validateWaitFor(on, run, at, path6, { onType }) {
16748
17019
  for (const [label2, source, exactIsLegal] of [
16749
17020
  ["run", run, true],
@@ -16842,10 +17113,11 @@ function normalizeLabel(on, run, index, path6) {
16842
17113
  const backend = validateBackend(on, run, at, path6, { localWorkspace: false });
16843
17114
  const waitFor = validateWaitFor(on, run, at, path6, { onType: on.type });
16844
17115
  const excludeTools = validateExcludeTools(on, run, at, path6);
17116
+ const mcp = validateMcp(on, run, at, path6, { kind: run.kind });
16845
17117
  const ref = validateRunModel(on, run, at, path6);
16846
17118
  return {
16847
17119
  on: { type: "label", any: predicate.any, all: predicate.all, none: predicate.none },
16848
- run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...ref }
17120
+ run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...mcp !== void 0 && { mcp }, ...ref }
16849
17121
  };
16850
17122
  }
16851
17123
  function normalizeComment(on, run, index, path6, state2) {
@@ -16877,10 +17149,11 @@ function normalizeComment(on, run, index, path6, state2) {
16877
17149
  const backend = validateBackend(on, run, at, path6, { localWorkspace: false });
16878
17150
  const waitFor = validateWaitFor(on, run, at, path6, { onType: on.type });
16879
17151
  const excludeTools = validateExcludeTools(on, run, at, path6);
17152
+ const mcp = validateMcp(on, run, at, path6, { kind: run.kind });
16880
17153
  const ref = validateRunModel(on, run, at, path6);
16881
17154
  return {
16882
17155
  on: { type: "comment", phrase: on.phrase },
16883
- run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...ref }
17156
+ run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...mcp !== void 0 && { mcp }, ...ref }
16884
17157
  };
16885
17158
  }
16886
17159
  function normalizeIssue(on, run, index, path6) {
@@ -16922,6 +17195,7 @@ function normalizeIssue(on, run, index, path6) {
16922
17195
  const backend = validateBackend(on, run, at, path6, { localWorkspace: false });
16923
17196
  const waitFor = validateWaitFor(on, run, at, path6, { onType: on.type });
16924
17197
  const excludeTools = validateExcludeTools(on, run, at, path6);
17198
+ const mcp = validateMcp(on, run, at, path6, { kind: run.kind });
16925
17199
  const ref = validateRunModel(on, run, at, path6);
16926
17200
  return {
16927
17201
  on: {
@@ -16932,7 +17206,7 @@ function normalizeIssue(on, run, index, path6) {
16932
17206
  ...number !== void 0 && { number },
16933
17207
  ...once !== void 0 && { once }
16934
17208
  },
16935
- run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...ref }
17209
+ run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...mcp !== void 0 && { mcp }, ...ref }
16936
17210
  };
16937
17211
  }
16938
17212
  function normalizePullRequest(on, run, index, path6) {
@@ -16983,6 +17257,7 @@ function normalizePullRequest(on, run, index, path6) {
16983
17257
  const backend = validateBackend(on, run, at, path6, { localWorkspace: false });
16984
17258
  const waitFor = validateWaitFor(on, run, at, path6, { onType: on.type });
16985
17259
  const excludeTools = validateExcludeTools(on, run, at, path6);
17260
+ const mcp = validateMcp(on, run, at, path6, { kind: run.kind });
16986
17261
  const ref = validateRunModel(on, run, at, path6);
16987
17262
  return {
16988
17263
  on: {
@@ -16998,7 +17273,7 @@ function normalizePullRequest(on, run, index, path6) {
16998
17273
  ...number !== void 0 && { number },
16999
17274
  ...once !== void 0 && { once }
17000
17275
  },
17001
- run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...ref }
17276
+ run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile }, ...waitFor !== void 0 && { waitFor }, ...backend !== void 0 && { backend }, ...excludeTools !== void 0 && { excludeTools }, ...mcp !== void 0 && { mcp }, ...ref }
17002
17277
  };
17003
17278
  }
17004
17279
  function validateReviewState(on, actions, run, at, path6) {
@@ -17020,7 +17295,7 @@ function validateReviewState(on, actions, run, at, path6) {
17020
17295
  }
17021
17296
  return [...on.reviewState];
17022
17297
  }
17023
- var ON_TYPES, DISARMED_TYPE, PR_ACTIONS, PR_CLOSE_ACTIONS, ISSUE_ACTIONS, REVIEW_STATES, REVIEW_ACTION, EXCLUDABLE_TOOL_NAMES, ID_CHARSET, REPLICAS_MAX, SECRETS_MAX, ENV_NAME, RESERVED_ENV_NAMES, INSTRUCTIONS_MAX, RUN_MODEL_NEAR_MISS_TARGETS;
17298
+ var ON_TYPES, DISARMED_TYPE, PR_ACTIONS, PR_CLOSE_ACTIONS, ISSUE_ACTIONS, REVIEW_STATES, REVIEW_ACTION, EXCLUDABLE_TOOL_NAMES, MCP_EXCLUDABLE_TOOL_NAMES, ID_CHARSET, REPLICAS_MAX, SECRETS_MAX, ENV_NAME, RESERVED_ENV_NAMES, INSTRUCTIONS_MAX, RUN_MODEL_NEAR_MISS_TARGETS, MCP_SERVERS_MAX, MCP_NEAR_MISSES;
17024
17299
  var init_triggers = __esm({
17025
17300
  "../worker/src/triggers.mjs"() {
17026
17301
  init_backends();
@@ -17029,6 +17304,7 @@ var init_triggers = __esm({
17029
17304
  init_flow_gate();
17030
17305
  init_forges();
17031
17306
  init_json_duplicates();
17307
+ init_mcp_config();
17032
17308
  init_model_ref();
17033
17309
  init_money();
17034
17310
  init_provider_steering();
@@ -17065,6 +17341,7 @@ var init_triggers = __esm({
17065
17341
  REVIEW_STATES = /* @__PURE__ */ new Set(["approved", "changes_requested", "commented"]);
17066
17342
  REVIEW_ACTION = "review_submitted";
17067
17343
  EXCLUDABLE_TOOL_NAMES = /* @__PURE__ */ new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
17344
+ MCP_EXCLUDABLE_TOOL_NAMES = /* @__PURE__ */ new Set(["codemode", "tool_search", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"]);
17068
17345
  ID_CHARSET = /^[A-Za-z0-9._-]+$/;
17069
17346
  REPLICAS_MAX = 3;
17070
17347
  SECRETS_MAX = 16;
@@ -17090,6 +17367,8 @@ var init_triggers = __esm({
17090
17367
  maxcostusd: "run.maxCostUsd",
17091
17368
  maxcost: "run.maxCostUsd"
17092
17369
  });
17370
+ MCP_SERVERS_MAX = 8;
17371
+ MCP_NEAR_MISSES = Object.freeze(["mcp", "mcps", "mcpserver", "mcpservers"]);
17093
17372
  }
17094
17373
  });
17095
17374
 
@@ -17138,7 +17417,7 @@ function swallowedRunSentence(jobId, existing) {
17138
17417
  const hhmm = at ? ` at ${String(at.getHours()).padStart(2, "0")}:${String(at.getMinutes()).padStart(2, "0")}` : "";
17139
17418
  return `an identical run was queued${hhmm} as ${jobId} (${existing?.state ?? "already queued or done"}); nothing new was queued.`;
17140
17419
  }
17141
- async function addLocalJob(queue, { folder, flow, task, command, provider, model, maxTurns, models, maxCostUsd, image, backend, excludeTools, skillsDir, secrets, secretsProfile, chainDepth, parentJobId, jobId, enqueueNonce, now = /* @__PURE__ */ new Date() }) {
17420
+ async function addLocalJob(queue, { folder, flow, task, command, provider, model, maxTurns, models, maxCostUsd, image, backend, excludeTools, mcp, skillsDir, secrets, secretsProfile, chainDepth, parentJobId, jobId, enqueueNonce, now = /* @__PURE__ */ new Date() }) {
17142
17421
  const minute = now.toISOString().slice(0, 16);
17143
17422
  const id = jobId ?? localJobId({ folder, flow: command !== void 0 ? `cmd:${command}` : flow, task, minute, provider, model, models });
17144
17423
  const data = {
@@ -17169,6 +17448,7 @@ async function addLocalJob(queue, { folder, flow, task, command, provider, model
17169
17448
  // same reason: `trigger` is copied verbatim into /job/event.json, and a permission boundary is the
17170
17449
  // worker's business, never agent-visible input to reason about.
17171
17450
  ...excludeTools !== void 0 && { excludeTools },
17451
+ ...mcp !== void 0 && { mcp },
17172
17452
  // The host directory of operator-authored skills this trigger injects (REQ-PER-TRIGGER-SKILLS).
17173
17453
  // Conditional like `image`, so an unflagged job's data stays byte-identical, and at JOB level rather
17174
17454
  // than inside `trigger` because a worker-host path is an execution knob, not a fact about the
@@ -17241,7 +17521,7 @@ var init_queue = __esm({
17241
17521
  // ../worker/src/git-dirty.mjs
17242
17522
  import { execFileSync as execFileSync2 } from "node:child_process";
17243
17523
  import { existsSync as existsSync2 } from "node:fs";
17244
- import { join as join7 } from "node:path";
17524
+ import { join as join8 } from "node:path";
17245
17525
  function gitDirty(folder, { exec: exec4 = execFileSync2 } = {}) {
17246
17526
  try {
17247
17527
  const out = exec4("git", [...GIT_READ_FLAGS, "-C", folder, "status", "--porcelain"], { encoding: "utf8" });
@@ -17251,7 +17531,7 @@ function gitDirty(folder, { exec: exec4 = execFileSync2 } = {}) {
17251
17531
  }
17252
17532
  }
17253
17533
  function localRepoProblem(folder, { exists = existsSync2, exec: exec4 = execFileSync2 } = {}) {
17254
- if (!exists(join7(folder, ".git"))) {
17534
+ if (!exists(join8(folder, ".git"))) {
17255
17535
  const root = enclosingRepoRoot(folder, exec4);
17256
17536
  if (root) return `${folder} is not the root of a git repository, and a local job needs one. Run \`git init\` here and commit, or, if this folder belongs to the repository at ${root}, run it on ${root}.`;
17257
17537
  return `${folder} is not a git repository. A local job needs one: run \`git init\` there and commit, then run again.`;
@@ -17328,6 +17608,7 @@ var init_import_pi = __esm({
17328
17608
  init_packages();
17329
17609
  init_host_pi();
17330
17610
  init_copy_tree();
17611
+ init_mcp_config();
17331
17612
  execFileAsync = promisify2(execFile2);
17332
17613
  }
17333
17614
  });
@@ -17444,13 +17725,13 @@ var init_container_spec = __esm({
17444
17725
 
17445
17726
  // ../worker/src/packages.mjs
17446
17727
  import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
17447
- import { basename as basename2, dirname as dirname2, join as join8, resolve as resolve3 } from "node:path";
17728
+ import { basename as basename3, dirname as dirname2, join as join9, resolve as resolve3 } from "node:path";
17448
17729
  function isNonEmptyString6(value) {
17449
17730
  return typeof value === "string" && value.trim() !== "";
17450
17731
  }
17451
17732
  function readStageManifest({ globalPiDir, readFile: readFile2 = readFileSync5, fileExists = existsSync3 } = {}) {
17452
17733
  if (!globalPiDir) return null;
17453
- const path6 = join8(globalPiDir, PACKAGES_SUBDIR, STAGE_MANIFEST);
17734
+ const path6 = join9(globalPiDir, PACKAGES_SUBDIR, STAGE_MANIFEST);
17454
17735
  try {
17455
17736
  if (!fileExists(path6)) return null;
17456
17737
  const parsed = JSON.parse(readFile2(path6, "utf8"));
@@ -17473,10 +17754,10 @@ function readStagedSkills({ globalPiDir, readFile: readFile2 = readFileSync5, fi
17473
17754
  const manifest = readStageManifest({ globalPiDir, readFile: readFile2, fileExists });
17474
17755
  if (!manifest) return out;
17475
17756
  for (const pkg of manifest.packages) {
17476
- const root = join8(globalPiDir, PACKAGES_SUBDIR, pkg.dir);
17757
+ const root = join9(globalPiDir, PACKAGES_SUBDIR, pkg.dir);
17477
17758
  let sources;
17478
17759
  try {
17479
- const parsed = JSON.parse(readFile2(join8(root, "package.json"), "utf8"));
17760
+ const parsed = JSON.parse(readFile2(join9(root, "package.json"), "utf8"));
17480
17761
  const pi = parsed !== null && typeof parsed === "object" && parsed.pi !== null && typeof parsed.pi === "object" ? parsed.pi : null;
17481
17762
  if (pi) {
17482
17763
  const entries = Array.isArray(pi.skills) ? pi.skills.filter((e2) => typeof e2 === "string") : [];
@@ -17486,23 +17767,23 @@ function readStagedSkills({ globalPiDir, readFile: readFile2 = readFileSync5, fi
17486
17767
  }
17487
17768
  sources = entries.filter((e2) => !e2.split(/[\\/]/).includes("..")).map((e2) => resolve3(root, e2));
17488
17769
  } else {
17489
- sources = [join8(root, "skills")];
17770
+ sources = [join9(root, "skills")];
17490
17771
  }
17491
17772
  } catch {
17492
17773
  continue;
17493
17774
  }
17494
17775
  for (const source of sources) {
17495
17776
  for (const skillFile of walkSkillFiles(source, fileExists, readDir)) {
17496
- out.skills.push({ name: basename2(dirname2(skillFile)), package: pkg.name, dir: pkg.dir });
17777
+ out.skills.push({ name: basename3(dirname2(skillFile)), package: pkg.name, dir: pkg.dir });
17497
17778
  }
17498
17779
  }
17499
17780
  }
17500
17781
  return out;
17501
17782
  }
17502
17783
  function walkSkillFiles(source, fileExists, readDir, depth = 3) {
17503
- if (basename2(source) === "SKILL.md") return fileExists(source) ? [source] : [];
17784
+ if (basename3(source) === "SKILL.md") return fileExists(source) ? [source] : [];
17504
17785
  const found = [];
17505
- const own = join8(source, "SKILL.md");
17786
+ const own = join9(source, "SKILL.md");
17506
17787
  if (fileExists(own)) found.push(own);
17507
17788
  if (depth === 0) return found;
17508
17789
  let children;
@@ -17513,7 +17794,7 @@ function walkSkillFiles(source, fileExists, readDir, depth = 3) {
17513
17794
  }
17514
17795
  for (const child of children) {
17515
17796
  if (!child.isDirectory?.()) continue;
17516
- found.push(...walkSkillFiles(join8(source, child.name), fileExists, readDir, depth - 1));
17797
+ found.push(...walkSkillFiles(join9(source, child.name), fileExists, readDir, depth - 1));
17517
17798
  }
17518
17799
  return found;
17519
17800
  }
@@ -19248,7 +19529,7 @@ var init_costs = __esm({
19248
19529
  });
19249
19530
 
19250
19531
  // src/dollar-windows.mjs
19251
- import { isAbsolute as isAbsolute9 } from "node:path";
19532
+ import { isAbsolute as isAbsolute10 } from "node:path";
19252
19533
  function deploymentDollarCaps(overlay, env2) {
19253
19534
  const caps = { day: null, week: null, month: null };
19254
19535
  const invalid = [];
@@ -19331,7 +19612,7 @@ function belongs(record, spec) {
19331
19612
  }
19332
19613
  function foldWindowRecords(records, spec) {
19333
19614
  if (spec.ledger === "model") return { records: null, unattributed: UNATTRIBUTED.model };
19334
- if (spec.ledger === "scope" && typeof spec.name === "string" && isAbsolute9(spec.name)) return { records: null, unattributed: UNATTRIBUTED.folder };
19615
+ if (spec.ledger === "scope" && typeof spec.name === "string" && isAbsolute10(spec.name)) return { records: null, unattributed: UNATTRIBUTED.folder };
19335
19616
  const basis = Object.fromEntries(DOLLAR_BASIS.map((b) => [b, 0]));
19336
19617
  let runs = 0;
19337
19618
  let settledMicros = 0;
@@ -19429,7 +19710,7 @@ var init_dollar_windows = __esm({
19429
19710
  // ../worker/src/service-env.mjs
19430
19711
  import { constants as fsConstants2 } from "node:fs";
19431
19712
  import { userInfo as userInfo2 } from "node:os";
19432
- import { dirname as dirname3, join as join9 } from "node:path";
19713
+ import { dirname as dirname3, join as join10 } from "node:path";
19433
19714
  function serviceEnvLoader(platform) {
19434
19715
  return platform === "linux" ? "systemd" : platform === "win32" ? "cmd" : "shell";
19435
19716
  }
@@ -19533,7 +19814,7 @@ function trustedChain(realPath, { fs: fs7, uid, names = {} }) {
19533
19814
  return null;
19534
19815
  }
19535
19816
  function readDeploymentEnv({ dir, fs: fs7, uid = process.geteuid?.(), names = accountNames(fs7) }) {
19536
- const path6 = join9(dir, ".env");
19817
+ const path6 = join10(dir, ".env");
19537
19818
  let real;
19538
19819
  try {
19539
19820
  real = fs7.realpathSync(path6);
@@ -19698,7 +19979,10 @@ var init_triggers_file = __esm({
19698
19979
  // Issue #501: a trigger's per-job dollar cap. Dropped by a receiver from before #501 (which tolerated the key as
19699
19980
  // unknown), the job would run under the deployment's wider cap, or with none, on a clean record. Any present
19700
19981
  // value counts as authored; whether it is a valid amount is the loader's question, not this one.
19701
- Object.freeze({ key: "maxCostUsd", authored: (v) => v !== void 0 && v !== null })
19982
+ Object.freeze({ key: "maxCostUsd", authored: (v) => v !== void 0 && v !== null }),
19983
+ // REQ-MCP-OPERATOR-SERVERS: the MCP servers a trigger's jobs connect. Not a widening when dropped but the same
19984
+ // silent outcome: the job runs its flow without the servers it was written against, on a clean record.
19985
+ Object.freeze({ key: "mcp", authored: (v) => Array.isArray(v) && v.length > 0 })
19702
19986
  ]);
19703
19987
  }
19704
19988
  });
@@ -19706,7 +19990,7 @@ var init_triggers_file = __esm({
19706
19990
  // src/read-model.mjs
19707
19991
  import * as nodeFs5 from "node:fs";
19708
19992
  import { randomBytes as randomBytes2 } from "node:crypto";
19709
- import { join as join10, delimiter as delimiter2, sep as sep2, isAbsolute as isAbsolute10, resolve as resolvePath } from "node:path";
19993
+ import { join as join11, delimiter as delimiter2, sep as sep2, isAbsolute as isAbsolute11, resolve as resolvePath } from "node:path";
19710
19994
  import { execFileSync as execFileSync3 } from "node:child_process";
19711
19995
  function resolvePaths(env2 = process.env) {
19712
19996
  return {
@@ -19790,7 +20074,7 @@ function panelEnv({ env: env2 = process.env, pointerDir = null, owned = [], fs:
19790
20074
  const res = deploymentServiceEnv({ env: own, dir, keys: PANEL_SERVICE_KEYS, platform, fs: fs7, uid });
19791
20075
  const out = res.env;
19792
20076
  for (const [key, value] of Object.entries(res.fromFile)) {
19793
- if (PANEL_PATH_KEYS.has(key) && value !== "" && !isAbsolute10(value)) out[key] = resolvePath(dir, value);
20077
+ if (PANEL_PATH_KEYS.has(key) && value !== "" && !isAbsolute11(value)) out[key] = resolvePath(dir, value);
19794
20078
  }
19795
20079
  for (const key of owned) if (out[key] === void 0 && env2[key] !== void 0) out[key] = env2[key];
19796
20080
  const said = [];
@@ -20732,9 +21016,9 @@ async function readCapacity({ url, env: env2 = process.env, window: window2 = "7
20732
21016
  ]);
20733
21017
  const coverage = { ...read.coverage, reason: [read.coverage.reason, fleet.unreachable ? `host registry unreadable (${fleet.unreachable})` : null].filter(Boolean).join("; ") || null };
20734
21018
  const report = computeCapacity({ records: read.records, live: fleet.hosts ?? [], windowStartMs: sinceMs, nowMs: nowMs2, bucketMs, coverage });
20735
- const shown = host === void 0 || host === null ? { report } : onlyHost(report, host);
20736
- if (shown.unknown) return { error: `no host named ${JSON.stringify(host)} in the last ${window2}${shown.unknown.length > 0 ? ` (hosts: ${shown.unknown.join(", ")})` : ""}` };
20737
- return { report: shown.report, text: capacityText(shown.report, { since: window2 }) };
21019
+ const shown2 = host === void 0 || host === null ? { report } : onlyHost(report, host);
21020
+ if (shown2.unknown) return { error: `no host named ${JSON.stringify(host)} in the last ${window2}${shown2.unknown.length > 0 ? ` (hosts: ${shown2.unknown.join(", ")})` : ""}` };
21021
+ return { report: shown2.report, text: capacityText(shown2.report, { since: window2 }) };
20738
21022
  } finally {
20739
21023
  try {
20740
21024
  redis?.disconnect?.();
@@ -20973,7 +21257,7 @@ function listRuns({ logsDir, limit: limit3 = 10, fs: fs7 = nodeFs5 }) {
20973
21257
  const records = [];
20974
21258
  for (const name of names) {
20975
21259
  if (!name.endsWith(".json")) continue;
20976
- const record = readJsonFile(join10(logsDir, name), fs7);
21260
+ const record = readJsonFile(join11(logsDir, name), fs7);
20977
21261
  if (record && typeof record === "object") records.push(record);
20978
21262
  }
20979
21263
  records.sort(byEndedAtDesc);
@@ -20992,7 +21276,7 @@ function scanRunRecords({ logsDir, sinceMs, nowMs: nowMs2 = Date.now(), fs: fs7
20992
21276
  const records = [];
20993
21277
  for (const name of names) {
20994
21278
  if (!name.endsWith(".json")) continue;
20995
- const record = readJsonFile(join10(logsDir, name), fs7);
21279
+ const record = readJsonFile(join11(logsDir, name), fs7);
20996
21280
  if (!record || typeof record !== "object") continue;
20997
21281
  const at = Date.parse(record.endedAt ?? record.startedAt ?? "");
20998
21282
  if (!Number.isFinite(at) || at < cutoffMs) continue;
@@ -21024,12 +21308,12 @@ function hostBudgetsOf(hosts) {
21024
21308
  return (Array.isArray(hosts) ? hosts : []).map((h2) => publishedBudget(h2));
21025
21309
  }
21026
21310
  function readRun({ logsDir, jobId, fs: fs7 = nodeFs5 }) {
21027
- return readJsonFile(join10(logsDir, `${sanitizeJobId(jobId)}.json`), fs7);
21311
+ return readJsonFile(join11(logsDir, `${sanitizeJobId(jobId)}.json`), fs7);
21028
21312
  }
21029
21313
  function readLogTail({ logsDir, jobId, lines = 200, fs: fs7 = nodeFs5, host = null, self: self2 = null }) {
21030
21314
  let text;
21031
21315
  try {
21032
- text = fs7.readFileSync(join10(logsDir, `${sanitizeJobId(jobId)}.log`), "utf8");
21316
+ text = fs7.readFileSync(join11(logsDir, `${sanitizeJobId(jobId)}.log`), "utf8");
21033
21317
  } catch {
21034
21318
  const foreign = typeof host === "string" && host !== "" && typeof self2 === "string" && self2 !== "" && host !== self2;
21035
21319
  return foreign ? { missing: true, elsewhere: host } : { missing: true };
@@ -21109,7 +21393,10 @@ function normalizeTriggerForDisplay(entry) {
21109
21393
  // a dollar amount, the operator's own words, never a value a job chose. Spread only when present, so a trigger
21110
21394
  // that sets neither keeps exactly its keys.
21111
21395
  ...Array.isArray(run.models) && run.models.length > 0 && run.models.every((m2) => typeof m2 === "string") && { models: [...run.models] },
21112
- ...(typeof run.maxCostUsd === "string" || typeof run.maxCostUsd === "number") && { maxCostUsd: String(run.maxCostUsd) }
21396
+ ...(typeof run.maxCostUsd === "string" || typeof run.maxCostUsd === "number") && { maxCostUsd: String(run.maxCostUsd) },
21397
+ // The overlay MCP servers the jobs connect (REQ-MCP-OPERATOR-SERVERS), by name: the operator's own words, never a
21398
+ // server's definition or a credential. Shown for `secrets`' reason: what a job can reach is set only in this file.
21399
+ ...Array.isArray(run.mcp) && run.mcp.length > 0 && run.mcp.every((name) => typeof name === "string") && { mcp: [...run.mcp] }
21113
21400
  };
21114
21401
  switch (on.type) {
21115
21402
  case "cron":
@@ -21226,6 +21513,26 @@ function readStagedPackages({ globalPiDir, fs: fs7 = nodeFs5, readManifest: read
21226
21513
  packages: entries.filter((p) => p && typeof p.name === "string").map(nameAtVersion)
21227
21514
  };
21228
21515
  }
21516
+ function readOverlayMcpView({ globalPiDir, read = readOverlayMcp } = {}) {
21517
+ const empty = { servers: [], refused: [], unreadable: null };
21518
+ if (typeof globalPiDir !== "string" || globalPiDir === "") return empty;
21519
+ let parsed;
21520
+ try {
21521
+ parsed = read(globalPiDir);
21522
+ } catch (err) {
21523
+ return { ...empty, unreadable: err?.mcpConfig ? "invalid" : err?.code ?? "unreadable" };
21524
+ }
21525
+ if (parsed === null) return empty;
21526
+ const servers = Object.entries(parsed.servers ?? {}).map(([name, server]) => ({
21527
+ name,
21528
+ transport: server.transport === "http" ? "http" : "stdio",
21529
+ where: mcpServerWhere(server),
21530
+ exposure: typeof server.exposure === "string" ? server.exposure : "codemode",
21531
+ enabled: server.enabled !== false,
21532
+ refs: Array.isArray(server.refs) ? [...server.refs] : []
21533
+ }));
21534
+ return { servers, refused: (parsed.refused ?? []).map((r2) => r2.name).filter((n) => typeof n === "string"), unreadable: null };
21535
+ }
21229
21536
  function nameAtVersion(pkg) {
21230
21537
  return typeof pkg.version === "string" && pkg.version !== "" ? `${pkg.name}@${pkg.version}` : pkg.name;
21231
21538
  }
@@ -21429,7 +21736,7 @@ function readInjectedSkills({ skillsDir, fs: fs7 = nodeFs5 } = {}) {
21429
21736
  for (const name of valid.slice(0, GRAPH_LIMITS.maxSkillsPerFolder)) {
21430
21737
  let aiTrigger = false;
21431
21738
  try {
21432
- const text = fs7.readFileSync(join10(skillsDir, name, "SKILL.md"), "utf8");
21739
+ const text = fs7.readFileSync(join11(skillsDir, name, "SKILL.md"), "utf8");
21433
21740
  aiTrigger = aiTriggerAllows(text);
21434
21741
  } catch {
21435
21742
  continue;
@@ -21440,7 +21747,7 @@ function readInjectedSkills({ skillsDir, fs: fs7 = nodeFs5 } = {}) {
21440
21747
  }
21441
21748
  function readOverlaySkills({ globalPiDir, fs: fs7 = nodeFs5 } = {}) {
21442
21749
  if (typeof globalPiDir !== "string" || globalPiDir === "") return { skills: [], truncated: false, unreachable: null };
21443
- const dir = join10(globalPiDir, "skills");
21750
+ const dir = join11(globalPiDir, "skills");
21444
21751
  let names;
21445
21752
  try {
21446
21753
  names = fs7.readdirSync(dir);
@@ -21454,7 +21761,7 @@ function readOverlaySkills({ globalPiDir, fs: fs7 = nodeFs5 } = {}) {
21454
21761
  for (const name of valid.slice(0, GRAPH_LIMITS.maxSkillsPerFolder)) {
21455
21762
  let present = false;
21456
21763
  try {
21457
- present = fs7.existsSync(join10(dir, name, "SKILL.md"));
21764
+ present = fs7.existsSync(join11(dir, name, "SKILL.md"));
21458
21765
  } catch {
21459
21766
  present = false;
21460
21767
  }
@@ -21583,6 +21890,7 @@ var init_read_model = __esm({
21583
21890
  init_runtime_settings();
21584
21891
  init_money();
21585
21892
  init_run_history();
21893
+ init_mcp_config();
21586
21894
  init_budget();
21587
21895
  init_pause_windows();
21588
21896
  init_scoped_limits();
@@ -21657,7 +21965,7 @@ return 1`;
21657
21965
  // src/write-guard.mjs
21658
21966
  import * as nodeFs6 from "node:fs";
21659
21967
  import { homedir as homedir3 } from "node:os";
21660
- import { dirname as dirname4, isAbsolute as isAbsolute11, join as join11, parse as parsePath, resolve as nodeResolve, sep as sep3 } from "node:path";
21968
+ import { dirname as dirname4, isAbsolute as isAbsolute12, join as join12, parse as parsePath, resolve as nodeResolve, sep as sep3 } from "node:path";
21661
21969
  import { fileURLToPath } from "node:url";
21662
21970
  function normalizeWindowsShellPath(filePath) {
21663
21971
  if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\")) return filePath;
@@ -21673,13 +21981,13 @@ function piResolveToCwd(input, cwd, { home = homedir3(), platform = process.plat
21673
21981
  if (at && s2.startsWith("@")) s2 = s2.slice(1);
21674
21982
  if (platform === "win32") s2 = normalizeWindowsShellPath(s2);
21675
21983
  if (s2 === "~") return home;
21676
- if (s2.startsWith("~/") || platform === "win32" && s2.startsWith("~\\")) return join11(home, s2.slice(2));
21984
+ if (s2.startsWith("~/") || platform === "win32" && s2.startsWith("~\\")) return join12(home, s2.slice(2));
21677
21985
  if (/^file:\/\//.test(s2)) return fileURLToPath(s2);
21678
21986
  return s2;
21679
21987
  };
21680
21988
  const target = normalize2(String(input), { spaces: true, at: true });
21681
21989
  const base = normalize2(String(cwd), { spaces: false, at: false });
21682
- return isAbsolute11(target) ? nodeResolve(target) : nodeResolve(base, target);
21990
+ return isAbsolute12(target) ? nodeResolve(target) : nodeResolve(base, target);
21683
21991
  }
21684
21992
  function idOf(st) {
21685
21993
  return `${st.dev}:${st.ino}`;
@@ -21704,7 +22012,7 @@ function targetIdentity(absPath, { fs: fs7 = nodeFs6 } = {}) {
21704
22012
  let cur = root;
21705
22013
  let restarted = false;
21706
22014
  for (let i2 = 0; i2 < segments.length; i2++) {
21707
- const next = join11(cur, segments[i2]);
22015
+ const next = join12(cur, segments[i2]);
21708
22016
  let st;
21709
22017
  try {
21710
22018
  st = fs7.lstatSync(next);
@@ -21754,7 +22062,7 @@ function readSmallRegularFile(path6, { fs: fs7 = nodeFs6, max = ENV_READ_MAX } =
21754
22062
  }
21755
22063
  }
21756
22064
  function envNamedFiles(dir, { fs: fs7 = nodeFs6, home = homedir3() } = {}) {
21757
- const text = readSmallRegularFile(join11(dir, ".env"), { fs: fs7 });
22065
+ const text = readSmallRegularFile(join12(dir, ".env"), { fs: fs7 });
21758
22066
  if (text === null) return [];
21759
22067
  const last = /* @__PURE__ */ new Map();
21760
22068
  for (const line of text.replace(/^\uFEFF/, "").split(/\r?\n/)) {
@@ -21772,7 +22080,7 @@ function envNamedFiles(dir, { fs: fs7 = nodeFs6, home = homedir3() } = {}) {
21772
22080
  }
21773
22081
  for (const r2 of readings) if (r2.trim() !== "") out.push({ path: nodeResolve(dir, r2), ...GUARDED_ENV_KEYS[key] });
21774
22082
  }
21775
- if (out.length > 0) out.push({ path: join11(dir, ".env"), label: "the deployment's .env, which names these files (PI_*_FILE)", tool: "the operator's own editor" });
22083
+ if (out.length > 0) out.push({ path: join12(dir, ".env"), label: "the deployment's .env, which names these files (PI_*_FILE)", tool: "the operator's own editor" });
21776
22084
  return out;
21777
22085
  }
21778
22086
  function sameFile(a, b) {
@@ -24982,9 +25290,9 @@ function buildInsightsHtml(payload, { now, fullPaths } = {}) {
24982
25290
  const fill = cls === "estimated" || cls === "seeded" ? PAGE_THEME.amber : PAGE_THEME.dim;
24983
25291
  const text = fmtCost(entry.cost);
24984
25292
  const cols = Math.max(2, Math.floor((pl.w - 20) / BADGE_COL_W));
24985
- const shown = drawnColumns(text) <= cols ? text : clipColumns(text, cols - 1);
24986
- const tip = shown !== text ? `<title>${escapeHtml(text)}</title>` : "";
24987
- spendParts.push(`<text x="${fmt(pl.x + 16)}" y="${fmt(pl.y + SPEND_BADGE_DY)}" font-size="10" fill="${fill}">${escapeHtml(shown)}${tip}</text>`);
25293
+ const shown2 = drawnColumns(text) <= cols ? text : clipColumns(text, cols - 1);
25294
+ const tip = shown2 !== text ? `<title>${escapeHtml(text)}</title>` : "";
25295
+ spendParts.push(`<text x="${fmt(pl.x + 16)}" y="${fmt(pl.y + SPEND_BADGE_DY)}" font-size="10" fill="${fill}">${escapeHtml(shown2)}${tip}</text>`);
24988
25296
  }
24989
25297
  const tips = [];
24990
25298
  const bodyParts = [];
@@ -37575,20 +37883,20 @@ function getName(value, options) {
37575
37883
  const name = "name" in value ? value.name : void 0;
37576
37884
  const explicitName = name && String(name) || "filename" in value && value.filename && String(value.filename);
37577
37885
  if (explicitName) {
37578
- return options?.stripFilename === false ? normalizeFilenamePath(explicitName) : basename3(explicitName);
37886
+ return options?.stripFilename === false ? normalizeFilenamePath(explicitName) : basename4(explicitName);
37579
37887
  }
37580
37888
  const url = "url" in value && value.url && String(value.url);
37581
37889
  if (url) {
37582
37890
  try {
37583
- return basename3(new URL(url).pathname);
37891
+ return basename4(new URL(url).pathname);
37584
37892
  } catch {
37585
- return basename3(url);
37893
+ return basename4(url);
37586
37894
  }
37587
37895
  }
37588
37896
  const path6 = "path" in value && value.path && String(value.path);
37589
- return path6 ? basename3(path6) : void 0;
37897
+ return path6 ? basename4(path6) : void 0;
37590
37898
  }
37591
- function basename3(value) {
37899
+ function basename4(value) {
37592
37900
  return value.split(/[\\/]/).pop() || void 0;
37593
37901
  }
37594
37902
  function normalizeFilenamePath(value) {
@@ -37692,7 +38000,7 @@ function getStreamingFileName(value, options) {
37692
38000
  if (typeof name !== "string" || !name) {
37693
38001
  throw new TypeError("Streaming upload file name must be a non-empty string");
37694
38002
  }
37695
- return options.stripFilenames === false ? normalizeFilenamePath(name) : basename3(name) ?? "unknown_file";
38003
+ return options.stripFilenames === false ? normalizeFilenamePath(name) : basename4(name) ?? "unknown_file";
37696
38004
  }
37697
38005
  return getName(value, { stripFilename: options.stripFilenames }) ?? "unknown_file";
37698
38006
  }
@@ -76140,7 +76448,7 @@ function timeoutError(timeoutMs) {
76140
76448
  error.body = "";
76141
76449
  return error;
76142
76450
  }
76143
- function isRecord(value) {
76451
+ function isRecord2(value) {
76144
76452
  return typeof value === "object" && value !== null && !Array.isArray(value);
76145
76453
  }
76146
76454
  function requiredNumber(label2, value, field) {
@@ -76192,7 +76500,7 @@ function tokenCount(value) {
76192
76500
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
76193
76501
  }
76194
76502
  function parseClassifierUsage(value, model) {
76195
- if (!isRecord(value) || value.input_tokens === void 0 && value.output_tokens === void 0)
76503
+ if (!isRecord2(value) || value.input_tokens === void 0 && value.output_tokens === void 0)
76196
76504
  return void 0;
76197
76505
  const input = tokenCount(value.input_tokens);
76198
76506
  const output = tokenCount(value.output_tokens);
@@ -76217,7 +76525,7 @@ var init_classifier_shared = __esm({
76217
76525
 
76218
76526
  // ../node_modules/@earendil-works/pi-ai/dist/api/system-one-shared.js
76219
76527
  function probabilities(label2, value, id) {
76220
- if (!isRecord(value))
76528
+ if (!isRecord2(value))
76221
76529
  throw new Error(`${label2} returned invalid probabilities for ${id}`);
76222
76530
  return Object.fromEntries(Object.entries(value).map(([key, probability]) => [
76223
76531
  key,
@@ -76225,12 +76533,12 @@ function probabilities(label2, value, id) {
76225
76533
  ]));
76226
76534
  }
76227
76535
  function parseAnswers(label2, value, context) {
76228
- if (!isRecord(value))
76536
+ if (!isRecord2(value))
76229
76537
  throw new Error(`${label2} returned an unexpected response`);
76230
76538
  const answers = [];
76231
76539
  for (const [id, question] of Object.entries(context.questions)) {
76232
76540
  const answer = value[id];
76233
- if (!isRecord(answer))
76541
+ if (!isRecord2(answer))
76234
76542
  throw new Error(`${label2} did not return an answer for ${id}`);
76235
76543
  if (question.type === "choice") {
76236
76544
  if (answer.type !== "choice" || typeof answer.choice !== "string") {
@@ -76320,7 +76628,7 @@ __export(cloudflare_workers_ai_system_one_exports, {
76320
76628
  });
76321
76629
  function cloudflareErrorMessage(errors) {
76322
76630
  if (Array.isArray(errors)) {
76323
- const messages = errors.map((error) => isRecord(error) && typeof error.message === "string" ? error.message : void 0).filter((message) => message !== void 0);
76631
+ const messages = errors.map((error) => isRecord2(error) && typeof error.message === "string" ? error.message : void 0).filter((message) => message !== void 0);
76324
76632
  if (messages.length > 0)
76325
76633
  return `${LABEL} error: ${messages.join("; ")}`;
76326
76634
  }
@@ -76338,19 +76646,19 @@ var init_cloudflare_workers_ai_system_one = __esm({
76338
76646
  url: (model) => new URL("run", `${model.baseUrl.replace(/\/+$/u, "")}/`),
76339
76647
  payload: (model, request) => ({ model: model.id, input: request }),
76340
76648
  output: (body) => {
76341
- if (!isRecord(body))
76649
+ if (!isRecord2(body))
76342
76650
  throw new Error(`${LABEL} returned an unexpected response`);
76343
76651
  if (body.success === false)
76344
76652
  throw new Error(cloudflareErrorMessage(body.errors));
76345
76653
  const result = body.result;
76346
- if (!isRecord(result))
76654
+ if (!isRecord2(result))
76347
76655
  throw new Error(`${LABEL} returned an unexpected response`);
76348
76656
  if ("answers" in result)
76349
76657
  return result;
76350
76658
  if (result.state !== "Completed") {
76351
76659
  throw new Error(`${LABEL} run did not complete (state: ${String(result.state)})`);
76352
76660
  }
76353
- if (!isRecord(result.result))
76661
+ if (!isRecord2(result.result))
76354
76662
  throw new Error(`${LABEL} returned an unexpected response`);
76355
76663
  return result.result;
76356
76664
  }
@@ -83273,7 +83581,7 @@ var require_node_domexception = __commonJS({
83273
83581
 
83274
83582
  // ../node_modules/fetch-blob/from.js
83275
83583
  import { statSync as statSync6, createReadStream as createReadStream2, promises as fs5 } from "node:fs";
83276
- import { basename as basename4 } from "node:path";
83584
+ import { basename as basename5 } from "node:path";
83277
83585
  var import_node_domexception, stat2, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
83278
83586
  var init_from = __esm({
83279
83587
  "../node_modules/fetch-blob/from.js"() {
@@ -83296,7 +83604,7 @@ var init_from = __esm({
83296
83604
  size: stat4.size,
83297
83605
  lastModified: stat4.mtimeMs,
83298
83606
  start: 0
83299
- })], basename4(path6), { type, lastModified: stat4.mtimeMs });
83607
+ })], basename5(path6), { type, lastModified: stat4.mtimeMs });
83300
83608
  BlobDataItem = class _BlobDataItem {
83301
83609
  #path;
83302
83610
  #start;
@@ -123213,7 +123521,7 @@ function choiceProbabilities(value, id) {
123213
123521
  if (!Array.isArray(value))
123214
123522
  throw new Error(`${LABEL2} returned invalid probabilities for ${id}`);
123215
123523
  return Object.fromEntries(value.map((entry) => {
123216
- if (!isRecord(entry) || typeof entry.value !== "string") {
123524
+ if (!isRecord2(entry) || typeof entry.value !== "string") {
123217
123525
  throw new Error(`${LABEL2} returned invalid probabilities for ${id}`);
123218
123526
  }
123219
123527
  return [entry.value, requiredNumber(LABEL2, entry.probability, `probability for ${id}.${entry.value}`)];
@@ -123251,7 +123559,7 @@ function parseAnswers2(value, context) {
123251
123559
  throw new Error(`${LABEL2} returned an unexpected response`);
123252
123560
  const byName = /* @__PURE__ */ new Map();
123253
123561
  for (const answer of value) {
123254
- if (isRecord(answer) && typeof answer.name === "string")
123562
+ if (isRecord2(answer) && typeof answer.name === "string")
123255
123563
  byName.set(answer.name, answer);
123256
123564
  }
123257
123565
  return Object.fromEntries(Object.entries(context.questions).map(([id, question]) => {
@@ -123292,7 +123600,7 @@ var init_openai_decisions = __esm({
123292
123600
  input: wireInput(context),
123293
123601
  questions: Object.entries(context.questions).map(([id, question]) => wireQuestion(id, question))
123294
123602
  }, options, NO_RETRY_STATUSES);
123295
- if (!isRecord(body))
123603
+ if (!isRecord2(body))
123296
123604
  throw new Error(`${LABEL2} returned an unexpected response`);
123297
123605
  const usage = parseClassifierUsage(body.usage, model);
123298
123606
  if (usage)
@@ -124829,7 +125137,7 @@ var init_typesafe_system_one = __esm({
124829
125137
  url: (model) => new URL("systemone", `${model.baseUrl.replace(/\/+$/u, "")}/`),
124830
125138
  payload: (model, request) => ({ model: model.id, ...request }),
124831
125139
  output: (body) => {
124832
- if (!isRecord(body))
125140
+ if (!isRecord2(body))
124833
125141
  throw new Error("System One API returned an unexpected response");
124834
125142
  return body;
124835
125143
  }
@@ -125958,7 +126266,7 @@ var init_all = __esm({
125958
126266
 
125959
126267
  // ../worker/src/pricing.mjs
125960
126268
  import { readFileSync as readFileSync6 } from "node:fs";
125961
- import { dirname as dirname6, join as join13 } from "node:path";
126269
+ import { dirname as dirname6, join as join14 } from "node:path";
125962
126270
  import { fileURLToPath as fileURLToPath2 } from "node:url";
125963
126271
  function listPricedModels() {
125964
126272
  const out = [];
@@ -125990,7 +126298,7 @@ function readPiAiVersion(readText) {
125990
126298
  let dir = dirname6(fileURLToPath2(import.meta.resolve("@earendil-works/pi-ai")));
125991
126299
  for (let hops = 0; hops < 8; hops++) {
125992
126300
  try {
125993
- const parsed = JSON.parse(readText(join13(dir, "package.json")));
126301
+ const parsed = JSON.parse(readText(join14(dir, "package.json")));
125994
126302
  if (parsed !== null && parsed.name === "@earendil-works/pi-ai") {
125995
126303
  const match2 = /^(\d+\.\d+\.\d+)/.exec(String(parsed.version ?? ""));
125996
126304
  return match2 === null ? null : match2[1];
@@ -126056,10 +126364,10 @@ function gateDialogs(ctx) {
126056
126364
  const gatedSelect = async (live, args) => {
126057
126365
  const cleaned = args.map(clean);
126058
126366
  const picked = await live.select(...cleaned);
126059
- const shown = cleaned[1];
126367
+ const shown2 = cleaned[1];
126060
126368
  const original = args[1];
126061
- if (!Array.isArray(shown) || !Array.isArray(original)) return picked;
126062
- const at = shown.indexOf(picked);
126369
+ if (!Array.isArray(shown2) || !Array.isArray(original)) return picked;
126370
+ const at = shown2.indexOf(picked);
126063
126371
  return at >= 0 ? original[at] : picked;
126064
126372
  };
126065
126373
  const wrapOne = (live, name) => typeof live[name] === "function" ? (...args) => live[name](...args.map(clean)) : live[name];
@@ -126243,7 +126551,7 @@ var init_backend_local = __esm({
126243
126551
  });
126244
126552
 
126245
126553
  // ../worker/src/docker-run.mjs
126246
- import { isAbsolute as isAbsolute13, relative } from "node:path";
126554
+ import { isAbsolute as isAbsolute14, relative } from "node:path";
126247
126555
  function dockerArgsFromSpec(spec) {
126248
126556
  if (spec?.userns !== null && spec?.userns !== void 0) {
126249
126557
  throw new Error(`docker run: refusing a spec with userns ${JSON.stringify(spec.userns)}: the docker CLI has no --userns=keep-id, and only a podman venue builds one`);
@@ -126330,7 +126638,7 @@ function buildPodmanRunArgs(opts) {
126330
126638
  function insideDir(outer, inner) {
126331
126639
  if (typeof outer !== "string" || typeof inner !== "string" || outer === "" || inner === "") return false;
126332
126640
  const rel = relative(outer, inner);
126333
- return rel !== "" && !rel.startsWith("..") && !isAbsolute13(rel);
126641
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute14(rel);
126334
126642
  }
126335
126643
  var ISOLATION_FLAGS, DOCKER_EXTRA_FORBIDDEN, DOCKER_EXTRA_ALLOWED, PODMAN_PINNED_FLAGS;
126336
126644
  var init_docker_run = __esm({
@@ -126539,6 +126847,7 @@ var CAPABILITY_GATES, FIELD_SEP, PI_VERSION_LABEL, PI_VERSION_TEMPLATE, FORGES_L
126539
126847
  var init_image_preflight = __esm({
126540
126848
  "../worker/src/image-preflight.mjs"() {
126541
126849
  init_image_ref();
126850
+ init_mcp_config();
126542
126851
  CAPABILITY_GATES = Object.freeze([
126543
126852
  // REQ-REPLICA-RUNS: an image built before it bakes a HARD_RULES.md whose rule 3 hard-codes `pi/issue-<n>`
126544
126853
  // as a SYSTEM rule, which the model treats as authoritative over the user prompt naming `pi/issue-<n>-r2`.
@@ -126597,6 +126906,27 @@ var init_image_preflight = __esm({
126597
126906
  reason: "job-image-cost-cap-unsupported",
126598
126907
  event: "refused_image_cost_cap_unsupported",
126599
126908
  comment: (image, declared) => `Refused: the job image "${image}" does not declare cost-cap support (\`dev.pi-dispatch.capabilities\` ${declared}), so its runner would ignore the per-job dollar cap and run this job with no cap at all. Rebuild the image from a version that has this feature. Not run.`
126909
+ }),
126910
+ // REQ-MCP-OPERATOR-SERVERS: a runner that predates run.mcp reads no /job/mcp.json, so the job would run without
126911
+ // the servers its flow was written against and record a clean exit while the file reads as though it named them.
126912
+ Object.freeze({
126913
+ token: "mcp",
126914
+ needed: (job) => Array.isArray(job?.mcp) && job.mcp.length > 0,
126915
+ result: "mcpUnsupported",
126916
+ reason: "job-image-mcp-unsupported",
126917
+ event: "refused_image_mcp_unsupported",
126918
+ comment: (image, declared) => `Refused: the job image "${image}" does not declare MCP support (\`dev.pi-dispatch.capabilities\` ${declared}), so its runner would not connect the servers \`run.mcp\` names. Rebuild the image from a version that has this feature. Not run.`
126919
+ }),
126920
+ // DES-FORGE-MCP: `forge` in run.mcp starts a server the IMAGE carries (github-mcp-server, mcp-gitlab), not one the
126921
+ // overlay names. An image with `mcp` and without them would pass every gate, reserve the budget, and refuse at the
126922
+ // container's start as mcp-command-missing; this row says so before anything is spent.
126923
+ Object.freeze({
126924
+ token: "forgeMcp",
126925
+ needed: (job) => Array.isArray(job?.mcp) && job.mcp.includes(FORGE_MCP_SERVER_NAME),
126926
+ result: "forgeMcpUnsupported",
126927
+ reason: "job-image-forge-mcp-unsupported",
126928
+ event: "refused_image_forge_mcp_unsupported",
126929
+ comment: (image, declared) => `Refused: the job image "${image}" does not declare the forge's own MCP server (\`dev.pi-dispatch.capabilities\` ${declared}), so it does not carry the server \`run.mcp\` names as \`forge\`. Rebuild the image from a version that has this feature. Not run.`
126600
126930
  })
126601
126931
  ]);
126602
126932
  FIELD_SEP = "|";
@@ -126950,6 +127280,34 @@ var init_secrets = __esm({
126950
127280
  }
126951
127281
  });
126952
127282
 
127283
+ // ../worker/src/egress-allowlist.mjs
127284
+ var init_egress_allowlist = __esm({
127285
+ "../worker/src/egress-allowlist.mjs"() {
127286
+ init_backends();
127287
+ init_model_endpoints();
127288
+ }
127289
+ });
127290
+
127291
+ // ../worker/src/mcp-preflight.mjs
127292
+ var MCP_REFUSAL_COMMENT;
127293
+ var init_mcp_preflight = __esm({
127294
+ "../worker/src/mcp-preflight.mjs"() {
127295
+ init_egress_allowlist();
127296
+ init_mcp_config();
127297
+ init_triggers();
127298
+ MCP_REFUSAL_COMMENT = Object.freeze({
127299
+ "mcp-list-invalid": "the list this job carries is not one the triggers file's loader writes (a service in this deployment may be at another version)",
127300
+ "mcp-config-invalid": "this deployment's list of MCP servers (mcp.json in the overlay) cannot be used",
127301
+ "mcp-server-unknown": "this deployment does not offer that server",
127302
+ "mcp-secret-reserved": "that server would be handed a credential this deployment keeps for the job itself",
127303
+ "mcp-secret-unmapped": "a credential that server needs is not passed by this trigger",
127304
+ "mcp-excluded": "this trigger's run.excludeTools removes every way the job could reach that server",
127305
+ "mcp-egress-unreachable": "the egress policy does not let a job reach that server",
127306
+ "mcp-package-runner": "the egress policy does not let a job reach the registry that server is fetched from"
127307
+ });
127308
+ }
127309
+ });
127310
+
126953
127311
  // ../worker/src/processor.mjs
126954
127312
  var OBSERVATION_COMMENT, TERMINAL_COMMENTS, JOB_USER_COMMENTS, VENUE_RUNTIME;
126955
127313
  var init_processor = __esm({
@@ -126962,6 +127320,7 @@ var init_processor = __esm({
126962
127320
  init_scoped_limits();
126963
127321
  init_secrets();
126964
127322
  init_triggers();
127323
+ init_mcp_preflight();
126965
127324
  init_exit_code();
126966
127325
  init_run_history();
126967
127326
  init_egress();
@@ -126996,6 +127355,16 @@ var init_processor = __esm({
126996
127355
  "model-not-allowed": "Stopped: the run tried to call an AI model this trigger does not allow, or to change an AI request in a way it does not allow, so the call was not made. Partial work may exist. Not retried.",
126997
127356
  "cost-cap-unenforceable": "Stopped: this run has a cost limit, and the job image could not enforce it before each AI call, so nothing was sent to the AI provider. The operator needs to update the job image. Not retried.",
126998
127357
  "model-policy-unenforceable": "Stopped: this run is limited to certain AI models, and the job image could not enforce that before each AI call, so nothing was sent to the AI provider. The operator needs to update the job image. Not retried.",
127358
+ // The runner's refusals of the job's MCP servers, each before the prompt. Never the server's name, its host or a
127359
+ // variable: the exit line carries the name and the cause in its message, which the worker never parses (it reaches
127360
+ // the worker's log as the container's output). Each says what the operator can check; the issue's author can act on
127361
+ // none of it.
127362
+ "mcp-unavailable": "Stopped: an MCP server this trigger names could not be used (it did not start, failed its handshake or refused the connection, an authentication error among them), so the run ended before the agent started. The operator needs to check that server and the job image. Not retried.",
127363
+ "mcp-config-invalid": "Stopped: the list of MCP servers the worker wrote for this job is not one the job image accepts, so the run ended before the agent started. The operator needs to run a worker and a job image of the same release. Not retried.",
127364
+ "mcp-config-mismatch": "Stopped: the MCP servers the worker wrote for this job are not the ones it named for it, so the run ended before the agent started. The operator needs to run a worker and a job image of the same release. Not retried.",
127365
+ "mcp-secret-missing": "Stopped: a credential an MCP server this trigger names needs was not set inside the job, so the run ended before the agent started. The operator needs to check the secrets this trigger passes. Not retried.",
127366
+ "mcp-command-missing": "Stopped: the job image does not have the program an MCP server this trigger names runs, so the run ended before the agent started. The operator needs to install that server in the job image or the overlay. Not retried.",
127367
+ "mcp-shadowed": "Stopped: another extension in this job registers a tool or command that only the job's MCP servers may, so the run ended before the agent started. The operator needs to remove that extension or this trigger's MCP servers. Not retried.",
126999
127368
  // Issue #596. Never the size or the project: both are operator configuration, and the reader may be an issue author
127000
127369
  // who can act on neither. The worker log and the run record carry them.
127001
127370
  [EXIT_OOM_KILLED]: "Stopped: the job's container ran out of memory and was stopped. Partial work may exist. Not retried, because the same size would stop the same way. The operator can raise this job's memory size."
@@ -128154,7 +128523,7 @@ var init_backend_podman = __esm({
128154
128523
 
128155
128524
  // ../worker/src/sandbox-store.mjs
128156
128525
  import { chmodSync as chmodSync4, chownSync as chownSync2, lstatSync as lstatSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync8, readdirSync as readdirSync5, renameSync as renameSync2, rmdirSync, rmSync, statSync as statSync9, writeFileSync as writeFileSync2 } from "node:fs";
128157
- import { isAbsolute as isAbsolute14, join as join14, relative as relative2 } from "node:path";
128526
+ import { isAbsolute as isAbsolute15, join as join15, relative as relative2 } from "node:path";
128158
128527
  function sandboxEntryName(jobId) {
128159
128528
  const name = sanitizeJobId(jobId);
128160
128529
  return name.startsWith(".") || name.startsWith("_") ? `_${name}` : name;
@@ -128162,9 +128531,9 @@ function sandboxEntryName(jobId) {
128162
128531
  function readManifest({ sandboxDir, jobId, fs: fs7 = defaultFs }) {
128163
128532
  if (!sandboxDir || jobId === void 0 || jobId === null) return null;
128164
128533
  const read = (name) => {
128165
- const dir = join14(sandboxDir, name);
128534
+ const dir = join15(sandboxDir, name);
128166
128535
  try {
128167
- const manifest = JSON.parse(fs7.readFileSync(join14(dir, SANDBOX_MANIFEST), "utf8"));
128536
+ const manifest = JSON.parse(fs7.readFileSync(join15(dir, SANDBOX_MANIFEST), "utf8"));
128168
128537
  return { ...manifest, dir };
128169
128538
  } catch {
128170
128539
  return null;
@@ -128196,7 +128565,7 @@ function readRetained(fs7, dir) {
128196
128565
  }
128197
128566
  let text;
128198
128567
  try {
128199
- text = String(fs7.readFileSync(join14(dir, SANDBOX_MANIFEST), "utf8"));
128568
+ text = String(fs7.readFileSync(join15(dir, SANDBOX_MANIFEST), "utf8"));
128200
128569
  } catch (err) {
128201
128570
  if (err?.code === "ENOENT" || err?.code === "ENOTDIR") return { absent: true };
128202
128571
  if (TRANSIENT_READ_ERRORS.has(err?.code)) return { transient: true, code: err.code };
@@ -128224,7 +128593,7 @@ var init_sandbox_store = __esm({
128224
128593
  // ../worker/src/sandbox.mjs
128225
128594
  import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
128226
128595
  import { existsSync as existsSync4, lstatSync as lstatSync4, readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync10 } from "node:fs";
128227
- import { basename as basename6, join as join15 } from "node:path";
128596
+ import { basename as basename7, join as join16 } from "node:path";
128228
128597
  import { release as osRelease2 } from "node:os";
128229
128598
  import { promisify as promisify6 } from "node:util";
128230
128599
  function sandboxLauncher(venue) {
@@ -128372,7 +128741,7 @@ function resolveSandbox({ jobId, sandboxDir, retentionHours, publish = [], fs: f
128372
128741
  if (refusal) return refusal;
128373
128742
  const lapsed = sandboxWindowRefusal({ jobId, manifest, retentionHours, at: now(), pin });
128374
128743
  if (lapsed) return lapsed;
128375
- return { manifest, name: `${SANDBOX_NAME_PREFIX}${basename6(manifest.dir)}`, publish, venue: sandboxVenueOf(manifest), identity: dirIdentity(fs7 ?? { lstatSync: lstatSync4 }, manifest.dir) };
128744
+ return { manifest, name: `${SANDBOX_NAME_PREFIX}${basename7(manifest.dir)}`, publish, venue: sandboxVenueOf(manifest), identity: dirIdentity(fs7 ?? { lstatSync: lstatSync4 }, manifest.dir) };
128376
128745
  }
128377
128746
  function dirIdentity(fs7, dir) {
128378
128747
  try {
@@ -128453,7 +128822,7 @@ async function openSandbox({
128453
128822
  };
128454
128823
  }
128455
128824
  }
128456
- const id = basename6(resolved.manifest.dir);
128825
+ const id = basename7(resolved.manifest.dir);
128457
128826
  const ask = (signal) => Promise.resolve().then(() => running(signal ? { bin, signal } : { bin })).then((ids) => ({ answered: true, live: new Set(ids) }), () => ({ answered: false, live: /* @__PURE__ */ new Set() }));
128458
128827
  const before = await ask();
128459
128828
  if (before.live.has(id)) {
@@ -128970,26 +129339,27 @@ function triggerLine(t2) {
128970
129339
  const res = t2?.resume === true ? " [resume]" : "";
128971
129340
  const rep = t2?.replicas > 1 ? ` [x${t2.replicas}]` : "";
128972
129341
  const sec = t2?.secrets > 0 ? ` [secrets ${t2.secrets}${t2.secretsProfile ? ` via ${t2.secretsProfile}` : ""}]` : "";
129342
+ const mcp = Array.isArray(t2?.mcp) && t2.mcp.length > 0 ? ` [mcp ${t2.mcp.join(", ")}]` : "";
128973
129343
  const shot = t2?.disarmed ? " [spent]" : t2?.once === true ? " [once]" : "";
128974
129344
  const pol = `${Array.isArray(t2?.models) && t2.models.length > 0 ? ` [models ${t2.models.join(", ")}]` : ""}${t2?.maxCostUsd ? ` [max $${t2.maxCostUsd}]` : ""}`;
128975
129345
  const pfo = t2?.portfolio === true ? " [portfolio]" : "";
128976
129346
  switch (t2?.type) {
128977
129347
  case "cron":
128978
- return `cron ${t2.id ?? "-"} ${t2.pattern ?? "-"} \u2192 ${t2.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${pol}${pfo}`;
129348
+ return `cron ${t2.id ?? "-"} ${t2.pattern ?? "-"} \u2192 ${t2.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${mcp}${pol}${pfo}`;
128979
129349
  case "label":
128980
- return `label ${ruleClauses(t2) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${pol}`;
129350
+ return `label ${ruleClauses(t2) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${mcp}${pol}`;
128981
129351
  case "comment":
128982
- return `comment "${t2.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${pol}`;
129352
+ return `comment "${t2.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${mcp}${pol}`;
128983
129353
  case "pull_request": {
128984
129354
  const clauses = ruleClauses(t2);
128985
129355
  const action = `action[${(t2.action ?? []).join(",")}]`;
128986
129356
  const num = Number.isInteger(t2.number) ? ` #${t2.number}` : "";
128987
- return `pull_request ${action}${num}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${pol}${shot}`;
129357
+ return `pull_request ${action}${num}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${mcp}${pol}${shot}`;
128988
129358
  }
128989
129359
  case "issue": {
128990
129360
  const action = `action[${(t2.action ?? []).join(",")}]`;
128991
129361
  const num = Number.isInteger(t2.number) ? ` #${t2.number}` : "";
128992
- return `issue ${action}${num} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${pol}${shot}`;
129362
+ return `issue ${action}${num} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${mcp}${pol}${shot}`;
128993
129363
  }
128994
129364
  default:
128995
129365
  return "(unknown trigger)";
@@ -129635,6 +130005,9 @@ function createDashboardDeps(paths, {
129635
130005
  // read-model.mjs -- this module never touches the filesystem -- and it degrades to a safe empty
129636
130006
  // shape rather than throwing, so a broken overlay cannot take the whole snapshot down.
129637
130007
  stagedPackages: readStagedPackages({ globalPiDir: paths.globalPiDir }),
130008
+ // The overlay's MCP servers (REQ-MCP-OPERATOR-SERVERS), for a trigger's `run.mcp` lines: the same plain,
130009
+ // never-throw read, names and where each runs, never a header, an argument or a value.
130010
+ overlayMcp: readOverlayMcpView({ globalPiDir: paths.globalPiDir }),
129638
130011
  // ONLY the id off the active Job -- a Job's `.data` holds issue title/body/username (PII), so it
129639
130012
  // never enters the snapshot (no-pii-in-logs, INT-RUN-HISTORY-FILE-CONTRACT).
129640
130013
  activeJobId: activeList?.[0]?.id ?? null
@@ -130328,7 +130701,7 @@ function renderPanelLines(snapshot, width, state2, styler) {
130328
130701
  const detailTitle = `trigger \xB7 ${scrubControl(String(t2?.type ?? "?"))}`;
130329
130702
  const dw = framed ? Math.min(Math.trunc(width), DRILL_WIDTH) : Math.trunc(width);
130330
130703
  const sched = cronSchedInfo(t2, snapshot);
130331
- const lines = renderTriggerDetail(t2, framed ? dw - 4 : 24, styler, sched, snapshot?.stagedPackages, snapshot?.fetchedAt, detailTrigger?.instructions);
130704
+ const lines = renderTriggerDetail(t2, framed ? dw - 4 : 24, styler, sched, snapshot?.stagedPackages, snapshot?.fetchedAt, detailTrigger?.instructions, snapshot?.overlayMcp);
130332
130705
  if (!framed) return [detailTitle, "", ...lines.map((l) => styler.stripAnsi(l)), "", pendingDelete ? "delete this trigger? y/n" : "e edit \xB7 x delete \xB7 esc back"];
130333
130706
  const boxed = frame(styler, { title: detailTitle, width: dw, lines, footer: triggerDetailHints(dw - 4, styler, pendingDelete) });
130334
130707
  return centerBlock(boxed, Math.trunc(width), dw);
@@ -130601,9 +130974,9 @@ function heldSection(held, inner, styler) {
130601
130974
  }
130602
130975
  const rows = Array.isArray(held.rows) ? held.rows : [];
130603
130976
  if (rows.length === 0) return [];
130604
- const shown = rows.slice(0, HELD_ON_DASHBOARD);
130605
- const hidden = rows.length - shown.length + (Number(held.more) || 0);
130606
- const body = shown.map((r2) => heldRow(r2, inner, styler));
130977
+ const shown2 = rows.slice(0, HELD_ON_DASHBOARD);
130978
+ const hidden = rows.length - shown2.length + (Number(held.more) || 0);
130979
+ const body = shown2.map((r2) => heldRow(r2, inner, styler));
130607
130980
  if (hidden > 0) body.push(styler.cell(`\u2193 ${hidden} more`, inner, { color: "dim" }));
130608
130981
  const total = rows.length + (Number(held.more) || 0);
130609
130982
  return [{ key: "held", priority: 5, viewKey: "h", head: ["held", `${total}${held.truncated ? "+" : ""} waiting \xB7 h view`], body }];
@@ -130653,9 +131026,9 @@ function failedSection(failed, inner, styler) {
130653
131026
  }
130654
131027
  const rows = Array.isArray(failed.rows) ? failed.rows : [];
130655
131028
  if (rows.length === 0) return [];
130656
- const shown = rows.slice(0, FAILED_ON_DASHBOARD);
130657
- const hidden = rows.length - shown.length + (Number(failed.more) || 0);
130658
- const body = shown.map((r2) => failedRow(r2, inner, styler));
131029
+ const shown2 = rows.slice(0, FAILED_ON_DASHBOARD);
131030
+ const hidden = rows.length - shown2.length + (Number(failed.more) || 0);
131031
+ const body = shown2.map((r2) => failedRow(r2, inner, styler));
130659
131032
  if (hidden > 0) body.push(styler.cell(`\u2193 ${hidden} more`, inner, { color: "dim" }));
130660
131033
  const total = rows.length + (Number(failed.more) || 0);
130661
131034
  return [{ key: "failed", priority: 6, viewKey: "f", head: ["failed", `${total} in the queue \xB7 f view`], body }];
@@ -130803,12 +131176,13 @@ function triggerRow(raw, sel, inner, styler, sched = null) {
130803
131176
  const skl = t2?.skillsDir ? rowBadge("accent", false, "[skills ", cellOf(skillsBasename(t2.skillsDir)), "]") : null;
130804
131177
  const ins = t2?.instructions === true ? rowBadge("accent", false, "[instructions]") : null;
130805
131178
  const sec = t2?.secrets > 0 ? rowBadge("warning", true, `[secrets ${t2.secrets}${t2.secretsProfile ? " via " : ""}`, t2.secretsProfile ? cellOf(t2.secretsProfile) : "", "]") : null;
131179
+ const mcp = Array.isArray(t2?.mcp) && t2.mcp.length > 0 ? rowBadge("warning", true, "[mcp ", cellOf(t2.mcp.join(", ")), "]") : null;
130806
131180
  const shot = t2?.disarmed ? rowBadge("dim", false, "[spent]") : t2?.once === true ? rowBadge("accent", false, "[once]") : null;
130807
131181
  const health = sched && (sched.overdueMs || sched.stallMax > 0 && sched.stalls >= sched.stallMax) ? rowBadge("warning", true, sched.overdueMs ? "\u26A0 overdue" : "\u26A0 stalled") : null;
130808
131182
  const mdl = Array.isArray(t2?.models) && t2.models.length > 0 ? rowBadge("accent", false, "[models ", cellOf(t2.models.join(", ")), "]") : null;
130809
131183
  const cap = t2?.maxCostUsd ? rowBadge("accent", false, "[max $", cellOf(t2.maxCostUsd), "]") : null;
130810
131184
  const pfo = t2?.portfolio === true ? rowBadge("accent", false, "[portfolio]") : null;
130811
- const badges = [pkgs, img, skl, ins, res, rep, sec, shot, mdl, cap, pfo, health].filter((b) => b !== null);
131185
+ const badges = [pkgs, img, skl, ins, res, rep, sec, mcp, shot, mdl, cap, pfo, health].filter((b) => b !== null);
130812
131186
  const match2 = matchColored(t2, styler);
130813
131187
  return fitTriggerRow(`${cursor} ${badge} `, match2, targetColored(t2, styler), badges, inner, styler, rowFloorMatch(t2, match2, styler));
130814
131188
  }
@@ -131034,10 +131408,10 @@ function callLines(text, iw, styler, indentMax) {
131034
131408
  if (rows.length > 1) for (const l of wrapColumns(CALL_JOIN_NOTE, Math.max(1, w - indentMax), styler)) out.push(fitLine(`${" ".repeat(indentMax)}${styler.fg("dim", l)}`, w, styler));
131035
131409
  return out;
131036
131410
  }
131037
- function sizingVerdict(dim, name, shown) {
131038
- if (dim.suggested) return `${name} ${shown(dim.suggested)} (${dim.reason}${dim.held === "cap" ? ", the most a live host offers" : ""})`;
131411
+ function sizingVerdict(dim, name, shown2) {
131412
+ if (dim.suggested) return `${name} ${shown2(dim.suggested)} (${dim.reason}${dim.held === "cap" ? ", the most a live host offers" : ""})`;
131039
131413
  if (dim.held === "largest") return `${name} stays (${dim.reason}, ${Number.isSafeInteger(dim.cap) && dim.current > dim.cap ? "already above the most a live host offers" : "already the largest size a live host offers"})`;
131040
- if (dim.held === "no-cap") return `${name} wants ${shown(dim.wanted)} (${dim.reason}, ${NO_CAP_WORDS[dim.capMissing] ?? NO_CAP_WORDS.unread}: no call)`;
131414
+ if (dim.held === "no-cap") return `${name} wants ${shown2(dim.wanted)} (${dim.reason}, ${NO_CAP_WORDS[dim.capMissing] ?? NO_CAP_WORDS.unread}: no call)`;
131041
131415
  return null;
131042
131416
  }
131043
131417
  function sizingLines(sizing, id, iw, styler) {
@@ -131408,9 +131782,9 @@ function runRow(row, sel, inner, styler) {
131408
131782
  }
131409
131783
  function outcomeColored(outcome, reason, styler) {
131410
131784
  if (outcome === "completed") return styler.fg("success", "\u2714 done");
131411
- const shown = (v) => scrubControl(String(v));
131412
- if (outcome === "policy") return styler.fg("warning", `\u26A0 ${shown(reason ?? "policy")}`);
131413
- return styler.fg("error", `\u2718 ${shown(reason ?? outcome ?? "failed")}`);
131785
+ const shown2 = (v) => scrubControl(String(v));
131786
+ if (outcome === "policy") return styler.fg("warning", `\u26A0 ${shown2(reason ?? "policy")}`);
131787
+ return styler.fg("error", `\u2718 ${shown2(reason ?? outcome ?? "failed")}`);
131414
131788
  }
131415
131789
  function settingsLines(settings2, inner, styler) {
131416
131790
  if (settings2 && settings2.invalid) return [styler.cell(`settings invalid: ${settings2.invalid}`, inner, { color: "error" })];
@@ -131423,7 +131797,7 @@ function settingsLines(settings2, inner, styler) {
131423
131797
  const l2 = [kv("dailyCap", o.dailyCap), tk("dailyTokenCap", o.dailyTokenCap), kv("concurrency", o.concurrency), styler.fg("muted", "softHold") + " " + styler.fg("text", pctVal)].join(gap);
131424
131798
  return [fitLine(l1, inner, styler), fitLine(l2, inner, styler)];
131425
131799
  }
131426
- function renderTriggerDetail(raw, inner, styler, sched = null, staged = null, fetchedAt, instructionsText = null) {
131800
+ function renderTriggerDetail(raw, inner, styler, sched = null, staged = null, fetchedAt, instructionsText = null, overlayMcp = null) {
131427
131801
  if (!raw) return [styler.cell("(no trigger)", inner, { color: "dim" })];
131428
131802
  const t2 = scrubTrigger(raw);
131429
131803
  const out = [];
@@ -131491,10 +131865,15 @@ function renderTriggerDetail(raw, inner, styler, sched = null, staged = null, fe
131491
131865
  out.push(kv("resume", t2.resume === true ? "continues the previous session" : "cold start", t2.resume === true ? "warning" : "dim"));
131492
131866
  kvWrap("secrets", t2.secrets > 0 ? `${t2.secrets} bound${t2.secretsProfile ? ` via profile ${cellOf(t2.secretsProfile)}` : ""}` : "none bound", t2.secrets > 0 ? "warning" : "dim");
131493
131867
  out.push(kv("replicas", t2.replicas > 1 ? `${t2.replicas} sandboxes race this flow` : "one run per delivery", t2.replicas > 1 ? "warning" : "dim"));
131868
+ kvWrap("mcp", Array.isArray(t2.mcp) && t2.mcp.length > 0 ? t2.mcp.map((n) => cellOf(n)).join(", ") : "none", Array.isArray(t2.mcp) && t2.mcp.length > 0 ? "warning" : "dim");
131494
131869
  blank();
131495
131870
  section("trust model");
131496
131871
  const bullet = (text, color) => wrapColumns(text, inner - 2, styler).forEach((part, i2) => out.push(fitLine(styler.fg("border", i2 === 0 ? "\xB7 " : " ") + styler.fg(color, part), inner, styler)));
131497
131872
  for (const line of trustModel(t2)) bullet(line, "text");
131873
+ if (Array.isArray(t2.mcp) && t2.mcp.length > 0) {
131874
+ for (const name of t2.mcp) bullet(mcpServerLine(name, overlayMcp), "warning");
131875
+ bullet("an MCP server's tools and text are input the job did not write; a stdio server runs code with the variables it names", "warning");
131876
+ }
131498
131877
  if (t2.packages === true) {
131499
131878
  const loads = (text) => bullet(text, "warning");
131500
131879
  loads(`packages loaded \xB7 ${stagedNames(staged)}`);
@@ -131525,6 +131904,20 @@ function spentMark(disarmed) {
131525
131904
  const by = disarmed.jobId ? ` by ${cellOf(disarmed.jobId)}` : "";
131526
131905
  return `spent ${cellOf(disarmed.at ?? "-")}${by}`;
131527
131906
  }
131907
+ function mcpServerLine(name, overlayMcp) {
131908
+ const label2 = `mcp ${cellOf(name)}`;
131909
+ if (name === FORGE_MCP_SERVER_NAME) return `${label2} \xB7 the forge's own server, as the job's own token`;
131910
+ if (overlayMcp?.unreadable === "invalid") return `${label2} \xB7 the overlay's mcp.json cannot be used, so every job is refused`;
131911
+ if (overlayMcp?.unreadable) return `${label2} \xB7 the overlay's mcp.json cannot be read just now (${cellOf(overlayMcp.unreadable)})`;
131912
+ const server = Array.isArray(overlayMcp?.servers) ? overlayMcp.servers.find((s2) => s2?.name === name) : void 0;
131913
+ if (!server) {
131914
+ const refused = Array.isArray(overlayMcp?.refused) && overlayMcp.refused.includes(name);
131915
+ return `${label2} \xB7 ${refused ? "refused by the overlay's mcp.json (pi-dispatch doctor says why)" : "not offered by the overlay"}, so every job is refused`;
131916
+ }
131917
+ if (server.enabled === false) return `${label2} \xB7 turned off in the overlay, so every job is refused`;
131918
+ const refs = Array.isArray(server.refs) && server.refs.length > 0 ? ` \xB7 handed ${server.refs.map((r2) => cellOf(r2)).join(", ")}` : "";
131919
+ return `${label2} \xB7 ${cellOf(server.transport)} ${cellOf(server.where)} \xB7 ${cellOf(server.exposure)}${refs}`;
131920
+ }
131528
131921
  function stagedNames(staged) {
131529
131922
  const list2 = Array.isArray(staged?.packages) ? staged.packages : [];
131530
131923
  return list2.length > 0 ? list2.map(cellOf).join(" \xB7 ") : "(none staged in the overlay)";
@@ -131911,12 +132304,12 @@ __export(setup_wizard_exports, {
131911
132304
  import * as nodeFs7 from "node:fs";
131912
132305
  import { execFileSync as execFileSync4, spawn as nodeSpawn2 } from "node:child_process";
131913
132306
  import { homedir as homedir5 } from "node:os";
131914
- import { join as join16 } from "node:path";
132307
+ import { join as join17 } from "node:path";
131915
132308
  function envIsSet(env2, key) {
131916
132309
  return typeof env2[key] === "string" && env2[key] !== "";
131917
132310
  }
131918
132311
  function hasCwdScaffold(cwd, fs7) {
131919
- return CWD_SCAFFOLD_FILES.every((name) => fs7.existsSync(join16(cwd, name)));
132312
+ return CWD_SCAFFOLD_FILES.every((name) => fs7.existsSync(join17(cwd, name)));
131920
132313
  }
131921
132314
  async function defaultProbeQueue(url) {
131922
132315
  return !(await readQueueState({ url })).unreachable;
@@ -132075,7 +132468,7 @@ function podmanPreferred(env2, dir, fs7) {
132075
132468
  return fromFile2.podmanUsed && !fromFile2.localUsed;
132076
132469
  }
132077
132470
  function deploymentBackends(dir, fs7, { writing = false, platform = process.platform, readBackFn = readStackKeys } = {}) {
132078
- const envPath = join16(dir, ".env");
132471
+ const envPath = join17(dir, ".env");
132079
132472
  let text;
132080
132473
  try {
132081
132474
  if (!fs7.existsSync(envPath)) return { absent: true };
@@ -132150,25 +132543,25 @@ function npmSpawnOptions(platform, dir) {
132150
132543
  }
132151
132544
  function listRepoSkills(cwd, fs7 = nodeFs7) {
132152
132545
  try {
132153
- const root = join16(cwd, ".pi", "skills");
132154
- return fs7.readdirSync(root).filter((name) => SKILL_NAME_RE.test(name) && fs7.existsSync(join16(root, name, "SKILL.md")));
132546
+ const root = join17(cwd, ".pi", "skills");
132547
+ return fs7.readdirSync(root).filter((name) => SKILL_NAME_RE.test(name) && fs7.existsSync(join17(root, name, "SKILL.md")));
132155
132548
  } catch {
132156
132549
  return [];
132157
132550
  }
132158
132551
  }
132159
132552
  function runtimeDirFor(dir, pkg = "pi-dispatch") {
132160
- return join16(dir, "node_modules", "@edgehero", pkg);
132553
+ return join17(dir, "node_modules", "@edgehero", pkg);
132161
132554
  }
132162
132555
  function readInstalledVersion(fs7, runtimeDir) {
132163
132556
  try {
132164
- const pkg = JSON.parse(fs7.readFileSync(join16(runtimeDir, "package.json"), "utf8"));
132557
+ const pkg = JSON.parse(fs7.readFileSync(join17(runtimeDir, "package.json"), "utf8"));
132165
132558
  return typeof pkg?.version === "string" ? pkg.version : void 0;
132166
132559
  } catch {
132167
132560
  return void 0;
132168
132561
  }
132169
132562
  }
132170
132563
  function ensureDeploymentPackageJson(fs7, dir) {
132171
- const rootPkg = join16(dir, "package.json");
132564
+ const rootPkg = join17(dir, "package.json");
132172
132565
  if (!fs7.existsSync(rootPkg)) {
132173
132566
  fs7.writeFileSync(rootPkg, `${JSON.stringify({ name: "pi-dispatch-deployment", private: true }, null, 2)}
132174
132567
  `);
@@ -132216,7 +132609,7 @@ async function runSetupWizard(paths, rawCtx, notify, deps = {}) {
132216
132609
  return;
132217
132610
  }
132218
132611
  if (intent !== "Guided setup") return;
132219
- const defaultDir = join16(homedirFn(), "pi-dispatch");
132612
+ const defaultDir = join17(homedirFn(), "pi-dispatch");
132220
132613
  const dirAnswer = await ui.input("deployment directory \u2014 where the runtime and its config live", defaultDir);
132221
132614
  const dir = dirAnswer === void 0 || dirAnswer.trim() === "" ? defaultDir : dirAnswer.trim();
132222
132615
  try {
@@ -132226,7 +132619,7 @@ async function runSetupWizard(paths, rawCtx, notify, deps = {}) {
132226
132619
  return;
132227
132620
  }
132228
132621
  const runtimeDir = runtimeDirFor(dir);
132229
- const cliPath = join16(runtimeDir, "src", "cli.mjs");
132622
+ const cliPath = join17(runtimeDir, "src", "cli.mjs");
132230
132623
  let runtime;
132231
132624
  if (podmanPreferred(env2, dir, fs7)) {
132232
132625
  if (!await ensurePodman(ui, notify, { platform, probePodmanFn })) return;
@@ -132245,7 +132638,7 @@ async function runSetupWizard(paths, rawCtx, notify, deps = {}) {
132245
132638
  }
132246
132639
  if ("value" in fileBackends && !venuesOf({ PI_BACKENDS: fileBackends.value }).podmanUsed) {
132247
132640
  notify?.(
132248
- `${join16(dir, ".env")} sets PI_BACKENDS=${fileBackends.value}, which does not list podman, and setup never overwrites a key you set. Change it to PI_BACKENDS=podman (or add podman to the list), then re-run /dispatch setup. Nothing was installed`,
132641
+ `${join17(dir, ".env")} sets PI_BACKENDS=${fileBackends.value}, which does not list podman, and setup never overwrites a key you set. Change it to PI_BACKENDS=podman (or add podman to the list), then re-run /dispatch setup. Nothing was installed`,
132249
132642
  "error"
132250
132643
  );
132251
132644
  return;
@@ -132326,11 +132719,11 @@ up shows each docker action and asks y/N before it \u2014 nothing is auto-accept
132326
132719
  version: POINTER_VERSION,
132327
132720
  deploymentDir: dir,
132328
132721
  env: {
132329
- PI_TRIGGERS_FILE: join16(dir, "triggers.json"),
132330
- PI_PAUSE_WINDOWS_FILE: join16(dir, "pause-windows.json"),
132331
- PI_SCOPED_LIMITS_FILE: join16(dir, "scoped-limits.json"),
132332
- PI_PROJECTS_FILE: join16(dir, "projects.json"),
132333
- PI_SUBSCRIPTIONS_FILE: join16(dir, "subscriptions.json")
132722
+ PI_TRIGGERS_FILE: join17(dir, "triggers.json"),
132723
+ PI_PAUSE_WINDOWS_FILE: join17(dir, "pause-windows.json"),
132724
+ PI_SCOPED_LIMITS_FILE: join17(dir, "scoped-limits.json"),
132725
+ PI_PROJECTS_FILE: join17(dir, "projects.json"),
132726
+ PI_SUBSCRIPTIONS_FILE: join17(dir, "subscriptions.json")
132334
132727
  }
132335
132728
  };
132336
132729
  const pPath = pointerPath(env2);
@@ -132348,7 +132741,7 @@ ${JSON.stringify(pointer, null, 2)}`);
132348
132741
  }
132349
132742
  }
132350
132743
  notify?.(
132351
- `provider key: set ANTHROPIC_API_KEY (or your provider's key) in ${join16(dir, ".env")} \u2014 already logged into pi on this machine? leave it blank and jobs use that login. Setup never reads or writes the key itself.`,
132744
+ `provider key: set ANTHROPIC_API_KEY (or your provider's key) in ${join17(dir, ".env")} \u2014 already logged into pi on this machine? leave it blank and jobs use that login. Setup never reads or writes the key itself.`,
132352
132745
  "info"
132353
132746
  );
132354
132747
  const workerChoice = await ui.select("Run the worker", [
@@ -132452,10 +132845,10 @@ then, in the same folder:
132452
132845
  return;
132453
132846
  }
132454
132847
  if (choice === EDGE_COMPOSE) {
132455
- const deployDir = join16(dir, "deploy");
132848
+ const deployDir = join17(dir, "deploy");
132456
132849
  const copies = [
132457
- [join16(runtimeDirFor(dir), "deploy", "docker-compose.yml"), join16(deployDir, "docker-compose.yml"), true],
132458
- [join16(runtimeDirFor(dir), "deploy", "egress-proxy.conf"), join16(deployDir, "egress-proxy.conf"), false]
132850
+ [join17(runtimeDirFor(dir), "deploy", "docker-compose.yml"), join17(deployDir, "docker-compose.yml"), true],
132851
+ [join17(runtimeDirFor(dir), "deploy", "egress-proxy.conf"), join17(deployDir, "egress-proxy.conf"), false]
132459
132852
  ];
132460
132853
  const dest = copies[0][1];
132461
132854
  for (const [src, to, required] of copies) {
@@ -132480,14 +132873,14 @@ then, in the same folder:
132480
132873
  }
132481
132874
  }
132482
132875
  const project = composeProjectName(dir);
132483
- const legacy = join16(dir, "docker-compose.yml");
132876
+ const legacy = join17(dir, "docker-compose.yml");
132484
132877
  if (fs7.existsSync(legacy)) {
132485
132878
  notify?.(
132486
132879
  `${legacy} is an earlier setup's copy: its ../ paths reach the folder above ${dir}, so it is not used. It ran as compose project "${project}" (its volume ${project}_valkey-data), which the command below keeps with -p ${project}. ${dest} is the one to run; move any edits of yours there, then remove it`,
132487
132880
  "warning"
132488
132881
  );
132489
132882
  }
132490
- const override = join16(dir, COMPOSE_VALKEY_OVERRIDE);
132883
+ const override = join17(dir, COMPOSE_VALKEY_OVERRIDE);
132491
132884
  const hasOverride = fs7.existsSync(override);
132492
132885
  const port = composeValkeyPort(dir, fs7);
132493
132886
  let real = dir;
@@ -132513,7 +132906,7 @@ then, in the same folder:
132513
132906
  const args = [...base, "--profile", "receiver", "up", "-d"];
132514
132907
  const pull = [...composeArgs({ project, override: hasOverride }), "--profile", "receiver", "pull"];
132515
132908
  const portPrefix = port !== 6379 ? `${VALKEY_PORT_KEY}=${port} ` : "";
132516
- const shown = `${portPrefix}docker ${args.join(" ")}`;
132909
+ const shown2 = `${portPrefix}docker ${args.join(" ")}`;
132517
132910
  const shownPull = `${portPrefix}docker ${pull.join(" ")}`;
132518
132911
  const writeLine = handover && !hasOverride ? `write ${override}:
132519
132912
  ${VALKEY_HANDOVER_OVERRIDE.replace(/^/gm, " ")}` : "";
@@ -132525,17 +132918,17 @@ ${VALKEY_HANDOVER_OVERRIDE.replace(/^/gm, " ")}` : "";
132525
132918
  const ok = await ui.confirm(
132526
132919
  "Start the receiver container",
132527
132920
  `Run in ${dir}:
132528
- ${handoverLines}${shown}
132921
+ ${handoverLines}${shown2}
132529
132922
 
132530
- the receiver profile reads ${join16(dir, ".env")}: WEBHOOK_SECRET and the forge credentials must be in there, or the container refuses to boot.`
132923
+ the receiver profile reads ${join17(dir, ".env")}: WEBHOOK_SECRET and the forge credentials must be in there, or the container refuses to boot.`
132531
132924
  );
132532
132925
  if (!ok) {
132533
132926
  notify?.(`later: ${handover ? `${shownPull}; ${writeLine ? `write ${override} with:
132534
- ${VALKEY_HANDOVER_OVERRIDE}` : ""}docker stop pi-dispatch-valkey && docker rm pi-dispatch-valkey; then ` : ""}${shown} (in ${dir})`, "info");
132927
+ ${VALKEY_HANDOVER_OVERRIDE}` : ""}docker stop pi-dispatch-valkey && docker rm pi-dispatch-valkey; then ` : ""}${shown2} (in ${dir})`, "info");
132535
132928
  return;
132536
132929
  }
132537
132930
  try {
132538
- const envPath = join16(dir, ".env");
132931
+ const envPath = join17(dir, ".env");
132539
132932
  if (fs7.existsSync(envPath)) {
132540
132933
  const read = deploymentServiceEnv({ env: {}, dir, keys: [VALKEY_PORT_KEY], fs: fs7, platform: "linux" });
132541
132934
  const found = read.fromFile[VALKEY_PORT_KEY] ?? (read.unread.length > 0 || read.hazardSkipped.length > 0 || read.untrusted ? "?" : void 0);
@@ -132595,22 +132988,22 @@ ${VALKEY_HANDOVER_OVERRIDE}` : ""}docker stop pi-dispatch-valkey && docker rm pi
132595
132988
  notify?.(`docker rm pi-dispatch-valkey failed; up's Valkey is STOPPED. \`docker start pi-dispatch-valkey\` brings it back on its volume.${putBack()}`, "error");
132596
132989
  return;
132597
132990
  }
132598
- if (!await step(args, shown, composeEnv)) {
132599
- notify?.(`${shown} failed after up's Valkey was removed: NO Valkey runs now. Its volume, pi-dispatch-valkey-data, and the queue in it are kept: \`node ${cliPath} up\` (in ${dir}) starts pi-dispatch-valkey on it again.${putBack()} Compose's own output above says why it failed`, "error");
132991
+ if (!await step(args, shown2, composeEnv)) {
132992
+ notify?.(`${shown2} failed after up's Valkey was removed: NO Valkey runs now. Its volume, pi-dispatch-valkey-data, and the queue in it are kept: \`node ${cliPath} up\` (in ${dir}) starts pi-dispatch-valkey on it again.${putBack()} Compose's own output above says why it failed`, "error");
132600
132993
  return;
132601
132994
  }
132602
132995
  await ownerMarker();
132603
132996
  if (plan.record) {
132604
132997
  try {
132605
- fs7.writeFileSync(join16(dir, VALKEY_VOLUME_RECORD), volumeRecordText(plan.record), { mode: 384, flag: "wx" });
132998
+ fs7.writeFileSync(join17(dir, VALKEY_VOLUME_RECORD), volumeRecordText(plan.record), { mode: 384, flag: "wx" });
132606
132999
  } catch (err) {
132607
- if (err?.code !== "EEXIST") notify?.(`the volume could not be recorded in ${join16(dir, VALKEY_VOLUME_RECORD)} (${err?.message ?? err}); a later \`up\` asks about it`, "warning");
133000
+ if (err?.code !== "EEXIST") notify?.(`the volume could not be recorded in ${join17(dir, VALKEY_VOLUME_RECORD)} (${err?.message ?? err}); a later \`up\` asks about it`, "warning");
132608
133001
  }
132609
133002
  }
132610
133003
  return;
132611
133004
  }
132612
- if (!await step(args, shown, composeEnv)) {
132613
- notify?.(`${shown} failed; compose's own output above says why (another container on 127.0.0.1:${port} is the usual one)`, "error");
133005
+ if (!await step(args, shown2, composeEnv)) {
133006
+ notify?.(`${shown2} failed; compose's own output above says why (another container on 127.0.0.1:${port} is the usual one)`, "error");
132614
133007
  return;
132615
133008
  }
132616
133009
  if (useOverride) await ownerMarker();
@@ -132642,7 +133035,7 @@ function podmanWriteVerify(envPath, prior, readBackFn = readStackKeys) {
132642
133035
  };
132643
133036
  }
132644
133037
  function recordPodmanVenue(dir, fs7, notify, platform, readBackFn = readStackKeys) {
132645
- const envPath = join16(dir, ".env");
133038
+ const envPath = join17(dir, ".env");
132646
133039
  if (!fs7.existsSync(envPath)) {
132647
133040
  notify?.(`no ${envPath} yet (\`up\` runs init, which writes it): add PI_BACKENDS=podman to it before \`service install\`, which reads only that file`, "warning");
132648
133041
  return;
@@ -132699,11 +133092,11 @@ async function offerFirstTrigger(repoCwd, deployDir, ui, notify, { fs: fs7, list
132699
133092
  const task = taskAnswer.trim() === "" ? "run the flow" : taskAnswer.trim();
132700
133093
  notify?.(`a local cron job edits ${repoCwd} IN PLACE with no undo \u2014 commit or stash before it first fires`, "warning");
132701
133094
  notify?.(
132702
- `to let AI-invoked runs use this flow, add \`ai-trigger: allow\` to ${join16(".pi", "skills", flow, "SKILL.md")} frontmatter \u2014 takes effect once committed. Setup never writes into the repo.`,
133095
+ `to let AI-invoked runs use this flow, add \`ai-trigger: allow\` to ${join17(".pi", "skills", flow, "SKILL.md")} frontmatter \u2014 takes effect once committed. Setup never writes into the repo.`,
132703
133096
  "info"
132704
133097
  );
132705
133098
  const entry = buildTriggerEntry("cron", { id, pattern, folder: repoCwd, flow, task });
132706
- const res = writeTriggersFn({ triggersPath: join16(deployDir, "triggers.json"), mutate: (list2) => [...list2, entry] });
133099
+ const res = writeTriggersFn({ triggersPath: join17(deployDir, "triggers.json"), mutate: (list2) => [...list2, entry] });
132707
133100
  notify?.(
132708
133101
  res.ok !== false && !res.invalid ? `trigger added \u2014 ${id} (${pattern}) runs ${flow} in ${repoCwd}` : `trigger rejected: ${res.invalid}`,
132709
133102
  res.invalid ? "error" : "info"
@@ -132719,8 +133112,8 @@ function registerNudge(pi, deps = {}) {
132719
133112
  if (POINTER_ENV_ALLOWLIST.some((key) => envIsSet(env2, key))) return;
132720
133113
  const cwd = typeof ctx?.cwd === "string" && ctx.cwd !== "" ? ctx.cwd : process.cwd();
132721
133114
  if (hasCwdScaffold(cwd, fs7)) return;
132722
- const agentDir = env2.PI_CODING_AGENT_DIR || join16(homedirFn(), ".pi", "agent");
132723
- const marker = join16(agentDir, NUDGE_MARKER_BASENAME);
133115
+ const agentDir = env2.PI_CODING_AGENT_DIR || join17(homedirFn(), ".pi", "agent");
133116
+ const marker = join17(agentDir, NUDGE_MARKER_BASENAME);
132724
133117
  if (fs7.existsSync(marker)) return;
132725
133118
  ctx?.ui?.notify?.("pi-dispatch: no deployment configured \u2014 /dispatch setup gets you started", "info");
132726
133119
  fs7.writeFileSync(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
@@ -132743,8 +133136,8 @@ var init_setup_wizard = __esm({
132743
133136
  init_connection();
132744
133137
  init_service_env();
132745
133138
  init_index();
132746
- RUNTIME_VERSION = "4.1.0";
132747
- RECEIVER_VERSION = "4.1.0";
133139
+ RUNTIME_VERSION = "4.2.0";
133140
+ RECEIVER_VERSION = "4.2.0";
132748
133141
  RUNTIME_PKG = "@edgehero/pi-dispatch";
132749
133142
  RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
132750
133143
  NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
@@ -132865,7 +133258,7 @@ import { VERSION as VERSION3 } from "@earendil-works/pi-coding-agent";
132865
133258
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
132866
133259
  import * as nodeFs8 from "node:fs";
132867
133260
  import { hostname as hostname2 } from "node:os";
132868
- import { basename as basename7, join as join17 } from "node:path";
133261
+ import { basename as basename8, join as join18 } from "node:path";
132869
133262
  function computePiVersionAdvisory(version, supported) {
132870
133263
  if (version === supported || version === "0.0.0") return void 0;
132871
133264
  return `pi-dispatch admin: running on pi ${version}, tested with pi ${supported} \u2014 things should work; report anything odd, and check for a newer @edgehero/pi-dispatch-admin`;
@@ -132934,15 +133327,15 @@ function guardedFiles() {
132934
133327
  { path: set("PI_SCOPED_LIMITS_FILE") ? paths.scopedLimitsPath : null, label: "scoped-limits.json (PI_SCOPED_LIMITS_FILE)", tool: "dispatch_limit_add, _edit or _delete" },
132935
133328
  { path: set("PI_TRIGGERS_FILE") ? paths.triggersPath : null, label: "triggers.json (PI_TRIGGERS_FILE)", tool: "dispatch_trigger_add, _edit or _delete" },
132936
133329
  { path: paths.settingsFile, label: "the settings overlay (PI_SETTINGS_FILE)", tool: "dispatch_set" },
132937
- { path: typeof dir === "string" && dir !== "" ? join17(dir, ".env") : null, label: "the deployment's .env, which names these files (PI_*_FILE)", tool: "the operator's own editor" },
133330
+ { path: typeof dir === "string" && dir !== "" ? join18(dir, ".env") : null, label: "the deployment's .env, which names these files (PI_*_FILE)", tool: "the operator's own editor" },
132938
133331
  { path: pointerPath(process.env), label: "the deployment pointer (PI_DISPATCH_DEPLOYMENT_FILE)", tool: "/dispatch setup" }
132939
133332
  ];
132940
133333
  if (!(typeof dir === "string" && dir !== "")) {
132941
133334
  const cwd = process.cwd();
132942
133335
  out.push(...envNamedFiles(cwd));
132943
- if (DEPLOYMENT_SCAFFOLD_FILES.every((f3) => nodeFs8.existsSync(join17(cwd, f3)))) {
133336
+ if (DEPLOYMENT_SCAFFOLD_FILES.every((f3) => nodeFs8.existsSync(join18(cwd, f3)))) {
132944
133337
  out.push(
132945
- { path: join17(cwd, ".env"), label: "the deployment's .env, which names these files (PI_*_FILE)", tool: "the operator's own editor" },
133338
+ { path: join18(cwd, ".env"), label: "the deployment's .env, which names these files (PI_*_FILE)", tool: "the operator's own editor" },
132946
133339
  { path: paths.triggersPath, label: "triggers.json (PI_TRIGGERS_FILE)", tool: "dispatch_trigger_add, _edit or _delete" },
132947
133340
  { path: paths.scopedLimitsPath, label: "scoped-limits.json (PI_SCOPED_LIMITS_FILE)", tool: "dispatch_limit_add, _edit or _delete" },
132948
133341
  { path: paths.projectsPath, label: "projects.json (PI_PROJECTS_FILE)", tool: "dispatch_project_add, _edit or _delete" }
@@ -133024,9 +133417,9 @@ function registerTools(pi) {
133024
133417
  const set = await killSwitchSet({ paused: true });
133025
133418
  if (set.refused) throw new Error(set.refused);
133026
133419
  const lines = [];
133027
- for (const { shown, res } of set.results) {
133028
- const at = set.results.length > 1 ? ` at ${shown}` : "";
133029
- if (res.unreachable && !res.partial) throw new Error(`could not reach the queue at ${shown}: ${res.unreachable}`);
133420
+ for (const { shown: shown2, res } of set.results) {
133421
+ const at = set.results.length > 1 ? ` at ${shown2}` : "";
133422
+ if (res.unreachable && !res.partial) throw new Error(`could not reach the queue at ${shown2}: ${res.unreachable}`);
133030
133423
  if (res.partial) throw new Error(`paused ${res.partial.done.join(", ")}${at} but FAILED at ${res.partial.failed}: the deployment is half paused: ${res.partial.error}`);
133031
133424
  lines.push(`paused${at}${res.queues.length > 1 ? ` (${res.queues.length} queues)` : ""}`);
133032
133425
  }
@@ -133042,9 +133435,9 @@ function registerTools(pi) {
133042
133435
  async execute() {
133043
133436
  const set = await killSwitchSet({ paused: false });
133044
133437
  if (set.refused) throw new Error(set.refused);
133045
- const { shown, res } = set.results[0];
133438
+ const { shown: shown2, res } = set.results[0];
133046
133439
  if (res.unreachable && !res.partial) {
133047
- throw new Error(`could not reach the queue at ${shown}: ${res.unreachable}`);
133440
+ throw new Error(`could not reach the queue at ${shown2}: ${res.unreachable}`);
133048
133441
  }
133049
133442
  if (res.partial) {
133050
133443
  throw new Error(`resumed ${res.partial.done.join(", ")} but FAILED at ${res.partial.failed} \u2014 the deployment is half resumed: ${res.partial.error}`);
@@ -133109,23 +133502,23 @@ function registerTools(pi) {
133109
133502
  }
133110
133503
  const paths = resolvePaths(deploymentEnv());
133111
133504
  const env2 = deploymentEnv();
133112
- const shown = { deploymentDir: pointerState().deploymentDir ?? null };
133505
+ const shown2 = { deploymentDir: pointerState().deploymentDir ?? null };
133113
133506
  const view = readSettingsView({ settingsFile: paths.settingsFile });
133114
133507
  if (view?.invalid && !blankSettingsFile(paths.settingsFile)) throw new Error(`rejected: ${overlayInvalidRefusal(view.invalid)}`);
133115
133508
  const unset = params.value === void 0 || params.value.trim() === "";
133116
133509
  const newVal = unset ? void 0 : coerceSettingValue(params.key, params.value.trim());
133117
133510
  if (!unset && DOLLAR_SETTING_KEYS.includes(params.key)) parseUsdMicros(newVal, params.key);
133118
133511
  const afterOverlay = unset ? Object.fromEntries(Object.entries(view?.overlay ?? {}).filter(([k]) => k !== params.key)) : { ...view?.overlay ?? {}, [params.key]: newVal };
133119
- const after = unset ? settingShown(params.key, afterOverlay, env2, shown) : String(newVal);
133512
+ const after = unset ? settingShown(params.key, afterOverlay, env2, shown2) : String(newVal);
133120
133513
  const isDollarKey = DOLLAR_SETTING_KEYS.includes(params.key);
133121
- const problem = isDollarKey ? mergedDollarProblem(afterOverlay, env2, shown) : null;
133514
+ const problem = isDollarKey ? mergedDollarProblem(afterOverlay, env2, shown2) : null;
133122
133515
  const broken = settingsEnvelopeRefusal(params.key, view?.overlay ?? {}, afterOverlay, paths, env2);
133123
133516
  if (broken) throw new Error(`rejected: ${escapeInterpreted(broken)}`);
133124
133517
  const result = await confirmedWrite(
133125
133518
  ctx,
133126
133519
  {
133127
133520
  title: unset ? `Unset ${params.key}` : `Set ${params.key}`,
133128
- message: `${params.key}: ${settingShown(params.key, view?.overlay, env2, shown)} -> ${after}${problem?.warning ? `
133521
+ message: `${params.key}: ${settingShown(params.key, view?.overlay, env2, shown2)} -> ${after}${problem?.warning ? `
133129
133522
 
133130
133523
  Warning: ${problem.warning}` : ""}`
133131
133524
  },
@@ -134029,14 +134422,17 @@ function sizeFieldsOf(f3) {
134029
134422
  }
134030
134423
  return out;
134031
134424
  }
134425
+ function mcpFieldNearMiss(key) {
134426
+ return key === "mcp" || isMcpNearMiss(key) ? "run.mcp" : null;
134427
+ }
134032
134428
  function refuseWideningFields(params, tool) {
134033
134429
  const objects = [["", params], ["run.", params?.run], ["on.", params?.on]];
134034
134430
  for (const [label2, source] of objects) {
134035
134431
  if (source === null || typeof source !== "object" || Array.isArray(source)) continue;
134036
134432
  for (const key of Object.keys(source)) {
134037
- const meant = runModelFieldNearMiss(key);
134433
+ const meant = runModelFieldNearMiss(key) ?? mcpFieldNearMiss(key);
134038
134434
  if (meant === null || !TOOL_PROOF_FIELDS.has(meant)) continue;
134039
- const what = meant === "run.models" ? "allowed-model list" : "per-job dollar cap";
134435
+ const what = meant === "run.models" ? "allowed-model list" : meant === "run.mcp" ? "list of MCP servers" : "per-job dollar cap";
134040
134436
  throw new Error(`${tool} cannot set ${meant} (sent as ${label2}${key}): a trigger's ${what} is set by hand in the triggers file, never by a tool`);
134041
134437
  }
134042
134438
  }
@@ -134213,11 +134609,11 @@ ${scopedText}` : ""}`);
134213
134609
  return;
134214
134610
  }
134215
134611
  if (set.disagreement) notify?.(`${set.disagreement}: pausing both`, "warning");
134216
- for (const { shown, res } of set.results) {
134217
- const at = set.results.length > 1 ? ` at ${shown}` : "";
134612
+ for (const { shown: shown2, res } of set.results) {
134613
+ const at = set.results.length > 1 ? ` at ${shown2}` : "";
134218
134614
  if (res.unreachable) {
134219
134615
  notify?.(
134220
- res.partial ? `${paused ? "paused" : "resumed"} ${res.partial.done.join(", ")}${at} but FAILED at ${res.partial.failed}: the deployment is half ${paused ? "paused" : "resumed"}` : `could not reach Valkey at ${shown}: ${valkeyDownHint(shown)}`,
134616
+ res.partial ? `${paused ? "paused" : "resumed"} ${res.partial.done.join(", ")}${at} but FAILED at ${res.partial.failed}: the deployment is half ${paused ? "paused" : "resumed"}` : `could not reach Valkey at ${shown2}: ${valkeyDownHint(shown2)}`,
134221
134617
  "error"
134222
134618
  );
134223
134619
  continue;
@@ -134391,7 +134787,7 @@ async function assembleAllocationView(paths, counts, { fullPaths = false } = {})
134391
134787
  const read = readEnvelope({ envelopeFile: paths.envelopeFile, projectsPath: paths.projectsFile, scopedLimitsPath: paths.scopedLimitsPath, maxCostMicros: envelopeJobCap({ settingsFile: paths.settingsFile, env: deploymentEnv() }) });
134392
134788
  if (!read.envelope) {
134393
134789
  const file = String(paths.envelopeFile);
134394
- const invalid = read.invalid !== void 0 && !fullPaths ? String(read.invalid).split(file).join(basename7(file)) : read.invalid;
134790
+ const invalid = read.invalid !== void 0 && !fullPaths ? String(read.invalid).split(file).join(basename8(file)) : read.invalid;
134395
134791
  return { problem: envelopeProblem({ ...read, invalid }) };
134396
134792
  }
134397
134793
  const e2 = read.envelope;
@@ -134840,11 +135236,11 @@ The project's runs could not be read (${sizing.unreachable}).`;
134840
135236
  const m2 = s2.memory;
134841
135237
  const c = s2.cpu;
134842
135238
  const peaks = `p95 peak ${m2.evidence.p95MiB === null ? "none" : formatMemory(m2.evidence.p95MiB)} over ${m2.evidence.samples} runs, p95 ${c.evidence.p95CoresCenti === null ? "none" : coresText(c.evidence.p95CoresCenti)} used over ${c.evidence.samples} runs`;
134843
- const said = (dim, words, shown) => {
134844
- if (dim.suggested) return `${shown(dim.suggested)} (${dim.reason}: ${words})`;
134845
- if (dim.held === "no-cap") return `${shown(dim.wanted)} (${dim.reason}: ${words}; budget not checked here)`;
135239
+ const said = (dim, words, shown2) => {
135240
+ if (dim.suggested) return `${shown2(dim.suggested)} (${dim.reason}: ${words})`;
135241
+ if (dim.held === "no-cap") return `${shown2(dim.wanted)} (${dim.reason}: ${words}; budget not checked here)`;
134846
135242
  if (dim.held === "largest") return `no larger size (${dim.reason}: ${words})`;
134847
- return dim.reason === "not-enough-runs" ? notEnoughRunsText(dim, shown === formatCpus ? cpusText : shown) : "fits";
135243
+ return dim.reason === "not-enough-runs" ? notEnoughRunsText(dim, shown2 === formatCpus ? cpusText : shown2) : "fits";
134848
135244
  };
134849
135245
  const facts = [s2.words.memoryFact, s2.words.cpuFact].filter(Boolean);
134850
135246
  return `
@@ -135279,7 +135675,7 @@ var init_index = __esm({
135279
135675
  hostShare: (v) => optInt(v),
135280
135676
  minJobs: (v) => optInt(v)
135281
135677
  };
135282
- TOOL_PROOF_FIELDS = /* @__PURE__ */ new Set(["run.maxCostUsd", "run.models"]);
135678
+ TOOL_PROOF_FIELDS = /* @__PURE__ */ new Set(["run.maxCostUsd", "run.models", "run.mcp"]);
135283
135679
  PRICING = { listPricedModels, getPricedModel, isZeroRated, reprice, piAiVersion };
135284
135680
  INSIGHTS_USAGE = "usage: /dispatch insights [7d|30d|mtd] [--no-open] [--full-paths] | /dispatch insights whatif <provider/model> --flow <flow>";
135285
135681
  SPLIT_REFUSALS2 = INSIGHTS_SPLIT_REFUSALS;