Restrict github token (#140)

This commit is contained in:
Colin McDonnell
2026-01-21 02:17:46 +00:00
committed by pullfrog[bot]
parent 04cc24bf64
commit 01ee59a96c
6 changed files with 48 additions and 18 deletions
+13 -7
View File
@@ -119192,9 +119192,6 @@ function filterEnv(isPublicRepo) {
if (isPublicRepo && isSensitive(key)) continue;
filtered[key] = value2;
}
if (process.env.ORIGINAL_GITHUB_TOKEN) {
filtered.GITHUB_TOKEN = process.env.ORIGINAL_GITHUB_TOKEN;
}
return filtered;
}
function spawnSandboxed(params) {
@@ -120772,10 +120769,11 @@ var PushBranch = type({
branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
force: type.boolean.describe("Force push (use with caution)").default(false)
});
function PushBranchTool(_ctx) {
function PushBranchTool(ctx) {
const defaultBranch = ctx.repo.repo.default_branch || "main";
return tool({
name: "push_branch",
description: "Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
description: "Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested. Pushes to the default branch are blocked.",
parameters: PushBranch,
execute: execute(async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
@@ -120790,6 +120788,11 @@ function PushBranchTool(_ctx) {
remoteBranch = mergeRef.replace("refs/heads/", "");
} catch {
}
if (remoteBranch === defaultBranch) {
throw new Error(
`Push blocked: cannot push directly to default branch '${remoteBranch}'. Create a feature branch and open a PR instead.`
);
}
const refspec = branch === remoteBranch ? branch : `${branch}:${remoteBranch}`;
const args3 = force ? ["push", "--force", "-u", remote, refspec] : ["push", "-u", remote, refspec];
log.debug(`pushing ${branch} to ${remote}/${remoteBranch}`);
@@ -140600,14 +140603,15 @@ async function setupGit(params) {
} catch {
log.debug("\xBB no existing authentication headers to remove");
}
const originToken = params.bashPermission === "enabled" ? params.token : params.originalToken || params.token;
if (params.event.is_pr !== true || !params.event.issue_number) {
const originUrl2 = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
const originUrl2 = `https://x-access-token:${originToken}@github.com/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl2], { cwd: repoDir });
log.info("\xBB updated origin URL with authentication token");
return;
}
const prNumber = params.event.issue_number;
const originUrl = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
const originUrl = `https://x-access-token:${originToken}@github.com/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
const prContext = await checkoutPrBranch({
octokit: params.octokit,
@@ -140693,6 +140697,8 @@ async function main() {
});
await setupGit({
token: tokenRef.token,
originalToken: process.env.ORIGINAL_GITHUB_TOKEN,
bashPermission: payload.bash,
owner: repo.owner,
name: repo.name,
event: payload.event,
+2
View File
@@ -62,6 +62,8 @@ export async function main(): Promise<MainResult> {
await setupGit({
token: tokenRef.token,
originalToken: process.env.ORIGINAL_GITHUB_TOKEN,
bashPermission: payload.bash,
owner: repo.owner,
name: repo.name,
event: payload.event,
+3 -5
View File
@@ -30,11 +30,9 @@ function filterEnv(isPublicRepo: boolean): Record<string, string> {
if (isPublicRepo && isSensitive(key)) continue;
filtered[key] = value;
}
// restore original GITHUB_TOKEN (the one set by GitHub Actions, not our installation token)
// this allows git operations in subprocesses to work while keeping our installation token secure
if (process.env.ORIGINAL_GITHUB_TOKEN) {
filtered.GITHUB_TOKEN = process.env.ORIGINAL_GITHUB_TOKEN;
}
// never restore GITHUB_TOKEN - agents must use MCP tools for git/gh operations
// this ensures all git operations go through auditable MCP tools where we can
// enforce branch protection, scan for secrets, etc.
return filtered;
}
+13 -3
View File
@@ -1,8 +1,8 @@
import { type } from "arktype";
import type { ToolContext } from "./server.ts";
import { log } from "../utils/cli.ts";
import { containsSecrets } from "../utils/secrets.ts";
import { $ } from "../utils/shell.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export function CreateBranchTool(ctx: ToolContext) {
@@ -141,11 +141,13 @@ export const PushBranch = type({
force: type.boolean.describe("Force push (use with caution)").default(false),
});
export function PushBranchTool(_ctx: ToolContext) {
export function PushBranchTool(ctx: ToolContext) {
const defaultBranch = ctx.repo.repo.default_branch || "main";
return tool({
name: "push_branch",
description:
"Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
"Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested. Pushes to the default branch are blocked.",
parameters: PushBranch,
execute: execute(async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
@@ -168,6 +170,14 @@ export function PushBranchTool(_ctx: ToolContext) {
// no configured merge ref, use local branch name
}
// block pushes to default branch
if (remoteBranch === defaultBranch) {
throw new Error(
`Push blocked: cannot push directly to default branch '${remoteBranch}'. ` +
`Create a feature branch and open a PR instead.`
);
}
// use refspec when local and remote branch names differ
const refspec = branch === remoteBranch ? branch : `${branch}:${remoteBranch}`;
const args = force
+3
View File
@@ -8,6 +8,9 @@ import { execute, tool } from "./shared.ts";
// fragment for nested replyTo (5 levels deep covers most threads)
// because in_reply_to_id generally points to the top-level comment
// this doesn't actually work as expected
// TOD: implement an API endpoint with aggressive caching that fetches the PR's reviewThreads via graphql
// separately fetch all the comments associated with the particular review
// merge them, then construct the full thread context
const REPLY_TO_FRAGMENT = `
replyTo {
databaseId
+14 -3
View File
@@ -2,7 +2,7 @@ import { execSync } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { PayloadEvent } from "../external.ts";
import type { BashPermission, PayloadEvent } from "../external.ts";
import { checkoutPrBranch } from "../mcp/checkout.ts";
import type { ToolState } from "../mcp/server.ts";
import { log } from "./cli.ts";
@@ -44,6 +44,8 @@ export function setupTestRepo(options: SetupOptions): void {
interface SetupGitParams {
token: string;
originalToken: string | undefined;
bashPermission: BashPermission;
owner: string;
name: string;
event: PayloadEvent;
@@ -127,9 +129,18 @@ export async function setupGit(params: SetupGitParams): Promise<void> {
log.debug("» no existing authentication headers to remove");
}
// choose token for origin based on bash permission:
// - enabled: installation token (full access)
// - restricted/disabled: workflow token (limited by permissions block)
// this protects the base repo while allowing fork PR edits via fork remote
const originToken =
params.bashPermission === "enabled"
? params.token
: (params.originalToken || params.token);
// non-PR events: set up origin with token, stay on default branch
if (params.event.is_pr !== true || !params.event.issue_number) {
const originUrl = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
const originUrl = `https://x-access-token:${originToken}@github.com/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
log.info("» updated origin URL with authentication token");
return;
@@ -139,7 +150,7 @@ export async function setupGit(params: SetupGitParams): Promise<void> {
const prNumber = params.event.issue_number;
// ensure origin is configured with auth token before checkout
const originUrl = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
const originUrl = `https://x-access-token:${originToken}@github.com/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
// use shared checkout helper (handles fork remotes, push config, etc.)