diff --git a/agents/opentoad.ts b/agents/opentoad.ts index 2d97a2d..31fa8b0 100644 --- a/agents/opentoad.ts +++ b/agents/opentoad.ts @@ -4,7 +4,6 @@ * transparently wraps OpenCode with a security layer: * - bash: "deny" via OPENCODE_CONFIG_CONTENT (agent cannot shell out) * - 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 server injected alongside project config (not replacing) * - ASKPASS handles git auth separately (token never in subprocess env) diff --git a/main.ts b/main.ts index 479ab0e..5c4ddfa 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,7 @@ // 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 { deleteProgressComment, reportProgress } from "./mcp/comment.ts"; import { startInstallation } from "./mcp/dependencies.ts"; @@ -337,6 +339,21 @@ export async function main(): Promise { 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 === "opentoad") { + 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 activityTimeout = createProcessOutputActivityTimeout({ timeoutMs: DEFAULT_ACTIVITY_TIMEOUT_MS, @@ -460,9 +477,12 @@ export async function main(): Promise { killTrackedChildren(); 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 { - 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 {} try { diff --git a/mcp/shell.ts b/mcp/shell.ts index e31f321..66f0375 100644 --- a/mcp/shell.ts +++ b/mcp/shell.ts @@ -65,7 +65,7 @@ function detectSandboxMethod(): SandboxMethod { // continue to try sudo } - // try sudo unshare (works on GHA runners) + // sudo unshare (works on GHA runners) try { const result = spawnSync("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], { timeout: 5000, @@ -81,7 +81,7 @@ function detectSandboxMethod(): SandboxMethod { } detectedSandboxMethod = "none"; - log.info("PID namespace isolation not available - falling back to env filtering only"); + log.info("PID namespace isolation not available"); return "none"; } @@ -97,6 +97,13 @@ const PROC_CLEANUP = function spawnShell(params: SpawnParams): ChildProcess { const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true }; 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") { return spawn( diff --git a/utils/instructions.ts b/utils/instructions.ts index 05c59b2..f6b8300 100644 --- a/utils/instructions.ts +++ b/utils/instructions.ts @@ -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 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 **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.