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/index.js
CHANGED
|
@@ -98334,14 +98334,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
98334
98334
|
} else if (node2.nodeType === 1) {
|
|
98335
98335
|
replacement = replacementForNode.call(self2, node2);
|
|
98336
98336
|
}
|
|
98337
|
-
return
|
|
98337
|
+
return join27(output, replacement);
|
|
98338
98338
|
}, "");
|
|
98339
98339
|
}
|
|
98340
98340
|
function postProcess(output) {
|
|
98341
98341
|
var self2 = this;
|
|
98342
98342
|
this.rules.forEach(function(rule) {
|
|
98343
98343
|
if (typeof rule.append === "function") {
|
|
98344
|
-
output =
|
|
98344
|
+
output = join27(output, rule.append(self2.options));
|
|
98345
98345
|
}
|
|
98346
98346
|
});
|
|
98347
98347
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -98353,7 +98353,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
98353
98353
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
98354
98354
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
98355
98355
|
}
|
|
98356
|
-
function
|
|
98356
|
+
function join27(output, replacement) {
|
|
98357
98357
|
var s1 = trimTrailingNewlines(output);
|
|
98358
98358
|
var s2 = trimLeadingNewlines(replacement);
|
|
98359
98359
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -103146,7 +103146,7 @@ var init_file_type = __esm({
|
|
|
103146
103146
|
// main.ts
|
|
103147
103147
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
103148
103148
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
103149
|
-
import { join as
|
|
103149
|
+
import { join as join26 } from "node:path";
|
|
103150
103150
|
|
|
103151
103151
|
// node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
|
|
103152
103152
|
function splitRunArgs(args2, valueFlags = []) {
|
|
@@ -103735,12 +103735,22 @@ var providers = {
|
|
|
103735
103735
|
displayName: "DeepSeek",
|
|
103736
103736
|
envVars: ["DEEPSEEK_API_KEY"],
|
|
103737
103737
|
models: {
|
|
103738
|
+
// same fork trap as `deepseek-flash` below, and Pro fell into it on
|
|
103739
|
+
// 2026-08-12: OpenRouter forked the 0813 release into its own id and left
|
|
103740
|
+
// the April preview live under the unversioned one at 2.7x the input and
|
|
103741
|
+
// 27x the CACHE-READ rate ($1.168/$2.336/$0.09855 vs
|
|
103742
|
+
// $0.435/$0.87/$0.003625). the direct route upgraded in place, so only the
|
|
103743
|
+
// OpenRouter side was stale — and that side is what OSS and Router runs
|
|
103744
|
+
// use. measured: $0.1719/run authoritative against the funded default's
|
|
103745
|
+
// $0.0405. there is no `~deepseek/*-pro-latest` pointer to track, so this
|
|
103746
|
+
// one has to be a dated pin; the `models-catalog` drift test is what
|
|
103747
|
+
// catches the next release.
|
|
103738
103748
|
"deepseek-pro": {
|
|
103739
103749
|
displayName: "DeepSeek Pro",
|
|
103740
103750
|
resolve: "deepseek/deepseek-v4-pro",
|
|
103741
103751
|
effort: ["high", "max"],
|
|
103742
|
-
openRouterEffort: ["high", "
|
|
103743
|
-
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
|
|
103752
|
+
openRouterEffort: ["low", "high", "max"],
|
|
103753
|
+
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro-0813",
|
|
103744
103754
|
preferred: true
|
|
103745
103755
|
},
|
|
103746
103756
|
// DeepSeek upgrades the `deepseek-v4-flash` API model in place, so the
|
|
@@ -103748,14 +103758,19 @@ var providers = {
|
|
|
103748
103758
|
// release into its own id and left the April preview live under the
|
|
103749
103759
|
// unversioned one at a HIGHER price ($0.14/$0.28 vs $0.09/$0.18) — so the
|
|
103750
103760
|
// OpenRouter route can never use the bare id. it tracks the rolling
|
|
103751
|
-
// `~deepseek/*-latest` pointer (rule 4), which
|
|
103752
|
-
//
|
|
103753
|
-
//
|
|
103754
|
-
//
|
|
103755
|
-
//
|
|
103756
|
-
//
|
|
103757
|
-
//
|
|
103758
|
-
//
|
|
103761
|
+
// `~deepseek/*-latest` pointer (rule 4), which follows the next in-place
|
|
103762
|
+
// upgrade on its own rather than pinning a dated snapshot that goes stale
|
|
103763
|
+
// every release. that pointer is NOT priced like 0731 — its listed rates
|
|
103764
|
+
// run +40% on output and +57.5% on cache read, and it costs 12% more per
|
|
103765
|
+
// run on authoritative spend ($0.0405 vs $0.0361). kept anyway: ~$45/yr
|
|
103766
|
+
// buys auto-following the upgrade, and the listed gap overstates the real
|
|
103767
|
+
// one because a rolling alias prices at its routing ceiling.
|
|
103768
|
+
// 0731 gained a `low` rung and renamed the top to `max`, and that ladder
|
|
103769
|
+
// now reads the same on BOTH routes — the in-place direct upgrade caught
|
|
103770
|
+
// up to the fork. so `low`, not `high`, is position 0 either way, which is
|
|
103771
|
+
// why the OSS effort floor in `main.ts` pins the `high` rung by name. Pro
|
|
103772
|
+
// is the same shape via OpenRouter since 0813; only its direct route still
|
|
103773
|
+
// starts at `high`.
|
|
103759
103774
|
"deepseek-flash": {
|
|
103760
103775
|
displayName: "DeepSeek Flash",
|
|
103761
103776
|
resolve: "deepseek/deepseek-v4-flash",
|
|
@@ -104154,11 +104169,13 @@ var providers = {
|
|
|
104154
104169
|
effort: ["low", "medium", "high"],
|
|
104155
104170
|
openRouterResolve: "openrouter/x-ai/grok-4.5"
|
|
104156
104171
|
},
|
|
104172
|
+
// dated pin, not the bare id — see `deepseek/deepseek-pro` for why
|
|
104173
|
+
// OpenRouter's unversioned `deepseek-v4-pro` is the stale April preview.
|
|
104157
104174
|
"deepseek-pro": {
|
|
104158
104175
|
displayName: "DeepSeek Pro",
|
|
104159
|
-
resolve: "openrouter/deepseek/deepseek-v4-pro",
|
|
104160
|
-
effort: ["high", "
|
|
104161
|
-
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro"
|
|
104176
|
+
resolve: "openrouter/deepseek/deepseek-v4-pro-0813",
|
|
104177
|
+
effort: ["low", "high", "max"],
|
|
104178
|
+
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro-0813"
|
|
104162
104179
|
},
|
|
104163
104180
|
// rolling pointer, not the bare id — see `deepseek/deepseek-flash` for why
|
|
104164
104181
|
// OpenRouter's unversioned `deepseek-v4-flash` is the stale April preview.
|
|
@@ -104349,6 +104366,17 @@ function formatMcpToolRef(agentId, toolName) {
|
|
|
104349
104366
|
return agentId;
|
|
104350
104367
|
}
|
|
104351
104368
|
}
|
|
104369
|
+
function extractMcpToolRefs(agentId, text) {
|
|
104370
|
+
const prefix = formatMcpToolRef(agentId, "");
|
|
104371
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
104372
|
+
return [...text.matchAll(new RegExp(`(?<![A-Za-z0-9_])${escaped}([a-z0-9_]+)`, "g"))].map(
|
|
104373
|
+
(match3) => match3[1]
|
|
104374
|
+
);
|
|
104375
|
+
}
|
|
104376
|
+
function findDanglingPromptToolRefs(params) {
|
|
104377
|
+
const registered = new Set(params.toolNames);
|
|
104378
|
+
return [...new Set(extractMcpToolRefs(params.agentId, params.prompt))].filter((name) => !registered.has(name)).sort();
|
|
104379
|
+
}
|
|
104352
104380
|
function hasSimilarIssues(params) {
|
|
104353
104381
|
return params.repoIntelligence && !!params.event.issue_number && !params.event.is_pr;
|
|
104354
104382
|
}
|
|
@@ -105122,7 +105150,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
105122
105150
|
// package.json
|
|
105123
105151
|
var package_default = {
|
|
105124
105152
|
name: "pullfrog",
|
|
105125
|
-
version: "0.1.
|
|
105153
|
+
version: "0.1.57",
|
|
105126
105154
|
type: "module",
|
|
105127
105155
|
bin: {
|
|
105128
105156
|
pullfrog: "dist/cli.mjs",
|
|
@@ -112797,11 +112825,12 @@ function bootstrapPullfrogDataDir() {
|
|
|
112797
112825
|
}
|
|
112798
112826
|
|
|
112799
112827
|
// utils/worktree.ts
|
|
112800
|
-
async function dirtyTrackedPaths() {
|
|
112828
|
+
async function dirtyTrackedPaths(params = {}) {
|
|
112801
112829
|
const result = await spawn({
|
|
112802
112830
|
cmd: "git",
|
|
112803
112831
|
args: ["diff", "--name-only", "HEAD"],
|
|
112804
112832
|
env: process.env,
|
|
112833
|
+
cwd: params.cwd ?? process.cwd(),
|
|
112805
112834
|
activityTimeout: 0
|
|
112806
112835
|
});
|
|
112807
112836
|
if (result.exitCode !== 0) {
|
|
@@ -112812,12 +112841,15 @@ async function dirtyTrackedPaths() {
|
|
|
112812
112841
|
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
112813
112842
|
}
|
|
112814
112843
|
async function restoreDirtiedSince(params) {
|
|
112815
|
-
const dirtied = [...await dirtyTrackedPaths(
|
|
112844
|
+
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
112845
|
+
(path4) => !params.before.has(path4)
|
|
112846
|
+
);
|
|
112816
112847
|
if (dirtied.length === 0) return;
|
|
112817
112848
|
const result = await spawn({
|
|
112818
112849
|
cmd: "git",
|
|
112819
112850
|
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
112820
112851
|
env: process.env,
|
|
112852
|
+
cwd: params.cwd ?? process.cwd(),
|
|
112821
112853
|
activityTimeout: 0
|
|
112822
112854
|
});
|
|
112823
112855
|
if (result.exitCode !== 0) {
|
|
@@ -188745,6 +188777,116 @@ function CommitInfoTool(ctx) {
|
|
|
188745
188777
|
});
|
|
188746
188778
|
}
|
|
188747
188779
|
|
|
188780
|
+
// utils/roleMirror.ts
|
|
188781
|
+
function mirrorRolePermissions(role) {
|
|
188782
|
+
switch (role) {
|
|
188783
|
+
case "admin":
|
|
188784
|
+
case "maintain":
|
|
188785
|
+
case "write":
|
|
188786
|
+
case "triage":
|
|
188787
|
+
return { contents: "read", issues: "write", pull_requests: "write" };
|
|
188788
|
+
case "read":
|
|
188789
|
+
case "none":
|
|
188790
|
+
case void 0:
|
|
188791
|
+
return null;
|
|
188792
|
+
default:
|
|
188793
|
+
return role;
|
|
188794
|
+
}
|
|
188795
|
+
}
|
|
188796
|
+
function formatPermissions(permissions) {
|
|
188797
|
+
return Object.entries(permissions).map((entry) => entry.join(":")).join(", ");
|
|
188798
|
+
}
|
|
188799
|
+
|
|
188800
|
+
// mcp/currentObject.ts
|
|
188801
|
+
async function resolveCurrentObject(ctx) {
|
|
188802
|
+
const issueNumber = ctx.payload.event.issue_number;
|
|
188803
|
+
if (!issueNumber) {
|
|
188804
|
+
throw new Error("this run was not triggered on an issue or pull request");
|
|
188805
|
+
}
|
|
188806
|
+
if (mirrorRolePermissions(ctx.payload.event.authorPermission)) {
|
|
188807
|
+
return { issueNumber, isPr: ctx.payload.event.is_pr === true };
|
|
188808
|
+
}
|
|
188809
|
+
const issue3 = await ctx.octokit.rest.issues.get({
|
|
188810
|
+
owner: ctx.repo.owner,
|
|
188811
|
+
repo: ctx.repo.name,
|
|
188812
|
+
issue_number: issueNumber
|
|
188813
|
+
});
|
|
188814
|
+
const author = issue3.data.user?.login;
|
|
188815
|
+
const triggerer = ctx.payload.triggerer;
|
|
188816
|
+
if (!author || !triggerer || author.toLowerCase() !== triggerer.toLowerCase()) {
|
|
188817
|
+
throw new Error(
|
|
188818
|
+
`${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.`
|
|
188819
|
+
);
|
|
188820
|
+
}
|
|
188821
|
+
return { issueNumber, isPr: issue3.data.pull_request !== void 0 };
|
|
188822
|
+
}
|
|
188823
|
+
var CloseCurrent = type({
|
|
188824
|
+
state_reason: type.enumerated("completed", "not_planned", "duplicate").describe(
|
|
188825
|
+
"why it is being closed: 'completed' (resolved), 'not_planned' (won't fix, out of scope), or 'duplicate' (already tracked elsewhere). ignored for pull requests."
|
|
188826
|
+
)
|
|
188827
|
+
});
|
|
188828
|
+
function CloseCurrentTool(ctx) {
|
|
188829
|
+
return tool({
|
|
188830
|
+
name: "close_current",
|
|
188831
|
+
mutates: true,
|
|
188832
|
+
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.',
|
|
188833
|
+
parameters: CloseCurrent,
|
|
188834
|
+
execute: execute(async (params) => {
|
|
188835
|
+
const target = await resolveCurrentObject(ctx);
|
|
188836
|
+
const result = target.isPr ? await ctx.octokit.rest.pulls.update({
|
|
188837
|
+
owner: ctx.repo.owner,
|
|
188838
|
+
repo: ctx.repo.name,
|
|
188839
|
+
pull_number: target.issueNumber,
|
|
188840
|
+
state: "closed"
|
|
188841
|
+
}) : await ctx.octokit.rest.issues.update({
|
|
188842
|
+
owner: ctx.repo.owner,
|
|
188843
|
+
repo: ctx.repo.name,
|
|
188844
|
+
issue_number: target.issueNumber,
|
|
188845
|
+
state: "closed",
|
|
188846
|
+
state_reason: params.state_reason
|
|
188847
|
+
});
|
|
188848
|
+
ctx.toolState.wasUpdated = true;
|
|
188849
|
+
log.info(`\xBB closed #${target.issueNumber} (${params.state_reason})`);
|
|
188850
|
+
return {
|
|
188851
|
+
success: true,
|
|
188852
|
+
number: result.data.number,
|
|
188853
|
+
url: result.data.html_url,
|
|
188854
|
+
state: result.data.state
|
|
188855
|
+
};
|
|
188856
|
+
}, "close_current")
|
|
188857
|
+
});
|
|
188858
|
+
}
|
|
188859
|
+
function ReopenCurrentTool(ctx) {
|
|
188860
|
+
return tool({
|
|
188861
|
+
name: "reopen_current",
|
|
188862
|
+
mutates: true,
|
|
188863
|
+
description: "Reopen the issue or pull request this run was triggered on.",
|
|
188864
|
+
parameters: type({}),
|
|
188865
|
+
execute: execute(async () => {
|
|
188866
|
+
const target = await resolveCurrentObject(ctx);
|
|
188867
|
+
const result = target.isPr ? await ctx.octokit.rest.pulls.update({
|
|
188868
|
+
owner: ctx.repo.owner,
|
|
188869
|
+
repo: ctx.repo.name,
|
|
188870
|
+
pull_number: target.issueNumber,
|
|
188871
|
+
state: "open"
|
|
188872
|
+
}) : await ctx.octokit.rest.issues.update({
|
|
188873
|
+
owner: ctx.repo.owner,
|
|
188874
|
+
repo: ctx.repo.name,
|
|
188875
|
+
issue_number: target.issueNumber,
|
|
188876
|
+
state: "open"
|
|
188877
|
+
});
|
|
188878
|
+
ctx.toolState.wasUpdated = true;
|
|
188879
|
+
log.info(`\xBB reopened #${target.issueNumber}`);
|
|
188880
|
+
return {
|
|
188881
|
+
success: true,
|
|
188882
|
+
number: result.data.number,
|
|
188883
|
+
url: result.data.html_url,
|
|
188884
|
+
state: result.data.state
|
|
188885
|
+
};
|
|
188886
|
+
}, "reopen_current")
|
|
188887
|
+
});
|
|
188888
|
+
}
|
|
188889
|
+
|
|
188748
188890
|
// prep/index.ts
|
|
188749
188891
|
import { performance as performance8 } from "node:perf_hooks";
|
|
188750
188892
|
|
|
@@ -189371,6 +189513,456 @@ function AwaitDependencyInstallationTool(ctx) {
|
|
|
189371
189513
|
});
|
|
189372
189514
|
}
|
|
189373
189515
|
|
|
189516
|
+
// mcp/gh.ts
|
|
189517
|
+
import { mkdirSync as mkdirSync8 } from "node:fs";
|
|
189518
|
+
import { join as join20 } from "node:path";
|
|
189519
|
+
|
|
189520
|
+
// mcp/shell.ts
|
|
189521
|
+
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
189522
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
189523
|
+
import { closeSync, openSync, writeFileSync as writeFileSync11 } from "node:fs";
|
|
189524
|
+
import { userInfo as userInfo2 } from "node:os";
|
|
189525
|
+
import { join as join19 } from "node:path";
|
|
189526
|
+
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
189527
|
+
var ShellParams = type({
|
|
189528
|
+
command: "string",
|
|
189529
|
+
// advisory and unread — no code path consumes it. kept in the schema only
|
|
189530
|
+
// because it nudges the model to state intent before running a command;
|
|
189531
|
+
// REQUIRING it cost a full rejected turn every time one was omitted. #1140.
|
|
189532
|
+
"description?": "string",
|
|
189533
|
+
"timeout?": type.number.describe(
|
|
189534
|
+
"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."
|
|
189535
|
+
),
|
|
189536
|
+
"working_directory?": "string",
|
|
189537
|
+
"background?": "boolean"
|
|
189538
|
+
});
|
|
189539
|
+
var detectedSandboxMethod;
|
|
189540
|
+
var usernsProbeError;
|
|
189541
|
+
function getSandboxMethod() {
|
|
189542
|
+
return detectSandboxMethod();
|
|
189543
|
+
}
|
|
189544
|
+
function detectSandboxMethod() {
|
|
189545
|
+
if (detectedSandboxMethod !== void 0) {
|
|
189546
|
+
return detectedSandboxMethod;
|
|
189547
|
+
}
|
|
189548
|
+
if (process.env.CI !== "true") {
|
|
189549
|
+
detectedSandboxMethod = "none";
|
|
189550
|
+
log.debug("sandbox disabled (CI !== true)");
|
|
189551
|
+
return "none";
|
|
189552
|
+
}
|
|
189553
|
+
try {
|
|
189554
|
+
const result = spawnSync6("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
|
|
189555
|
+
timeout: 5e3,
|
|
189556
|
+
stdio: "ignore"
|
|
189557
|
+
});
|
|
189558
|
+
if (result.status === 0) {
|
|
189559
|
+
detectedSandboxMethod = "unshare";
|
|
189560
|
+
log.debug("PID namespace isolation enabled (unprivileged unshare)");
|
|
189561
|
+
return "unshare";
|
|
189562
|
+
}
|
|
189563
|
+
} catch {
|
|
189564
|
+
}
|
|
189565
|
+
try {
|
|
189566
|
+
const result = spawnSync6("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
|
|
189567
|
+
timeout: 5e3,
|
|
189568
|
+
stdio: "ignore"
|
|
189569
|
+
});
|
|
189570
|
+
if (result.status === 0) {
|
|
189571
|
+
detectedSandboxMethod = "sudo-unshare";
|
|
189572
|
+
log.debug("PID namespace isolation enabled (sudo unshare)");
|
|
189573
|
+
return "sudo-unshare";
|
|
189574
|
+
}
|
|
189575
|
+
} catch {
|
|
189576
|
+
}
|
|
189577
|
+
try {
|
|
189578
|
+
const result = spawnSync6(
|
|
189579
|
+
"unshare",
|
|
189580
|
+
[
|
|
189581
|
+
"--user",
|
|
189582
|
+
"--map-root-user",
|
|
189583
|
+
"--pid",
|
|
189584
|
+
"--fork",
|
|
189585
|
+
"--mount",
|
|
189586
|
+
"setpriv",
|
|
189587
|
+
"--no-new-privs",
|
|
189588
|
+
"--bounding-set",
|
|
189589
|
+
"-all",
|
|
189590
|
+
"--inh-caps",
|
|
189591
|
+
"-all",
|
|
189592
|
+
"--ambient-caps",
|
|
189593
|
+
"-all",
|
|
189594
|
+
"true"
|
|
189595
|
+
],
|
|
189596
|
+
{ timeout: 5e3, stdio: ["ignore", "ignore", "pipe"] }
|
|
189597
|
+
);
|
|
189598
|
+
if (result.status === 0) {
|
|
189599
|
+
detectedSandboxMethod = "userns-unshare";
|
|
189600
|
+
log.debug("PID namespace isolation enabled (unprivileged userns unshare)");
|
|
189601
|
+
return "userns-unshare";
|
|
189602
|
+
}
|
|
189603
|
+
usernsProbeError = result.stderr?.toString().trim() || result.error?.message;
|
|
189604
|
+
} catch {
|
|
189605
|
+
}
|
|
189606
|
+
detectedSandboxMethod = "none";
|
|
189607
|
+
log.info("PID namespace isolation not available");
|
|
189608
|
+
return "none";
|
|
189609
|
+
}
|
|
189610
|
+
var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
|
189611
|
+
var SOCKET_CLEANUP = [
|
|
189612
|
+
"/var/run/docker.sock",
|
|
189613
|
+
"/run/docker.sock",
|
|
189614
|
+
"/var/run/podman/podman.sock",
|
|
189615
|
+
"/run/podman/podman.sock",
|
|
189616
|
+
"/run/containerd/containerd.sock",
|
|
189617
|
+
"/var/run/crio/crio.sock"
|
|
189618
|
+
].map((path4) => `mount --bind /dev/null ${path4} 2>/dev/null;`).join(" ");
|
|
189619
|
+
function buildFsMounts(repoDir) {
|
|
189620
|
+
const escaped = repoDir.replace(/'/g, "'\\''");
|
|
189621
|
+
return [
|
|
189622
|
+
`mkdir -p /var/lib/pullfrog 2>/dev/null;`,
|
|
189623
|
+
`mount -t tmpfs tmpfs /var/lib/pullfrog 2>/dev/null;`,
|
|
189624
|
+
`[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
|
|
189625
|
+
`[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`
|
|
189626
|
+
].join(" ");
|
|
189627
|
+
}
|
|
189628
|
+
var _repoRoot;
|
|
189629
|
+
function resolveRepoRoot() {
|
|
189630
|
+
if (_repoRoot) return _repoRoot;
|
|
189631
|
+
const fromEnv = process.env.GITHUB_WORKSPACE;
|
|
189632
|
+
if (fromEnv) {
|
|
189633
|
+
_repoRoot = fromEnv;
|
|
189634
|
+
return _repoRoot;
|
|
189635
|
+
}
|
|
189636
|
+
try {
|
|
189637
|
+
_repoRoot = spawnSync6("git", ["rev-parse", "--show-toplevel"], {
|
|
189638
|
+
cwd: process.cwd(),
|
|
189639
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
189640
|
+
encoding: "utf-8"
|
|
189641
|
+
}).stdout?.trim();
|
|
189642
|
+
} catch {
|
|
189643
|
+
}
|
|
189644
|
+
if (!_repoRoot) _repoRoot = process.cwd();
|
|
189645
|
+
return _repoRoot;
|
|
189646
|
+
}
|
|
189647
|
+
function spawnShell(params) {
|
|
189648
|
+
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
|
189649
|
+
const sandboxMethod = detectSandboxMethod();
|
|
189650
|
+
const ci = process.env.CI === "true";
|
|
189651
|
+
if (ci && sandboxMethod === "none") {
|
|
189652
|
+
throw new Error(
|
|
189653
|
+
"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"
|
|
189654
|
+
);
|
|
189655
|
+
}
|
|
189656
|
+
const repoRoot = resolveRepoRoot();
|
|
189657
|
+
const fsMounts = buildFsMounts(repoRoot);
|
|
189658
|
+
if (sandboxMethod === "unshare") {
|
|
189659
|
+
return spawn3(
|
|
189660
|
+
"unshare",
|
|
189661
|
+
[
|
|
189662
|
+
"--pid",
|
|
189663
|
+
"--fork",
|
|
189664
|
+
"--mount-proc",
|
|
189665
|
+
"bash",
|
|
189666
|
+
"-c",
|
|
189667
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`
|
|
189668
|
+
],
|
|
189669
|
+
spawnOpts
|
|
189670
|
+
);
|
|
189671
|
+
}
|
|
189672
|
+
if (sandboxMethod === "userns-unshare") {
|
|
189673
|
+
const escaped = params.command.replace(/'/g, "'\\''");
|
|
189674
|
+
return spawn3(
|
|
189675
|
+
"unshare",
|
|
189676
|
+
[
|
|
189677
|
+
"--user",
|
|
189678
|
+
"--map-root-user",
|
|
189679
|
+
"--pid",
|
|
189680
|
+
"--fork",
|
|
189681
|
+
"--mount",
|
|
189682
|
+
"bash",
|
|
189683
|
+
"-c",
|
|
189684
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec setpriv --no-new-privs --bounding-set -all --inh-caps -all --ambient-caps -all bash -c '${escaped}'`
|
|
189685
|
+
],
|
|
189686
|
+
spawnOpts
|
|
189687
|
+
);
|
|
189688
|
+
}
|
|
189689
|
+
if (sandboxMethod === "sudo-unshare") {
|
|
189690
|
+
const envArgs = [];
|
|
189691
|
+
for (const [k2, v] of Object.entries(params.env)) {
|
|
189692
|
+
if (v !== void 0) {
|
|
189693
|
+
envArgs.push(`${k2}=${v}`);
|
|
189694
|
+
}
|
|
189695
|
+
}
|
|
189696
|
+
const username = userInfo2().username;
|
|
189697
|
+
const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
|
|
189698
|
+
const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
|
|
189699
|
+
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
|
189700
|
+
return spawn3(
|
|
189701
|
+
"sudo",
|
|
189702
|
+
[
|
|
189703
|
+
"env",
|
|
189704
|
+
...envArgs,
|
|
189705
|
+
"unshare",
|
|
189706
|
+
"--pid",
|
|
189707
|
+
"--fork",
|
|
189708
|
+
"--mount-proc",
|
|
189709
|
+
"bash",
|
|
189710
|
+
"-c",
|
|
189711
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
|
189712
|
+
],
|
|
189713
|
+
{ ...spawnOpts, env: {} }
|
|
189714
|
+
);
|
|
189715
|
+
}
|
|
189716
|
+
return spawn3("bash", ["-c", params.command], spawnOpts);
|
|
189717
|
+
}
|
|
189718
|
+
async function killProcessGroup(proc) {
|
|
189719
|
+
if (!proc.pid) return;
|
|
189720
|
+
try {
|
|
189721
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
189722
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
189723
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
189724
|
+
} catch {
|
|
189725
|
+
try {
|
|
189726
|
+
proc.kill("SIGKILL");
|
|
189727
|
+
} catch {
|
|
189728
|
+
}
|
|
189729
|
+
}
|
|
189730
|
+
}
|
|
189731
|
+
function getTempDir() {
|
|
189732
|
+
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
189733
|
+
if (!tempDir) {
|
|
189734
|
+
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
189735
|
+
}
|
|
189736
|
+
return tempDir;
|
|
189737
|
+
}
|
|
189738
|
+
var MAX_OUTPUT_CHARS = 5e3;
|
|
189739
|
+
async function runSandboxed(params) {
|
|
189740
|
+
const proc = spawnShell({
|
|
189741
|
+
command: params.command,
|
|
189742
|
+
env: params.env,
|
|
189743
|
+
cwd: params.cwd,
|
|
189744
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
189745
|
+
});
|
|
189746
|
+
let stdout = "", stderr = "", timedOut = false, exited = false, spawnError = "";
|
|
189747
|
+
proc.stdout?.on("data", (chunk) => {
|
|
189748
|
+
stdout += chunk.toString();
|
|
189749
|
+
});
|
|
189750
|
+
proc.stderr?.on("data", (chunk) => {
|
|
189751
|
+
stderr += chunk.toString();
|
|
189752
|
+
});
|
|
189753
|
+
const timeoutId = setTimeout(async () => {
|
|
189754
|
+
if (!exited) {
|
|
189755
|
+
timedOut = true;
|
|
189756
|
+
await killProcessGroup(proc);
|
|
189757
|
+
}
|
|
189758
|
+
}, params.timeout);
|
|
189759
|
+
const exitCode = await new Promise((resolve3) => {
|
|
189760
|
+
const done = (code) => {
|
|
189761
|
+
exited = true;
|
|
189762
|
+
clearTimeout(timeoutId);
|
|
189763
|
+
resolve3(code);
|
|
189764
|
+
};
|
|
189765
|
+
proc.on("exit", done);
|
|
189766
|
+
proc.on("error", (err) => {
|
|
189767
|
+
spawnError = err.message;
|
|
189768
|
+
done(null);
|
|
189769
|
+
});
|
|
189770
|
+
});
|
|
189771
|
+
proc.stdout?.destroy();
|
|
189772
|
+
proc.stderr?.destroy();
|
|
189773
|
+
let output = stderr ? stdout ? `${stdout}
|
|
189774
|
+
${stderr}` : stderr : stdout;
|
|
189775
|
+
if (spawnError) output = output ? `${output}
|
|
189776
|
+
${spawnError}` : spawnError;
|
|
189777
|
+
if (timedOut)
|
|
189778
|
+
output = output ? `${output}
|
|
189779
|
+
[timed out after ${params.timeout}ms]` : `[timed out after ${params.timeout}ms]`;
|
|
189780
|
+
return {
|
|
189781
|
+
output: output.trim(),
|
|
189782
|
+
exitCode: exitCode ?? (timedOut ? 124 : -1),
|
|
189783
|
+
timedOut
|
|
189784
|
+
};
|
|
189785
|
+
}
|
|
189786
|
+
function capOutput(output) {
|
|
189787
|
+
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
189788
|
+
const fullPath = join19(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
189789
|
+
writeFileSync11(fullPath, output);
|
|
189790
|
+
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
189791
|
+
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
189792
|
+
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
189793
|
+
}
|
|
189794
|
+
function isGitCommand(command) {
|
|
189795
|
+
const trimmed = command.trim();
|
|
189796
|
+
if (trimmed === "git" || trimmed.startsWith("git ")) return true;
|
|
189797
|
+
if (trimmed.startsWith("sudo git")) return true;
|
|
189798
|
+
return /[;&|]\s*(?:sudo\s+)?git(?:\s|$)/.test(trimmed);
|
|
189799
|
+
}
|
|
189800
|
+
function ShellTool(ctx) {
|
|
189801
|
+
return tool({
|
|
189802
|
+
name: "shell",
|
|
189803
|
+
timeoutMs: 12e4,
|
|
189804
|
+
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
189805
|
+
|
|
189806
|
+
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
189807
|
+
|
|
189808
|
+
Use this tool to:
|
|
189809
|
+
- Run shell commands (ls, cat, grep, find, etc.)
|
|
189810
|
+
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
189811
|
+
- Run tests and linters
|
|
189812
|
+
|
|
189813
|
+
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.
|
|
189814
|
+
|
|
189815
|
+
Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
|
|
189816
|
+
parameters: ShellParams,
|
|
189817
|
+
execute: execute(async (params) => {
|
|
189818
|
+
if (isGitCommand(params.command)) {
|
|
189819
|
+
throw new Error(
|
|
189820
|
+
'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'
|
|
189821
|
+
);
|
|
189822
|
+
}
|
|
189823
|
+
const timeout = Math.min(params.timeout ?? 3e4, 12e4);
|
|
189824
|
+
const cwd = params.working_directory ?? process.cwd();
|
|
189825
|
+
const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
189826
|
+
if (params.command.includes("agent-browser")) {
|
|
189827
|
+
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
|
189828
|
+
if (daemonError) {
|
|
189829
|
+
return {
|
|
189830
|
+
output: `browser daemon unavailable: ${daemonError}`,
|
|
189831
|
+
exit_code: 1,
|
|
189832
|
+
timed_out: false
|
|
189833
|
+
};
|
|
189834
|
+
}
|
|
189835
|
+
const binDir = ctx.toolState.browserDaemon?.binDir;
|
|
189836
|
+
if (binDir) {
|
|
189837
|
+
env2.PATH = `${binDir}:${env2.PATH ?? ""}`;
|
|
189838
|
+
}
|
|
189839
|
+
}
|
|
189840
|
+
if (params.background) {
|
|
189841
|
+
const tempDir = getTempDir();
|
|
189842
|
+
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
189843
|
+
const outputPath = join19(tempDir, `${handle}.log`);
|
|
189844
|
+
const pidPath = join19(tempDir, `${handle}.pid`);
|
|
189845
|
+
const logFd = openSync(outputPath, "a");
|
|
189846
|
+
let proc;
|
|
189847
|
+
try {
|
|
189848
|
+
proc = spawnShell({
|
|
189849
|
+
command: params.command,
|
|
189850
|
+
env: env2,
|
|
189851
|
+
cwd,
|
|
189852
|
+
stdio: ["ignore", logFd, logFd]
|
|
189853
|
+
});
|
|
189854
|
+
} finally {
|
|
189855
|
+
closeSync(logFd);
|
|
189856
|
+
}
|
|
189857
|
+
if (!proc.pid) {
|
|
189858
|
+
throw new Error("failed to start background process");
|
|
189859
|
+
}
|
|
189860
|
+
proc.unref();
|
|
189861
|
+
writeFileSync11(pidPath, `${proc.pid}
|
|
189862
|
+
`);
|
|
189863
|
+
ctx.toolState.backgroundProcesses.set(handle, { pid: proc.pid, outputPath, pidPath });
|
|
189864
|
+
return {
|
|
189865
|
+
handle,
|
|
189866
|
+
outputPath,
|
|
189867
|
+
pidPath,
|
|
189868
|
+
message: `started background process ${handle} (pid ${proc.pid})`
|
|
189869
|
+
};
|
|
189870
|
+
}
|
|
189871
|
+
const result = await runSandboxed({ command: params.command, env: env2, cwd, timeout });
|
|
189872
|
+
if (result.exitCode !== 0) {
|
|
189873
|
+
log.info(`shell command failed with exit code ${result.exitCode}: ${params.command}`);
|
|
189874
|
+
if (result.output) log.info(`output: ${result.output}`);
|
|
189875
|
+
}
|
|
189876
|
+
return {
|
|
189877
|
+
output: capOutput(result.output),
|
|
189878
|
+
exit_code: result.exitCode,
|
|
189879
|
+
timed_out: result.timedOut
|
|
189880
|
+
};
|
|
189881
|
+
})
|
|
189882
|
+
});
|
|
189883
|
+
}
|
|
189884
|
+
var KillBackgroundParams = type({
|
|
189885
|
+
handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)")
|
|
189886
|
+
});
|
|
189887
|
+
function KillBackgroundTool(ctx) {
|
|
189888
|
+
return tool({
|
|
189889
|
+
name: "kill_background",
|
|
189890
|
+
mutates: true,
|
|
189891
|
+
description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
|
|
189892
|
+
parameters: KillBackgroundParams,
|
|
189893
|
+
execute: execute(async (params) => {
|
|
189894
|
+
const proc = ctx.toolState.backgroundProcesses.get(params.handle);
|
|
189895
|
+
if (!proc) {
|
|
189896
|
+
return {
|
|
189897
|
+
success: false,
|
|
189898
|
+
message: `no background process with handle ${params.handle}`
|
|
189899
|
+
};
|
|
189900
|
+
}
|
|
189901
|
+
try {
|
|
189902
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
189903
|
+
} catch {
|
|
189904
|
+
}
|
|
189905
|
+
await sleep3(200);
|
|
189906
|
+
try {
|
|
189907
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
189908
|
+
} catch {
|
|
189909
|
+
}
|
|
189910
|
+
ctx.toolState.backgroundProcesses.delete(params.handle);
|
|
189911
|
+
return {
|
|
189912
|
+
success: true,
|
|
189913
|
+
message: `killed background process ${params.handle} (pid ${proc.pid})`
|
|
189914
|
+
};
|
|
189915
|
+
})
|
|
189916
|
+
});
|
|
189917
|
+
}
|
|
189918
|
+
|
|
189919
|
+
// mcp/gh.ts
|
|
189920
|
+
var GH_TIMEOUT_MS = 12e4;
|
|
189921
|
+
var GhParams = type({
|
|
189922
|
+
args: type.string.array().describe(
|
|
189923
|
+
'gh arguments as an argv array, e.g. ["pr", "list", "--json", "number,title"] or ["api", "graphql", "-f", "query=..."]'
|
|
189924
|
+
)
|
|
189925
|
+
});
|
|
189926
|
+
function shellQuote(arg) {
|
|
189927
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
189928
|
+
}
|
|
189929
|
+
function GhTool(ctx) {
|
|
189930
|
+
const permissions = mirrorRolePermissions(ctx.payload.event.authorPermission);
|
|
189931
|
+
return tool({
|
|
189932
|
+
name: "gh",
|
|
189933
|
+
// denied to subagents. `shell` is deliberately unmarked because git is
|
|
189934
|
+
// blocked inside it, but `gh` reaches PR and issue state changes — the
|
|
189935
|
+
// shape of the 2026-05-18 cross-branch clobber the gate exists for.
|
|
189936
|
+
mutates: true,
|
|
189937
|
+
timeoutMs: GH_TIMEOUT_MS,
|
|
189938
|
+
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.`,
|
|
189939
|
+
parameters: GhParams,
|
|
189940
|
+
execute: execute(async (params) => {
|
|
189941
|
+
const configDir = join20(ctx.tmpdir, "gh-config");
|
|
189942
|
+
mkdirSync8(configDir, { recursive: true });
|
|
189943
|
+
const command = ["gh", ...params.args.map(shellQuote)].join(" ");
|
|
189944
|
+
const result = await runSandboxed({
|
|
189945
|
+
command,
|
|
189946
|
+
cwd: process.cwd(),
|
|
189947
|
+
timeout: GH_TIMEOUT_MS,
|
|
189948
|
+
env: {
|
|
189949
|
+
...filterEnvForUntrustedCode(),
|
|
189950
|
+
GH_TOKEN: ctx.ghToken,
|
|
189951
|
+
GH_CONFIG_DIR: configDir,
|
|
189952
|
+
GH_PROMPT_DISABLED: "1",
|
|
189953
|
+
GH_NO_UPDATE_NOTIFIER: "1"
|
|
189954
|
+
}
|
|
189955
|
+
});
|
|
189956
|
+
log.info(`\xBB gh ${params.args.join(" ")} (exit ${result.exitCode})`);
|
|
189957
|
+
return {
|
|
189958
|
+
output: capOutput(result.output),
|
|
189959
|
+
exit_code: result.exitCode,
|
|
189960
|
+
timed_out: result.timedOut
|
|
189961
|
+
};
|
|
189962
|
+
}, "gh")
|
|
189963
|
+
});
|
|
189964
|
+
}
|
|
189965
|
+
|
|
189374
189966
|
// mcp/issue.ts
|
|
189375
189967
|
var Issue = type({
|
|
189376
189968
|
title: type.string.describe("the title of the issue"),
|
|
@@ -189415,65 +190007,6 @@ function IssueTool(ctx) {
|
|
|
189415
190007
|
})
|
|
189416
190008
|
});
|
|
189417
190009
|
}
|
|
189418
|
-
var CloseIssue = type({
|
|
189419
|
-
issue_number: type.number.describe("the issue number to close"),
|
|
189420
|
-
state_reason: type.enumerated("completed", "not_planned", "duplicate").describe(
|
|
189421
|
-
"why the issue is being closed: 'completed' (resolved/done), 'not_planned' (won't fix, out of scope), or 'duplicate' (already tracked elsewhere)."
|
|
189422
|
-
)
|
|
189423
|
-
});
|
|
189424
|
-
function CloseIssueTool(ctx) {
|
|
189425
|
-
return tool({
|
|
189426
|
-
name: "close_issue",
|
|
189427
|
-
mutates: true,
|
|
189428
|
-
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.',
|
|
189429
|
-
parameters: CloseIssue,
|
|
189430
|
-
execute: execute(async (params) => {
|
|
189431
|
-
const result = await ctx.octokit.rest.issues.update({
|
|
189432
|
-
owner: ctx.repo.owner,
|
|
189433
|
-
repo: ctx.repo.name,
|
|
189434
|
-
issue_number: params.issue_number,
|
|
189435
|
-
state: "closed",
|
|
189436
|
-
state_reason: params.state_reason
|
|
189437
|
-
});
|
|
189438
|
-
ctx.toolState.wasUpdated = true;
|
|
189439
|
-
log.info(`\xBB closed issue #${params.issue_number} (${params.state_reason})`);
|
|
189440
|
-
return {
|
|
189441
|
-
success: true,
|
|
189442
|
-
number: result.data.number,
|
|
189443
|
-
url: result.data.html_url,
|
|
189444
|
-
state: result.data.state,
|
|
189445
|
-
stateReason: result.data.state_reason
|
|
189446
|
-
};
|
|
189447
|
-
})
|
|
189448
|
-
});
|
|
189449
|
-
}
|
|
189450
|
-
var ReopenIssue = type({
|
|
189451
|
-
issue_number: type.number.describe("the issue number to reopen")
|
|
189452
|
-
});
|
|
189453
|
-
function ReopenIssueTool(ctx) {
|
|
189454
|
-
return tool({
|
|
189455
|
-
name: "reopen_issue",
|
|
189456
|
-
mutates: true,
|
|
189457
|
-
description: "Reopen a previously closed GitHub issue. Example: `reopen_issue({ issue_number: 1234 })`.",
|
|
189458
|
-
parameters: ReopenIssue,
|
|
189459
|
-
execute: execute(async (params) => {
|
|
189460
|
-
const result = await ctx.octokit.rest.issues.update({
|
|
189461
|
-
owner: ctx.repo.owner,
|
|
189462
|
-
repo: ctx.repo.name,
|
|
189463
|
-
issue_number: params.issue_number,
|
|
189464
|
-
state: "open"
|
|
189465
|
-
});
|
|
189466
|
-
ctx.toolState.wasUpdated = true;
|
|
189467
|
-
log.info(`\xBB reopened issue #${params.issue_number}`);
|
|
189468
|
-
return {
|
|
189469
|
-
success: true,
|
|
189470
|
-
number: result.data.number,
|
|
189471
|
-
url: result.data.html_url,
|
|
189472
|
-
state: result.data.state
|
|
189473
|
-
};
|
|
189474
|
-
})
|
|
189475
|
-
});
|
|
189476
|
-
}
|
|
189477
190010
|
|
|
189478
190011
|
// utils/body.ts
|
|
189479
190012
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
@@ -189970,33 +190503,6 @@ function buildPrBodyWithFooter(ctx, body) {
|
|
|
189970
190503
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
189971
190504
|
return `${bodyWithoutFooter}${footer}`;
|
|
189972
190505
|
}
|
|
189973
|
-
var ClosePullRequest = type({
|
|
189974
|
-
pull_number: type.number.describe("the pull request number to close")
|
|
189975
|
-
});
|
|
189976
|
-
function ClosePullRequestTool(ctx) {
|
|
189977
|
-
return tool({
|
|
189978
|
-
name: "close_pull_request",
|
|
189979
|
-
mutates: true,
|
|
189980
|
-
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.",
|
|
189981
|
-
parameters: ClosePullRequest,
|
|
189982
|
-
execute: execute(async (params) => {
|
|
189983
|
-
const result = await ctx.octokit.rest.pulls.update({
|
|
189984
|
-
owner: ctx.repo.owner,
|
|
189985
|
-
repo: ctx.repo.name,
|
|
189986
|
-
pull_number: params.pull_number,
|
|
189987
|
-
state: "closed"
|
|
189988
|
-
});
|
|
189989
|
-
ctx.toolState.wasUpdated = true;
|
|
189990
|
-
log.info(`\xBB closed pull request #${params.pull_number}`);
|
|
189991
|
-
return {
|
|
189992
|
-
success: true,
|
|
189993
|
-
number: result.data.number,
|
|
189994
|
-
url: result.data.html_url,
|
|
189995
|
-
state: result.data.state
|
|
189996
|
-
};
|
|
189997
|
-
})
|
|
189998
|
-
});
|
|
189999
|
-
}
|
|
190000
190506
|
var UpdatePullRequestBody = type({
|
|
190001
190507
|
pull_number: type.number.describe("the pull request number to update"),
|
|
190002
190508
|
body: type.string.describe("the new body content for the pull request")
|
|
@@ -190156,8 +190662,8 @@ function PullRequestInfoTool(ctx) {
|
|
|
190156
190662
|
}
|
|
190157
190663
|
|
|
190158
190664
|
// mcp/reviewComments.ts
|
|
190159
|
-
import { writeFileSync as
|
|
190160
|
-
import { join as
|
|
190665
|
+
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
190666
|
+
import { join as join21 } from "node:path";
|
|
190161
190667
|
var COMMENTS_PER_THREAD = 50;
|
|
190162
190668
|
var REVIEW_THREADS_QUERY = `
|
|
190163
190669
|
query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
|
|
@@ -190630,8 +191136,8 @@ function GetReviewCommentsTool(ctx) {
|
|
|
190630
191136
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
190631
191137
|
}
|
|
190632
191138
|
const filename = `review-${params.review_id}-threads.md`;
|
|
190633
|
-
const commentsPath =
|
|
190634
|
-
|
|
191139
|
+
const commentsPath = join21(tempDir, filename);
|
|
191140
|
+
writeFileSync12(commentsPath, formatted.content);
|
|
190635
191141
|
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
190636
191142
|
return {
|
|
190637
191143
|
review_id: params.review_id,
|
|
@@ -190877,394 +191383,6 @@ ${addendum}`,
|
|
|
190877
191383
|
});
|
|
190878
191384
|
}
|
|
190879
191385
|
|
|
190880
|
-
// mcp/shell.ts
|
|
190881
|
-
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
190882
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
190883
|
-
import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
|
|
190884
|
-
import { userInfo as userInfo2 } from "node:os";
|
|
190885
|
-
import { join as join20 } from "node:path";
|
|
190886
|
-
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
190887
|
-
var ShellParams = type({
|
|
190888
|
-
command: "string",
|
|
190889
|
-
// advisory and unread — no code path consumes it. kept in the schema only
|
|
190890
|
-
// because it nudges the model to state intent before running a command;
|
|
190891
|
-
// REQUIRING it cost a full rejected turn every time one was omitted. #1140.
|
|
190892
|
-
"description?": "string",
|
|
190893
|
-
"timeout?": type.number.describe(
|
|
190894
|
-
"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."
|
|
190895
|
-
),
|
|
190896
|
-
"working_directory?": "string",
|
|
190897
|
-
"background?": "boolean"
|
|
190898
|
-
});
|
|
190899
|
-
var detectedSandboxMethod;
|
|
190900
|
-
var usernsProbeError;
|
|
190901
|
-
function getSandboxMethod() {
|
|
190902
|
-
return detectSandboxMethod();
|
|
190903
|
-
}
|
|
190904
|
-
function detectSandboxMethod() {
|
|
190905
|
-
if (detectedSandboxMethod !== void 0) {
|
|
190906
|
-
return detectedSandboxMethod;
|
|
190907
|
-
}
|
|
190908
|
-
if (process.env.CI !== "true") {
|
|
190909
|
-
detectedSandboxMethod = "none";
|
|
190910
|
-
log.debug("sandbox disabled (CI !== true)");
|
|
190911
|
-
return "none";
|
|
190912
|
-
}
|
|
190913
|
-
try {
|
|
190914
|
-
const result = spawnSync6("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
|
|
190915
|
-
timeout: 5e3,
|
|
190916
|
-
stdio: "ignore"
|
|
190917
|
-
});
|
|
190918
|
-
if (result.status === 0) {
|
|
190919
|
-
detectedSandboxMethod = "unshare";
|
|
190920
|
-
log.debug("PID namespace isolation enabled (unprivileged unshare)");
|
|
190921
|
-
return "unshare";
|
|
190922
|
-
}
|
|
190923
|
-
} catch {
|
|
190924
|
-
}
|
|
190925
|
-
try {
|
|
190926
|
-
const result = spawnSync6("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
|
|
190927
|
-
timeout: 5e3,
|
|
190928
|
-
stdio: "ignore"
|
|
190929
|
-
});
|
|
190930
|
-
if (result.status === 0) {
|
|
190931
|
-
detectedSandboxMethod = "sudo-unshare";
|
|
190932
|
-
log.debug("PID namespace isolation enabled (sudo unshare)");
|
|
190933
|
-
return "sudo-unshare";
|
|
190934
|
-
}
|
|
190935
|
-
} catch {
|
|
190936
|
-
}
|
|
190937
|
-
try {
|
|
190938
|
-
const result = spawnSync6(
|
|
190939
|
-
"unshare",
|
|
190940
|
-
[
|
|
190941
|
-
"--user",
|
|
190942
|
-
"--map-root-user",
|
|
190943
|
-
"--pid",
|
|
190944
|
-
"--fork",
|
|
190945
|
-
"--mount",
|
|
190946
|
-
"setpriv",
|
|
190947
|
-
"--no-new-privs",
|
|
190948
|
-
"--bounding-set",
|
|
190949
|
-
"-all",
|
|
190950
|
-
"--inh-caps",
|
|
190951
|
-
"-all",
|
|
190952
|
-
"--ambient-caps",
|
|
190953
|
-
"-all",
|
|
190954
|
-
"true"
|
|
190955
|
-
],
|
|
190956
|
-
{ timeout: 5e3, stdio: ["ignore", "ignore", "pipe"] }
|
|
190957
|
-
);
|
|
190958
|
-
if (result.status === 0) {
|
|
190959
|
-
detectedSandboxMethod = "userns-unshare";
|
|
190960
|
-
log.debug("PID namespace isolation enabled (unprivileged userns unshare)");
|
|
190961
|
-
return "userns-unshare";
|
|
190962
|
-
}
|
|
190963
|
-
usernsProbeError = result.stderr?.toString().trim() || result.error?.message;
|
|
190964
|
-
} catch {
|
|
190965
|
-
}
|
|
190966
|
-
detectedSandboxMethod = "none";
|
|
190967
|
-
log.info("PID namespace isolation not available");
|
|
190968
|
-
return "none";
|
|
190969
|
-
}
|
|
190970
|
-
var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
|
190971
|
-
var SOCKET_CLEANUP = [
|
|
190972
|
-
"/var/run/docker.sock",
|
|
190973
|
-
"/run/docker.sock",
|
|
190974
|
-
"/var/run/podman/podman.sock",
|
|
190975
|
-
"/run/podman/podman.sock",
|
|
190976
|
-
"/run/containerd/containerd.sock",
|
|
190977
|
-
"/var/run/crio/crio.sock"
|
|
190978
|
-
].map((path4) => `mount --bind /dev/null ${path4} 2>/dev/null;`).join(" ");
|
|
190979
|
-
function buildFsMounts(repoDir) {
|
|
190980
|
-
const escaped = repoDir.replace(/'/g, "'\\''");
|
|
190981
|
-
return [
|
|
190982
|
-
`mkdir -p /var/lib/pullfrog 2>/dev/null;`,
|
|
190983
|
-
`mount -t tmpfs tmpfs /var/lib/pullfrog 2>/dev/null;`,
|
|
190984
|
-
`[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
|
|
190985
|
-
`[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`
|
|
190986
|
-
].join(" ");
|
|
190987
|
-
}
|
|
190988
|
-
var _repoRoot;
|
|
190989
|
-
function resolveRepoRoot() {
|
|
190990
|
-
if (_repoRoot) return _repoRoot;
|
|
190991
|
-
const fromEnv = process.env.GITHUB_WORKSPACE;
|
|
190992
|
-
if (fromEnv) {
|
|
190993
|
-
_repoRoot = fromEnv;
|
|
190994
|
-
return _repoRoot;
|
|
190995
|
-
}
|
|
190996
|
-
try {
|
|
190997
|
-
_repoRoot = spawnSync6("git", ["rev-parse", "--show-toplevel"], {
|
|
190998
|
-
cwd: process.cwd(),
|
|
190999
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
191000
|
-
encoding: "utf-8"
|
|
191001
|
-
}).stdout?.trim();
|
|
191002
|
-
} catch {
|
|
191003
|
-
}
|
|
191004
|
-
if (!_repoRoot) _repoRoot = process.cwd();
|
|
191005
|
-
return _repoRoot;
|
|
191006
|
-
}
|
|
191007
|
-
function spawnShell(params) {
|
|
191008
|
-
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
|
191009
|
-
const sandboxMethod = detectSandboxMethod();
|
|
191010
|
-
const ci = process.env.CI === "true";
|
|
191011
|
-
if (ci && sandboxMethod === "none") {
|
|
191012
|
-
throw new Error(
|
|
191013
|
-
"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"
|
|
191014
|
-
);
|
|
191015
|
-
}
|
|
191016
|
-
const repoRoot = resolveRepoRoot();
|
|
191017
|
-
const fsMounts = buildFsMounts(repoRoot);
|
|
191018
|
-
if (sandboxMethod === "unshare") {
|
|
191019
|
-
return spawn3(
|
|
191020
|
-
"unshare",
|
|
191021
|
-
[
|
|
191022
|
-
"--pid",
|
|
191023
|
-
"--fork",
|
|
191024
|
-
"--mount-proc",
|
|
191025
|
-
"bash",
|
|
191026
|
-
"-c",
|
|
191027
|
-
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`
|
|
191028
|
-
],
|
|
191029
|
-
spawnOpts
|
|
191030
|
-
);
|
|
191031
|
-
}
|
|
191032
|
-
if (sandboxMethod === "userns-unshare") {
|
|
191033
|
-
const escaped = params.command.replace(/'/g, "'\\''");
|
|
191034
|
-
return spawn3(
|
|
191035
|
-
"unshare",
|
|
191036
|
-
[
|
|
191037
|
-
"--user",
|
|
191038
|
-
"--map-root-user",
|
|
191039
|
-
"--pid",
|
|
191040
|
-
"--fork",
|
|
191041
|
-
"--mount",
|
|
191042
|
-
"bash",
|
|
191043
|
-
"-c",
|
|
191044
|
-
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec setpriv --no-new-privs --bounding-set -all --inh-caps -all --ambient-caps -all bash -c '${escaped}'`
|
|
191045
|
-
],
|
|
191046
|
-
spawnOpts
|
|
191047
|
-
);
|
|
191048
|
-
}
|
|
191049
|
-
if (sandboxMethod === "sudo-unshare") {
|
|
191050
|
-
const envArgs = [];
|
|
191051
|
-
for (const [k2, v] of Object.entries(params.env)) {
|
|
191052
|
-
if (v !== void 0) {
|
|
191053
|
-
envArgs.push(`${k2}=${v}`);
|
|
191054
|
-
}
|
|
191055
|
-
}
|
|
191056
|
-
const username = userInfo2().username;
|
|
191057
|
-
const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
|
|
191058
|
-
const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
|
|
191059
|
-
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
|
191060
|
-
return spawn3(
|
|
191061
|
-
"sudo",
|
|
191062
|
-
[
|
|
191063
|
-
"env",
|
|
191064
|
-
...envArgs,
|
|
191065
|
-
"unshare",
|
|
191066
|
-
"--pid",
|
|
191067
|
-
"--fork",
|
|
191068
|
-
"--mount-proc",
|
|
191069
|
-
"bash",
|
|
191070
|
-
"-c",
|
|
191071
|
-
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
|
191072
|
-
],
|
|
191073
|
-
{ ...spawnOpts, env: {} }
|
|
191074
|
-
);
|
|
191075
|
-
}
|
|
191076
|
-
return spawn3("bash", ["-c", params.command], spawnOpts);
|
|
191077
|
-
}
|
|
191078
|
-
async function killProcessGroup(proc) {
|
|
191079
|
-
if (!proc.pid) return;
|
|
191080
|
-
try {
|
|
191081
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
191082
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
191083
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
191084
|
-
} catch {
|
|
191085
|
-
try {
|
|
191086
|
-
proc.kill("SIGKILL");
|
|
191087
|
-
} catch {
|
|
191088
|
-
}
|
|
191089
|
-
}
|
|
191090
|
-
}
|
|
191091
|
-
function getTempDir() {
|
|
191092
|
-
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
191093
|
-
if (!tempDir) {
|
|
191094
|
-
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
191095
|
-
}
|
|
191096
|
-
return tempDir;
|
|
191097
|
-
}
|
|
191098
|
-
var MAX_OUTPUT_CHARS = 5e3;
|
|
191099
|
-
function capOutput(output) {
|
|
191100
|
-
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
191101
|
-
const fullPath = join20(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
191102
|
-
writeFileSync12(fullPath, output);
|
|
191103
|
-
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
191104
|
-
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
191105
|
-
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
191106
|
-
}
|
|
191107
|
-
function isGitCommand(command) {
|
|
191108
|
-
const trimmed = command.trim();
|
|
191109
|
-
if (trimmed === "git" || trimmed.startsWith("git ")) return true;
|
|
191110
|
-
if (trimmed.startsWith("sudo git")) return true;
|
|
191111
|
-
return /[;&|]\s*(?:sudo\s+)?git(?:\s|$)/.test(trimmed);
|
|
191112
|
-
}
|
|
191113
|
-
function ShellTool(ctx) {
|
|
191114
|
-
return tool({
|
|
191115
|
-
name: "shell",
|
|
191116
|
-
timeoutMs: 12e4,
|
|
191117
|
-
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
191118
|
-
|
|
191119
|
-
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
191120
|
-
|
|
191121
|
-
Use this tool to:
|
|
191122
|
-
- Run shell commands (ls, cat, grep, find, etc.)
|
|
191123
|
-
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
191124
|
-
- Run tests and linters
|
|
191125
|
-
|
|
191126
|
-
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.
|
|
191127
|
-
|
|
191128
|
-
Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
|
|
191129
|
-
parameters: ShellParams,
|
|
191130
|
-
execute: execute(async (params) => {
|
|
191131
|
-
if (isGitCommand(params.command)) {
|
|
191132
|
-
throw new Error(
|
|
191133
|
-
'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'
|
|
191134
|
-
);
|
|
191135
|
-
}
|
|
191136
|
-
const timeout = Math.min(params.timeout ?? 3e4, 12e4);
|
|
191137
|
-
const cwd = params.working_directory ?? process.cwd();
|
|
191138
|
-
const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
191139
|
-
if (params.command.includes("agent-browser")) {
|
|
191140
|
-
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
|
191141
|
-
if (daemonError) {
|
|
191142
|
-
return {
|
|
191143
|
-
output: `browser daemon unavailable: ${daemonError}`,
|
|
191144
|
-
exit_code: 1,
|
|
191145
|
-
timed_out: false
|
|
191146
|
-
};
|
|
191147
|
-
}
|
|
191148
|
-
const binDir = ctx.toolState.browserDaemon?.binDir;
|
|
191149
|
-
if (binDir) {
|
|
191150
|
-
env2.PATH = `${binDir}:${env2.PATH ?? ""}`;
|
|
191151
|
-
}
|
|
191152
|
-
}
|
|
191153
|
-
if (params.background) {
|
|
191154
|
-
const tempDir = getTempDir();
|
|
191155
|
-
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
191156
|
-
const outputPath = join20(tempDir, `${handle}.log`);
|
|
191157
|
-
const pidPath = join20(tempDir, `${handle}.pid`);
|
|
191158
|
-
const logFd = openSync(outputPath, "a");
|
|
191159
|
-
let proc2;
|
|
191160
|
-
try {
|
|
191161
|
-
proc2 = spawnShell({
|
|
191162
|
-
command: params.command,
|
|
191163
|
-
env: env2,
|
|
191164
|
-
cwd,
|
|
191165
|
-
stdio: ["ignore", logFd, logFd]
|
|
191166
|
-
});
|
|
191167
|
-
} finally {
|
|
191168
|
-
closeSync(logFd);
|
|
191169
|
-
}
|
|
191170
|
-
if (!proc2.pid) {
|
|
191171
|
-
throw new Error("failed to start background process");
|
|
191172
|
-
}
|
|
191173
|
-
proc2.unref();
|
|
191174
|
-
writeFileSync12(pidPath, `${proc2.pid}
|
|
191175
|
-
`);
|
|
191176
|
-
ctx.toolState.backgroundProcesses.set(handle, { pid: proc2.pid, outputPath, pidPath });
|
|
191177
|
-
return {
|
|
191178
|
-
handle,
|
|
191179
|
-
outputPath,
|
|
191180
|
-
pidPath,
|
|
191181
|
-
message: `started background process ${handle} (pid ${proc2.pid})`
|
|
191182
|
-
};
|
|
191183
|
-
}
|
|
191184
|
-
const proc = spawnShell({
|
|
191185
|
-
command: params.command,
|
|
191186
|
-
env: env2,
|
|
191187
|
-
cwd,
|
|
191188
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
191189
|
-
});
|
|
191190
|
-
let stdout = "", stderr = "", timedOut = false, exited = false;
|
|
191191
|
-
proc.stdout?.on("data", (chunk) => {
|
|
191192
|
-
stdout += chunk.toString();
|
|
191193
|
-
});
|
|
191194
|
-
proc.stderr?.on("data", (chunk) => {
|
|
191195
|
-
stderr += chunk.toString();
|
|
191196
|
-
});
|
|
191197
|
-
const timeoutId = setTimeout(async () => {
|
|
191198
|
-
if (!exited) {
|
|
191199
|
-
timedOut = true;
|
|
191200
|
-
await killProcessGroup(proc);
|
|
191201
|
-
}
|
|
191202
|
-
}, timeout);
|
|
191203
|
-
const exitCode = await new Promise((resolve3) => {
|
|
191204
|
-
const done = (code) => {
|
|
191205
|
-
exited = true;
|
|
191206
|
-
clearTimeout(timeoutId);
|
|
191207
|
-
resolve3(code);
|
|
191208
|
-
};
|
|
191209
|
-
proc.on("exit", done);
|
|
191210
|
-
proc.on("error", () => done(null));
|
|
191211
|
-
});
|
|
191212
|
-
proc.stdout?.destroy();
|
|
191213
|
-
proc.stderr?.destroy();
|
|
191214
|
-
let output = stderr ? stdout ? `${stdout}
|
|
191215
|
-
${stderr}` : stderr : stdout;
|
|
191216
|
-
if (timedOut)
|
|
191217
|
-
output = output ? `${output}
|
|
191218
|
-
[timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
|
|
191219
|
-
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
|
191220
|
-
const trimmed = output.trim();
|
|
191221
|
-
if (finalExitCode !== 0) {
|
|
191222
|
-
log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
|
|
191223
|
-
if (trimmed) log.info(`output: ${trimmed}`);
|
|
191224
|
-
}
|
|
191225
|
-
return {
|
|
191226
|
-
output: capOutput(trimmed),
|
|
191227
|
-
exit_code: finalExitCode,
|
|
191228
|
-
timed_out: timedOut
|
|
191229
|
-
};
|
|
191230
|
-
})
|
|
191231
|
-
});
|
|
191232
|
-
}
|
|
191233
|
-
var KillBackgroundParams = type({
|
|
191234
|
-
handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)")
|
|
191235
|
-
});
|
|
191236
|
-
function KillBackgroundTool(ctx) {
|
|
191237
|
-
return tool({
|
|
191238
|
-
name: "kill_background",
|
|
191239
|
-
mutates: true,
|
|
191240
|
-
description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
|
|
191241
|
-
parameters: KillBackgroundParams,
|
|
191242
|
-
execute: execute(async (params) => {
|
|
191243
|
-
const proc = ctx.toolState.backgroundProcesses.get(params.handle);
|
|
191244
|
-
if (!proc) {
|
|
191245
|
-
return {
|
|
191246
|
-
success: false,
|
|
191247
|
-
message: `no background process with handle ${params.handle}`
|
|
191248
|
-
};
|
|
191249
|
-
}
|
|
191250
|
-
try {
|
|
191251
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
191252
|
-
} catch {
|
|
191253
|
-
}
|
|
191254
|
-
await sleep3(200);
|
|
191255
|
-
try {
|
|
191256
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
191257
|
-
} catch {
|
|
191258
|
-
}
|
|
191259
|
-
ctx.toolState.backgroundProcesses.delete(params.handle);
|
|
191260
|
-
return {
|
|
191261
|
-
success: true,
|
|
191262
|
-
message: `killed background process ${params.handle} (pid ${proc.pid})`
|
|
191263
|
-
};
|
|
191264
|
-
})
|
|
191265
|
-
});
|
|
191266
|
-
}
|
|
191267
|
-
|
|
191268
191386
|
// mcp/similarIssues.ts
|
|
191269
191387
|
var SimilarIssues = type({
|
|
191270
191388
|
issue_number: type.number.describe("The issue number to find older duplicate candidates for")
|
|
@@ -191352,8 +191470,8 @@ function UploadFileTool(ctx) {
|
|
|
191352
191470
|
}
|
|
191353
191471
|
|
|
191354
191472
|
// mcp/xrepo.ts
|
|
191355
|
-
import { mkdirSync as
|
|
191356
|
-
import { join as
|
|
191473
|
+
import { mkdirSync as mkdirSync9, rmSync as rmSync2 } from "node:fs";
|
|
191474
|
+
import { join as join22 } from "node:path";
|
|
191357
191475
|
function assertValidRepoName(name) {
|
|
191358
191476
|
if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
|
|
191359
191477
|
throw new Error(
|
|
@@ -191433,8 +191551,8 @@ function CheckoutRepoTool(ctx) {
|
|
|
191433
191551
|
return { path: existing.dir, access: existing.access, note: "already checked out." };
|
|
191434
191552
|
}
|
|
191435
191553
|
const access = accessFor(ctx, repo);
|
|
191436
|
-
const dir =
|
|
191437
|
-
|
|
191554
|
+
const dir = join22(ctx.tmpdir, "xrepo", repo);
|
|
191555
|
+
mkdirSync9(dir, { recursive: true });
|
|
191438
191556
|
const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
|
|
191439
191557
|
const rc = resolveRepoCtx(ctx, repo);
|
|
191440
191558
|
try {
|
|
@@ -191512,8 +191630,8 @@ function buildCommonTools(ctx, outputSchema) {
|
|
|
191512
191630
|
EditCommentTool(ctx),
|
|
191513
191631
|
ReplyToReviewCommentTool(ctx),
|
|
191514
191632
|
IssueTool(ctx),
|
|
191515
|
-
|
|
191516
|
-
|
|
191633
|
+
CloseCurrentTool(ctx),
|
|
191634
|
+
ReopenCurrentTool(ctx),
|
|
191517
191635
|
IssueInfoTool(ctx),
|
|
191518
191636
|
GetIssueCommentsTool(ctx),
|
|
191519
191637
|
GetIssueEventsTool(ctx),
|
|
@@ -191545,6 +191663,9 @@ function buildCommonTools(ctx, outputSchema) {
|
|
|
191545
191663
|
tools.push(ShellTool(ctx));
|
|
191546
191664
|
tools.push(KillBackgroundTool(ctx));
|
|
191547
191665
|
}
|
|
191666
|
+
if (ctx.ghToken) {
|
|
191667
|
+
tools.push(GhTool(ctx));
|
|
191668
|
+
}
|
|
191548
191669
|
return tools;
|
|
191549
191670
|
}
|
|
191550
191671
|
function buildOrchestratorTools(ctx, outputSchema) {
|
|
@@ -191556,8 +191677,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
191556
191677
|
PushTagsTool(ctx),
|
|
191557
191678
|
DeleteBranchTool(ctx),
|
|
191558
191679
|
CreatePullRequestTool(ctx),
|
|
191559
|
-
UpdatePullRequestBodyTool(ctx)
|
|
191560
|
-
ClosePullRequestTool(ctx)
|
|
191680
|
+
UpdatePullRequestBodyTool(ctx)
|
|
191561
191681
|
];
|
|
191562
191682
|
if (ctx.signedCommits) {
|
|
191563
191683
|
tools.push(CommitChangesTool(ctx));
|
|
@@ -191647,6 +191767,10 @@ async function startMcpHttpServer(ctx, options) {
|
|
|
191647
191767
|
let disposed = false;
|
|
191648
191768
|
return {
|
|
191649
191769
|
url: startResult.url,
|
|
191770
|
+
// the names actually registered, handed back so the prompt can be checked
|
|
191771
|
+
// against them rather than against a second derivation. see
|
|
191772
|
+
// assertPromptToolRefs.
|
|
191773
|
+
toolNames: tools.map((registered) => registered.name),
|
|
191650
191774
|
[Symbol.asyncDispose]: async () => {
|
|
191651
191775
|
if (disposed) return;
|
|
191652
191776
|
disposed = true;
|
|
@@ -192334,7 +192458,7 @@ async function checkConfiguredCredentials(params) {
|
|
|
192334
192458
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
192335
192459
|
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
192336
192460
|
import { createServer as createServer3 } from "node:http";
|
|
192337
|
-
import { join as
|
|
192461
|
+
import { join as join23 } from "node:path";
|
|
192338
192462
|
var REVOKED_TRAP_MS = 6e4;
|
|
192339
192463
|
function revokeGitHubToken(token) {
|
|
192340
192464
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -192403,7 +192527,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
192403
192527
|
function writeAskpassScript(code) {
|
|
192404
192528
|
const scriptId = randomUUID5();
|
|
192405
192529
|
const scriptName = `askpass-${scriptId}.js`;
|
|
192406
|
-
const scriptPath =
|
|
192530
|
+
const scriptPath = join23(tmpdir3, scriptName);
|
|
192407
192531
|
const content = [
|
|
192408
192532
|
`#!/usr/bin/env node`,
|
|
192409
192533
|
`var a=process.argv[2]||"";`,
|
|
@@ -192855,7 +192979,7 @@ function resolveInstructions(ctx) {
|
|
|
192855
192979
|
|
|
192856
192980
|
// utils/learnings.ts
|
|
192857
192981
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
192858
|
-
import { dirname as dirname4, join as
|
|
192982
|
+
import { dirname as dirname4, join as join24 } from "node:path";
|
|
192859
192983
|
|
|
192860
192984
|
// utils/learningsTruncate.ts
|
|
192861
192985
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -192873,10 +192997,10 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
192873
192997
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
192874
192998
|
var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
|
|
192875
192999
|
function learningsFilePath(tmpdir3) {
|
|
192876
|
-
return
|
|
193000
|
+
return join24(tmpdir3, LEARNINGS_FILE_NAME);
|
|
192877
193001
|
}
|
|
192878
193002
|
function xrepoLearningsFilePath(tmpdir3) {
|
|
192879
|
-
return
|
|
193003
|
+
return join24(tmpdir3, XREPO_LEARNINGS_FILE_NAME);
|
|
192880
193004
|
}
|
|
192881
193005
|
async function seedLearningsFile(params) {
|
|
192882
193006
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -193420,6 +193544,9 @@ async function resolveTokens(params) {
|
|
|
193420
193544
|
mcpToken: externalToken,
|
|
193421
193545
|
// external token is whatever scope the user granted — reuse for reads too.
|
|
193422
193546
|
readToken: params.xrepo ? externalToken : void 0,
|
|
193547
|
+
// the scope is the user's to choose here, but the role threshold still
|
|
193548
|
+
// decides whether the agent gets a CLI credential at all.
|
|
193549
|
+
ghToken: mirrorRolePermissions(params.authorPermission) ? externalToken : void 0,
|
|
193423
193550
|
async [Symbol.asyncDispose]() {
|
|
193424
193551
|
mcpTokenValue = void 0;
|
|
193425
193552
|
}
|
|
@@ -193451,6 +193578,15 @@ async function resolveTokens(params) {
|
|
|
193451
193578
|
log.info(
|
|
193452
193579
|
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
193453
193580
|
);
|
|
193581
|
+
const ghPermissions = mirrorRolePermissions(params.authorPermission);
|
|
193582
|
+
let ghToken;
|
|
193583
|
+
if (ghPermissions) {
|
|
193584
|
+
ghToken = await acquireNewToken({ permissions: ghPermissions });
|
|
193585
|
+
if (isGitHubActions) core7.setSecret(ghToken);
|
|
193586
|
+
log.info(
|
|
193587
|
+
`\xBB acquired gh token mirroring ${params.authorPermission} (${formatPermissions(ghPermissions)})`
|
|
193588
|
+
);
|
|
193589
|
+
}
|
|
193454
193590
|
let readToken;
|
|
193455
193591
|
if (params.xrepo) {
|
|
193456
193592
|
readToken = await acquireNewToken({
|
|
@@ -193540,7 +193676,9 @@ async function resolveTokens(params) {
|
|
|
193540
193676
|
await Promise.all([
|
|
193541
193677
|
revokeGitHubInstallationToken(currentGitToken),
|
|
193542
193678
|
revokeGitHubInstallationToken(currentMcpToken),
|
|
193543
|
-
...currentReadToken ? [revokeGitHubInstallationToken(currentReadToken)] : []
|
|
193679
|
+
...currentReadToken ? [revokeGitHubInstallationToken(currentReadToken)] : [],
|
|
193680
|
+
// revoking this one is what bounds a `gh` leak to the run's lifetime
|
|
193681
|
+
...ghToken ? [revokeGitHubInstallationToken(ghToken)] : []
|
|
193544
193682
|
]);
|
|
193545
193683
|
} finally {
|
|
193546
193684
|
removeSignalHandler();
|
|
@@ -193555,6 +193693,7 @@ async function resolveTokens(params) {
|
|
|
193555
193693
|
},
|
|
193556
193694
|
mcpToken,
|
|
193557
193695
|
readToken,
|
|
193696
|
+
ghToken,
|
|
193558
193697
|
refreshGitToken,
|
|
193559
193698
|
[Symbol.asyncDispose]: dispose
|
|
193560
193699
|
};
|
|
@@ -193796,7 +193935,7 @@ async function runProxyResolution(ctx) {
|
|
|
193796
193935
|
|
|
193797
193936
|
// utils/prSummary.ts
|
|
193798
193937
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
193799
|
-
import { dirname as dirname5, join as
|
|
193938
|
+
import { dirname as dirname5, join as join25 } from "node:path";
|
|
193800
193939
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
193801
193940
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
193802
193941
|
|
|
@@ -193806,7 +193945,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
193806
193945
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
193807
193946
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
193808
193947
|
function summaryFilePath(tmpdir3) {
|
|
193809
|
-
return
|
|
193948
|
+
return join25(tmpdir3, SUMMARY_FILE_NAME);
|
|
193810
193949
|
}
|
|
193811
193950
|
async function seedSummaryFile(params) {
|
|
193812
193951
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -194028,7 +194167,10 @@ function warnIfPinnedToSha() {
|
|
|
194028
194167
|
}
|
|
194029
194168
|
}
|
|
194030
194169
|
async function resolveRunContextData(params) {
|
|
194031
|
-
|
|
194170
|
+
const actionRef = process.env.GITHUB_ACTION_REF;
|
|
194171
|
+
log.info(
|
|
194172
|
+
`\xBB running Pullfrog v${package_default.version}${actionRef ? ` (ref: ${actionRef})` : ""}...`
|
|
194173
|
+
);
|
|
194032
194174
|
warnIfPinnedToSha();
|
|
194033
194175
|
const repoContext = parseRepoContext();
|
|
194034
194176
|
let oidcToken;
|
|
@@ -195118,6 +195260,7 @@ async function main() {
|
|
|
195118
195260
|
try {
|
|
195119
195261
|
const _commentTokenRef = __using(_stack, await resolveTokens({
|
|
195120
195262
|
push: "disabled",
|
|
195263
|
+
authorPermission: void 0,
|
|
195121
195264
|
oidc: oidcCredentials
|
|
195122
195265
|
}), true);
|
|
195123
195266
|
const errorMessage = runContext.commercialRefused === "subscription_unpaid" ? "Pro renewal failed for this organization" : "Pro plan required for this organization";
|
|
@@ -195136,6 +195279,8 @@ async function main() {
|
|
|
195136
195279
|
}
|
|
195137
195280
|
}
|
|
195138
195281
|
const tmpdir3 = createTempDirectory();
|
|
195282
|
+
const repoDir = payload.cwd;
|
|
195283
|
+
const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
|
|
195139
195284
|
const opencodeCliPath = await agents.opencode.install();
|
|
195140
195285
|
captureBaselineModels(opencodeCliPath);
|
|
195141
195286
|
if (runContext.dbSecrets) {
|
|
@@ -195150,6 +195295,13 @@ async function main() {
|
|
|
195150
195295
|
}
|
|
195151
195296
|
installCodexAuth();
|
|
195152
195297
|
captureAuthorizedModels(opencodeCliPath);
|
|
195298
|
+
if (preIntrospectionDirty && repoDir) {
|
|
195299
|
+
await restoreDirtiedSince({
|
|
195300
|
+
before: preIntrospectionDirty,
|
|
195301
|
+
actor: "model introspection",
|
|
195302
|
+
cwd: repoDir
|
|
195303
|
+
});
|
|
195304
|
+
}
|
|
195153
195305
|
if (runContext.repoSettings.envAllowlist) {
|
|
195154
195306
|
setEnvAllowlist(runContext.repoSettings.envAllowlist);
|
|
195155
195307
|
}
|
|
@@ -195161,6 +195313,7 @@ async function main() {
|
|
|
195161
195313
|
}
|
|
195162
195314
|
const tokenRef = __using(_stack3, await resolveTokens({
|
|
195163
195315
|
push: payload.push,
|
|
195316
|
+
authorPermission: payload.event.authorPermission,
|
|
195164
195317
|
xrepo: payload.xrepo,
|
|
195165
195318
|
oidc: oidcCredentials
|
|
195166
195319
|
}), true);
|
|
@@ -195327,6 +195480,7 @@ async function main() {
|
|
|
195327
195480
|
},
|
|
195328
195481
|
refreshGitToken: tokenRef.refreshGitToken,
|
|
195329
195482
|
readToken: tokenRef.readToken,
|
|
195483
|
+
ghToken: tokenRef.ghToken,
|
|
195330
195484
|
xrepo: payload.xrepo,
|
|
195331
195485
|
apiToken: runContext.apiToken,
|
|
195332
195486
|
modes: modes2,
|
|
@@ -195418,6 +195572,16 @@ async function main() {
|
|
|
195418
195572
|
xrepoLearningsFilePath: toolState.xrepoLearningsFilePath ?? null,
|
|
195419
195573
|
xrepoLearningsHeadings: runContext.repoSettings.xrepoLearningsHeadings
|
|
195420
195574
|
});
|
|
195575
|
+
const danglingToolRefs = findDanglingPromptToolRefs({
|
|
195576
|
+
agentId,
|
|
195577
|
+
prompt: instructions.full,
|
|
195578
|
+
toolNames: mcpHttpServer.toolNames
|
|
195579
|
+
});
|
|
195580
|
+
if (danglingToolRefs.length > 0) {
|
|
195581
|
+
log.warning(
|
|
195582
|
+
`\xBB prompt advertises ${danglingToolRefs.length} tool(s) the MCP server did not register: ${danglingToolRefs.join(", ")}`
|
|
195583
|
+
);
|
|
195584
|
+
}
|
|
195421
195585
|
const logParts = [
|
|
195422
195586
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
195423
195587
|
${instructions.eventInstructions}` : null,
|
|
@@ -195432,7 +195596,7 @@ ${instructions.user}` : null,
|
|
|
195432
195596
|
log.info(instructions.full);
|
|
195433
195597
|
});
|
|
195434
195598
|
if (agentId === "opencode") {
|
|
195435
|
-
const pluginDir =
|
|
195599
|
+
const pluginDir = join26(process.cwd(), ".opencode", "plugin");
|
|
195436
195600
|
const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
195437
195601
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
195438
195602
|
log.info(
|