Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5ab3706db | |||
| f8a871f723 | |||
| 52ec35790a | |||
| edd240f535 | |||
| cd1ea5267c | |||
| 4f1e4a2e7a | |||
| 73836d9c8f | |||
| da72d0d6ee | |||
| b472aa1ba9 |
@@ -39,6 +39,8 @@ jobs:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
||||
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
|
||||
OPENCODE_MODEL_MINI: ${{ vars.OPENCODE_MODEL_MINI }}
|
||||
OPENCODE_MODEL_MAX: ${{ vars.OPENCODE_MODEL_MAX }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- test preview system --> <!-- test bypass 2 -->
|
||||
<!-- test preview system --> <!-- test bypass 2 --> <!-- trigger preview repo creation -->
|
||||
<p align="center">
|
||||
<h1 align="center">
|
||||
<picture>
|
||||
|
||||
+218
-22
@@ -1,7 +1,7 @@
|
||||
// changes to effort level configuration should be reflected in wiki/effort.md and docs/effort.mdx
|
||||
// changes to tool permissions should be reflected in wiki/granular-tools.md
|
||||
// changes to web search configuration should be reflected in wiki/websearch.md
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
@@ -38,6 +38,159 @@ function detectProviderError(text: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
type OpenCodeConfig = {
|
||||
mcp?: Record<string, unknown>;
|
||||
permission?: Record<string, unknown>;
|
||||
provider?: Record<string, unknown>;
|
||||
model?: string;
|
||||
enabled_providers?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type RecordPropertyContext = {
|
||||
value: unknown;
|
||||
key: string;
|
||||
};
|
||||
|
||||
type RepoConfigLoadContext = {
|
||||
repoConfigPath: string;
|
||||
};
|
||||
|
||||
type ProviderFromModelContext = {
|
||||
model: string;
|
||||
};
|
||||
|
||||
type InlineConfigOverrideContext = {
|
||||
model: string;
|
||||
};
|
||||
|
||||
type InlineConfigOverride = {
|
||||
providerId: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type ModelOverrideResolutionContext = {
|
||||
effort: AgentRunContext["payload"]["effort"];
|
||||
env: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
type ModelOverrideResolution = {
|
||||
model: string;
|
||||
source: "OPENCODE_MODEL_MINI" | "OPENCODE_MODEL_MAX" | "OPENCODE_MODEL";
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function getRecordProperty(ctx: RecordPropertyContext): Record<string, unknown> | undefined {
|
||||
if (!isRecord(ctx.value)) {
|
||||
return undefined;
|
||||
}
|
||||
const propertyValue = ctx.value[ctx.key];
|
||||
if (!isRecord(propertyValue)) {
|
||||
return undefined;
|
||||
}
|
||||
return propertyValue;
|
||||
}
|
||||
|
||||
function loadRepoOpenCodeConfig(ctx: RepoConfigLoadContext): OpenCodeConfig | undefined {
|
||||
if (!existsSync(ctx.repoConfigPath)) {
|
||||
log.info(`» repo opencode.json not found at ${ctx.repoConfigPath}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const rawConfig = readFileSync(ctx.repoConfigPath, "utf-8");
|
||||
const parsedConfig = JSON.parse(rawConfig);
|
||||
if (!isRecord(parsedConfig)) {
|
||||
log.warning(`» repo opencode.json is not an object: ${ctx.repoConfigPath}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const providerConfig = getRecordProperty({ value: parsedConfig, key: "provider" });
|
||||
if (providerConfig) {
|
||||
const providerNames = Object.keys(providerConfig);
|
||||
log.info(`» repo opencode provider config detected: ${providerNames.join(", ")}`);
|
||||
}
|
||||
|
||||
const result: OpenCodeConfig = parsedConfig;
|
||||
log.info(`» loaded repo opencode.json from ${ctx.repoConfigPath}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log.warning(`» failed to parse repo opencode.json at ${ctx.repoConfigPath}: ${errorMessage}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function parseProviderFromModel(ctx: ProviderFromModelContext): string | undefined {
|
||||
const trimmedModel = ctx.model.trim();
|
||||
const slashIndex = trimmedModel.indexOf("/");
|
||||
if (slashIndex <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const providerId = trimmedModel.slice(0, slashIndex).trim().toLowerCase();
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
return providerId;
|
||||
}
|
||||
|
||||
function buildInlineConfigOverride(
|
||||
ctx: InlineConfigOverrideContext
|
||||
): InlineConfigOverride | undefined {
|
||||
const providerId = parseProviderFromModel({ model: ctx.model });
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const inlineConfig: OpenCodeConfig = {
|
||||
model: ctx.model,
|
||||
enabled_providers: [providerId],
|
||||
};
|
||||
return {
|
||||
providerId,
|
||||
content: JSON.stringify(inlineConfig),
|
||||
};
|
||||
}
|
||||
|
||||
function readNonEmptyEnvVar(ctx: { env: NodeJS.ProcessEnv; name: string }): string | undefined {
|
||||
const value = ctx.env[ctx.name];
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolveModelOverride(
|
||||
ctx: ModelOverrideResolutionContext
|
||||
): ModelOverrideResolution | undefined {
|
||||
if (ctx.effort === "mini") {
|
||||
const miniModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MINI" });
|
||||
if (miniModel) {
|
||||
return { model: miniModel, source: "OPENCODE_MODEL_MINI" };
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.effort === "max") {
|
||||
const maxModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MAX" });
|
||||
if (maxModel) {
|
||||
return { model: maxModel, source: "OPENCODE_MODEL_MAX" };
|
||||
}
|
||||
}
|
||||
|
||||
const baseModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL" });
|
||||
if (!baseModel) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { model: baseModel, source: "OPENCODE_MODEL" };
|
||||
}
|
||||
|
||||
async function installOpencode(): Promise<string> {
|
||||
return await installFromNpmTarball({
|
||||
packageName: "opencode-ai",
|
||||
@@ -66,13 +219,18 @@ export const opencode = agent({
|
||||
// this is critical for debugging since opencode run suppresses errors by default (Issue #752).
|
||||
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||
|
||||
// only override model when OPENCODE_MODEL is set (e.g., test environments with
|
||||
// restricted API quotas). in production, OpenCode auto-selects the best available
|
||||
// model based on which provider API keys are present.
|
||||
const modelOverride = process.env.OPENCODE_MODEL;
|
||||
// resolve model override from environment.
|
||||
// precedence:
|
||||
// 1) effort-specific overrides (OPENCODE_MODEL_MINI / OPENCODE_MODEL_MAX)
|
||||
// 2) OPENCODE_MODEL fallback
|
||||
// 3) OpenCode auto-select
|
||||
const modelOverride = resolveModelOverride({
|
||||
effort: ctx.payload.effort,
|
||||
env: process.env,
|
||||
});
|
||||
if (modelOverride) {
|
||||
args.push("--model", modelOverride);
|
||||
log.info(`» model: ${modelOverride} (override)`);
|
||||
args.push("--model", modelOverride.model);
|
||||
log.info(`» model: ${modelOverride.model} (override via ${modelOverride.source})`);
|
||||
} else {
|
||||
log.info(`» model: auto-selected by OpenCode`);
|
||||
}
|
||||
@@ -89,6 +247,31 @@ export const opencode = agent({
|
||||
GOOGLE_GENERATIVE_AI_API_KEY:
|
||||
process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY,
|
||||
};
|
||||
|
||||
if (modelOverride) {
|
||||
const inlineOverride = buildInlineConfigOverride({ model: modelOverride.model });
|
||||
if (inlineOverride) {
|
||||
env.OPENCODE_CONFIG_CONTENT = inlineOverride.content;
|
||||
log.info(
|
||||
`» OpenCode inline config override enabled: provider=${inlineOverride.providerId}, model=${modelOverride.model}`
|
||||
);
|
||||
} else {
|
||||
log.warning(
|
||||
`» skipping OpenCode inline config override: unable to parse provider from model "${modelOverride.model}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hasOpenRouterKey = Boolean(env.OPENROUTER_API_KEY);
|
||||
const hasAnthropicKey = Boolean(env.ANTHROPIC_API_KEY);
|
||||
const hasOpenAiKey = Boolean(env.OPENAI_API_KEY);
|
||||
const hasGoogleKey = Boolean(
|
||||
env.GOOGLE_API_KEY || env.GEMINI_API_KEY || env.GOOGLE_GENERATIVE_AI_API_KEY
|
||||
);
|
||||
log.info(
|
||||
`» provider key presence: OPENROUTER=${hasOpenRouterKey ? "set" : "unset"}, ANTHROPIC=${hasAnthropicKey ? "set" : "unset"}, OPENAI=${hasOpenAiKey ? "set" : "unset"}, GOOGLE=${hasGoogleKey ? "set" : "unset"}`
|
||||
);
|
||||
|
||||
// OpenCode doesn't support GitHub App installation tokens
|
||||
delete env.GITHUB_TOKEN;
|
||||
|
||||
@@ -194,7 +377,7 @@ export const opencode = agent({
|
||||
lastProviderError = providerError;
|
||||
log.info(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
||||
} else {
|
||||
// OpenCode's --print-logs output goes to stderr. demote internal
|
||||
//agent OpenCode's --print-logs output goes to stderr. demote internal
|
||||
// INFO/DEBUG bus traffic to debug so it doesn't drown out tool
|
||||
// call logs in the GitHub Actions step output.
|
||||
log.debug(trimmed);
|
||||
@@ -301,28 +484,41 @@ function configureOpenCode(ctx: AgentRunContext): void {
|
||||
const configDir = join(ctx.tmpdir, ".config", "opencode");
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
const configPath = join(configDir, "opencode.json");
|
||||
const repoConfigPath = join(process.cwd(), "opencode.json");
|
||||
const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath });
|
||||
if (repoConfig?.model) {
|
||||
log.info(`» repo opencode model configured: ${repoConfig.model}`);
|
||||
}
|
||||
|
||||
// build MCP servers config
|
||||
const opencodeMcpServers = {
|
||||
[ghPullfrogMcpName]: { type: "remote" as const, url: ctx.mcpServerUrl },
|
||||
};
|
||||
const opencodeMcpServers: Record<string, unknown> = {};
|
||||
const repoMcpServers = getRecordProperty({ value: repoConfig, key: "mcp" });
|
||||
if (repoMcpServers) {
|
||||
Object.assign(opencodeMcpServers, repoMcpServers);
|
||||
}
|
||||
opencodeMcpServers[ghPullfrogMcpName] = { type: "remote" as const, url: ctx.mcpServerUrl };
|
||||
|
||||
// build permission object based on tool permissions
|
||||
// note: OpenCode has no built-in web search tool
|
||||
const shell = ctx.payload.shell;
|
||||
const permission = {
|
||||
edit: "deny",
|
||||
read: "deny",
|
||||
bash: shell !== "enabled" ? "deny" : "allow",
|
||||
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
|
||||
external_directory: "deny",
|
||||
};
|
||||
const permission: Record<string, unknown> = {};
|
||||
const repoPermission = getRecordProperty({ value: repoConfig, key: "permission" });
|
||||
if (repoPermission) {
|
||||
Object.assign(permission, repoPermission);
|
||||
}
|
||||
permission.edit = "deny";
|
||||
permission.read = "deny";
|
||||
permission.bash = shell !== "enabled" ? "deny" : "allow";
|
||||
permission.webfetch = ctx.payload.web === "disabled" ? "deny" : "allow";
|
||||
permission.external_directory = "deny";
|
||||
|
||||
// build complete config in one object
|
||||
const config = {
|
||||
mcp: opencodeMcpServers,
|
||||
permission,
|
||||
};
|
||||
const config: OpenCodeConfig = {};
|
||||
if (repoConfig) {
|
||||
Object.assign(config, repoConfig);
|
||||
}
|
||||
config.mcp = opencodeMcpServers;
|
||||
config.permission = permission;
|
||||
|
||||
const configJson = JSON.stringify(config, null, 2);
|
||||
try {
|
||||
|
||||
@@ -56274,7 +56274,7 @@ var require_snapshot_recorder = __commonJS({
|
||||
"node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
|
||||
"use strict";
|
||||
var { writeFile, readFile, mkdir } = __require("node:fs/promises");
|
||||
var { dirname: dirname2, resolve: resolve3 } = __require("node:path");
|
||||
var { dirname: dirname3, resolve: resolve3 } = __require("node:path");
|
||||
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
|
||||
var { InvalidArgumentError, UndiciError } = require_errors4();
|
||||
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
||||
@@ -56498,7 +56498,7 @@ var require_snapshot_recorder = __commonJS({
|
||||
throw new InvalidArgumentError("Snapshot path is required");
|
||||
}
|
||||
const resolvedPath = resolve3(path3);
|
||||
await mkdir(dirname2(resolvedPath), { recursive: true });
|
||||
await mkdir(dirname3(resolvedPath), { recursive: true });
|
||||
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
|
||||
hash: hash2,
|
||||
snapshot: snapshot2
|
||||
@@ -97612,6 +97612,7 @@ var require_semver2 = __commonJS({
|
||||
|
||||
// entry.ts
|
||||
var core5 = __toESM(require_core(), 1);
|
||||
import { dirname as dirname2 } from "node:path";
|
||||
|
||||
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
||||
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
||||
@@ -141626,7 +141627,10 @@ function DelegateTool(ctx) {
|
||||
if (!ctx.toolState.selectedMode) {
|
||||
log.info(`\xBB warning: delegating without calling select_mode first (mode=${mode})`);
|
||||
}
|
||||
log.info(`\xBB delegating ${params.tasks.length} task(s) in parallel (mode=${mode})`);
|
||||
const n = params.tasks.length;
|
||||
log.info(
|
||||
`\xBB delegating ${n} task${n === 1 ? "" : "s"}${n > 1 ? " in parallel" : ""} (mode=${mode})`
|
||||
);
|
||||
const taskEntries = params.tasks.map((task) => {
|
||||
const effort = task.effort ?? "auto";
|
||||
const subagent = createSubagentState({ ctx, mode, label: task.label });
|
||||
@@ -141646,10 +141650,10 @@ function DelegateTool(ctx) {
|
||||
const results = taskEntries.map((entry, i) => {
|
||||
const outcome = settled[i];
|
||||
const error49 = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
|
||||
log.debug(
|
||||
`\xBB task "${entry.task.label}" result: output=${entry.subagent.output !== void 0}, status=${entry.subagent.status}`
|
||||
);
|
||||
return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49);
|
||||
const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49);
|
||||
const status = result.success ? "succeeded" : "failed";
|
||||
log.box(result.summary, { title: `task "${entry.task.label}" ${status}` });
|
||||
return result;
|
||||
});
|
||||
const succeeded = results.filter((r) => r.success).length;
|
||||
log.info(`\xBB delegation completed: ${succeeded}/${results.length} succeeded (mode=${mode})`);
|
||||
@@ -142071,12 +142075,15 @@ var installNodeDependencies = {
|
||||
const result = await spawn({
|
||||
cmd: resolved.command,
|
||||
args: resolved.args,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStdout: (chunk) => process.stdout.write(chunk),
|
||||
onStderr: (chunk) => process.stderr.write(chunk)
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }
|
||||
});
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
if (output) {
|
||||
log.startGroup(`${fullCommand} output`);
|
||||
log.info(output);
|
||||
log.endGroup();
|
||||
}
|
||||
if (result.exitCode !== 0) {
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
const errorMessage = output || `exited with code ${result.exitCode}`;
|
||||
return {
|
||||
language: "node",
|
||||
@@ -142213,20 +142220,26 @@ var installPythonDependencies = {
|
||||
}
|
||||
}
|
||||
const [cmd, ...args2] = config3.installCmd;
|
||||
log.info(`\xBB running: ${cmd} ${args2.join(" ")}`);
|
||||
const fullCommand = `${cmd} ${args2.join(" ")}`;
|
||||
log.info(`\xBB running: ${fullCommand}`);
|
||||
const result = await spawn({
|
||||
cmd,
|
||||
args: args2,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStderr: (chunk) => process.stderr.write(chunk)
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }
|
||||
});
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
if (output) {
|
||||
log.startGroup(`${fullCommand} output`);
|
||||
log.info(output);
|
||||
log.endGroup();
|
||||
}
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
language: "python",
|
||||
packageManager: config3.tool,
|
||||
configFile: config3.file,
|
||||
dependenciesInstalled: false,
|
||||
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`]
|
||||
issues: [output || `${cmd} exited with code ${result.exitCode}`]
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -143909,6 +143922,7 @@ function SelectModeTool(ctx) {
|
||||
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
||||
import { randomUUID as randomUUID3 } from "node:crypto";
|
||||
import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs";
|
||||
import { userInfo } from "node:os";
|
||||
import { join as join9 } from "node:path";
|
||||
var ShellParams = type({
|
||||
command: "string",
|
||||
@@ -143955,13 +143969,14 @@ function detectSandboxMethod() {
|
||||
log.info("PID namespace isolation not available - falling back to env filtering only");
|
||||
return "none";
|
||||
}
|
||||
var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
||||
function spawnShell(params) {
|
||||
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
||||
const sandboxMethod = detectSandboxMethod();
|
||||
if (sandboxMethod === "unshare") {
|
||||
return spawn2(
|
||||
"unshare",
|
||||
["--pid", "--fork", "--mount-proc", "bash", "-c", params.command],
|
||||
["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`],
|
||||
spawnOpts
|
||||
);
|
||||
}
|
||||
@@ -143972,6 +143987,8 @@ function spawnShell(params) {
|
||||
envArgs.push(`${k}=${v}`);
|
||||
}
|
||||
}
|
||||
const username = userInfo().username;
|
||||
const escaped = params.command.replace(/'/g, "'\\''");
|
||||
return spawn2(
|
||||
"sudo",
|
||||
[
|
||||
@@ -143983,10 +144000,9 @@ function spawnShell(params) {
|
||||
"--mount-proc",
|
||||
"bash",
|
||||
"-c",
|
||||
params.command
|
||||
`${PROC_CLEANUP} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
||||
],
|
||||
{ ...spawnOpts, env: {} }
|
||||
// empty env since we pass via sudo env
|
||||
);
|
||||
}
|
||||
return spawn2("bash", ["-c", params.command], spawnOpts);
|
||||
@@ -144238,16 +144254,28 @@ function isAddressInUse(error49) {
|
||||
const message = getErrorMessage(error49).toLowerCase();
|
||||
return message.includes("eaddrinuse") || message.includes("address already in use");
|
||||
}
|
||||
function buildSubagentTools(ctx) {
|
||||
function buildCommonTools(ctx) {
|
||||
const tools = [
|
||||
StartDependencyInstallationTool(ctx),
|
||||
AwaitDependencyInstallationTool(ctx),
|
||||
CreateCommentTool(ctx),
|
||||
EditCommentTool(ctx),
|
||||
ReplyToReviewCommentTool(ctx),
|
||||
IssueTool(ctx),
|
||||
IssueInfoTool(ctx),
|
||||
GetIssueCommentsTool(ctx),
|
||||
GetIssueEventsTool(ctx),
|
||||
CreatePullRequestReviewTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CommitInfoTool(ctx),
|
||||
CheckoutPrTool(ctx),
|
||||
GetReviewCommentsTool(ctx),
|
||||
ListPullRequestReviewsTool(ctx),
|
||||
ResolveReviewThreadTool(ctx),
|
||||
GetCheckSuiteLogsTool(ctx),
|
||||
AddLabelsTool(ctx),
|
||||
GitTool(ctx),
|
||||
GitFetchTool(ctx),
|
||||
UploadFileTool(ctx),
|
||||
SetOutputTool(ctx),
|
||||
FileReadTool(ctx),
|
||||
@@ -144263,34 +144291,8 @@ function buildSubagentTools(ctx) {
|
||||
return tools;
|
||||
}
|
||||
function buildOrchestratorTools(ctx) {
|
||||
const tools = [
|
||||
StartDependencyInstallationTool(ctx),
|
||||
AwaitDependencyInstallationTool(ctx),
|
||||
IssueInfoTool(ctx),
|
||||
GetIssueCommentsTool(ctx),
|
||||
GetIssueEventsTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CommitInfoTool(ctx),
|
||||
CheckoutPrTool(ctx),
|
||||
GetReviewCommentsTool(ctx),
|
||||
ListPullRequestReviewsTool(ctx),
|
||||
GetCheckSuiteLogsTool(ctx),
|
||||
GitTool(ctx),
|
||||
GitFetchTool(ctx),
|
||||
UploadFileTool(ctx),
|
||||
SetOutputTool(ctx),
|
||||
FileReadTool(ctx),
|
||||
FileWriteTool(ctx),
|
||||
FileEditTool(ctx),
|
||||
FileDeleteTool(ctx),
|
||||
ListDirectoryTool(ctx),
|
||||
CreateCommentTool(ctx),
|
||||
EditCommentTool(ctx),
|
||||
ReplyToReviewCommentTool(ctx),
|
||||
CreatePullRequestReviewTool(ctx),
|
||||
ResolveReviewThreadTool(ctx),
|
||||
IssueTool(ctx),
|
||||
AddLabelsTool(ctx),
|
||||
return [
|
||||
...buildCommonTools(ctx),
|
||||
ReportProgressTool(ctx),
|
||||
SelectModeTool(ctx),
|
||||
DelegateTool(ctx),
|
||||
@@ -144301,11 +144303,9 @@ function buildOrchestratorTools(ctx) {
|
||||
CreatePullRequestTool(ctx),
|
||||
UpdatePullRequestBodyTool(ctx)
|
||||
];
|
||||
if (ctx.payload.shell === "restricted") {
|
||||
tools.push(ShellTool(ctx));
|
||||
tools.push(KillBackgroundTool(ctx));
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
function buildSubagentTools(ctx) {
|
||||
return buildCommonTools(ctx);
|
||||
}
|
||||
async function tryStartMcpServer(ctx, tools, port) {
|
||||
const server = new FastMCP({ name: ghPullfrogMcpName, version: "0.0.1" });
|
||||
@@ -144639,7 +144639,7 @@ import { join as join11 } from "node:path";
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.171",
|
||||
version: "0.0.173",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
@@ -146040,7 +146040,7 @@ function configureGeminiSettings(ctx) {
|
||||
}
|
||||
|
||||
// agents/opencode.ts
|
||||
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
||||
import { existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync12 } from "node:fs";
|
||||
import { join as join15 } from "node:path";
|
||||
import { performance as performance7 } from "node:perf_hooks";
|
||||
var OPENCODE_CLI_VERSION = "1.1.56";
|
||||
@@ -146061,6 +146061,101 @@ function detectProviderError(text) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function isRecord(value2) {
|
||||
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
||||
}
|
||||
function getRecordProperty(ctx) {
|
||||
if (!isRecord(ctx.value)) {
|
||||
return void 0;
|
||||
}
|
||||
const propertyValue = ctx.value[ctx.key];
|
||||
if (!isRecord(propertyValue)) {
|
||||
return void 0;
|
||||
}
|
||||
return propertyValue;
|
||||
}
|
||||
function loadRepoOpenCodeConfig(ctx) {
|
||||
if (!existsSync7(ctx.repoConfigPath)) {
|
||||
log.info(`\xBB repo opencode.json not found at ${ctx.repoConfigPath}`);
|
||||
return void 0;
|
||||
}
|
||||
try {
|
||||
const rawConfig = readFileSync7(ctx.repoConfigPath, "utf-8");
|
||||
const parsedConfig = JSON.parse(rawConfig);
|
||||
if (!isRecord(parsedConfig)) {
|
||||
log.warning(`\xBB repo opencode.json is not an object: ${ctx.repoConfigPath}`);
|
||||
return void 0;
|
||||
}
|
||||
const providerConfig = getRecordProperty({ value: parsedConfig, key: "provider" });
|
||||
if (providerConfig) {
|
||||
const providerNames = Object.keys(providerConfig);
|
||||
log.info(`\xBB repo opencode provider config detected: ${providerNames.join(", ")}`);
|
||||
}
|
||||
const result = parsedConfig;
|
||||
log.info(`\xBB loaded repo opencode.json from ${ctx.repoConfigPath}`);
|
||||
return result;
|
||||
} catch (error49) {
|
||||
const errorMessage = error49 instanceof Error ? error49.message : String(error49);
|
||||
log.warning(`\xBB failed to parse repo opencode.json at ${ctx.repoConfigPath}: ${errorMessage}`);
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
function parseProviderFromModel(ctx) {
|
||||
const trimmedModel = ctx.model.trim();
|
||||
const slashIndex = trimmedModel.indexOf("/");
|
||||
if (slashIndex <= 0) {
|
||||
return void 0;
|
||||
}
|
||||
const providerId = trimmedModel.slice(0, slashIndex).trim().toLowerCase();
|
||||
if (!providerId) {
|
||||
return void 0;
|
||||
}
|
||||
return providerId;
|
||||
}
|
||||
function buildInlineConfigOverride(ctx) {
|
||||
const providerId = parseProviderFromModel({ model: ctx.model });
|
||||
if (!providerId) {
|
||||
return void 0;
|
||||
}
|
||||
const inlineConfig = {
|
||||
model: ctx.model,
|
||||
enabled_providers: [providerId]
|
||||
};
|
||||
return {
|
||||
providerId,
|
||||
content: JSON.stringify(inlineConfig)
|
||||
};
|
||||
}
|
||||
function readNonEmptyEnvVar(ctx) {
|
||||
const value2 = ctx.env[ctx.name];
|
||||
if (!value2) {
|
||||
return void 0;
|
||||
}
|
||||
const trimmed = value2.trim();
|
||||
if (!trimmed) {
|
||||
return void 0;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
function resolveModelOverride(ctx) {
|
||||
if (ctx.effort === "mini") {
|
||||
const miniModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MINI" });
|
||||
if (miniModel) {
|
||||
return { model: miniModel, source: "OPENCODE_MODEL_MINI" };
|
||||
}
|
||||
}
|
||||
if (ctx.effort === "max") {
|
||||
const maxModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MAX" });
|
||||
if (maxModel) {
|
||||
return { model: maxModel, source: "OPENCODE_MODEL_MAX" };
|
||||
}
|
||||
}
|
||||
const baseModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL" });
|
||||
if (!baseModel) {
|
||||
return void 0;
|
||||
}
|
||||
return { model: baseModel, source: "OPENCODE_MODEL" };
|
||||
}
|
||||
async function installOpencode() {
|
||||
return await installFromNpmTarball({
|
||||
packageName: "opencode-ai",
|
||||
@@ -146079,10 +146174,13 @@ var opencode = agent({
|
||||
mkdirSync9(configDir, { recursive: true });
|
||||
configureOpenCode(ctx);
|
||||
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||
const modelOverride = process.env.OPENCODE_MODEL;
|
||||
const modelOverride = resolveModelOverride({
|
||||
effort: ctx.payload.effort,
|
||||
env: process.env
|
||||
});
|
||||
if (modelOverride) {
|
||||
args2.push("--model", modelOverride);
|
||||
log.info(`\xBB model: ${modelOverride} (override)`);
|
||||
args2.push("--model", modelOverride.model);
|
||||
log.info(`\xBB model: ${modelOverride.model} (override via ${modelOverride.source})`);
|
||||
} else {
|
||||
log.info(`\xBB model: auto-selected by OpenCode`);
|
||||
}
|
||||
@@ -146094,6 +146192,28 @@ var opencode = agent({
|
||||
// set GOOGLE_GENERATIVE_AI_API_KEY alias for Google provider compatibility (if not already set)
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
||||
};
|
||||
if (modelOverride) {
|
||||
const inlineOverride = buildInlineConfigOverride({ model: modelOverride.model });
|
||||
if (inlineOverride) {
|
||||
env2.OPENCODE_CONFIG_CONTENT = inlineOverride.content;
|
||||
log.info(
|
||||
`\xBB OpenCode inline config override enabled: provider=${inlineOverride.providerId}, model=${modelOverride.model}`
|
||||
);
|
||||
} else {
|
||||
log.warning(
|
||||
`\xBB skipping OpenCode inline config override: unable to parse provider from model "${modelOverride.model}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
const hasOpenRouterKey = Boolean(env2.OPENROUTER_API_KEY);
|
||||
const hasAnthropicKey = Boolean(env2.ANTHROPIC_API_KEY);
|
||||
const hasOpenAiKey = Boolean(env2.OPENAI_API_KEY);
|
||||
const hasGoogleKey = Boolean(
|
||||
env2.GOOGLE_API_KEY || env2.GEMINI_API_KEY || env2.GOOGLE_GENERATIVE_AI_API_KEY
|
||||
);
|
||||
log.info(
|
||||
`\xBB provider key presence: OPENROUTER=${hasOpenRouterKey ? "set" : "unset"}, ANTHROPIC=${hasAnthropicKey ? "set" : "unset"}, OPENAI=${hasOpenAiKey ? "set" : "unset"}, GOOGLE=${hasGoogleKey ? "set" : "unset"}`
|
||||
);
|
||||
delete env2.GITHUB_TOKEN;
|
||||
const repoDir = process.cwd();
|
||||
log.debug(`\xBB starting OpenCode: ${cliPath} ${args2.join(" ")}`);
|
||||
@@ -146246,21 +146366,34 @@ function configureOpenCode(ctx) {
|
||||
const configDir = join15(ctx.tmpdir, ".config", "opencode");
|
||||
mkdirSync9(configDir, { recursive: true });
|
||||
const configPath = join15(configDir, "opencode.json");
|
||||
const opencodeMcpServers = {
|
||||
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
||||
};
|
||||
const repoConfigPath = join15(process.cwd(), "opencode.json");
|
||||
const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath });
|
||||
if (repoConfig?.model) {
|
||||
log.info(`\xBB repo opencode model configured: ${repoConfig.model}`);
|
||||
}
|
||||
const opencodeMcpServers = {};
|
||||
const repoMcpServers = getRecordProperty({ value: repoConfig, key: "mcp" });
|
||||
if (repoMcpServers) {
|
||||
Object.assign(opencodeMcpServers, repoMcpServers);
|
||||
}
|
||||
opencodeMcpServers[ghPullfrogMcpName] = { type: "remote", url: ctx.mcpServerUrl };
|
||||
const shell = ctx.payload.shell;
|
||||
const permission = {
|
||||
edit: "deny",
|
||||
read: "deny",
|
||||
bash: shell !== "enabled" ? "deny" : "allow",
|
||||
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
|
||||
external_directory: "deny"
|
||||
};
|
||||
const config3 = {
|
||||
mcp: opencodeMcpServers,
|
||||
permission
|
||||
};
|
||||
const permission = {};
|
||||
const repoPermission = getRecordProperty({ value: repoConfig, key: "permission" });
|
||||
if (repoPermission) {
|
||||
Object.assign(permission, repoPermission);
|
||||
}
|
||||
permission.edit = "deny";
|
||||
permission.read = "deny";
|
||||
permission.bash = shell !== "enabled" ? "deny" : "allow";
|
||||
permission.webfetch = ctx.payload.web === "disabled" ? "deny" : "allow";
|
||||
permission.external_directory = "deny";
|
||||
const config3 = {};
|
||||
if (repoConfig) {
|
||||
Object.assign(config3, repoConfig);
|
||||
}
|
||||
config3.mcp = opencodeMcpServers;
|
||||
config3.permission = permission;
|
||||
const configJson = JSON.stringify(config3, null, 2);
|
||||
try {
|
||||
writeFileSync12(configPath, configJson, "utf-8");
|
||||
@@ -147580,6 +147713,7 @@ ${instructions.user}` : null,
|
||||
}
|
||||
|
||||
// entry.ts
|
||||
process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`;
|
||||
async function run() {
|
||||
try {
|
||||
const result = await main();
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
* entry point for pullfrog/pullfrog - unified action
|
||||
*/
|
||||
|
||||
import { dirname } from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import { main } from "./main.ts";
|
||||
|
||||
// GitHub Actions runs the action entry point with the node24 binary specified
|
||||
// in action.yml, but doesn't add that binary's directory to PATH. Without this,
|
||||
// spawned processes (pnpm, npm, etc.) resolve to the runner's default node (v20).
|
||||
process.env.PATH = `${dirname(process.execPath)}:${process.env.PATH}`;
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const result = await main();
|
||||
|
||||
+8
-5
@@ -77,7 +77,10 @@ export function DelegateTool(ctx: ToolContext) {
|
||||
}
|
||||
|
||||
// matched by delegate test validators — update tests if changed
|
||||
log.info(`» delegating ${params.tasks.length} task(s) in parallel (mode=${mode})`);
|
||||
const n = params.tasks.length;
|
||||
log.info(
|
||||
`» delegating ${n} task${n === 1 ? "" : "s"}${n > 1 ? " in parallel" : ""} (mode=${mode})`
|
||||
);
|
||||
|
||||
const taskEntries = params.tasks.map((task) => {
|
||||
const effort = task.effort ?? "auto";
|
||||
@@ -100,10 +103,10 @@ export function DelegateTool(ctx: ToolContext) {
|
||||
const results: DelegateTaskResult[] = taskEntries.map((entry, i) => {
|
||||
const outcome = settled[i];
|
||||
const error = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
|
||||
log.debug(
|
||||
`» task "${entry.task.label}" result: output=${entry.subagent.output !== undefined}, status=${entry.subagent.status}`
|
||||
);
|
||||
return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error);
|
||||
const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error);
|
||||
const status = result.success ? "succeeded" : "failed";
|
||||
log.box(result.summary, { title: `task "${entry.task.label}" ${status}` });
|
||||
return result;
|
||||
});
|
||||
|
||||
const succeeded = results.filter((r) => r.success).length;
|
||||
|
||||
+30
-48
@@ -188,50 +188,27 @@ function isAddressInUse(error: unknown): boolean {
|
||||
return message.includes("eaddrinuse") || message.includes("address already in use");
|
||||
}
|
||||
|
||||
// subagent tools: file ops, shell, read-only GitHub, upload, set_output.
|
||||
// no git/checkout (mutates shared state), no dependencies (shared state),
|
||||
// no GitHub-write (user-facing side effects), no delegation/remote-mutating.
|
||||
function buildSubagentTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
const tools: Tool<any, any>[] = [
|
||||
IssueInfoTool(ctx),
|
||||
GetIssueCommentsTool(ctx),
|
||||
GetIssueEventsTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CommitInfoTool(ctx),
|
||||
GetReviewCommentsTool(ctx),
|
||||
ListPullRequestReviewsTool(ctx),
|
||||
GetCheckSuiteLogsTool(ctx),
|
||||
UploadFileTool(ctx),
|
||||
SetOutputTool(ctx),
|
||||
FileReadTool(ctx),
|
||||
FileWriteTool(ctx),
|
||||
FileEditTool(ctx),
|
||||
FileDeleteTool(ctx),
|
||||
ListDirectoryTool(ctx),
|
||||
];
|
||||
|
||||
if (ctx.payload.shell === "restricted") {
|
||||
tools.push(ShellTool(ctx));
|
||||
tools.push(KillBackgroundTool(ctx));
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
// orchestrator gets everything: file ops, shell, git, GitHub, delegation, remote-mutating
|
||||
function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
// tools shared by both orchestrator and subagent servers
|
||||
function buildCommonTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
const tools: Tool<any, any>[] = [
|
||||
StartDependencyInstallationTool(ctx),
|
||||
AwaitDependencyInstallationTool(ctx),
|
||||
CreateCommentTool(ctx),
|
||||
EditCommentTool(ctx),
|
||||
ReplyToReviewCommentTool(ctx),
|
||||
IssueTool(ctx),
|
||||
IssueInfoTool(ctx),
|
||||
GetIssueCommentsTool(ctx),
|
||||
GetIssueEventsTool(ctx),
|
||||
CreatePullRequestReviewTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CommitInfoTool(ctx),
|
||||
CheckoutPrTool(ctx),
|
||||
GetReviewCommentsTool(ctx),
|
||||
ListPullRequestReviewsTool(ctx),
|
||||
ResolveReviewThreadTool(ctx),
|
||||
GetCheckSuiteLogsTool(ctx),
|
||||
AddLabelsTool(ctx),
|
||||
GitTool(ctx),
|
||||
GitFetchTool(ctx),
|
||||
UploadFileTool(ctx),
|
||||
@@ -241,22 +218,6 @@ function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
FileEditTool(ctx),
|
||||
FileDeleteTool(ctx),
|
||||
ListDirectoryTool(ctx),
|
||||
CreateCommentTool(ctx),
|
||||
EditCommentTool(ctx),
|
||||
ReplyToReviewCommentTool(ctx),
|
||||
CreatePullRequestReviewTool(ctx),
|
||||
ResolveReviewThreadTool(ctx),
|
||||
IssueTool(ctx),
|
||||
AddLabelsTool(ctx),
|
||||
ReportProgressTool(ctx),
|
||||
SelectModeTool(ctx),
|
||||
DelegateTool(ctx),
|
||||
AskQuestionTool(ctx),
|
||||
PushBranchTool(ctx),
|
||||
PushTagsTool(ctx),
|
||||
DeleteBranchTool(ctx),
|
||||
CreatePullRequestTool(ctx),
|
||||
UpdatePullRequestBodyTool(ctx),
|
||||
];
|
||||
|
||||
// only add ShellTool when shell is "restricted"
|
||||
@@ -271,6 +232,27 @@ function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
return tools;
|
||||
}
|
||||
|
||||
// orchestrator gets common tools + delegation + remote-mutating tools
|
||||
function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
return [
|
||||
...buildCommonTools(ctx),
|
||||
ReportProgressTool(ctx),
|
||||
SelectModeTool(ctx),
|
||||
DelegateTool(ctx),
|
||||
AskQuestionTool(ctx),
|
||||
PushBranchTool(ctx),
|
||||
PushTagsTool(ctx),
|
||||
DeleteBranchTool(ctx),
|
||||
CreatePullRequestTool(ctx),
|
||||
UpdatePullRequestBodyTool(ctx),
|
||||
];
|
||||
}
|
||||
|
||||
// subagent gets only common tools (no delegation, no remote mutation)
|
||||
function buildSubagentTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
return buildCommonTools(ctx);
|
||||
}
|
||||
|
||||
type McpStartResult = {
|
||||
server: FastMCP;
|
||||
url: string;
|
||||
|
||||
+18
-11
@@ -2,6 +2,7 @@
|
||||
import { type ChildProcess, type StdioOptions, spawn, spawnSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { closeSync, openSync, writeFileSync } from "node:fs";
|
||||
import { userInfo } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { type } from "arktype";
|
||||
import { log } from "../utils/log.ts";
|
||||
@@ -82,33 +83,39 @@ function detectSandboxMethod(): SandboxMethod {
|
||||
return "none";
|
||||
}
|
||||
|
||||
// strip inherited proc mount that sits underneath --mount-proc's overlay.
|
||||
// --mount-proc mounts fresh proc on top, but `umount /proc` peels it off and exposes the
|
||||
// host's proc with all host PIDs — allowing /proc/<pid>/environ exfiltration.
|
||||
// double-umount removes both layers, then a clean mount gives only sandbox PIDs.
|
||||
// on unprivileged systems where umount fails, --mount-proc still provides isolation
|
||||
// (the agent also can't umount in that case).
|
||||
const PROC_CLEANUP =
|
||||
"umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
||||
|
||||
function spawnShell(params: SpawnParams): ChildProcess {
|
||||
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
||||
const sandboxMethod = detectSandboxMethod();
|
||||
|
||||
if (sandboxMethod === "unshare") {
|
||||
// use PID namespace isolation to prevent reading /proc/$PPID/environ
|
||||
// this creates a new PID namespace where:
|
||||
// 1. the subprocess becomes PID 1 in its namespace
|
||||
// 2. parent PIDs are not visible (PPID = 0)
|
||||
// 3. fresh /proc is mounted showing only sandbox PIDs
|
||||
// combined with resolveEnv("restricted"), this prevents all /proc-based secret theft
|
||||
return spawn(
|
||||
"unshare",
|
||||
["--pid", "--fork", "--mount-proc", "bash", "-c", params.command],
|
||||
["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`],
|
||||
spawnOpts
|
||||
);
|
||||
}
|
||||
|
||||
if (sandboxMethod === "sudo-unshare") {
|
||||
// on GHA runners, unprivileged namespaces are blocked but sudo works
|
||||
// pass filtered env via sudo env command since sudo clears environment
|
||||
const envArgs: string[] = [];
|
||||
for (const [k, v] of Object.entries(params.env)) {
|
||||
if (v !== undefined) {
|
||||
envArgs.push(`${k}=${v}`);
|
||||
}
|
||||
}
|
||||
// drop back to original user after PROC_CLEANUP so files aren't owned by root.
|
||||
// sudo is only needed for unshare; the actual command should run as the normal user
|
||||
// to avoid ownership mismatches with file_write/file_edit (which run in the Node.js parent).
|
||||
const username = userInfo().username;
|
||||
const escaped = params.command.replace(/'/g, "'\\''");
|
||||
return spawn(
|
||||
"sudo",
|
||||
[
|
||||
@@ -120,9 +127,9 @@ function spawnShell(params: SpawnParams): ChildProcess {
|
||||
"--mount-proc",
|
||||
"bash",
|
||||
"-c",
|
||||
params.command,
|
||||
`${PROC_CLEANUP} exec su -p -s /bin/bash ${username} -c '${escaped}'`,
|
||||
],
|
||||
{ ...spawnOpts, env: {} } // empty env since we pass via sudo env
|
||||
{ ...spawnOpts, env: {} }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/pullfrog",
|
||||
"version": "0.0.171",
|
||||
"version": "0.0.173",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -41235,7 +41235,7 @@ var Effort = type.enumerated("mini", "auto", "max");
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.171",
|
||||
version: "0.0.173",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
|
||||
@@ -159,13 +159,16 @@ export const installNodeDependencies: PrepDefinition = {
|
||||
cmd: resolved.command,
|
||||
args: resolved.args,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStdout: (chunk) => process.stdout.write(chunk),
|
||||
onStderr: (chunk) => process.stderr.write(chunk),
|
||||
});
|
||||
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
if (output) {
|
||||
log.startGroup(`${fullCommand} output`);
|
||||
log.info(output);
|
||||
log.endGroup();
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
// combine stdout and stderr for better error context (pnpm often outputs errors to stdout)
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
const errorMessage = output || `exited with code ${result.exitCode}`;
|
||||
return {
|
||||
language: "node",
|
||||
|
||||
@@ -162,21 +162,28 @@ export const installPythonDependencies: PrepDefinition = {
|
||||
|
||||
// run the install command
|
||||
const [cmd, ...args] = config.installCmd;
|
||||
log.info(`» running: ${cmd} ${args.join(" ")}`);
|
||||
const fullCommand = `${cmd} ${args.join(" ")}`;
|
||||
log.info(`» running: ${fullCommand}`);
|
||||
const result = await spawn({
|
||||
cmd,
|
||||
args,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStderr: (chunk) => process.stderr.write(chunk),
|
||||
});
|
||||
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
if (output) {
|
||||
log.startGroup(`${fullCommand} output`);
|
||||
log.info(output);
|
||||
log.endGroup();
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
language: "python",
|
||||
packageManager: config.tool,
|
||||
configFile: config.file,
|
||||
dependenciesInstalled: false,
|
||||
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`],
|
||||
issues: [output || `${cmd} exited with code ${result.exitCode}`],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ const expectedAgentEnvVars = [
|
||||
"GITHUB_TOKEN",
|
||||
...new Set(Object.values(agentsManifest).flatMap((a) => a.apiKeyNames)),
|
||||
"GEMINI_MODEL",
|
||||
"OPENCODE_MODEL_MAX",
|
||||
"OPENCODE_MODEL_MINI",
|
||||
"OPENCODE_MODEL",
|
||||
].sort();
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ const testEnvAllowList = new Set([
|
||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
"CURSOR_API_KEY",
|
||||
"OPENCODE_MODEL", // override OpenCode model (e.g. google/gemini-3-flash-preview) for tests or user preference
|
||||
"OPENCODE_MODEL_MINI", // effort-specific OpenCode model override for mini effort
|
||||
"OPENCODE_MODEL_MAX", // effort-specific OpenCode model override for max effort
|
||||
"GEMINI_MODEL", // override Gemini model (e.g. gemini-3-pro-preview) for tests or user preference
|
||||
"LOG_LEVEL",
|
||||
"DEBUG",
|
||||
|
||||
Reference in New Issue
Block a user