fix: install runner runtime git helper with resource bundles

This commit is contained in:
AgentRun Codex
2026-06-11 19:15:56 +08:00
parent e624835bc8
commit c4ee4bf9f9
2 changed files with 63 additions and 25 deletions
+56 -22
View File
@@ -11,6 +11,7 @@ import { stableHash } from "../common/validation.js";
const maxPromptRefBytes = 16 * 1024;
const maxInitialPromptBytes = 64 * 1024;
const skillSummaryChars = 600;
const runtimeBundledToolNames = Object.freeze(["agentrun-git"]);
export interface MaterializedResourceBundle {
workspacePath: string;
@@ -271,48 +272,81 @@ async function prepareGitBundleTools(workspacePath: string, env: NodeJS.ProcessE
const sourceBinPath = path.join(workspacePath, "tools");
const installedBinPath = optionalNonEmpty(env.AGENTRUN_RESOURCE_BIN_PATH);
const runtimeBinPath = installedBinPath ?? sourceBinPath;
let entries;
const runtimeToolTargetBinPath = runtimeBinPath;
let entries: { name: string; isFile(): boolean }[] = [];
let sourceToolsAvailable = true;
try {
entries = await readdir(sourceBinPath, { withFileTypes: true });
} catch (error) {
if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") return { event: { count: 0, names: [], binPath: null, sourceBinPath: null, installedBinPath: null, installed: false, valuesPrinted: false } };
throw error;
if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") sourceToolsAvailable = false;
else throw error;
}
const names: string[] = [];
const items: JsonRecord[] = [];
if (installedBinPath) await mkdir(installedBinPath, { recursive: true });
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
if (!entry.isFile()) continue;
const filePath = path.join(sourceBinPath, entry.name);
const text = await readFile(filePath, "utf8");
const firstLine = text.split(/\r?\n/u, 1)[0] ?? "";
if (!firstLine.startsWith("#!")) continue;
await chmod(filePath, 0o755);
if (installedBinPath) {
const targetPath = path.join(installedBinPath, entry.name);
if (targetPath !== filePath) {
await writeFile(targetPath, installedToolShim(filePath), "utf8");
await chmod(targetPath, 0o755);
await mkdir(runtimeToolTargetBinPath, { recursive: true });
if (sourceToolsAvailable) {
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
if (!entry.isFile()) continue;
const filePath = path.join(sourceBinPath, entry.name);
const text = await readFile(filePath, "utf8");
const firstLine = text.split(/\r?\n/u, 1)[0] ?? "";
if (!firstLine.startsWith("#!")) continue;
await chmod(filePath, 0o755);
if (installedBinPath) {
const targetPath = path.join(installedBinPath, entry.name);
if (targetPath !== filePath) {
await writeFile(targetPath, installedToolShim(filePath), "utf8");
await chmod(targetPath, 0o755);
}
}
names.push(entry.name);
items.push({ name: entry.name, sha256: sha256Text(text), bytes: Buffer.byteLength(text, "utf8"), shebang: firstLine.slice(0, 80), valuesPrinted: false });
}
names.push(entry.name);
items.push({ name: entry.name, sha256: sha256Text(text), bytes: Buffer.byteLength(text, "utf8"), shebang: firstLine.slice(0, 80), valuesPrinted: false });
}
const runtimeTools = await installRuntimeBundledTools(runtimeToolTargetBinPath, names);
const hasInstalledTools = names.length > 0 || runtimeTools.names.length > 0;
return {
...(names.length > 0 ? { binPath: runtimeBinPath } : {}),
...(hasInstalledTools ? { binPath: runtimeBinPath } : {}),
event: {
count: names.length,
names,
items,
binPath: names.length > 0 ? pathSummary(runtimeBinPath) : null,
sourceBinPath: pathSummary(sourceBinPath),
runtimeTools: runtimeTools.event,
binPath: hasInstalledTools ? pathSummary(runtimeBinPath) : null,
sourceBinPath: sourceToolsAvailable ? pathSummary(sourceBinPath) : null,
installedBinPath: installedBinPath ? pathSummary(installedBinPath) : null,
installed: Boolean(installedBinPath && names.length > 0),
installed: Boolean(installedBinPath && hasInstalledTools),
valuesPrinted: false,
},
};
}
async function installRuntimeBundledTools(targetBinPath: string | undefined, resourceToolNames: string[]): Promise<{ names: string[]; event: JsonRecord }> {
const names: string[] = [];
const items: JsonRecord[] = [];
if (!targetBinPath) return { names, event: { count: 0, names, items, installed: false, installedBinPath: null, source: "agentrun-runtime", valuesPrinted: false } };
for (const name of runtimeBundledToolNames) {
const sourcePath = path.resolve(import.meta.dirname, "../../tools", name);
let text: string;
try {
text = await readFile(sourcePath, "utf8");
} catch (error) {
throw new AgentRunError("infra-failed", `runner runtime tool ${name} is not readable`, { httpStatus: 503, details: { name, sourcePath: pathSummary(sourcePath), error: fileErrorSummary(error), valuesPrinted: false } });
}
const firstLine = text.split(/\r?\n/u, 1)[0] ?? "";
if (!firstLine.startsWith("#!")) throw new AgentRunError("infra-failed", `runner runtime tool ${name} is not executable`, { httpStatus: 503, details: { name, sourcePath: pathSummary(sourcePath), valuesPrinted: false } });
await chmod(sourcePath, 0o755);
const targetPath = path.join(targetBinPath, name);
if (targetPath !== sourcePath) {
await writeFile(targetPath, installedToolShim(sourcePath), "utf8");
await chmod(targetPath, 0o755);
}
names.push(name);
items.push({ name, source: "agentrun-runtime", sha256: sha256Text(text), bytes: Buffer.byteLength(text, "utf8"), shebang: firstLine.slice(0, 80), overridesResourceTool: resourceToolNames.includes(name), valuesPrinted: false });
}
return { names, event: { count: names.length, names, items, installed: true, installedBinPath: pathSummary(targetBinPath), source: "agentrun-runtime", valuesPrinted: false } };
}
async function materializePromptRefs(checkoutPath: string, refs: NonNullable<ResourceBundleRef["promptRefs"]>): Promise<{ items: MaterializedPromptRef[]; event: JsonRecord }> {
const items: MaterializedPromptRef[] = [];
const eventItems: JsonRecord[] = [];
@@ -102,6 +102,9 @@ process.exit(1);
await access(path.join(resourceBinPath, "hwpod"));
const resourceBinExec = await execFile(path.join(resourceBinPath, "hwpod"), ["--selftest"]);
assert.match(resourceBinExec.stdout, /hwpod-selftest/u);
await access(path.join(resourceBinPath, "agentrun-git"));
const agentrunGitHelp = JSON.parse((await execFile(path.join(resourceBinPath, "agentrun-git"), ["--help"])).stdout) as JsonRecord;
assert.equal(agentrunGitHelp.tool, "agentrun-git");
const session = await store.getSession("hwlab-session-resume");
assert.equal(session?.threadId, "thread_selftest_1");
const resultEnvelope = await client.get(`/api/v1/runs/${sessionRun.runId}/commands/${sessionRun.commandId}/result`) as JsonRecord;
@@ -114,8 +117,10 @@ process.exit(1);
const resultBundleTargets = (((resultEnvelope.resourceBundleRef as JsonRecord).bundles as JsonRecord).items as JsonRecord[]).map((item) => item.targetPath);
assert.deepEqual(resultBundleTargets, ["tools", ".agents/skills"]);
const materialized = ((resultEnvelope.resourceBundleRef as JsonRecord).materialized as JsonRecord);
assert.deepEqual(((materialized.tools as JsonRecord).names), ["agentrun-git", "apply_patch", "hwpod", "tran", "trans"]);
assert.deepEqual(((materialized.tools as JsonRecord).names), ["apply_patch", "hwpod", "tran", "trans"]);
assert.equal(((materialized.tools as JsonRecord).installed), true);
assert.deepEqual((((materialized.tools as JsonRecord).runtimeTools as JsonRecord).names), ["agentrun-git"]);
assert.equal(((((materialized.tools as JsonRecord).runtimeTools as JsonRecord).items as JsonRecord[])[0]?.overridesResourceTool), false);
assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["dad-dev", "hwpod-cli", "hwpod-ctl"]);
const requiredSkillItems = ((materialized.requiredSkills as JsonRecord).items as JsonRecord[]);
assert.deepEqual(((materialized.requiredSkills as JsonRecord).names), ["dad-dev"]);
@@ -295,7 +300,6 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
await writeFile(path.join(repo, "tools", "tran"), "#!/usr/bin/env sh\necho tran-selftest\n", "utf8");
await writeFile(path.join(repo, "tools", "trans"), "#!/usr/bin/env sh\necho trans-selftest\n", "utf8");
await writeFile(path.join(repo, "tools", "apply_patch"), "#!/usr/bin/env sh\necho apply-patch-selftest\n", "utf8");
await writeFile(path.join(repo, "tools", "agentrun-git"), "#!/usr/bin/env sh\necho agentrun-git-selftest\n", "utf8");
await writeFile(path.join(repo, "tools", "hwpod-cli.ts"), "import { hwpodSelftestName } from './src/hwpod-harness-lib.ts';\nconsole.log(JSON.stringify({ ok: true, cli: hwpodSelftestName(), argv: process.argv.slice(2) }));\n", "utf8");
await writeFile(path.join(repo, "tools", "src", "hwpod-harness-lib.ts"), "export function hwpodSelftestName() { return 'hwpod-selftest'; }\n", "utf8");
await writeFile(path.join(repo, "tools", "hwpod-node.test.ts"), "console.log('test-only source file without shebang');\n", "utf8");
@@ -336,7 +340,7 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
"Use hwpod-ctl for HWPOD runtime inspection and control-plane state.",
].join("\n"), "utf8");
await writeFile(path.join(repo, "skills", "hwpod-ctl", "scripts", "hwpod-ctl.mjs"), "console.log(JSON.stringify({ ok: true, cli: 'hwpod-ctl-skill-selftest' }));\n", "utf8");
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/tran", "tools/trans", "tools/apply_patch", "tools/agentrun-git", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/dad-dev/SKILL.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/tran", "tools/trans", "tools/apply_patch", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/dad-dev/SKILL.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", "bundle selftest"], { cwd: repo });
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd: repo });
return { repoUrl: repo, commitId: stdout.trim(), requiredSkills: [{ name: "dad-dev" }] };