pullfrog 0.1.55 → 0.1.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +681 -517
- package/dist/external.d.ts +35 -0
- package/dist/index.js +680 -516
- package/dist/internal.js +30 -13
- package/dist/mcp/currentObject.d.ts +14 -0
- package/dist/mcp/gh.d.ts +32 -0
- package/dist/mcp/issue.d.ts +0 -23
- package/dist/mcp/pr.d.ts +0 -10
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/shell.d.ts +24 -0
- package/dist/utils/billingErrors.d.ts +9 -1
- package/dist/utils/github.d.ts +1 -1
- package/dist/utils/roleMirror.d.ts +42 -0
- package/dist/utils/token.d.ts +3 -1
- package/dist/utils/worktree.d.ts +14 -2
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -98607,14 +98607,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
98607
98607
|
} else if (node2.nodeType === 1) {
|
|
98608
98608
|
replacement = replacementForNode.call(self2, node2);
|
|
98609
98609
|
}
|
|
98610
|
-
return
|
|
98610
|
+
return join28(output, replacement);
|
|
98611
98611
|
}, "");
|
|
98612
98612
|
}
|
|
98613
98613
|
function postProcess(output) {
|
|
98614
98614
|
var self2 = this;
|
|
98615
98615
|
this.rules.forEach(function(rule) {
|
|
98616
98616
|
if (typeof rule.append === "function") {
|
|
98617
|
-
output =
|
|
98617
|
+
output = join28(output, rule.append(self2.options));
|
|
98618
98618
|
}
|
|
98619
98619
|
});
|
|
98620
98620
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -98626,7 +98626,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
98626
98626
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
98627
98627
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
98628
98628
|
}
|
|
98629
|
-
function
|
|
98629
|
+
function join28(output, replacement) {
|
|
98630
98630
|
var s1 = trimTrailingNewlines(output);
|
|
98631
98631
|
var s2 = trimLeadingNewlines(replacement);
|
|
98632
98632
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -105313,7 +105313,7 @@ import { dirname as dirname6 } from "node:path";
|
|
|
105313
105313
|
// main.ts
|
|
105314
105314
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
105315
105315
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
105316
|
-
import { join as
|
|
105316
|
+
import { join as join27 } from "node:path";
|
|
105317
105317
|
|
|
105318
105318
|
// node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
|
|
105319
105319
|
function splitRunArgs(args2, valueFlags = []) {
|
|
@@ -105902,12 +105902,22 @@ var providers = {
|
|
|
105902
105902
|
displayName: "DeepSeek",
|
|
105903
105903
|
envVars: ["DEEPSEEK_API_KEY"],
|
|
105904
105904
|
models: {
|
|
105905
|
+
// same fork trap as `deepseek-flash` below, and Pro fell into it on
|
|
105906
|
+
// 2026-08-12: OpenRouter forked the 0813 release into its own id and left
|
|
105907
|
+
// the April preview live under the unversioned one at 2.7x the input and
|
|
105908
|
+
// 27x the CACHE-READ rate ($1.168/$2.336/$0.09855 vs
|
|
105909
|
+
// $0.435/$0.87/$0.003625). the direct route upgraded in place, so only the
|
|
105910
|
+
// OpenRouter side was stale — and that side is what OSS and Router runs
|
|
105911
|
+
// use. measured: $0.1719/run authoritative against the funded default's
|
|
105912
|
+
// $0.0405. there is no `~deepseek/*-pro-latest` pointer to track, so this
|
|
105913
|
+
// one has to be a dated pin; the `models-catalog` drift test is what
|
|
105914
|
+
// catches the next release.
|
|
105905
105915
|
"deepseek-pro": {
|
|
105906
105916
|
displayName: "DeepSeek Pro",
|
|
105907
105917
|
resolve: "deepseek/deepseek-v4-pro",
|
|
105908
105918
|
effort: ["high", "max"],
|
|
105909
|
-
openRouterEffort: ["high", "
|
|
105910
|
-
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
|
|
105919
|
+
openRouterEffort: ["low", "high", "max"],
|
|
105920
|
+
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro-0813",
|
|
105911
105921
|
preferred: true
|
|
105912
105922
|
},
|
|
105913
105923
|
// DeepSeek upgrades the `deepseek-v4-flash` API model in place, so the
|
|
@@ -105915,14 +105925,19 @@ var providers = {
|
|
|
105915
105925
|
// release into its own id and left the April preview live under the
|
|
105916
105926
|
// unversioned one at a HIGHER price ($0.14/$0.28 vs $0.09/$0.18) — so the
|
|
105917
105927
|
// OpenRouter route can never use the bare id. it tracks the rolling
|
|
105918
|
-
// `~deepseek/*-latest` pointer (rule 4), which
|
|
105919
|
-
//
|
|
105920
|
-
//
|
|
105921
|
-
//
|
|
105922
|
-
//
|
|
105923
|
-
//
|
|
105924
|
-
//
|
|
105925
|
-
//
|
|
105928
|
+
// `~deepseek/*-latest` pointer (rule 4), which follows the next in-place
|
|
105929
|
+
// upgrade on its own rather than pinning a dated snapshot that goes stale
|
|
105930
|
+
// every release. that pointer is NOT priced like 0731 — its listed rates
|
|
105931
|
+
// run +40% on output and +57.5% on cache read, and it costs 12% more per
|
|
105932
|
+
// run on authoritative spend ($0.0405 vs $0.0361). kept anyway: ~$45/yr
|
|
105933
|
+
// buys auto-following the upgrade, and the listed gap overstates the real
|
|
105934
|
+
// one because a rolling alias prices at its routing ceiling.
|
|
105935
|
+
// 0731 gained a `low` rung and renamed the top to `max`, and that ladder
|
|
105936
|
+
// now reads the same on BOTH routes — the in-place direct upgrade caught
|
|
105937
|
+
// up to the fork. so `low`, not `high`, is position 0 either way, which is
|
|
105938
|
+
// why the OSS effort floor in `main.ts` pins the `high` rung by name. Pro
|
|
105939
|
+
// is the same shape via OpenRouter since 0813; only its direct route still
|
|
105940
|
+
// starts at `high`.
|
|
105926
105941
|
"deepseek-flash": {
|
|
105927
105942
|
displayName: "DeepSeek Flash",
|
|
105928
105943
|
resolve: "deepseek/deepseek-v4-flash",
|
|
@@ -106321,11 +106336,13 @@ var providers = {
|
|
|
106321
106336
|
effort: ["low", "medium", "high"],
|
|
106322
106337
|
openRouterResolve: "openrouter/x-ai/grok-4.5"
|
|
106323
106338
|
},
|
|
106339
|
+
// dated pin, not the bare id — see `deepseek/deepseek-pro` for why
|
|
106340
|
+
// OpenRouter's unversioned `deepseek-v4-pro` is the stale April preview.
|
|
106324
106341
|
"deepseek-pro": {
|
|
106325
106342
|
displayName: "DeepSeek Pro",
|
|
106326
|
-
resolve: "openrouter/deepseek/deepseek-v4-pro",
|
|
106327
|
-
effort: ["high", "
|
|
106328
|
-
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro"
|
|
106343
|
+
resolve: "openrouter/deepseek/deepseek-v4-pro-0813",
|
|
106344
|
+
effort: ["low", "high", "max"],
|
|
106345
|
+
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro-0813"
|
|
106329
106346
|
},
|
|
106330
106347
|
// rolling pointer, not the bare id — see `deepseek/deepseek-flash` for why
|
|
106331
106348
|
// OpenRouter's unversioned `deepseek-v4-flash` is the stale April preview.
|
|
@@ -106516,6 +106533,17 @@ function formatMcpToolRef(agentId, toolName) {
|
|
|
106516
106533
|
return agentId;
|
|
106517
106534
|
}
|
|
106518
106535
|
}
|
|
106536
|
+
function extractMcpToolRefs(agentId, text) {
|
|
106537
|
+
const prefix = formatMcpToolRef(agentId, "");
|
|
106538
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106539
|
+
return [...text.matchAll(new RegExp(`(?<![A-Za-z0-9_])${escaped}([a-z0-9_]+)`, "g"))].map(
|
|
106540
|
+
(match3) => match3[1]
|
|
106541
|
+
);
|
|
106542
|
+
}
|
|
106543
|
+
function findDanglingPromptToolRefs(params) {
|
|
106544
|
+
const registered = new Set(params.toolNames);
|
|
106545
|
+
return [...new Set(extractMcpToolRefs(params.agentId, params.prompt))].filter((name) => !registered.has(name)).sort();
|
|
106546
|
+
}
|
|
106519
106547
|
function hasSimilarIssues(params) {
|
|
106520
106548
|
return params.repoIntelligence && !!params.event.issue_number && !params.event.is_pr;
|
|
106521
106549
|
}
|
|
@@ -107289,7 +107317,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
107289
107317
|
// package.json
|
|
107290
107318
|
var package_default = {
|
|
107291
107319
|
name: "pullfrog",
|
|
107292
|
-
version: "0.1.
|
|
107320
|
+
version: "0.1.57",
|
|
107293
107321
|
type: "module",
|
|
107294
107322
|
bin: {
|
|
107295
107323
|
pullfrog: "dist/cli.mjs",
|
|
@@ -114921,11 +114949,12 @@ function bootstrapPullfrogDataDir() {
|
|
|
114921
114949
|
}
|
|
114922
114950
|
|
|
114923
114951
|
// utils/worktree.ts
|
|
114924
|
-
async function dirtyTrackedPaths() {
|
|
114952
|
+
async function dirtyTrackedPaths(params = {}) {
|
|
114925
114953
|
const result = await spawn3({
|
|
114926
114954
|
cmd: "git",
|
|
114927
114955
|
args: ["diff", "--name-only", "HEAD"],
|
|
114928
114956
|
env: process.env,
|
|
114957
|
+
cwd: params.cwd ?? process.cwd(),
|
|
114929
114958
|
activityTimeout: 0
|
|
114930
114959
|
});
|
|
114931
114960
|
if (result.exitCode !== 0) {
|
|
@@ -114936,12 +114965,15 @@ async function dirtyTrackedPaths() {
|
|
|
114936
114965
|
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
114937
114966
|
}
|
|
114938
114967
|
async function restoreDirtiedSince(params) {
|
|
114939
|
-
const dirtied = [...await dirtyTrackedPaths(
|
|
114968
|
+
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
114969
|
+
(path4) => !params.before.has(path4)
|
|
114970
|
+
);
|
|
114940
114971
|
if (dirtied.length === 0) return;
|
|
114941
114972
|
const result = await spawn3({
|
|
114942
114973
|
cmd: "git",
|
|
114943
114974
|
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
114944
114975
|
env: process.env,
|
|
114976
|
+
cwd: params.cwd ?? process.cwd(),
|
|
114945
114977
|
activityTimeout: 0
|
|
114946
114978
|
});
|
|
114947
114979
|
if (result.exitCode !== 0) {
|
|
@@ -190869,6 +190901,116 @@ function CommitInfoTool(ctx) {
|
|
|
190869
190901
|
});
|
|
190870
190902
|
}
|
|
190871
190903
|
|
|
190904
|
+
// utils/roleMirror.ts
|
|
190905
|
+
function mirrorRolePermissions(role) {
|
|
190906
|
+
switch (role) {
|
|
190907
|
+
case "admin":
|
|
190908
|
+
case "maintain":
|
|
190909
|
+
case "write":
|
|
190910
|
+
case "triage":
|
|
190911
|
+
return { contents: "read", issues: "write", pull_requests: "write" };
|
|
190912
|
+
case "read":
|
|
190913
|
+
case "none":
|
|
190914
|
+
case void 0:
|
|
190915
|
+
return null;
|
|
190916
|
+
default:
|
|
190917
|
+
return role;
|
|
190918
|
+
}
|
|
190919
|
+
}
|
|
190920
|
+
function formatPermissions(permissions) {
|
|
190921
|
+
return Object.entries(permissions).map((entry) => entry.join(":")).join(", ");
|
|
190922
|
+
}
|
|
190923
|
+
|
|
190924
|
+
// mcp/currentObject.ts
|
|
190925
|
+
async function resolveCurrentObject(ctx) {
|
|
190926
|
+
const issueNumber = ctx.payload.event.issue_number;
|
|
190927
|
+
if (!issueNumber) {
|
|
190928
|
+
throw new Error("this run was not triggered on an issue or pull request");
|
|
190929
|
+
}
|
|
190930
|
+
if (mirrorRolePermissions(ctx.payload.event.authorPermission)) {
|
|
190931
|
+
return { issueNumber, isPr: ctx.payload.event.is_pr === true };
|
|
190932
|
+
}
|
|
190933
|
+
const issue3 = await ctx.octokit.rest.issues.get({
|
|
190934
|
+
owner: ctx.repo.owner,
|
|
190935
|
+
repo: ctx.repo.name,
|
|
190936
|
+
issue_number: issueNumber
|
|
190937
|
+
});
|
|
190938
|
+
const author = issue3.data.user?.login;
|
|
190939
|
+
const triggerer = ctx.payload.triggerer;
|
|
190940
|
+
if (!author || !triggerer || author.toLowerCase() !== triggerer.toLowerCase()) {
|
|
190941
|
+
throw new Error(
|
|
190942
|
+
`${triggerer ?? "the triggering user"} did not open #${issueNumber} and has no write access to ${ctx.repo.owner}/${ctx.repo.name}, so this run cannot change its state.`
|
|
190943
|
+
);
|
|
190944
|
+
}
|
|
190945
|
+
return { issueNumber, isPr: issue3.data.pull_request !== void 0 };
|
|
190946
|
+
}
|
|
190947
|
+
var CloseCurrent = type({
|
|
190948
|
+
state_reason: type.enumerated("completed", "not_planned", "duplicate").describe(
|
|
190949
|
+
"why it is being closed: 'completed' (resolved), 'not_planned' (won't fix, out of scope), or 'duplicate' (already tracked elsewhere). ignored for pull requests."
|
|
190950
|
+
)
|
|
190951
|
+
});
|
|
190952
|
+
function CloseCurrentTool(ctx) {
|
|
190953
|
+
return tool({
|
|
190954
|
+
name: "close_current",
|
|
190955
|
+
mutates: true,
|
|
190956
|
+
description: 'Close the issue or pull request this run was triggered on. Example: `close_current({ state_reason: "duplicate" })`. Comment first to explain the decision \u2014 closing alone is opaque to the author.',
|
|
190957
|
+
parameters: CloseCurrent,
|
|
190958
|
+
execute: execute(async (params) => {
|
|
190959
|
+
const target = await resolveCurrentObject(ctx);
|
|
190960
|
+
const result = target.isPr ? await ctx.octokit.rest.pulls.update({
|
|
190961
|
+
owner: ctx.repo.owner,
|
|
190962
|
+
repo: ctx.repo.name,
|
|
190963
|
+
pull_number: target.issueNumber,
|
|
190964
|
+
state: "closed"
|
|
190965
|
+
}) : await ctx.octokit.rest.issues.update({
|
|
190966
|
+
owner: ctx.repo.owner,
|
|
190967
|
+
repo: ctx.repo.name,
|
|
190968
|
+
issue_number: target.issueNumber,
|
|
190969
|
+
state: "closed",
|
|
190970
|
+
state_reason: params.state_reason
|
|
190971
|
+
});
|
|
190972
|
+
ctx.toolState.wasUpdated = true;
|
|
190973
|
+
log2.info(`\xBB closed #${target.issueNumber} (${params.state_reason})`);
|
|
190974
|
+
return {
|
|
190975
|
+
success: true,
|
|
190976
|
+
number: result.data.number,
|
|
190977
|
+
url: result.data.html_url,
|
|
190978
|
+
state: result.data.state
|
|
190979
|
+
};
|
|
190980
|
+
}, "close_current")
|
|
190981
|
+
});
|
|
190982
|
+
}
|
|
190983
|
+
function ReopenCurrentTool(ctx) {
|
|
190984
|
+
return tool({
|
|
190985
|
+
name: "reopen_current",
|
|
190986
|
+
mutates: true,
|
|
190987
|
+
description: "Reopen the issue or pull request this run was triggered on.",
|
|
190988
|
+
parameters: type({}),
|
|
190989
|
+
execute: execute(async () => {
|
|
190990
|
+
const target = await resolveCurrentObject(ctx);
|
|
190991
|
+
const result = target.isPr ? await ctx.octokit.rest.pulls.update({
|
|
190992
|
+
owner: ctx.repo.owner,
|
|
190993
|
+
repo: ctx.repo.name,
|
|
190994
|
+
pull_number: target.issueNumber,
|
|
190995
|
+
state: "open"
|
|
190996
|
+
}) : await ctx.octokit.rest.issues.update({
|
|
190997
|
+
owner: ctx.repo.owner,
|
|
190998
|
+
repo: ctx.repo.name,
|
|
190999
|
+
issue_number: target.issueNumber,
|
|
191000
|
+
state: "open"
|
|
191001
|
+
});
|
|
191002
|
+
ctx.toolState.wasUpdated = true;
|
|
191003
|
+
log2.info(`\xBB reopened #${target.issueNumber}`);
|
|
191004
|
+
return {
|
|
191005
|
+
success: true,
|
|
191006
|
+
number: result.data.number,
|
|
191007
|
+
url: result.data.html_url,
|
|
191008
|
+
state: result.data.state
|
|
191009
|
+
};
|
|
191010
|
+
}, "reopen_current")
|
|
191011
|
+
});
|
|
191012
|
+
}
|
|
191013
|
+
|
|
190872
191014
|
// prep/index.ts
|
|
190873
191015
|
import { performance as performance8 } from "node:perf_hooks";
|
|
190874
191016
|
|
|
@@ -191495,6 +191637,456 @@ function AwaitDependencyInstallationTool(ctx) {
|
|
|
191495
191637
|
});
|
|
191496
191638
|
}
|
|
191497
191639
|
|
|
191640
|
+
// mcp/gh.ts
|
|
191641
|
+
import { mkdirSync as mkdirSync8 } from "node:fs";
|
|
191642
|
+
import { join as join21 } from "node:path";
|
|
191643
|
+
|
|
191644
|
+
// mcp/shell.ts
|
|
191645
|
+
import { spawn as spawn5, spawnSync as spawnSync6 } from "node:child_process";
|
|
191646
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
191647
|
+
import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
|
|
191648
|
+
import { userInfo as userInfo2 } from "node:os";
|
|
191649
|
+
import { join as join20 } from "node:path";
|
|
191650
|
+
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
191651
|
+
var ShellParams = type({
|
|
191652
|
+
command: "string",
|
|
191653
|
+
// advisory and unread — no code path consumes it. kept in the schema only
|
|
191654
|
+
// because it nudges the model to state intent before running a command;
|
|
191655
|
+
// REQUIRING it cost a full rejected turn every time one was omitted. #1140.
|
|
191656
|
+
"description?": "string",
|
|
191657
|
+
"timeout?": type.number.describe(
|
|
191658
|
+
"Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately."
|
|
191659
|
+
),
|
|
191660
|
+
"working_directory?": "string",
|
|
191661
|
+
"background?": "boolean"
|
|
191662
|
+
});
|
|
191663
|
+
var detectedSandboxMethod;
|
|
191664
|
+
var usernsProbeError;
|
|
191665
|
+
function getSandboxMethod() {
|
|
191666
|
+
return detectSandboxMethod();
|
|
191667
|
+
}
|
|
191668
|
+
function detectSandboxMethod() {
|
|
191669
|
+
if (detectedSandboxMethod !== void 0) {
|
|
191670
|
+
return detectedSandboxMethod;
|
|
191671
|
+
}
|
|
191672
|
+
if (process.env.CI !== "true") {
|
|
191673
|
+
detectedSandboxMethod = "none";
|
|
191674
|
+
log2.debug("sandbox disabled (CI !== true)");
|
|
191675
|
+
return "none";
|
|
191676
|
+
}
|
|
191677
|
+
try {
|
|
191678
|
+
const result = spawnSync6("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
|
|
191679
|
+
timeout: 5e3,
|
|
191680
|
+
stdio: "ignore"
|
|
191681
|
+
});
|
|
191682
|
+
if (result.status === 0) {
|
|
191683
|
+
detectedSandboxMethod = "unshare";
|
|
191684
|
+
log2.debug("PID namespace isolation enabled (unprivileged unshare)");
|
|
191685
|
+
return "unshare";
|
|
191686
|
+
}
|
|
191687
|
+
} catch {
|
|
191688
|
+
}
|
|
191689
|
+
try {
|
|
191690
|
+
const result = spawnSync6("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
|
|
191691
|
+
timeout: 5e3,
|
|
191692
|
+
stdio: "ignore"
|
|
191693
|
+
});
|
|
191694
|
+
if (result.status === 0) {
|
|
191695
|
+
detectedSandboxMethod = "sudo-unshare";
|
|
191696
|
+
log2.debug("PID namespace isolation enabled (sudo unshare)");
|
|
191697
|
+
return "sudo-unshare";
|
|
191698
|
+
}
|
|
191699
|
+
} catch {
|
|
191700
|
+
}
|
|
191701
|
+
try {
|
|
191702
|
+
const result = spawnSync6(
|
|
191703
|
+
"unshare",
|
|
191704
|
+
[
|
|
191705
|
+
"--user",
|
|
191706
|
+
"--map-root-user",
|
|
191707
|
+
"--pid",
|
|
191708
|
+
"--fork",
|
|
191709
|
+
"--mount",
|
|
191710
|
+
"setpriv",
|
|
191711
|
+
"--no-new-privs",
|
|
191712
|
+
"--bounding-set",
|
|
191713
|
+
"-all",
|
|
191714
|
+
"--inh-caps",
|
|
191715
|
+
"-all",
|
|
191716
|
+
"--ambient-caps",
|
|
191717
|
+
"-all",
|
|
191718
|
+
"true"
|
|
191719
|
+
],
|
|
191720
|
+
{ timeout: 5e3, stdio: ["ignore", "ignore", "pipe"] }
|
|
191721
|
+
);
|
|
191722
|
+
if (result.status === 0) {
|
|
191723
|
+
detectedSandboxMethod = "userns-unshare";
|
|
191724
|
+
log2.debug("PID namespace isolation enabled (unprivileged userns unshare)");
|
|
191725
|
+
return "userns-unshare";
|
|
191726
|
+
}
|
|
191727
|
+
usernsProbeError = result.stderr?.toString().trim() || result.error?.message;
|
|
191728
|
+
} catch {
|
|
191729
|
+
}
|
|
191730
|
+
detectedSandboxMethod = "none";
|
|
191731
|
+
log2.info("PID namespace isolation not available");
|
|
191732
|
+
return "none";
|
|
191733
|
+
}
|
|
191734
|
+
var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
|
191735
|
+
var SOCKET_CLEANUP = [
|
|
191736
|
+
"/var/run/docker.sock",
|
|
191737
|
+
"/run/docker.sock",
|
|
191738
|
+
"/var/run/podman/podman.sock",
|
|
191739
|
+
"/run/podman/podman.sock",
|
|
191740
|
+
"/run/containerd/containerd.sock",
|
|
191741
|
+
"/var/run/crio/crio.sock"
|
|
191742
|
+
].map((path4) => `mount --bind /dev/null ${path4} 2>/dev/null;`).join(" ");
|
|
191743
|
+
function buildFsMounts(repoDir) {
|
|
191744
|
+
const escaped = repoDir.replace(/'/g, "'\\''");
|
|
191745
|
+
return [
|
|
191746
|
+
`mkdir -p /var/lib/pullfrog 2>/dev/null;`,
|
|
191747
|
+
`mount -t tmpfs tmpfs /var/lib/pullfrog 2>/dev/null;`,
|
|
191748
|
+
`[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
|
|
191749
|
+
`[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`
|
|
191750
|
+
].join(" ");
|
|
191751
|
+
}
|
|
191752
|
+
var _repoRoot;
|
|
191753
|
+
function resolveRepoRoot() {
|
|
191754
|
+
if (_repoRoot) return _repoRoot;
|
|
191755
|
+
const fromEnv = process.env.GITHUB_WORKSPACE;
|
|
191756
|
+
if (fromEnv) {
|
|
191757
|
+
_repoRoot = fromEnv;
|
|
191758
|
+
return _repoRoot;
|
|
191759
|
+
}
|
|
191760
|
+
try {
|
|
191761
|
+
_repoRoot = spawnSync6("git", ["rev-parse", "--show-toplevel"], {
|
|
191762
|
+
cwd: process.cwd(),
|
|
191763
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
191764
|
+
encoding: "utf-8"
|
|
191765
|
+
}).stdout?.trim();
|
|
191766
|
+
} catch {
|
|
191767
|
+
}
|
|
191768
|
+
if (!_repoRoot) _repoRoot = process.cwd();
|
|
191769
|
+
return _repoRoot;
|
|
191770
|
+
}
|
|
191771
|
+
function spawnShell(params) {
|
|
191772
|
+
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
|
191773
|
+
const sandboxMethod = detectSandboxMethod();
|
|
191774
|
+
const ci = process.env.CI === "true";
|
|
191775
|
+
if (ci && sandboxMethod === "none") {
|
|
191776
|
+
throw new Error(
|
|
191777
|
+
"PID-namespace isolation is required in CI but unavailable: unprivileged unshare, sudo unshare, and userns-nested unshare all failed. " + (usernsProbeError ? `userns probe said: ${usernsProbeError}. ` : "") + "on a containerized / Kubernetes self-hosted runner the cause is usually one of: the pod's seccomp profile blocks user-namespace creation (Pod Security Standards baseline/restricted); the pod runs as root without CAP_SETFCAP, which the kernel requires to map uid 0; or the node kernel disallows unprivileged user namespaces (Ubuntu >= 23.10, Bottlerocket, Talos). elsewhere, check that `unshare` and `setpriv` (util-linux >= 2.31, which is what added --ambient-caps) are on PATH. see https://docs.pullfrog.com/security#self-hosted-runners"
|
|
191778
|
+
);
|
|
191779
|
+
}
|
|
191780
|
+
const repoRoot = resolveRepoRoot();
|
|
191781
|
+
const fsMounts = buildFsMounts(repoRoot);
|
|
191782
|
+
if (sandboxMethod === "unshare") {
|
|
191783
|
+
return spawn5(
|
|
191784
|
+
"unshare",
|
|
191785
|
+
[
|
|
191786
|
+
"--pid",
|
|
191787
|
+
"--fork",
|
|
191788
|
+
"--mount-proc",
|
|
191789
|
+
"bash",
|
|
191790
|
+
"-c",
|
|
191791
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`
|
|
191792
|
+
],
|
|
191793
|
+
spawnOpts
|
|
191794
|
+
);
|
|
191795
|
+
}
|
|
191796
|
+
if (sandboxMethod === "userns-unshare") {
|
|
191797
|
+
const escaped = params.command.replace(/'/g, "'\\''");
|
|
191798
|
+
return spawn5(
|
|
191799
|
+
"unshare",
|
|
191800
|
+
[
|
|
191801
|
+
"--user",
|
|
191802
|
+
"--map-root-user",
|
|
191803
|
+
"--pid",
|
|
191804
|
+
"--fork",
|
|
191805
|
+
"--mount",
|
|
191806
|
+
"bash",
|
|
191807
|
+
"-c",
|
|
191808
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec setpriv --no-new-privs --bounding-set -all --inh-caps -all --ambient-caps -all bash -c '${escaped}'`
|
|
191809
|
+
],
|
|
191810
|
+
spawnOpts
|
|
191811
|
+
);
|
|
191812
|
+
}
|
|
191813
|
+
if (sandboxMethod === "sudo-unshare") {
|
|
191814
|
+
const envArgs = [];
|
|
191815
|
+
for (const [k2, v] of Object.entries(params.env)) {
|
|
191816
|
+
if (v !== void 0) {
|
|
191817
|
+
envArgs.push(`${k2}=${v}`);
|
|
191818
|
+
}
|
|
191819
|
+
}
|
|
191820
|
+
const username = userInfo2().username;
|
|
191821
|
+
const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
|
|
191822
|
+
const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
|
|
191823
|
+
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
|
191824
|
+
return spawn5(
|
|
191825
|
+
"sudo",
|
|
191826
|
+
[
|
|
191827
|
+
"env",
|
|
191828
|
+
...envArgs,
|
|
191829
|
+
"unshare",
|
|
191830
|
+
"--pid",
|
|
191831
|
+
"--fork",
|
|
191832
|
+
"--mount-proc",
|
|
191833
|
+
"bash",
|
|
191834
|
+
"-c",
|
|
191835
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
|
191836
|
+
],
|
|
191837
|
+
{ ...spawnOpts, env: {} }
|
|
191838
|
+
);
|
|
191839
|
+
}
|
|
191840
|
+
return spawn5("bash", ["-c", params.command], spawnOpts);
|
|
191841
|
+
}
|
|
191842
|
+
async function killProcessGroup(proc) {
|
|
191843
|
+
if (!proc.pid) return;
|
|
191844
|
+
try {
|
|
191845
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
191846
|
+
await new Promise((r2) => setTimeout(r2, 200));
|
|
191847
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
191848
|
+
} catch {
|
|
191849
|
+
try {
|
|
191850
|
+
proc.kill("SIGKILL");
|
|
191851
|
+
} catch {
|
|
191852
|
+
}
|
|
191853
|
+
}
|
|
191854
|
+
}
|
|
191855
|
+
function getTempDir() {
|
|
191856
|
+
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
191857
|
+
if (!tempDir) {
|
|
191858
|
+
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
191859
|
+
}
|
|
191860
|
+
return tempDir;
|
|
191861
|
+
}
|
|
191862
|
+
var MAX_OUTPUT_CHARS = 5e3;
|
|
191863
|
+
async function runSandboxed(params) {
|
|
191864
|
+
const proc = spawnShell({
|
|
191865
|
+
command: params.command,
|
|
191866
|
+
env: params.env,
|
|
191867
|
+
cwd: params.cwd,
|
|
191868
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
191869
|
+
});
|
|
191870
|
+
let stdout2 = "", stderr = "", timedOut = false, exited = false, spawnError = "";
|
|
191871
|
+
proc.stdout?.on("data", (chunk) => {
|
|
191872
|
+
stdout2 += chunk.toString();
|
|
191873
|
+
});
|
|
191874
|
+
proc.stderr?.on("data", (chunk) => {
|
|
191875
|
+
stderr += chunk.toString();
|
|
191876
|
+
});
|
|
191877
|
+
const timeoutId = setTimeout(async () => {
|
|
191878
|
+
if (!exited) {
|
|
191879
|
+
timedOut = true;
|
|
191880
|
+
await killProcessGroup(proc);
|
|
191881
|
+
}
|
|
191882
|
+
}, params.timeout);
|
|
191883
|
+
const exitCode = await new Promise((resolve3) => {
|
|
191884
|
+
const done = (code) => {
|
|
191885
|
+
exited = true;
|
|
191886
|
+
clearTimeout(timeoutId);
|
|
191887
|
+
resolve3(code);
|
|
191888
|
+
};
|
|
191889
|
+
proc.on("exit", done);
|
|
191890
|
+
proc.on("error", (err) => {
|
|
191891
|
+
spawnError = err.message;
|
|
191892
|
+
done(null);
|
|
191893
|
+
});
|
|
191894
|
+
});
|
|
191895
|
+
proc.stdout?.destroy();
|
|
191896
|
+
proc.stderr?.destroy();
|
|
191897
|
+
let output = stderr ? stdout2 ? `${stdout2}
|
|
191898
|
+
${stderr}` : stderr : stdout2;
|
|
191899
|
+
if (spawnError) output = output ? `${output}
|
|
191900
|
+
${spawnError}` : spawnError;
|
|
191901
|
+
if (timedOut)
|
|
191902
|
+
output = output ? `${output}
|
|
191903
|
+
[timed out after ${params.timeout}ms]` : `[timed out after ${params.timeout}ms]`;
|
|
191904
|
+
return {
|
|
191905
|
+
output: output.trim(),
|
|
191906
|
+
exitCode: exitCode ?? (timedOut ? 124 : -1),
|
|
191907
|
+
timedOut
|
|
191908
|
+
};
|
|
191909
|
+
}
|
|
191910
|
+
function capOutput(output) {
|
|
191911
|
+
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
191912
|
+
const fullPath = join20(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
191913
|
+
writeFileSync12(fullPath, output);
|
|
191914
|
+
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
191915
|
+
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
191916
|
+
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
191917
|
+
}
|
|
191918
|
+
function isGitCommand(command) {
|
|
191919
|
+
const trimmed = command.trim();
|
|
191920
|
+
if (trimmed === "git" || trimmed.startsWith("git ")) return true;
|
|
191921
|
+
if (trimmed.startsWith("sudo git")) return true;
|
|
191922
|
+
return /[;&|]\s*(?:sudo\s+)?git(?:\s|$)/.test(trimmed);
|
|
191923
|
+
}
|
|
191924
|
+
function ShellTool(ctx) {
|
|
191925
|
+
return tool({
|
|
191926
|
+
name: "shell",
|
|
191927
|
+
timeoutMs: 12e4,
|
|
191928
|
+
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
191929
|
+
|
|
191930
|
+
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
191931
|
+
|
|
191932
|
+
Use this tool to:
|
|
191933
|
+
- Run shell commands (ls, cat, grep, find, etc.)
|
|
191934
|
+
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
191935
|
+
- Run tests and linters
|
|
191936
|
+
|
|
191937
|
+
Output is capped at ${MAX_OUTPUT_CHARS} chars: if exceeded, only the tail is returned and the full body is saved to a tempfile (path included in the response). Re-read the tempfile with cat/tail/grep when you need more.
|
|
191938
|
+
|
|
191939
|
+
Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
|
|
191940
|
+
parameters: ShellParams,
|
|
191941
|
+
execute: execute(async (params) => {
|
|
191942
|
+
if (isGitCommand(params.command)) {
|
|
191943
|
+
throw new Error(
|
|
191944
|
+
'git commands are not allowed in the shell tool. use the dedicated git tools instead:\n- git: local operations, e.g. git({ command: "log", args: ["--oneline"] }) \u2014 `command` is the subcommand ONLY\n- push_branch: push to remote (handles authentication)\n- git_fetch: fetch from remote (handles authentication)\n- checkout_pr: check out PR branches'
|
|
191945
|
+
);
|
|
191946
|
+
}
|
|
191947
|
+
const timeout = Math.min(params.timeout ?? 3e4, 12e4);
|
|
191948
|
+
const cwd = params.working_directory ?? process.cwd();
|
|
191949
|
+
const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
191950
|
+
if (params.command.includes("agent-browser")) {
|
|
191951
|
+
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
|
191952
|
+
if (daemonError) {
|
|
191953
|
+
return {
|
|
191954
|
+
output: `browser daemon unavailable: ${daemonError}`,
|
|
191955
|
+
exit_code: 1,
|
|
191956
|
+
timed_out: false
|
|
191957
|
+
};
|
|
191958
|
+
}
|
|
191959
|
+
const binDir = ctx.toolState.browserDaemon?.binDir;
|
|
191960
|
+
if (binDir) {
|
|
191961
|
+
env2.PATH = `${binDir}:${env2.PATH ?? ""}`;
|
|
191962
|
+
}
|
|
191963
|
+
}
|
|
191964
|
+
if (params.background) {
|
|
191965
|
+
const tempDir = getTempDir();
|
|
191966
|
+
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
191967
|
+
const outputPath = join20(tempDir, `${handle}.log`);
|
|
191968
|
+
const pidPath = join20(tempDir, `${handle}.pid`);
|
|
191969
|
+
const logFd = openSync(outputPath, "a");
|
|
191970
|
+
let proc;
|
|
191971
|
+
try {
|
|
191972
|
+
proc = spawnShell({
|
|
191973
|
+
command: params.command,
|
|
191974
|
+
env: env2,
|
|
191975
|
+
cwd,
|
|
191976
|
+
stdio: ["ignore", logFd, logFd]
|
|
191977
|
+
});
|
|
191978
|
+
} finally {
|
|
191979
|
+
closeSync(logFd);
|
|
191980
|
+
}
|
|
191981
|
+
if (!proc.pid) {
|
|
191982
|
+
throw new Error("failed to start background process");
|
|
191983
|
+
}
|
|
191984
|
+
proc.unref();
|
|
191985
|
+
writeFileSync12(pidPath, `${proc.pid}
|
|
191986
|
+
`);
|
|
191987
|
+
ctx.toolState.backgroundProcesses.set(handle, { pid: proc.pid, outputPath, pidPath });
|
|
191988
|
+
return {
|
|
191989
|
+
handle,
|
|
191990
|
+
outputPath,
|
|
191991
|
+
pidPath,
|
|
191992
|
+
message: `started background process ${handle} (pid ${proc.pid})`
|
|
191993
|
+
};
|
|
191994
|
+
}
|
|
191995
|
+
const result = await runSandboxed({ command: params.command, env: env2, cwd, timeout });
|
|
191996
|
+
if (result.exitCode !== 0) {
|
|
191997
|
+
log2.info(`shell command failed with exit code ${result.exitCode}: ${params.command}`);
|
|
191998
|
+
if (result.output) log2.info(`output: ${result.output}`);
|
|
191999
|
+
}
|
|
192000
|
+
return {
|
|
192001
|
+
output: capOutput(result.output),
|
|
192002
|
+
exit_code: result.exitCode,
|
|
192003
|
+
timed_out: result.timedOut
|
|
192004
|
+
};
|
|
192005
|
+
})
|
|
192006
|
+
});
|
|
192007
|
+
}
|
|
192008
|
+
var KillBackgroundParams = type({
|
|
192009
|
+
handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)")
|
|
192010
|
+
});
|
|
192011
|
+
function KillBackgroundTool(ctx) {
|
|
192012
|
+
return tool({
|
|
192013
|
+
name: "kill_background",
|
|
192014
|
+
mutates: true,
|
|
192015
|
+
description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
|
|
192016
|
+
parameters: KillBackgroundParams,
|
|
192017
|
+
execute: execute(async (params) => {
|
|
192018
|
+
const proc = ctx.toolState.backgroundProcesses.get(params.handle);
|
|
192019
|
+
if (!proc) {
|
|
192020
|
+
return {
|
|
192021
|
+
success: false,
|
|
192022
|
+
message: `no background process with handle ${params.handle}`
|
|
192023
|
+
};
|
|
192024
|
+
}
|
|
192025
|
+
try {
|
|
192026
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
192027
|
+
} catch {
|
|
192028
|
+
}
|
|
192029
|
+
await sleep3(200);
|
|
192030
|
+
try {
|
|
192031
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
192032
|
+
} catch {
|
|
192033
|
+
}
|
|
192034
|
+
ctx.toolState.backgroundProcesses.delete(params.handle);
|
|
192035
|
+
return {
|
|
192036
|
+
success: true,
|
|
192037
|
+
message: `killed background process ${params.handle} (pid ${proc.pid})`
|
|
192038
|
+
};
|
|
192039
|
+
})
|
|
192040
|
+
});
|
|
192041
|
+
}
|
|
192042
|
+
|
|
192043
|
+
// mcp/gh.ts
|
|
192044
|
+
var GH_TIMEOUT_MS = 12e4;
|
|
192045
|
+
var GhParams = type({
|
|
192046
|
+
args: type.string.array().describe(
|
|
192047
|
+
'gh arguments as an argv array, e.g. ["pr", "list", "--json", "number,title"] or ["api", "graphql", "-f", "query=..."]'
|
|
192048
|
+
)
|
|
192049
|
+
});
|
|
192050
|
+
function shellQuote(arg6) {
|
|
192051
|
+
return `'${arg6.replace(/'/g, "'\\''")}'`;
|
|
192052
|
+
}
|
|
192053
|
+
function GhTool(ctx) {
|
|
192054
|
+
const permissions = mirrorRolePermissions(ctx.payload.event.authorPermission);
|
|
192055
|
+
return tool({
|
|
192056
|
+
name: "gh",
|
|
192057
|
+
// denied to subagents. `shell` is deliberately unmarked because git is
|
|
192058
|
+
// blocked inside it, but `gh` reaches PR and issue state changes — the
|
|
192059
|
+
// shape of the 2026-05-18 cross-branch clobber the gate exists for.
|
|
192060
|
+
mutates: true,
|
|
192061
|
+
timeoutMs: GH_TIMEOUT_MS,
|
|
192062
|
+
description: `Run the GitHub CLI against ${ctx.repo.owner}/${ctx.repo.name}. Pass argv as an array \u2014 there is no shell, so pipes, redirects and \`&&\` do not work; use gh's own \`--json\`/\`--jq\` flags instead. \`gh api graphql\` is available. Authenticated with ${permissions ? formatPermissions(permissions) : "no permissions"} on this repo only; anything else 403s. Pushing, merging and ref writes are not available \u2014 use the git tools.`,
|
|
192063
|
+
parameters: GhParams,
|
|
192064
|
+
execute: execute(async (params) => {
|
|
192065
|
+
const configDir = join21(ctx.tmpdir, "gh-config");
|
|
192066
|
+
mkdirSync8(configDir, { recursive: true });
|
|
192067
|
+
const command = ["gh", ...params.args.map(shellQuote)].join(" ");
|
|
192068
|
+
const result = await runSandboxed({
|
|
192069
|
+
command,
|
|
192070
|
+
cwd: process.cwd(),
|
|
192071
|
+
timeout: GH_TIMEOUT_MS,
|
|
192072
|
+
env: {
|
|
192073
|
+
...filterEnvForUntrustedCode(),
|
|
192074
|
+
GH_TOKEN: ctx.ghToken,
|
|
192075
|
+
GH_CONFIG_DIR: configDir,
|
|
192076
|
+
GH_PROMPT_DISABLED: "1",
|
|
192077
|
+
GH_NO_UPDATE_NOTIFIER: "1"
|
|
192078
|
+
}
|
|
192079
|
+
});
|
|
192080
|
+
log2.info(`\xBB gh ${params.args.join(" ")} (exit ${result.exitCode})`);
|
|
192081
|
+
return {
|
|
192082
|
+
output: capOutput(result.output),
|
|
192083
|
+
exit_code: result.exitCode,
|
|
192084
|
+
timed_out: result.timedOut
|
|
192085
|
+
};
|
|
192086
|
+
}, "gh")
|
|
192087
|
+
});
|
|
192088
|
+
}
|
|
192089
|
+
|
|
191498
192090
|
// mcp/issue.ts
|
|
191499
192091
|
var Issue = type({
|
|
191500
192092
|
title: type.string.describe("the title of the issue"),
|
|
@@ -191539,65 +192131,6 @@ function IssueTool(ctx) {
|
|
|
191539
192131
|
})
|
|
191540
192132
|
});
|
|
191541
192133
|
}
|
|
191542
|
-
var CloseIssue = type({
|
|
191543
|
-
issue_number: type.number.describe("the issue number to close"),
|
|
191544
|
-
state_reason: type.enumerated("completed", "not_planned", "duplicate").describe(
|
|
191545
|
-
"why the issue is being closed: 'completed' (resolved/done), 'not_planned' (won't fix, out of scope), or 'duplicate' (already tracked elsewhere)."
|
|
191546
|
-
)
|
|
191547
|
-
});
|
|
191548
|
-
function CloseIssueTool(ctx) {
|
|
191549
|
-
return tool({
|
|
191550
|
-
name: "close_issue",
|
|
191551
|
-
mutates: true,
|
|
191552
|
-
description: 'Close a GitHub issue with a reason. Example: `close_issue({ issue_number: 1234, state_reason: "not_planned" })`. Comment first to explain the decision \u2014 closing alone is opaque to the author.',
|
|
191553
|
-
parameters: CloseIssue,
|
|
191554
|
-
execute: execute(async (params) => {
|
|
191555
|
-
const result = await ctx.octokit.rest.issues.update({
|
|
191556
|
-
owner: ctx.repo.owner,
|
|
191557
|
-
repo: ctx.repo.name,
|
|
191558
|
-
issue_number: params.issue_number,
|
|
191559
|
-
state: "closed",
|
|
191560
|
-
state_reason: params.state_reason
|
|
191561
|
-
});
|
|
191562
|
-
ctx.toolState.wasUpdated = true;
|
|
191563
|
-
log2.info(`\xBB closed issue #${params.issue_number} (${params.state_reason})`);
|
|
191564
|
-
return {
|
|
191565
|
-
success: true,
|
|
191566
|
-
number: result.data.number,
|
|
191567
|
-
url: result.data.html_url,
|
|
191568
|
-
state: result.data.state,
|
|
191569
|
-
stateReason: result.data.state_reason
|
|
191570
|
-
};
|
|
191571
|
-
})
|
|
191572
|
-
});
|
|
191573
|
-
}
|
|
191574
|
-
var ReopenIssue = type({
|
|
191575
|
-
issue_number: type.number.describe("the issue number to reopen")
|
|
191576
|
-
});
|
|
191577
|
-
function ReopenIssueTool(ctx) {
|
|
191578
|
-
return tool({
|
|
191579
|
-
name: "reopen_issue",
|
|
191580
|
-
mutates: true,
|
|
191581
|
-
description: "Reopen a previously closed GitHub issue. Example: `reopen_issue({ issue_number: 1234 })`.",
|
|
191582
|
-
parameters: ReopenIssue,
|
|
191583
|
-
execute: execute(async (params) => {
|
|
191584
|
-
const result = await ctx.octokit.rest.issues.update({
|
|
191585
|
-
owner: ctx.repo.owner,
|
|
191586
|
-
repo: ctx.repo.name,
|
|
191587
|
-
issue_number: params.issue_number,
|
|
191588
|
-
state: "open"
|
|
191589
|
-
});
|
|
191590
|
-
ctx.toolState.wasUpdated = true;
|
|
191591
|
-
log2.info(`\xBB reopened issue #${params.issue_number}`);
|
|
191592
|
-
return {
|
|
191593
|
-
success: true,
|
|
191594
|
-
number: result.data.number,
|
|
191595
|
-
url: result.data.html_url,
|
|
191596
|
-
state: result.data.state
|
|
191597
|
-
};
|
|
191598
|
-
})
|
|
191599
|
-
});
|
|
191600
|
-
}
|
|
191601
192134
|
|
|
191602
192135
|
// utils/body.ts
|
|
191603
192136
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
@@ -192094,33 +192627,6 @@ function buildPrBodyWithFooter(ctx, body) {
|
|
|
192094
192627
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
192095
192628
|
return `${bodyWithoutFooter}${footer}`;
|
|
192096
192629
|
}
|
|
192097
|
-
var ClosePullRequest = type({
|
|
192098
|
-
pull_number: type.number.describe("the pull request number to close")
|
|
192099
|
-
});
|
|
192100
|
-
function ClosePullRequestTool(ctx) {
|
|
192101
|
-
return tool({
|
|
192102
|
-
name: "close_pull_request",
|
|
192103
|
-
mutates: true,
|
|
192104
|
-
description: "Close an open pull request WITHOUT merging it. Example: `close_pull_request({ pull_number: 1234 })`. Comment first to explain why \u2014 closing alone is opaque to the author. Merging is a human action and is never done here.",
|
|
192105
|
-
parameters: ClosePullRequest,
|
|
192106
|
-
execute: execute(async (params) => {
|
|
192107
|
-
const result = await ctx.octokit.rest.pulls.update({
|
|
192108
|
-
owner: ctx.repo.owner,
|
|
192109
|
-
repo: ctx.repo.name,
|
|
192110
|
-
pull_number: params.pull_number,
|
|
192111
|
-
state: "closed"
|
|
192112
|
-
});
|
|
192113
|
-
ctx.toolState.wasUpdated = true;
|
|
192114
|
-
log2.info(`\xBB closed pull request #${params.pull_number}`);
|
|
192115
|
-
return {
|
|
192116
|
-
success: true,
|
|
192117
|
-
number: result.data.number,
|
|
192118
|
-
url: result.data.html_url,
|
|
192119
|
-
state: result.data.state
|
|
192120
|
-
};
|
|
192121
|
-
})
|
|
192122
|
-
});
|
|
192123
|
-
}
|
|
192124
192630
|
var UpdatePullRequestBody = type({
|
|
192125
192631
|
pull_number: type.number.describe("the pull request number to update"),
|
|
192126
192632
|
body: type.string.describe("the new body content for the pull request")
|
|
@@ -192280,8 +192786,8 @@ function PullRequestInfoTool(ctx) {
|
|
|
192280
192786
|
}
|
|
192281
192787
|
|
|
192282
192788
|
// mcp/reviewComments.ts
|
|
192283
|
-
import { writeFileSync as
|
|
192284
|
-
import { join as
|
|
192789
|
+
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
192790
|
+
import { join as join22 } from "node:path";
|
|
192285
192791
|
var COMMENTS_PER_THREAD = 50;
|
|
192286
192792
|
var REVIEW_THREADS_QUERY = `
|
|
192287
192793
|
query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
|
|
@@ -192754,8 +193260,8 @@ function GetReviewCommentsTool(ctx) {
|
|
|
192754
193260
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
192755
193261
|
}
|
|
192756
193262
|
const filename = `review-${params.review_id}-threads.md`;
|
|
192757
|
-
const commentsPath =
|
|
192758
|
-
|
|
193263
|
+
const commentsPath = join22(tempDir, filename);
|
|
193264
|
+
writeFileSync13(commentsPath, formatted.content);
|
|
192759
193265
|
log2.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
192760
193266
|
return {
|
|
192761
193267
|
review_id: params.review_id,
|
|
@@ -193001,394 +193507,6 @@ ${addendum}`,
|
|
|
193001
193507
|
});
|
|
193002
193508
|
}
|
|
193003
193509
|
|
|
193004
|
-
// mcp/shell.ts
|
|
193005
|
-
import { spawn as spawn5, spawnSync as spawnSync6 } from "node:child_process";
|
|
193006
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
193007
|
-
import { closeSync, openSync, writeFileSync as writeFileSync13 } from "node:fs";
|
|
193008
|
-
import { userInfo as userInfo2 } from "node:os";
|
|
193009
|
-
import { join as join21 } from "node:path";
|
|
193010
|
-
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
193011
|
-
var ShellParams = type({
|
|
193012
|
-
command: "string",
|
|
193013
|
-
// advisory and unread — no code path consumes it. kept in the schema only
|
|
193014
|
-
// because it nudges the model to state intent before running a command;
|
|
193015
|
-
// REQUIRING it cost a full rejected turn every time one was omitted. #1140.
|
|
193016
|
-
"description?": "string",
|
|
193017
|
-
"timeout?": type.number.describe(
|
|
193018
|
-
"Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately."
|
|
193019
|
-
),
|
|
193020
|
-
"working_directory?": "string",
|
|
193021
|
-
"background?": "boolean"
|
|
193022
|
-
});
|
|
193023
|
-
var detectedSandboxMethod;
|
|
193024
|
-
var usernsProbeError;
|
|
193025
|
-
function getSandboxMethod() {
|
|
193026
|
-
return detectSandboxMethod();
|
|
193027
|
-
}
|
|
193028
|
-
function detectSandboxMethod() {
|
|
193029
|
-
if (detectedSandboxMethod !== void 0) {
|
|
193030
|
-
return detectedSandboxMethod;
|
|
193031
|
-
}
|
|
193032
|
-
if (process.env.CI !== "true") {
|
|
193033
|
-
detectedSandboxMethod = "none";
|
|
193034
|
-
log2.debug("sandbox disabled (CI !== true)");
|
|
193035
|
-
return "none";
|
|
193036
|
-
}
|
|
193037
|
-
try {
|
|
193038
|
-
const result = spawnSync6("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
|
|
193039
|
-
timeout: 5e3,
|
|
193040
|
-
stdio: "ignore"
|
|
193041
|
-
});
|
|
193042
|
-
if (result.status === 0) {
|
|
193043
|
-
detectedSandboxMethod = "unshare";
|
|
193044
|
-
log2.debug("PID namespace isolation enabled (unprivileged unshare)");
|
|
193045
|
-
return "unshare";
|
|
193046
|
-
}
|
|
193047
|
-
} catch {
|
|
193048
|
-
}
|
|
193049
|
-
try {
|
|
193050
|
-
const result = spawnSync6("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
|
|
193051
|
-
timeout: 5e3,
|
|
193052
|
-
stdio: "ignore"
|
|
193053
|
-
});
|
|
193054
|
-
if (result.status === 0) {
|
|
193055
|
-
detectedSandboxMethod = "sudo-unshare";
|
|
193056
|
-
log2.debug("PID namespace isolation enabled (sudo unshare)");
|
|
193057
|
-
return "sudo-unshare";
|
|
193058
|
-
}
|
|
193059
|
-
} catch {
|
|
193060
|
-
}
|
|
193061
|
-
try {
|
|
193062
|
-
const result = spawnSync6(
|
|
193063
|
-
"unshare",
|
|
193064
|
-
[
|
|
193065
|
-
"--user",
|
|
193066
|
-
"--map-root-user",
|
|
193067
|
-
"--pid",
|
|
193068
|
-
"--fork",
|
|
193069
|
-
"--mount",
|
|
193070
|
-
"setpriv",
|
|
193071
|
-
"--no-new-privs",
|
|
193072
|
-
"--bounding-set",
|
|
193073
|
-
"-all",
|
|
193074
|
-
"--inh-caps",
|
|
193075
|
-
"-all",
|
|
193076
|
-
"--ambient-caps",
|
|
193077
|
-
"-all",
|
|
193078
|
-
"true"
|
|
193079
|
-
],
|
|
193080
|
-
{ timeout: 5e3, stdio: ["ignore", "ignore", "pipe"] }
|
|
193081
|
-
);
|
|
193082
|
-
if (result.status === 0) {
|
|
193083
|
-
detectedSandboxMethod = "userns-unshare";
|
|
193084
|
-
log2.debug("PID namespace isolation enabled (unprivileged userns unshare)");
|
|
193085
|
-
return "userns-unshare";
|
|
193086
|
-
}
|
|
193087
|
-
usernsProbeError = result.stderr?.toString().trim() || result.error?.message;
|
|
193088
|
-
} catch {
|
|
193089
|
-
}
|
|
193090
|
-
detectedSandboxMethod = "none";
|
|
193091
|
-
log2.info("PID namespace isolation not available");
|
|
193092
|
-
return "none";
|
|
193093
|
-
}
|
|
193094
|
-
var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
|
193095
|
-
var SOCKET_CLEANUP = [
|
|
193096
|
-
"/var/run/docker.sock",
|
|
193097
|
-
"/run/docker.sock",
|
|
193098
|
-
"/var/run/podman/podman.sock",
|
|
193099
|
-
"/run/podman/podman.sock",
|
|
193100
|
-
"/run/containerd/containerd.sock",
|
|
193101
|
-
"/var/run/crio/crio.sock"
|
|
193102
|
-
].map((path4) => `mount --bind /dev/null ${path4} 2>/dev/null;`).join(" ");
|
|
193103
|
-
function buildFsMounts(repoDir) {
|
|
193104
|
-
const escaped = repoDir.replace(/'/g, "'\\''");
|
|
193105
|
-
return [
|
|
193106
|
-
`mkdir -p /var/lib/pullfrog 2>/dev/null;`,
|
|
193107
|
-
`mount -t tmpfs tmpfs /var/lib/pullfrog 2>/dev/null;`,
|
|
193108
|
-
`[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
|
|
193109
|
-
`[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`
|
|
193110
|
-
].join(" ");
|
|
193111
|
-
}
|
|
193112
|
-
var _repoRoot;
|
|
193113
|
-
function resolveRepoRoot() {
|
|
193114
|
-
if (_repoRoot) return _repoRoot;
|
|
193115
|
-
const fromEnv = process.env.GITHUB_WORKSPACE;
|
|
193116
|
-
if (fromEnv) {
|
|
193117
|
-
_repoRoot = fromEnv;
|
|
193118
|
-
return _repoRoot;
|
|
193119
|
-
}
|
|
193120
|
-
try {
|
|
193121
|
-
_repoRoot = spawnSync6("git", ["rev-parse", "--show-toplevel"], {
|
|
193122
|
-
cwd: process.cwd(),
|
|
193123
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
193124
|
-
encoding: "utf-8"
|
|
193125
|
-
}).stdout?.trim();
|
|
193126
|
-
} catch {
|
|
193127
|
-
}
|
|
193128
|
-
if (!_repoRoot) _repoRoot = process.cwd();
|
|
193129
|
-
return _repoRoot;
|
|
193130
|
-
}
|
|
193131
|
-
function spawnShell(params) {
|
|
193132
|
-
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
|
193133
|
-
const sandboxMethod = detectSandboxMethod();
|
|
193134
|
-
const ci = process.env.CI === "true";
|
|
193135
|
-
if (ci && sandboxMethod === "none") {
|
|
193136
|
-
throw new Error(
|
|
193137
|
-
"PID-namespace isolation is required in CI but unavailable: unprivileged unshare, sudo unshare, and userns-nested unshare all failed. " + (usernsProbeError ? `userns probe said: ${usernsProbeError}. ` : "") + "on a containerized / Kubernetes self-hosted runner the cause is usually one of: the pod's seccomp profile blocks user-namespace creation (Pod Security Standards baseline/restricted); the pod runs as root without CAP_SETFCAP, which the kernel requires to map uid 0; or the node kernel disallows unprivileged user namespaces (Ubuntu >= 23.10, Bottlerocket, Talos). elsewhere, check that `unshare` and `setpriv` (util-linux >= 2.31, which is what added --ambient-caps) are on PATH. see https://docs.pullfrog.com/security#self-hosted-runners"
|
|
193138
|
-
);
|
|
193139
|
-
}
|
|
193140
|
-
const repoRoot = resolveRepoRoot();
|
|
193141
|
-
const fsMounts = buildFsMounts(repoRoot);
|
|
193142
|
-
if (sandboxMethod === "unshare") {
|
|
193143
|
-
return spawn5(
|
|
193144
|
-
"unshare",
|
|
193145
|
-
[
|
|
193146
|
-
"--pid",
|
|
193147
|
-
"--fork",
|
|
193148
|
-
"--mount-proc",
|
|
193149
|
-
"bash",
|
|
193150
|
-
"-c",
|
|
193151
|
-
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`
|
|
193152
|
-
],
|
|
193153
|
-
spawnOpts
|
|
193154
|
-
);
|
|
193155
|
-
}
|
|
193156
|
-
if (sandboxMethod === "userns-unshare") {
|
|
193157
|
-
const escaped = params.command.replace(/'/g, "'\\''");
|
|
193158
|
-
return spawn5(
|
|
193159
|
-
"unshare",
|
|
193160
|
-
[
|
|
193161
|
-
"--user",
|
|
193162
|
-
"--map-root-user",
|
|
193163
|
-
"--pid",
|
|
193164
|
-
"--fork",
|
|
193165
|
-
"--mount",
|
|
193166
|
-
"bash",
|
|
193167
|
-
"-c",
|
|
193168
|
-
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec setpriv --no-new-privs --bounding-set -all --inh-caps -all --ambient-caps -all bash -c '${escaped}'`
|
|
193169
|
-
],
|
|
193170
|
-
spawnOpts
|
|
193171
|
-
);
|
|
193172
|
-
}
|
|
193173
|
-
if (sandboxMethod === "sudo-unshare") {
|
|
193174
|
-
const envArgs = [];
|
|
193175
|
-
for (const [k2, v] of Object.entries(params.env)) {
|
|
193176
|
-
if (v !== void 0) {
|
|
193177
|
-
envArgs.push(`${k2}=${v}`);
|
|
193178
|
-
}
|
|
193179
|
-
}
|
|
193180
|
-
const username = userInfo2().username;
|
|
193181
|
-
const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
|
|
193182
|
-
const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
|
|
193183
|
-
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
|
193184
|
-
return spawn5(
|
|
193185
|
-
"sudo",
|
|
193186
|
-
[
|
|
193187
|
-
"env",
|
|
193188
|
-
...envArgs,
|
|
193189
|
-
"unshare",
|
|
193190
|
-
"--pid",
|
|
193191
|
-
"--fork",
|
|
193192
|
-
"--mount-proc",
|
|
193193
|
-
"bash",
|
|
193194
|
-
"-c",
|
|
193195
|
-
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
|
193196
|
-
],
|
|
193197
|
-
{ ...spawnOpts, env: {} }
|
|
193198
|
-
);
|
|
193199
|
-
}
|
|
193200
|
-
return spawn5("bash", ["-c", params.command], spawnOpts);
|
|
193201
|
-
}
|
|
193202
|
-
async function killProcessGroup(proc) {
|
|
193203
|
-
if (!proc.pid) return;
|
|
193204
|
-
try {
|
|
193205
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
193206
|
-
await new Promise((r2) => setTimeout(r2, 200));
|
|
193207
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
193208
|
-
} catch {
|
|
193209
|
-
try {
|
|
193210
|
-
proc.kill("SIGKILL");
|
|
193211
|
-
} catch {
|
|
193212
|
-
}
|
|
193213
|
-
}
|
|
193214
|
-
}
|
|
193215
|
-
function getTempDir() {
|
|
193216
|
-
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
193217
|
-
if (!tempDir) {
|
|
193218
|
-
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
193219
|
-
}
|
|
193220
|
-
return tempDir;
|
|
193221
|
-
}
|
|
193222
|
-
var MAX_OUTPUT_CHARS = 5e3;
|
|
193223
|
-
function capOutput(output) {
|
|
193224
|
-
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
193225
|
-
const fullPath = join21(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
193226
|
-
writeFileSync13(fullPath, output);
|
|
193227
|
-
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
193228
|
-
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
193229
|
-
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
193230
|
-
}
|
|
193231
|
-
function isGitCommand(command) {
|
|
193232
|
-
const trimmed = command.trim();
|
|
193233
|
-
if (trimmed === "git" || trimmed.startsWith("git ")) return true;
|
|
193234
|
-
if (trimmed.startsWith("sudo git")) return true;
|
|
193235
|
-
return /[;&|]\s*(?:sudo\s+)?git(?:\s|$)/.test(trimmed);
|
|
193236
|
-
}
|
|
193237
|
-
function ShellTool(ctx) {
|
|
193238
|
-
return tool({
|
|
193239
|
-
name: "shell",
|
|
193240
|
-
timeoutMs: 12e4,
|
|
193241
|
-
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
193242
|
-
|
|
193243
|
-
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
193244
|
-
|
|
193245
|
-
Use this tool to:
|
|
193246
|
-
- Run shell commands (ls, cat, grep, find, etc.)
|
|
193247
|
-
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
193248
|
-
- Run tests and linters
|
|
193249
|
-
|
|
193250
|
-
Output is capped at ${MAX_OUTPUT_CHARS} chars: if exceeded, only the tail is returned and the full body is saved to a tempfile (path included in the response). Re-read the tempfile with cat/tail/grep when you need more.
|
|
193251
|
-
|
|
193252
|
-
Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
|
|
193253
|
-
parameters: ShellParams,
|
|
193254
|
-
execute: execute(async (params) => {
|
|
193255
|
-
if (isGitCommand(params.command)) {
|
|
193256
|
-
throw new Error(
|
|
193257
|
-
'git commands are not allowed in the shell tool. use the dedicated git tools instead:\n- git: local operations, e.g. git({ command: "log", args: ["--oneline"] }) \u2014 `command` is the subcommand ONLY\n- push_branch: push to remote (handles authentication)\n- git_fetch: fetch from remote (handles authentication)\n- checkout_pr: check out PR branches'
|
|
193258
|
-
);
|
|
193259
|
-
}
|
|
193260
|
-
const timeout = Math.min(params.timeout ?? 3e4, 12e4);
|
|
193261
|
-
const cwd = params.working_directory ?? process.cwd();
|
|
193262
|
-
const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
193263
|
-
if (params.command.includes("agent-browser")) {
|
|
193264
|
-
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
|
193265
|
-
if (daemonError) {
|
|
193266
|
-
return {
|
|
193267
|
-
output: `browser daemon unavailable: ${daemonError}`,
|
|
193268
|
-
exit_code: 1,
|
|
193269
|
-
timed_out: false
|
|
193270
|
-
};
|
|
193271
|
-
}
|
|
193272
|
-
const binDir = ctx.toolState.browserDaemon?.binDir;
|
|
193273
|
-
if (binDir) {
|
|
193274
|
-
env2.PATH = `${binDir}:${env2.PATH ?? ""}`;
|
|
193275
|
-
}
|
|
193276
|
-
}
|
|
193277
|
-
if (params.background) {
|
|
193278
|
-
const tempDir = getTempDir();
|
|
193279
|
-
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
193280
|
-
const outputPath = join21(tempDir, `${handle}.log`);
|
|
193281
|
-
const pidPath = join21(tempDir, `${handle}.pid`);
|
|
193282
|
-
const logFd = openSync(outputPath, "a");
|
|
193283
|
-
let proc2;
|
|
193284
|
-
try {
|
|
193285
|
-
proc2 = spawnShell({
|
|
193286
|
-
command: params.command,
|
|
193287
|
-
env: env2,
|
|
193288
|
-
cwd,
|
|
193289
|
-
stdio: ["ignore", logFd, logFd]
|
|
193290
|
-
});
|
|
193291
|
-
} finally {
|
|
193292
|
-
closeSync(logFd);
|
|
193293
|
-
}
|
|
193294
|
-
if (!proc2.pid) {
|
|
193295
|
-
throw new Error("failed to start background process");
|
|
193296
|
-
}
|
|
193297
|
-
proc2.unref();
|
|
193298
|
-
writeFileSync13(pidPath, `${proc2.pid}
|
|
193299
|
-
`);
|
|
193300
|
-
ctx.toolState.backgroundProcesses.set(handle, { pid: proc2.pid, outputPath, pidPath });
|
|
193301
|
-
return {
|
|
193302
|
-
handle,
|
|
193303
|
-
outputPath,
|
|
193304
|
-
pidPath,
|
|
193305
|
-
message: `started background process ${handle} (pid ${proc2.pid})`
|
|
193306
|
-
};
|
|
193307
|
-
}
|
|
193308
|
-
const proc = spawnShell({
|
|
193309
|
-
command: params.command,
|
|
193310
|
-
env: env2,
|
|
193311
|
-
cwd,
|
|
193312
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
193313
|
-
});
|
|
193314
|
-
let stdout2 = "", stderr = "", timedOut = false, exited = false;
|
|
193315
|
-
proc.stdout?.on("data", (chunk) => {
|
|
193316
|
-
stdout2 += chunk.toString();
|
|
193317
|
-
});
|
|
193318
|
-
proc.stderr?.on("data", (chunk) => {
|
|
193319
|
-
stderr += chunk.toString();
|
|
193320
|
-
});
|
|
193321
|
-
const timeoutId = setTimeout(async () => {
|
|
193322
|
-
if (!exited) {
|
|
193323
|
-
timedOut = true;
|
|
193324
|
-
await killProcessGroup(proc);
|
|
193325
|
-
}
|
|
193326
|
-
}, timeout);
|
|
193327
|
-
const exitCode = await new Promise((resolve3) => {
|
|
193328
|
-
const done = (code) => {
|
|
193329
|
-
exited = true;
|
|
193330
|
-
clearTimeout(timeoutId);
|
|
193331
|
-
resolve3(code);
|
|
193332
|
-
};
|
|
193333
|
-
proc.on("exit", done);
|
|
193334
|
-
proc.on("error", () => done(null));
|
|
193335
|
-
});
|
|
193336
|
-
proc.stdout?.destroy();
|
|
193337
|
-
proc.stderr?.destroy();
|
|
193338
|
-
let output = stderr ? stdout2 ? `${stdout2}
|
|
193339
|
-
${stderr}` : stderr : stdout2;
|
|
193340
|
-
if (timedOut)
|
|
193341
|
-
output = output ? `${output}
|
|
193342
|
-
[timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
|
|
193343
|
-
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
|
193344
|
-
const trimmed = output.trim();
|
|
193345
|
-
if (finalExitCode !== 0) {
|
|
193346
|
-
log2.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
|
|
193347
|
-
if (trimmed) log2.info(`output: ${trimmed}`);
|
|
193348
|
-
}
|
|
193349
|
-
return {
|
|
193350
|
-
output: capOutput(trimmed),
|
|
193351
|
-
exit_code: finalExitCode,
|
|
193352
|
-
timed_out: timedOut
|
|
193353
|
-
};
|
|
193354
|
-
})
|
|
193355
|
-
});
|
|
193356
|
-
}
|
|
193357
|
-
var KillBackgroundParams = type({
|
|
193358
|
-
handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)")
|
|
193359
|
-
});
|
|
193360
|
-
function KillBackgroundTool(ctx) {
|
|
193361
|
-
return tool({
|
|
193362
|
-
name: "kill_background",
|
|
193363
|
-
mutates: true,
|
|
193364
|
-
description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
|
|
193365
|
-
parameters: KillBackgroundParams,
|
|
193366
|
-
execute: execute(async (params) => {
|
|
193367
|
-
const proc = ctx.toolState.backgroundProcesses.get(params.handle);
|
|
193368
|
-
if (!proc) {
|
|
193369
|
-
return {
|
|
193370
|
-
success: false,
|
|
193371
|
-
message: `no background process with handle ${params.handle}`
|
|
193372
|
-
};
|
|
193373
|
-
}
|
|
193374
|
-
try {
|
|
193375
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
193376
|
-
} catch {
|
|
193377
|
-
}
|
|
193378
|
-
await sleep3(200);
|
|
193379
|
-
try {
|
|
193380
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
193381
|
-
} catch {
|
|
193382
|
-
}
|
|
193383
|
-
ctx.toolState.backgroundProcesses.delete(params.handle);
|
|
193384
|
-
return {
|
|
193385
|
-
success: true,
|
|
193386
|
-
message: `killed background process ${params.handle} (pid ${proc.pid})`
|
|
193387
|
-
};
|
|
193388
|
-
})
|
|
193389
|
-
});
|
|
193390
|
-
}
|
|
193391
|
-
|
|
193392
193510
|
// mcp/similarIssues.ts
|
|
193393
193511
|
var SimilarIssues = type({
|
|
193394
193512
|
issue_number: type.number.describe("The issue number to find older duplicate candidates for")
|
|
@@ -193476,8 +193594,8 @@ function UploadFileTool(ctx) {
|
|
|
193476
193594
|
}
|
|
193477
193595
|
|
|
193478
193596
|
// mcp/xrepo.ts
|
|
193479
|
-
import { mkdirSync as
|
|
193480
|
-
import { join as
|
|
193597
|
+
import { mkdirSync as mkdirSync9, rmSync as rmSync3 } from "node:fs";
|
|
193598
|
+
import { join as join23 } from "node:path";
|
|
193481
193599
|
function assertValidRepoName(name) {
|
|
193482
193600
|
if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
|
|
193483
193601
|
throw new Error(
|
|
@@ -193557,8 +193675,8 @@ function CheckoutRepoTool(ctx) {
|
|
|
193557
193675
|
return { path: existing.dir, access: existing.access, note: "already checked out." };
|
|
193558
193676
|
}
|
|
193559
193677
|
const access = accessFor(ctx, repo);
|
|
193560
|
-
const dir =
|
|
193561
|
-
|
|
193678
|
+
const dir = join23(ctx.tmpdir, "xrepo", repo);
|
|
193679
|
+
mkdirSync9(dir, { recursive: true });
|
|
193562
193680
|
const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
|
|
193563
193681
|
const rc = resolveRepoCtx(ctx, repo);
|
|
193564
193682
|
try {
|
|
@@ -193636,8 +193754,8 @@ function buildCommonTools(ctx, outputSchema) {
|
|
|
193636
193754
|
EditCommentTool(ctx),
|
|
193637
193755
|
ReplyToReviewCommentTool(ctx),
|
|
193638
193756
|
IssueTool(ctx),
|
|
193639
|
-
|
|
193640
|
-
|
|
193757
|
+
CloseCurrentTool(ctx),
|
|
193758
|
+
ReopenCurrentTool(ctx),
|
|
193641
193759
|
IssueInfoTool(ctx),
|
|
193642
193760
|
GetIssueCommentsTool(ctx),
|
|
193643
193761
|
GetIssueEventsTool(ctx),
|
|
@@ -193669,6 +193787,9 @@ function buildCommonTools(ctx, outputSchema) {
|
|
|
193669
193787
|
tools.push(ShellTool(ctx));
|
|
193670
193788
|
tools.push(KillBackgroundTool(ctx));
|
|
193671
193789
|
}
|
|
193790
|
+
if (ctx.ghToken) {
|
|
193791
|
+
tools.push(GhTool(ctx));
|
|
193792
|
+
}
|
|
193672
193793
|
return tools;
|
|
193673
193794
|
}
|
|
193674
193795
|
function buildOrchestratorTools(ctx, outputSchema) {
|
|
@@ -193680,8 +193801,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
193680
193801
|
PushTagsTool(ctx),
|
|
193681
193802
|
DeleteBranchTool(ctx),
|
|
193682
193803
|
CreatePullRequestTool(ctx),
|
|
193683
|
-
UpdatePullRequestBodyTool(ctx)
|
|
193684
|
-
ClosePullRequestTool(ctx)
|
|
193804
|
+
UpdatePullRequestBodyTool(ctx)
|
|
193685
193805
|
];
|
|
193686
193806
|
if (ctx.signedCommits) {
|
|
193687
193807
|
tools.push(CommitChangesTool(ctx));
|
|
@@ -193771,6 +193891,10 @@ async function startMcpHttpServer(ctx, options) {
|
|
|
193771
193891
|
let disposed = false;
|
|
193772
193892
|
return {
|
|
193773
193893
|
url: startResult.url,
|
|
193894
|
+
// the names actually registered, handed back so the prompt can be checked
|
|
193895
|
+
// against them rather than against a second derivation. see
|
|
193896
|
+
// assertPromptToolRefs.
|
|
193897
|
+
toolNames: tools.map((registered) => registered.name),
|
|
193774
193898
|
[Symbol.asyncDispose]: async () => {
|
|
193775
193899
|
if (disposed) return;
|
|
193776
193900
|
disposed = true;
|
|
@@ -194458,7 +194582,7 @@ async function checkConfiguredCredentials(params) {
|
|
|
194458
194582
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
194459
194583
|
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
194460
194584
|
import { createServer as createServer3 } from "node:http";
|
|
194461
|
-
import { join as
|
|
194585
|
+
import { join as join24 } from "node:path";
|
|
194462
194586
|
var REVOKED_TRAP_MS = 6e4;
|
|
194463
194587
|
function revokeGitHubToken(token) {
|
|
194464
194588
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -194527,7 +194651,7 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
194527
194651
|
function writeAskpassScript(code) {
|
|
194528
194652
|
const scriptId = randomUUID5();
|
|
194529
194653
|
const scriptName = `askpass-${scriptId}.js`;
|
|
194530
|
-
const scriptPath =
|
|
194654
|
+
const scriptPath = join24(tmpdir4, scriptName);
|
|
194531
194655
|
const content = [
|
|
194532
194656
|
`#!/usr/bin/env node`,
|
|
194533
194657
|
`var a=process.argv[2]||"";`,
|
|
@@ -194979,7 +195103,7 @@ function resolveInstructions(ctx) {
|
|
|
194979
195103
|
|
|
194980
195104
|
// utils/learnings.ts
|
|
194981
195105
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
194982
|
-
import { dirname as dirname4, join as
|
|
195106
|
+
import { dirname as dirname4, join as join25 } from "node:path";
|
|
194983
195107
|
|
|
194984
195108
|
// utils/learningsTruncate.ts
|
|
194985
195109
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -194997,10 +195121,10 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
194997
195121
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
194998
195122
|
var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
|
|
194999
195123
|
function learningsFilePath(tmpdir4) {
|
|
195000
|
-
return
|
|
195124
|
+
return join25(tmpdir4, LEARNINGS_FILE_NAME);
|
|
195001
195125
|
}
|
|
195002
195126
|
function xrepoLearningsFilePath(tmpdir4) {
|
|
195003
|
-
return
|
|
195127
|
+
return join25(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
|
|
195004
195128
|
}
|
|
195005
195129
|
async function seedLearningsFile(params) {
|
|
195006
195130
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -195544,6 +195668,9 @@ async function resolveTokens(params) {
|
|
|
195544
195668
|
mcpToken: externalToken,
|
|
195545
195669
|
// external token is whatever scope the user granted — reuse for reads too.
|
|
195546
195670
|
readToken: params.xrepo ? externalToken : void 0,
|
|
195671
|
+
// the scope is the user's to choose here, but the role threshold still
|
|
195672
|
+
// decides whether the agent gets a CLI credential at all.
|
|
195673
|
+
ghToken: mirrorRolePermissions(params.authorPermission) ? externalToken : void 0,
|
|
195547
195674
|
async [Symbol.asyncDispose]() {
|
|
195548
195675
|
mcpTokenValue = void 0;
|
|
195549
195676
|
}
|
|
@@ -195575,6 +195702,15 @@ async function resolveTokens(params) {
|
|
|
195575
195702
|
log2.info(
|
|
195576
195703
|
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
195577
195704
|
);
|
|
195705
|
+
const ghPermissions = mirrorRolePermissions(params.authorPermission);
|
|
195706
|
+
let ghToken;
|
|
195707
|
+
if (ghPermissions) {
|
|
195708
|
+
ghToken = await acquireNewToken({ permissions: ghPermissions });
|
|
195709
|
+
if (isGitHubActions) core7.setSecret(ghToken);
|
|
195710
|
+
log2.info(
|
|
195711
|
+
`\xBB acquired gh token mirroring ${params.authorPermission} (${formatPermissions(ghPermissions)})`
|
|
195712
|
+
);
|
|
195713
|
+
}
|
|
195578
195714
|
let readToken;
|
|
195579
195715
|
if (params.xrepo) {
|
|
195580
195716
|
readToken = await acquireNewToken({
|
|
@@ -195664,7 +195800,9 @@ async function resolveTokens(params) {
|
|
|
195664
195800
|
await Promise.all([
|
|
195665
195801
|
revokeGitHubInstallationToken(currentGitToken),
|
|
195666
195802
|
revokeGitHubInstallationToken(currentMcpToken),
|
|
195667
|
-
...currentReadToken ? [revokeGitHubInstallationToken(currentReadToken)] : []
|
|
195803
|
+
...currentReadToken ? [revokeGitHubInstallationToken(currentReadToken)] : [],
|
|
195804
|
+
// revoking this one is what bounds a `gh` leak to the run's lifetime
|
|
195805
|
+
...ghToken ? [revokeGitHubInstallationToken(ghToken)] : []
|
|
195668
195806
|
]);
|
|
195669
195807
|
} finally {
|
|
195670
195808
|
removeSignalHandler();
|
|
@@ -195679,6 +195817,7 @@ async function resolveTokens(params) {
|
|
|
195679
195817
|
},
|
|
195680
195818
|
mcpToken,
|
|
195681
195819
|
readToken,
|
|
195820
|
+
ghToken,
|
|
195682
195821
|
refreshGitToken,
|
|
195683
195822
|
[Symbol.asyncDispose]: dispose
|
|
195684
195823
|
};
|
|
@@ -195920,7 +196059,7 @@ async function runProxyResolution(ctx) {
|
|
|
195920
196059
|
|
|
195921
196060
|
// utils/prSummary.ts
|
|
195922
196061
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
195923
|
-
import { dirname as dirname5, join as
|
|
196062
|
+
import { dirname as dirname5, join as join26 } from "node:path";
|
|
195924
196063
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
195925
196064
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
195926
196065
|
|
|
@@ -195930,7 +196069,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
195930
196069
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
195931
196070
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
195932
196071
|
function summaryFilePath(tmpdir4) {
|
|
195933
|
-
return
|
|
196072
|
+
return join26(tmpdir4, SUMMARY_FILE_NAME);
|
|
195934
196073
|
}
|
|
195935
196074
|
async function seedSummaryFile(params) {
|
|
195936
196075
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -196152,7 +196291,10 @@ function warnIfPinnedToSha() {
|
|
|
196152
196291
|
}
|
|
196153
196292
|
}
|
|
196154
196293
|
async function resolveRunContextData(params) {
|
|
196155
|
-
|
|
196294
|
+
const actionRef = process.env.GITHUB_ACTION_REF;
|
|
196295
|
+
log2.info(
|
|
196296
|
+
`\xBB running Pullfrog v${package_default.version}${actionRef ? ` (ref: ${actionRef})` : ""}...`
|
|
196297
|
+
);
|
|
196156
196298
|
warnIfPinnedToSha();
|
|
196157
196299
|
const repoContext = parseRepoContext();
|
|
196158
196300
|
let oidcToken;
|
|
@@ -197242,6 +197384,7 @@ async function main() {
|
|
|
197242
197384
|
try {
|
|
197243
197385
|
const _commentTokenRef = __using(_stack, await resolveTokens({
|
|
197244
197386
|
push: "disabled",
|
|
197387
|
+
authorPermission: void 0,
|
|
197245
197388
|
oidc: oidcCredentials
|
|
197246
197389
|
}), true);
|
|
197247
197390
|
const errorMessage = runContext.commercialRefused === "subscription_unpaid" ? "Pro renewal failed for this organization" : "Pro plan required for this organization";
|
|
@@ -197260,6 +197403,8 @@ async function main() {
|
|
|
197260
197403
|
}
|
|
197261
197404
|
}
|
|
197262
197405
|
const tmpdir4 = createTempDirectory();
|
|
197406
|
+
const repoDir = payload.cwd;
|
|
197407
|
+
const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
|
|
197263
197408
|
const opencodeCliPath = await agents.opencode.install();
|
|
197264
197409
|
captureBaselineModels(opencodeCliPath);
|
|
197265
197410
|
if (runContext.dbSecrets) {
|
|
@@ -197274,6 +197419,13 @@ async function main() {
|
|
|
197274
197419
|
}
|
|
197275
197420
|
installCodexAuth();
|
|
197276
197421
|
captureAuthorizedModels(opencodeCliPath);
|
|
197422
|
+
if (preIntrospectionDirty && repoDir) {
|
|
197423
|
+
await restoreDirtiedSince({
|
|
197424
|
+
before: preIntrospectionDirty,
|
|
197425
|
+
actor: "model introspection",
|
|
197426
|
+
cwd: repoDir
|
|
197427
|
+
});
|
|
197428
|
+
}
|
|
197277
197429
|
if (runContext.repoSettings.envAllowlist) {
|
|
197278
197430
|
setEnvAllowlist(runContext.repoSettings.envAllowlist);
|
|
197279
197431
|
}
|
|
@@ -197285,6 +197437,7 @@ async function main() {
|
|
|
197285
197437
|
}
|
|
197286
197438
|
const tokenRef = __using(_stack3, await resolveTokens({
|
|
197287
197439
|
push: payload.push,
|
|
197440
|
+
authorPermission: payload.event.authorPermission,
|
|
197288
197441
|
xrepo: payload.xrepo,
|
|
197289
197442
|
oidc: oidcCredentials
|
|
197290
197443
|
}), true);
|
|
@@ -197451,6 +197604,7 @@ async function main() {
|
|
|
197451
197604
|
},
|
|
197452
197605
|
refreshGitToken: tokenRef.refreshGitToken,
|
|
197453
197606
|
readToken: tokenRef.readToken,
|
|
197607
|
+
ghToken: tokenRef.ghToken,
|
|
197454
197608
|
xrepo: payload.xrepo,
|
|
197455
197609
|
apiToken: runContext.apiToken,
|
|
197456
197610
|
modes: modes2,
|
|
@@ -197542,6 +197696,16 @@ async function main() {
|
|
|
197542
197696
|
xrepoLearningsFilePath: toolState.xrepoLearningsFilePath ?? null,
|
|
197543
197697
|
xrepoLearningsHeadings: runContext.repoSettings.xrepoLearningsHeadings
|
|
197544
197698
|
});
|
|
197699
|
+
const danglingToolRefs = findDanglingPromptToolRefs({
|
|
197700
|
+
agentId,
|
|
197701
|
+
prompt: instructions.full,
|
|
197702
|
+
toolNames: mcpHttpServer.toolNames
|
|
197703
|
+
});
|
|
197704
|
+
if (danglingToolRefs.length > 0) {
|
|
197705
|
+
log2.warning(
|
|
197706
|
+
`\xBB prompt advertises ${danglingToolRefs.length} tool(s) the MCP server did not register: ${danglingToolRefs.join(", ")}`
|
|
197707
|
+
);
|
|
197708
|
+
}
|
|
197545
197709
|
const logParts = [
|
|
197546
197710
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
197547
197711
|
${instructions.eventInstructions}` : null,
|
|
@@ -197556,7 +197720,7 @@ ${instructions.user}` : null,
|
|
|
197556
197720
|
log2.info(instructions.full);
|
|
197557
197721
|
});
|
|
197558
197722
|
if (agentId === "opencode") {
|
|
197559
|
-
const pluginDir =
|
|
197723
|
+
const pluginDir = join27(process.cwd(), ".opencode", "plugin");
|
|
197560
197724
|
const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
197561
197725
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
197562
197726
|
log2.info(
|
|
@@ -198732,7 +198896,7 @@ async function runCli4(input) {
|
|
|
198732
198896
|
}
|
|
198733
198897
|
|
|
198734
198898
|
// cli.ts
|
|
198735
|
-
var VERSION10 = "0.1.
|
|
198899
|
+
var VERSION10 = "0.1.57";
|
|
198736
198900
|
var bin = basename2(process.argv[1] || "");
|
|
198737
198901
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
198738
198902
|
var rawArgs = process.argv.slice(2);
|