Merge pull request #171 from pikasTech/fix/issue79-runner-agentrun-git
fix: install runner runtime git helper with resource bundles
This commit is contained in:
@@ -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" }] };
|
||||
|
||||
Reference in New Issue
Block a user