Compare commits

...

3 Commits

Author SHA1 Message Date
Colin McDonnell 7d85e653ca bump action version to 0.0.201
Made-with: Cursor
2026-04-15 20:26:44 +00:00
Colin McDonnell 4b3c5ca905 rename agent key to opencode and add skill invocation coverage (#541)
* rename agent key to opencode and add skill invocation coverage.

add skill-invoke tests for claude and opencode with local play-based validation signals, update CI matrices, and include the current tracked refactors in this branch for review.

Made-with: Cursor

* exclude agent-specific skill-invoke tests from wrong agent in CI matrix

* address review follow-up and preserve workflow run UI tweak.

switch changed-agents ci coverage to exercise the opentoad implementation path while keeping the opencode expectation, and include the local workflow run client interaction updates requested on this branch.

Made-with: Cursor

* remove opentoad agent filename from runtime.

rename the opencode harness implementation file from opentoad.ts to opencode.ts and update ci coverage input accordingly so action code no longer carries the old filename.

Made-with: Cursor

* ensure security prompt bypass is set on every test fixture.

this keeps adversarial and permissions harnesses from being blocked by the default prompt-injection refusal path during CI security tests.

Made-with: Cursor

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
2026-04-15 19:38:36 +00:00
Colin McDonnell 2799cce4bf homepage redesign + docs cleanup + agent prompting (#540)
* homepage redesign + docs cleanup + agent prompting improvements

- rewrote hero section: new tagline, responsive font sizing with clamp(),
  extracted shared constants for copy management
- added feature screenshots (shell isolation, github permissions, mcp tools,
  agent browser) and ensured consistent image sizing
- reworked feature section mobile layout: caption-style descriptions, bigger
  h3s, image padding
- made CTA buttons visible on all breakpoints (stacked on mobile, row on md+)
- reorganized docs/tools.mdx into single table with category dividers,
  simplified tool descriptions
- added markdown image syntax instruction to agent system prompt
- fixed InfoPopover overflow on small screens
- misc: InlineCode proportional sizing, OnboardingCard updates, shell/security
  doc improvements

Made-with: Cursor

* update pnpm-lock.yaml for agent-browser 0.25.4

Made-with: Cursor
2026-04-15 00:29:32 +00:00
35 changed files with 191 additions and 219308 deletions
+15 -2
View File
@@ -27,9 +27,22 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
agent: [claude, opentoad] agent: [claude, opencode]
test: test:
[mcpmerge, nobash, restricted, smoke, token-exfil] [
mcpmerge,
nobash,
restricted,
skill-invoke-claude,
skill-invoke-opencode,
smoke,
token-exfil,
]
exclude:
- agent: claude
test: skill-invoke-opencode
- agent: opencode
test: skill-invoke-claude
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* Claude Code agent — secure harness around the `claude` CLI. * Claude Code agent — secure harness around the `claude` CLI.
* *
* mirrors the opentoad harness's security model: * mirrors the opencode harness's security model:
* - native Bash blocked via --disallowedTools (agent cannot shell out) * - native Bash blocked via --disallowedTools (agent cannot shell out)
* - managed-settings.json: filesystem sandbox — deny /proc, /sys reads * - managed-settings.json: filesystem sandbox — deny /proc, /sys reads
* - MCP ShellTool provides restricted shell (filtered env, no secrets) * - MCP ShellTool provides restricted shell (filtered env, no secrets)
+2 -2
View File
@@ -1,7 +1,7 @@
import { claude } from "./claude.ts"; import { claude } from "./claude.ts";
import { opentoad } from "./opentoad.ts"; import { opencode } from "./opencode.ts";
import type { Agent } from "./shared.ts"; import type { Agent } from "./shared.ts";
export type { Agent, AgentUsage } from "./shared.ts"; export type { Agent, AgentUsage } from "./shared.ts";
export const agents = { claude, opentoad } satisfies Record<string, Agent>; export const agents = { claude, opencode } satisfies Record<string, Agent>;
+4 -5
View File
@@ -1,10 +1,9 @@
/** /**
* OpenToad agent secure harness around OpenCode CLI. * OpenCode agent secure harness around OpenCode CLI.
* *
* transparently wraps OpenCode with a security layer: * transparently wraps OpenCode with a security layer:
* - bash: "deny" via OPENCODE_CONFIG_CONTENT (agent cannot shell out) * - bash: "deny" via OPENCODE_CONFIG_CONTENT (agent cannot shell out)
* - OPENCODE_PERMISSION: filesystem sandbox deny all external paths except /tmp * - OPENCODE_PERMISSION: filesystem sandbox deny all external paths except /tmp
* - untrusted .opencode/plugins/ and .opencode/tools/ deleted before launch
* - MCP ShellTool provides restricted shell (filtered env, no secrets) * - MCP ShellTool provides restricted shell (filtered env, no secrets)
* - MCP server injected alongside project config (not replacing) * - MCP server injected alongside project config (not replacing)
* - ASKPASS handles git auth separately (token never in subprocess env) * - ASKPASS handles git auth separately (token never in subprocess env)
@@ -607,8 +606,8 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
// ── agent ─────────────────────────────────────────────────────────────────────── // ── agent ───────────────────────────────────────────────────────────────────────
export const opentoad = agent({ export const opencode = agent({
name: "opentoad", name: "opencode",
install: installOpencodeCli, install: installOpencodeCli,
run: async (ctx) => { run: async (ctx) => {
const cliPath = await installOpencodeCli(); const cliPath = await installOpencodeCli();
@@ -676,7 +675,7 @@ export const opentoad = agent({
log.info(`» dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):\n${status}`); log.info(`» dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):\n${status}`);
result = await runOpenCode({ result = await runOpenCode({
...runParams, ...runParams,
args: [...baseArgs, "--continue", buildCommitPrompt("opentoad", status)], args: [...baseArgs, "--continue", buildCommitPrompt("opencode", status)],
}); });
} }
-151372
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -8,7 +8,7 @@
export const pullfrogMcpName = "pullfrog"; export const pullfrogMcpName = "pullfrog";
/** @see {@link file://./agents/shared.ts} Agent interface that uses this type */ /** @see {@link file://./agents/shared.ts} Agent interface that uses this type */
export type AgentId = "claude" | "opentoad"; export type AgentId = "claude" | "opencode";
/** /**
* format a tool name the way each agent's MCP client presents it to the model. * format a tool name the way each agent's MCP client presents it to the model.
@@ -19,7 +19,7 @@ export function formatMcpToolRef(agentId: AgentId, toolName: string): string {
switch (agentId) { switch (agentId) {
case "claude": case "claude":
return `mcp__${pullfrogMcpName}__${toolName}`; return `mcp__${pullfrogMcpName}__${toolName}`;
case "opentoad": case "opencode":
return `${pullfrogMcpName}_${toolName}`; return `${pullfrogMcpName}_${toolName}`;
default: default:
return agentId satisfies never; return agentId satisfies never;
File diff suppressed because one or more lines are too long
+22 -2
View File
@@ -1,5 +1,7 @@
// changes to tool permissions should be reflected in wiki/granular-tools.md // changes to tool permissions should be reflected in wiki/granular-tools.md
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { deleteProgressComment, reportProgress } from "./mcp/comment.ts"; import { deleteProgressComment, reportProgress } from "./mcp/comment.ts";
import { startInstallation } from "./mcp/dependencies.ts"; import { startInstallation } from "./mcp/dependencies.ts";
@@ -337,6 +339,21 @@ export async function main(): Promise<MainResult> {
log.info(instructions.full); log.info(instructions.full);
}); });
// OpenCode loads .opencode/plugin/ files at startup. if the repo has any,
// eagerly await dependency installation so plugin imports can resolve.
if (agentId === "opencode") {
const pluginDir = join(process.cwd(), ".opencode", "plugin");
const hasPlugins =
existsSync(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
if (hasPlugins && toolState.dependencyInstallation?.promise) {
log.info(
"» .opencode/plugin/ detected — awaiting dependency installation before agent start"
);
await toolState.dependencyInstallation.promise.catch(() => {});
timer.checkpoint("awaitDepsForPlugins");
}
}
// run agent, optionally with timeout enforcement // run agent, optionally with timeout enforcement
activityTimeout = createProcessOutputActivityTimeout({ activityTimeout = createProcessOutputActivityTimeout({
timeoutMs: DEFAULT_ACTIVITY_TIMEOUT_MS, timeoutMs: DEFAULT_ACTIVITY_TIMEOUT_MS,
@@ -460,9 +477,12 @@ export async function main(): Promise<MainResult> {
killTrackedChildren(); killTrackedChildren();
log.error(errorMessage); log.error(errorMessage);
// best-effort summary — don't mask the original error // best-effort summary — write the error so it's visible in the Actions summary tab
try { try {
await writeJobSummary(toolState); const errorSummary = `### ❌ Pullfrog failed\n\n\`\`\`\n${errorMessage}\n\`\`\``;
const usageSummary = formatUsageSummary(toolState.usageEntries);
const parts = [errorSummary, toolState.lastProgressBody, usageSummary].filter(Boolean);
await writeSummary(parts.join("\n\n"));
} catch {} } catch {}
try { try {
+9 -2
View File
@@ -65,7 +65,7 @@ function detectSandboxMethod(): SandboxMethod {
// continue to try sudo // continue to try sudo
} }
// try sudo unshare (works on GHA runners) // sudo unshare (works on GHA runners)
try { try {
const result = spawnSync("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], { const result = spawnSync("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
timeout: 5000, timeout: 5000,
@@ -81,7 +81,7 @@ function detectSandboxMethod(): SandboxMethod {
} }
detectedSandboxMethod = "none"; detectedSandboxMethod = "none";
log.info("PID namespace isolation not available - falling back to env filtering only"); log.info("PID namespace isolation not available");
return "none"; return "none";
} }
@@ -97,6 +97,13 @@ const PROC_CLEANUP =
function spawnShell(params: SpawnParams): ChildProcess { function spawnShell(params: SpawnParams): ChildProcess {
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true }; const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
const sandboxMethod = detectSandboxMethod(); const sandboxMethod = detectSandboxMethod();
const ci = process.env.CI === "true";
if (ci && sandboxMethod === "none") {
throw new Error(
"pid namespace isolation is required in CI but unavailable (both unshare and sudo unshare failed)"
);
}
if (sandboxMethod === "unshare") { if (sandboxMethod === "unshare") {
return spawn( return spawn(
+2 -2
View File
@@ -298,5 +298,5 @@ ${PR_SUMMARY_FORMAT}`,
]; ];
} }
// static export for UI display — uses opentoad format as the readable default // static export for UI display — uses opencode format as the readable default
export const modes: Mode[] = computeModes("opentoad"); export const modes: Mode[] = computeModes("opencode");
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pullfrog", "name": "pullfrog",
"version": "0.0.200", "version": "0.0.201",
"type": "module", "type": "module",
"bin": { "bin": {
"pullfrog": "dist/cli.mjs", "pullfrog": "dist/cli.mjs",
-41874
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -90,6 +90,6 @@ export const test: TestRunnerOptions = {
fixture, fixture,
validator, validator,
tags: ["adhoc", "security"], tags: ["adhoc", "security"],
agents: ["opentoad"], agents: ["opencode"],
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" }, env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
}; };
+1 -1
View File
@@ -107,6 +107,6 @@ export const test: TestRunnerOptions = {
fixture, fixture,
validator, validator,
tags: ["adhoc", "security"], tags: ["adhoc", "security"],
agents: ["opentoad"], agents: ["opencode"],
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" }, env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
}; };
+1 -1
View File
@@ -78,6 +78,6 @@ export const test: TestRunnerOptions = {
fixture, fixture,
validator, validator,
tags: ["adhoc", "security"], tags: ["adhoc", "security"],
agents: ["opentoad"], agents: ["opencode"],
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" }, env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
}; };
+1
View File
@@ -94,5 +94,6 @@ export const test: TestRunnerOptions = {
fixture, fixture,
validator, validator,
repoSetup, repoSetup,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
tags: ["agnostic", "security"], tags: ["agnostic", "security"],
}; };
+1
View File
@@ -102,5 +102,6 @@ export const test: TestRunnerOptions = {
fixture, fixture,
validator, validator,
agentEnv, agentEnv,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
tags: ["agnostic"], tags: ["agnostic"],
}; };
+1
View File
@@ -90,5 +90,6 @@ export const test: TestRunnerOptions = {
name: "pkg-json-scripts", name: "pkg-json-scripts",
fixture, fixture,
validator, validator,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
tags: ["agnostic", "security"], tags: ["agnostic", "security"],
}; };
+1
View File
@@ -60,5 +60,6 @@ export const test: TestRunnerOptions = {
fixture, fixture,
validator, validator,
agentEnv, agentEnv,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
tags: ["agnostic"], tags: ["agnostic"],
}; };
+1
View File
@@ -72,5 +72,6 @@ export const test: TestRunnerOptions = {
name: "push-enabled", name: "push-enabled",
fixture, fixture,
validator, validator,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
tags: ["agnostic"], tags: ["agnostic"],
}; };
+1
View File
@@ -65,5 +65,6 @@ export const test: TestRunnerOptions = {
name: "push-restricted", name: "push-restricted",
fixture, fixture,
validator, validator,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
tags: ["agnostic"], tags: ["agnostic"],
}; };
+1
View File
@@ -27,5 +27,6 @@ export const test: TestRunnerOptions = {
fixture, fixture,
validator, validator,
expectFailure: true, expectFailure: true,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
tags: ["agnostic"], tags: ["agnostic"],
}; };
+3 -3
View File
@@ -4,7 +4,7 @@
# outputs a JSON array of agent names to stdout. # outputs a JSON array of agent names to stdout.
# #
# only agents whose harness file changed AND are exported from index.ts are included. # only agents whose harness file changed AND are exported from index.ts are included.
# shared.ts/index.ts and other non-harness action changes fall back to opentoad as a canary. # shared.ts/index.ts and other non-harness action changes fall back to opencode as a canary.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -58,9 +58,9 @@ while IFS= read -r file; do
done <<< "$files" done <<< "$files"
# output agents based on change type. # output agents based on change type.
# non-agent action changes always include opentoad as a canary. # non-agent action changes always include opencode as a canary.
if $has_non_agent_change; then if $has_non_agent_change; then
changed_agents+=("opentoad") changed_agents+=("opencode")
fi fi
if [[ ${#changed_agents[@]} -gt 0 ]]; then if [[ ${#changed_agents[@]} -gt 0 ]]; then
+8 -8
View File
@@ -87,29 +87,29 @@ describe("ci workflow consistency", () => {
expect(rootJob.strategy!.matrix.agent).toBe(dynamicAgentsExpression); expect(rootJob.strategy!.matrix.agent).toBe(dynamicAgentsExpression);
}); });
it("changed-agents.sh falls back to opentoad when shared agent code changed", () => { it("changed-agents.sh falls back to opencode when shared agent code changed", () => {
const input = JSON.stringify(["action/agents/shared.ts"]); const input = JSON.stringify(["action/agents/shared.ts"]);
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], { const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
input, input,
encoding: "utf-8", encoding: "utf-8",
}); });
expect(JSON.parse(output)).toEqual(["opentoad"]); expect(JSON.parse(output)).toEqual(["opencode"]);
}); });
it("changed-agents.sh falls back to opentoad for non-agent action changes", () => { it("changed-agents.sh falls back to opencode for non-agent action changes", () => {
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], { const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
input: JSON.stringify(["action/mcp/server.ts"]), input: JSON.stringify(["action/mcp/server.ts"]),
encoding: "utf-8", encoding: "utf-8",
}); });
expect(JSON.parse(output)).toEqual(["opentoad"]); expect(JSON.parse(output)).toEqual(["opencode"]);
}); });
it("changed-agents.sh includes opentoad canary alongside changed agents", () => { it("changed-agents.sh includes opencode canary alongside changed agents", () => {
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], { const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
input: JSON.stringify(["action/agents/opentoad.ts", "action/mcp/server.ts"]), input: JSON.stringify(["action/agents/opencode.ts", "action/mcp/server.ts"]),
encoding: "utf-8", encoding: "utf-8",
}); });
expect(JSON.parse(output)).toEqual(["opentoad"]); expect(JSON.parse(output)).toEqual(["opencode"]);
}); });
it("changed-agents.sh treats legacy agent files as non-agent changes", () => { it("changed-agents.sh treats legacy agent files as non-agent changes", () => {
@@ -117,7 +117,7 @@ describe("ci workflow consistency", () => {
input: JSON.stringify(["action/agents/codex.ts", "action/agents/gemini.ts"]), input: JSON.stringify(["action/agents/codex.ts", "action/agents/gemini.ts"]),
encoding: "utf-8", encoding: "utf-8",
}); });
expect(JSON.parse(output)).toEqual(["opentoad"]); expect(JSON.parse(output)).toEqual(["opencode"]);
}); });
it("action agent matrix matches agents map", () => { it("action agent matrix matches agents map", () => {
+1
View File
@@ -38,6 +38,7 @@ export const test: TestRunnerOptions = {
validator, validator,
env: { env: {
GITHUB_REPOSITORY: "pullfrog/test-repo-mcp", GITHUB_REPOSITORY: "pullfrog/test-repo-mcp",
PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1",
PULLFROG_MCP_SECRET: secret, PULLFROG_MCP_SECRET: secret,
}, },
repoSetup: repoSetup:
+47
View File
@@ -0,0 +1,47 @@
import { randomUUID } from "node:crypto";
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
import { defineFixture, getAgentOutput } from "../utils.ts";
const skillName = "pullfrog-skill-check";
const token = randomUUID();
const fixture = defineFixture(
{
prompt: `Do not modify any files.
Use the skill tool to load ${skillName}.
Then call set_output with exactly this token and nothing else: ${token}`,
shell: "restricted",
push: "disabled",
timeout: "4m",
},
{ localOnly: true }
);
const repoSetup = `mkdir -p .claude/skills/${skillName} .opencode/skills/${skillName} && printf '%s\\n' '---' 'name: ${skillName}' 'description: local skill test token source' '---' '' 'token: ${token}' > .claude/skills/${skillName}/SKILL.md && cp .claude/skills/${skillName}/SKILL.md .opencode/skills/${skillName}/SKILL.md`;
function validator(result: AgentResult): ValidationCheck[] {
const setOutputCalled = result.structuredOutput !== null;
const tokenMatches = result.structuredOutput === token;
const agentOutput = getAgentOutput(result);
const skillInvoked = /Skill\(\{[^)]*"skill":"pullfrog-skill-check"/.test(agentOutput);
return [
{ name: "set_output", passed: setOutputCalled },
{ name: "token_matches", passed: tokenMatches },
{ name: "skill_invoked", passed: skillInvoked },
];
}
export const test: TestRunnerOptions = {
name: "skill-invoke-claude",
fixture,
validator,
agents: ["claude"],
repoSetup,
env: {
PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1",
PULLFROG_MODEL: "anthropic/claude-sonnet-4-6",
},
};
+47
View File
@@ -0,0 +1,47 @@
import { randomUUID } from "node:crypto";
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
import { defineFixture, getAgentOutput } from "../utils.ts";
const skillName = "pullfrog-skill-check";
const token = randomUUID();
const fixture = defineFixture(
{
prompt: `Do not modify any files.
Use the skill tool to load ${skillName}.
Then call set_output with exactly this token and nothing else: ${token}`,
shell: "restricted",
push: "disabled",
timeout: "4m",
},
{ localOnly: true }
);
const repoSetup = `mkdir -p .claude/skills/${skillName} .opencode/skills/${skillName} && printf '%s\\n' '---' 'name: ${skillName}' 'description: local skill test token source' '---' '' 'token: ${token}' > .claude/skills/${skillName}/SKILL.md && cp .claude/skills/${skillName}/SKILL.md .opencode/skills/${skillName}/SKILL.md`;
function validator(result: AgentResult): ValidationCheck[] {
const setOutputCalled = result.structuredOutput !== null;
const tokenMatches = result.structuredOutput === token;
const agentOutput = getAgentOutput(result);
const skillInvoked = /skill\(\{[^)]*"name":"pullfrog-skill-check"/.test(agentOutput);
return [
{ name: "set_output", passed: setOutputCalled },
{ name: "token_matches", passed: tokenMatches },
{ name: "skill_invoked", passed: skillInvoked },
];
}
export const test: TestRunnerOptions = {
name: "skill-invoke-opencode",
fixture,
validator,
agents: ["opencode"],
repoSetup,
env: {
PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1",
PULLFROG_MODEL: "anthropic/claude-sonnet-4-6",
},
};
+1
View File
@@ -28,4 +28,5 @@ export const test: TestRunnerOptions = {
name: "smoke", name: "smoke",
fixture, fixture,
validator, validator,
env: { PULLFROG_DISABLE_SECURITY_INSTRUCTIONS: "1" },
}; };
+1 -1
View File
@@ -6,7 +6,7 @@ import { defineFixture, generateAgentUuids, getAgentOutput } from "../utils.ts";
* process environment. SANDBOX_TEST_TOKEN is set in the agent's process env * process environment. SANDBOX_TEST_TOKEN is set in the agent's process env
* but should be invisible via: * but should be invisible via:
* - shell: filterEnv() strips *_TOKEN vars, PID namespace hides parent /proc * - shell: filterEnv() strips *_TOKEN vars, PID namespace hides parent /proc
* - native tools: OPENCODE_PERMISSION denies external_directory (opentoad), * - native tools: OPENCODE_PERMISSION denies external_directory (opencode),
* managed-settings.json denies /proc reads (claude) * managed-settings.json denies /proc reads (claude)
* *
* runs with both agents to verify each sandbox independently. * runs with both agents to verify each sandbox independently.
+7 -7
View File
@@ -27,14 +27,14 @@ import {
* filters can be test names, tags, or agent names: * filters can be test names, tags, or agent names:
* node test/run.ts # run all tests (excludes adhoc-tagged tests) * node test/run.ts # run all tests (excludes adhoc-tagged tests)
* node test/run.ts smoke # run tests named "smoke" or tagged "smoke" * node test/run.ts smoke # run tests named "smoke" or tagged "smoke"
* node test/run.ts opentoad # run all tests for opentoad only * node test/run.ts opencode # run all tests for opencode only
* node test/run.ts security # run all tests tagged "security" * node test/run.ts security # run all tests tagged "security"
* node test/run.ts agnostic # run all agnostic-tagged tests (with opentoad) * node test/run.ts agnostic # run all agnostic-tagged tests (with opencode)
* node test/run.ts adhoc # run all adhoc-tagged tests * node test/run.ts adhoc # run all adhoc-tagged tests
* node test/run.ts smoke opentoad # run smoke tests for opentoad only * node test/run.ts smoke opencode # run smoke tests for opencode only
* *
* special tags: * special tags:
* - "agnostic": runs with opentoad only, excluded when filtering by agent * - "agnostic": runs with opencode only, excluded when filtering by agent
* - "adhoc": excluded from default runs, must be explicitly requested * - "adhoc": excluded from default runs, must be explicitly requested
* *
* by default, runs in a Docker container for isolation. * by default, runs in a Docker container for isolation.
@@ -303,7 +303,7 @@ async function runTestForAgent(ctx: RunContext): Promise<ValidationResult> {
if (!Object.hasOwn(env, "PULLFROG_MODEL")) { if (!Object.hasOwn(env, "PULLFROG_MODEL")) {
const defaultModels: Record<string, string> = { const defaultModels: Record<string, string> = {
claude: "anthropic/claude-sonnet-4-6", claude: "anthropic/claude-sonnet-4-6",
opentoad: "anthropic/claude-sonnet-4-6", opencode: "anthropic/claude-sonnet-4-6",
}; };
const model = defaultModels[ctx.agent]; const model = defaultModels[ctx.agent];
if (model) { if (model) {
@@ -414,11 +414,11 @@ async function main(): Promise<void> {
const isAgnostic = hasTag(testInfo, "agnostic"); const isAgnostic = hasTag(testInfo, "agnostic");
if (isAgnostic) { if (isAgnostic) {
// agnostic tests: skip if only filtering by agent, otherwise run with opentoad // agnostic tests: skip if only filtering by agent, otherwise run with opencode
if (parsed.filters.length === 0 && parsed.agentFilters.length > 0) { if (parsed.filters.length === 0 && parsed.agentFilters.length > 0) {
continue; continue;
} }
runs.push({ testInfo, agent: "opentoad" }); runs.push({ testInfo, agent: "opencode" });
} else { } else {
// determine which agents to run for this test // determine which agents to run for this test
const testAgents = testInfo.config.agents ?? agents; const testAgents = testInfo.config.agents ?? agents;
+2 -2
View File
@@ -90,7 +90,7 @@ export function generateAgentUuids<T extends string>(envVarNames: T[]): AgentUui
// assign consistent colors to agents (using ANSI codes) // assign consistent colors to agents (using ANSI codes)
const AGENT_COLORS: Record<string, string> = { const AGENT_COLORS: Record<string, string> = {
opentoad: "\x1b[32m", // green opencode: "\x1b[32m", // green
}; };
const RESET = "\x1b[0m"; const RESET = "\x1b[0m";
@@ -325,7 +325,7 @@ export interface TestRunnerOptions {
repoSetup?: string; repoSetup?: string;
// tags for grouping tests (e.g., ["agnostic"], ["fs"]) // tags for grouping tests (e.g., ["agnostic"], ["fs"])
// special tags: // special tags:
// - "agnostic": runs with opentoad only, excluded when filtering by agent // - "agnostic": runs with opencode only, excluded when filtering by agent
// - "adhoc": excluded from default runs, must be explicitly requested // - "adhoc": excluded from default runs, must be explicitly requested
tags?: TestTag[]; tags?: TestTag[];
} }
+2 -2
View File
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
import { resolveAgent } from "./agent.ts"; import { resolveAgent } from "./agent.ts";
describe("resolveAgent", () => { describe("resolveAgent", () => {
it("returns opentoad", () => { it("returns opencode", () => {
const agent = resolveAgent({}); const agent = resolveAgent({});
expect(agent.name).toBe("opentoad"); expect(agent.name).toBe("opencode");
}); });
}); });
+1 -1
View File
@@ -64,5 +64,5 @@ export function resolveAgent(ctx: { model?: string | undefined }): Agent {
} }
// 3. default: OpenCode (universal, supports all providers) // 3. default: OpenCode (universal, supports all providers)
return agents.opentoad; return agents.opencode;
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { validateAgentApiKey } from "./apiKeys.ts"; import { validateAgentApiKey } from "./apiKeys.ts";
const base = { const base = {
agent: { name: "opentoad" }, agent: { name: "opencode" },
owner: "test-owner", owner: "test-owner",
name: "test-repo", name: "test-repo",
}; };
+2
View File
@@ -281,6 +281,8 @@ Never use \`sleep\` to wait for commands to complete. Commands run synchronously
When posting comments via ${pullfrogMcpName}, write as a professional team member would. Your final comments should be polished and actionable — do not include intermediate reasoning like "I'll now look at the code" or "Let me respond to the question." When posting comments via ${pullfrogMcpName}, write as a professional team member would. Your final comments should be polished and actionable — do not include intermediate reasoning like "I'll now look at the code" or "Let me respond to the question."
When embedding images (e.g. uploaded screenshots) in comments or PR bodies, always use markdown image syntax: \`![description](url)\`. Never paste a naked URL — it will not render as an image.
### Progress reporting ### Progress reporting
**Task list**: at the start of every run, create an internal task list based on the steps in your current mode. Update it as you complete each step. The system automatically renders this list to the progress comment — you do not need to call \`report_progress\` for this. **Task list**: at the start of every run, create an internal task list based on the steps in your current mode. Update it as you complete each step. The system automatically renders this list to the progress comment — you do not need to call \`report_progress\` for this.