@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.
- package/README.md +3 -0
- package/dist/index.mjs +596 -200
- 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
|
|
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
|
|
10672
|
-
const shownAsUtc = Date.UTC(
|
|
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 (
|
|
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
|
|
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}` :
|
|
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
|
|
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 (!
|
|
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" || !
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
|
14192
|
-
return `a character (${
|
|
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
|
|
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 =
|
|
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
|
|
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 ||
|
|
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" || !
|
|
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" && !
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
15867
|
-
const { fromMs, source: _source, truncated, ...counts } =
|
|
15868
|
-
return { report: { ...report, hosts: [
|
|
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 (!
|
|
16738
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
17757
|
+
const root = join9(globalPiDir, PACKAGES_SUBDIR, pkg.dir);
|
|
17477
17758
|
let sources;
|
|
17478
17759
|
try {
|
|
17479
|
-
const parsed = JSON.parse(readFile2(
|
|
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 = [
|
|
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:
|
|
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 (
|
|
17784
|
+
if (basename3(source) === "SKILL.md") return fileExists(source) ? [source] : [];
|
|
17504
17785
|
const found = [];
|
|
17505
|
-
const own =
|
|
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(
|
|
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
|
|
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" &&
|
|
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
|
|
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 =
|
|
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
|
|
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 !== "" && !
|
|
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
|
|
20736
|
-
if (
|
|
20737
|
-
return { report:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
24986
|
-
const tip =
|
|
24987
|
-
spendParts.push(`<text x="${fmt(pl.x + 16)}" y="${fmt(pl.y + SPEND_BADGE_DY)}" font-size="10" fill="${fill}">${escapeHtml(
|
|
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) :
|
|
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
|
|
37891
|
+
return basename4(new URL(url).pathname);
|
|
37584
37892
|
} catch {
|
|
37585
|
-
return
|
|
37893
|
+
return basename4(url);
|
|
37586
37894
|
}
|
|
37587
37895
|
}
|
|
37588
37896
|
const path6 = "path" in value && value.path && String(value.path);
|
|
37589
|
-
return path6 ?
|
|
37897
|
+
return path6 ? basename4(path6) : void 0;
|
|
37590
37898
|
}
|
|
37591
|
-
function
|
|
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) :
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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) =>
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
-
})],
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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(
|
|
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
|
|
126367
|
+
const shown2 = cleaned[1];
|
|
126060
126368
|
const original = args[1];
|
|
126061
|
-
if (!Array.isArray(
|
|
126062
|
-
const at =
|
|
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
|
|
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("..") && !
|
|
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
|
|
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 =
|
|
128534
|
+
const dir = join15(sandboxDir, name);
|
|
128166
128535
|
try {
|
|
128167
|
-
const manifest = JSON.parse(fs7.readFileSync(
|
|
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(
|
|
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
|
|
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}${
|
|
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 =
|
|
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
|
|
130605
|
-
const hidden = rows.length -
|
|
130606
|
-
const body =
|
|
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
|
|
130657
|
-
const hidden = rows.length -
|
|
130658
|
-
const body =
|
|
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,
|
|
131038
|
-
if (dim.suggested) return `${name} ${
|
|
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 ${
|
|
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
|
|
131412
|
-
if (outcome === "policy") return styler.fg("warning", `\u26A0 ${
|
|
131413
|
-
return styler.fg("error", `\u2718 ${
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
132154
|
-
return fs7.readdirSync(root).filter((name) => SKILL_NAME_RE.test(name) && fs7.existsSync(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
`${
|
|
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:
|
|
132330
|
-
PI_PAUSE_WINDOWS_FILE:
|
|
132331
|
-
PI_SCOPED_LIMITS_FILE:
|
|
132332
|
-
PI_PROJECTS_FILE:
|
|
132333
|
-
PI_SUBSCRIPTIONS_FILE:
|
|
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 ${
|
|
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 =
|
|
132848
|
+
const deployDir = join17(dir, "deploy");
|
|
132456
132849
|
const copies = [
|
|
132457
|
-
[
|
|
132458
|
-
[
|
|
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 =
|
|
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 =
|
|
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
|
|
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}${
|
|
132921
|
+
${handoverLines}${shown2}
|
|
132529
132922
|
|
|
132530
|
-
the receiver profile reads ${
|
|
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 ` : ""}${
|
|
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 =
|
|
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,
|
|
132599
|
-
notify?.(`${
|
|
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(
|
|
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 ${
|
|
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,
|
|
132613
|
-
notify?.(`${
|
|
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 =
|
|
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 ${
|
|
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:
|
|
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 ||
|
|
132723
|
-
const marker =
|
|
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.
|
|
132747
|
-
RECEIVER_VERSION = "4.
|
|
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
|
|
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 !== "" ?
|
|
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(
|
|
133336
|
+
if (DEPLOYMENT_SCAFFOLD_FILES.every((f3) => nodeFs8.existsSync(join18(cwd, f3)))) {
|
|
132944
133337
|
out.push(
|
|
132945
|
-
{ path:
|
|
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 ${
|
|
133029
|
-
if (res.unreachable && !res.partial) throw new Error(`could not reach the queue at ${
|
|
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 ${
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
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,
|
|
134844
|
-
if (dim.suggested) return `${
|
|
134845
|
-
if (dim.held === "no-cap") return `${
|
|
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,
|
|
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;
|