6d25adfd1a
* agent & model refactor with ASKPASS git auth, UI restructure, clerk v7 Made-with: Cursor * fix stale agent/effort refs, add tests for askpass + model resolution - reviewCleanup.ts: payload.agent -> payload.model, remove effort - selectMode.ts PlanEdit: remove delegation/subagent/effort references - pullfrog.yml.ts: update env vars (drop GOOGLE_API_KEY/CURSOR_API_KEY, add GOOGLE_GENERATIVE_AI_API_KEY/XAI_API_KEY/MOONSHOT_API_KEY/OPENCODE_API_KEY) - FlagsSettings/RepoInstructionsSection: remove stale effort/timeout copy - new: gitAuthServer.test.ts (10 tests — lifecycle, token delivery, tamper detection, script gen) - new: agent.test.ts (4 tests — default opentoad, AGENT_OVERRIDE, invalid override) - new: models.test.ts (19 tests — parseModel, resolution, registry invariants) - update models.dev snapshot Made-with: Cursor * fix changed-agents.sh to filter legacy agent files from CI matrix legacy agent files (claude.ts, codex.ts, etc.) are @ts-nocheck and not exported from index.ts. changed-agents.sh now reads index.ts imports to build the active agent set and treats changes to inactive files as non-agent changes (opentoad canary only). Made-with: Cursor * remove MCP file tools, old agent harnesses, and obsolete security tests ASKPASS-based git auth makes the old MCP file tool security layer unnecessary: - token never in subprocess env, so symlink/gitattributes/hook attacks can't exfiltrate it - agents now use native file tools (OpenCode builtin read/edit) deleted: - action/mcp/file.ts (file_read, file_write, file_edit, file_delete, list_directory) - action/mcp/index.ts (dead re-export) - agent harnesses: claude.ts, codex.ts, cursor.ts, gemini.ts, opencode.ts - opencode-runner.ts (inlined into opentoad.ts) - security tests that validated MCP file tool restrictions - commented-out three-step review flow (~300 lines) - sanitizeSchema/wrapSchema dead code from mcp/shared.ts - OPENCODE_MODEL_MINI/MAX env vars (effort-level model overrides removed) updated test prompts to use generic file ops instead of MCP tool names. restored pkg-json-scripts + requirements-txt-attack (test --ignore-scripts defense). Made-with: Cursor * bump actions/checkout v4 → v6 (node 24) node 20 actions deprecated june 2, 2026. Made-with: Cursor * temporarily disable fail-fast on agnostic tests to debug checkout@v6 Made-with: Cursor * re-enable fail-fast on agnostic tests Made-with: Cursor * fix test token mismatch: mint OIDC tokens scoped to target repo CI tests override GITHUB_REPOSITORY to pullfrog/test-repo but inherit the runner's GITHUB_TOKEN (scoped to pullfrog/app), causing 401s on every run-context fetch. Clear GITHUB_TOKEN in the test subprocess so ensureGitHubToken() mints a properly scoped token via OIDC. Also centralizes the default GITHUB_REPOSITORY in runAgentStreaming instead of repeating it in every test file, and fixes preview-cleanup to remove workers from all queues (not just name-matching ones). Made-with: Cursor * fix ensureGitHubToken to try OIDC when app credentials are absent ensureGitHubToken only attempted token minting when GITHUB_APP_ID and GITHUB_PRIVATE_KEY were set. In CI, OIDC is available but app creds aren't exposed — so the guard prevented minting entirely. Made-with: Cursor * dead code cleanup: remove remnants of deleted agents, file tools, effort system remove unused @anthropic-ai/claude-agent-sdk and @openai/codex-sdk deps, orphaned file-tool security tests, dead GEMINI_MODEL passthrough, stale opencode-runner wiki refs, deleted test file references, and MCP file tool docs. rename docs/effort → docs/models. fix vitest setup: move dotenv to globalSetup (runs once before forks instead of per-file, 19s → 200ms). Made-with: Cursor * address review feedback: remove dead code, update stale references - remove AGENT_OVERRIDE (only opentoad exists) - remove shellToolName plumbing (always restricted shell) - bump action version to 0.0.179 - remove CURSOR_API_KEY from all workflows/configs - remove OPENCODE_MODEL_MINI/MAX from workflows/docs - delete wiki/effort.md, rewrite docs/effort.mdx as "Models" - rewrite wiki/modes.md: orchestrator/subagent → single agent - simplify flag system: drop builtin flag extraction (debug, effort, timeout, agent), keep custom flag replacement only - reserve all legacy flag names to prevent custom flag conflicts Made-with: Cursor * regenerate lockfile after removing claude-agent-sdk and codex-sdk Made-with: Cursor * fix import ordering, add lockfile check to pre-push hook Made-with: Cursor * remove dead debug payload field, stale packageExtensions Made-with: Cursor * merge proc-sandbox and token-exfil into a single test proc-sandbox and token-exfil were duplicative — both tested that SANDBOX_TEST_TOKEN couldn't be exfiltrated. consolidated into token-exfil with shell:restricted (which actually exercises filterEnv) and the /proc attack vector hints from proc-sandbox. Made-with: Cursor * fix wiki adversarial.md to match actual tokenExfil validator Made-with: Cursor
180 lines
5.9 KiB
TypeScript
180 lines
5.9 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import { mkdtemp } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import arg from "arg";
|
|
import { config } from "dotenv";
|
|
import type { AgentResult } from "./agents/shared.ts";
|
|
import { type Inputs, main } from "./main.ts";
|
|
import { defineFixture } from "./test/utils.ts";
|
|
import { log } from "./utils/cli.ts";
|
|
import { runInDocker } from "./utils/docker.ts";
|
|
import { ensureGitHubToken } from "./utils/github.ts";
|
|
import { isInsideDocker } from "./utils/globals.ts";
|
|
import { runPostCleanup } from "./utils/postCleanup.ts";
|
|
import { setupTestRepo } from "./utils/setup.ts";
|
|
|
|
/**
|
|
* default play fixture for ad-hoc testing.
|
|
* change this freely without affecting any tests.
|
|
*/
|
|
export const playFixture = defineFixture(
|
|
{
|
|
prompt: `List every MCP tool you have access to. Call set_output with a JSON array of all tool names you can see.`,
|
|
},
|
|
{ localOnly: true }
|
|
);
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// load action's .env file in case it exists for local dev
|
|
config();
|
|
// also load .env from repo root (for monorepo structure)
|
|
config({ path: join(__dirname, "..", ".env") });
|
|
|
|
export async function run(inputsOrPrompt: Inputs | string): Promise<AgentResult> {
|
|
await ensureGitHubToken();
|
|
|
|
// create unique temp directory path in OS temp location for parallel execution
|
|
// use a parent dir from mkdtemp, then clone into a 'repo' subdirectory
|
|
const tempParent = await mkdtemp(join(tmpdir(), "pullfrog-play-"));
|
|
const tempDir = join(tempParent, "repo");
|
|
const originalCwd = process.cwd();
|
|
|
|
try {
|
|
setupTestRepo({ tempDir });
|
|
process.chdir(tempDir);
|
|
|
|
// run repo setup commands if provided (for pre-planting test state like symlinks).
|
|
// this runs AFTER clone but BEFORE the agent, simulating pre-existing repo content.
|
|
if (process.env.PULLFROG_TEST_REPO_SETUP) {
|
|
log.info("» running repo setup commands...");
|
|
execSync(process.env.PULLFROG_TEST_REPO_SETUP, { cwd: tempDir, stdio: "pipe" });
|
|
}
|
|
|
|
// set GITHUB_WORKSPACE to tempDir so main() doesn't try to chdir to the CI checkout path
|
|
process.env.GITHUB_WORKSPACE = tempDir;
|
|
|
|
// allow passing full Inputs object or just a prompt string
|
|
const inputs: Inputs =
|
|
typeof inputsOrPrompt === "string" ? { prompt: inputsOrPrompt } : inputsOrPrompt;
|
|
|
|
// set INPUT_* env vars for @actions/core.getInput()
|
|
for (const [key, value] of Object.entries(inputs)) {
|
|
if (value !== undefined && value !== null) {
|
|
process.env[`INPUT_${key.toUpperCase()}`] = String(value);
|
|
}
|
|
}
|
|
|
|
// wrap main() so post cleanup runs even on failure (mirrors action.yml post-if: "failure() || cancelled()")
|
|
let result: AgentResult;
|
|
try {
|
|
result = await main();
|
|
} finally {
|
|
await runPostCleanup();
|
|
}
|
|
|
|
process.chdir(originalCwd);
|
|
|
|
if (result.success) {
|
|
log.success("Action completed successfully");
|
|
return { success: true, output: result.output || undefined, error: undefined };
|
|
} else {
|
|
log.error(`Action failed: ${result.error || "Unknown error"}`);
|
|
return { success: false, error: result.error || undefined, output: undefined };
|
|
}
|
|
} catch (err) {
|
|
const errorMessage = (err as Error).message;
|
|
log.error(`Error: ${errorMessage}`);
|
|
return { success: false, error: errorMessage, output: undefined };
|
|
} finally {
|
|
// cleanup temp directory - use sudo rm because sandbox isolation may create
|
|
// files with different ownership that rmSync can't delete
|
|
process.chdir(originalCwd);
|
|
try {
|
|
execSync(`sudo rm -rf "${tempParent}"`, { stdio: "ignore" });
|
|
} catch {
|
|
// ignore - cleanup failure is not critical
|
|
}
|
|
}
|
|
}
|
|
|
|
const isDirectExecution = process.argv[1]
|
|
? import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
|
: false;
|
|
|
|
if (isDirectExecution) {
|
|
const args = arg({
|
|
"--help": Boolean,
|
|
"--raw": String,
|
|
"--local": Boolean,
|
|
"-h": "--help",
|
|
"-l": "--local",
|
|
});
|
|
|
|
if (args["--help"]) {
|
|
log.info(`
|
|
Usage: node play.ts [options]
|
|
|
|
Test the Pullfrog action with the inline playFixture.
|
|
|
|
Options:
|
|
--raw [input] Use raw string as prompt, or JSON object as full fixture
|
|
--local, -l Run locally (default: runs in Docker)
|
|
-h, --help Show this help message
|
|
|
|
Environment:
|
|
PLAY_LOCAL=1 Same as --local
|
|
|
|
Examples:
|
|
node play.ts # Run inline playFixture
|
|
node play.ts --raw "Hello world" # Use raw string as prompt
|
|
node play.ts --raw '{"prompt":"Hello","timeout":"5s"}' # Use JSON fixture
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// default: run in Docker (unless --local, PLAY_LOCAL=1, or already inside Docker)
|
|
const useLocal = args["--local"] || process.env.PLAY_LOCAL === "1" || isInsideDocker;
|
|
|
|
if (!useLocal) {
|
|
const passArgs = process.argv
|
|
.slice(2)
|
|
.map((a) => `'${a.replace(/'/g, "'\\''")}'`)
|
|
.join(" ");
|
|
const nodeCmd = `node play.ts ${passArgs}`;
|
|
|
|
const volumeName = "pullfrog-action-node-modules";
|
|
|
|
const result = runInDocker({
|
|
actionDir: __dirname,
|
|
args: process.argv.slice(2),
|
|
nodeCmd,
|
|
volumeName,
|
|
envFilterMode: "passthrough",
|
|
onStart: () => log.info("» running in Docker container..."),
|
|
});
|
|
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
if (args["--raw"]) {
|
|
const raw = args["--raw"];
|
|
// try to parse as JSON, otherwise treat as prompt string
|
|
let input: Inputs | string = raw;
|
|
try {
|
|
input = JSON.parse(raw) as Inputs;
|
|
} catch {
|
|
// not valid JSON, use as prompt string
|
|
}
|
|
const result = await run(input);
|
|
process.exit(result.success ? 0 : 1);
|
|
}
|
|
|
|
// no args - use inline playFixture
|
|
const result = await run(playFixture);
|
|
process.exit(result.success ? 0 : 1);
|
|
}
|