feat: assemble resource bundle tool aliases
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { AgentRunError } from "../common/errors.js";
|
||||
import { redactText } from "../common/redaction.js";
|
||||
@@ -8,6 +8,7 @@ import { stableHash } from "../common/validation.js";
|
||||
|
||||
export interface MaterializedResourceBundle {
|
||||
workspacePath: string;
|
||||
binPath?: string;
|
||||
event: JsonRecord;
|
||||
}
|
||||
|
||||
@@ -30,8 +31,10 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
if (actualCommit !== resourceBundleRef.commitId) throw new AgentRunError("infra-failed", "resource bundle checkout did not land on requested commit", { httpStatus: 500, details: { expectedCommit: resourceBundleRef.commitId, actualCommit } });
|
||||
const treeId = (await git(["rev-parse", "HEAD^{tree}"], checkoutPath)).stdout.trim();
|
||||
const workspacePath = resolveWorkspacePath(checkoutPath, resourceBundleRef.subdir);
|
||||
const toolAliases = await materializeToolAliases(checkoutPath, resourceBundleRef.toolAliases ?? [], env);
|
||||
return {
|
||||
workspacePath,
|
||||
...(toolAliases.binPath ? { binPath: toolAliases.binPath } : {}),
|
||||
event: {
|
||||
phase: "resource-bundle-materialized",
|
||||
kind: "git",
|
||||
@@ -42,11 +45,34 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
workspacePath: pathSummary(workspacePath),
|
||||
subdir: resourceBundleRef.subdir ?? null,
|
||||
sparsePathCount: resourceBundleRef.sparsePaths?.length ?? 0,
|
||||
toolAliases: toolAliases.event,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function materializeToolAliases(checkoutPath: string, aliases: NonNullable<ResourceBundleRef["toolAliases"]>, env: NodeJS.ProcessEnv): Promise<{ binPath?: string; event: JsonRecord }> {
|
||||
if (aliases.length === 0) return { event: { count: 0, names: [], binPath: null, valuesPrinted: false } };
|
||||
const binPath = path.resolve(env.AGENTRUN_RESOURCE_BIN_PATH ?? path.join(path.dirname(checkoutPath), ".bin"));
|
||||
await mkdir(binPath, { recursive: true });
|
||||
const names: string[] = [];
|
||||
for (const alias of aliases) {
|
||||
const target = resolveBundlePath(checkoutPath, alias.path, `toolAliases.${alias.name}.path`);
|
||||
const wrapper = path.join(binPath, alias.name);
|
||||
await writeFile(wrapper, aliasWrapper(alias.kind, target), "utf8");
|
||||
await chmod(wrapper, 0o755);
|
||||
names.push(alias.name);
|
||||
}
|
||||
return { binPath, event: { count: names.length, names, binPath: pathSummary(binPath), valuesPrinted: false } };
|
||||
}
|
||||
|
||||
function aliasWrapper(kind: string, target: string): string {
|
||||
if (kind === "node-script") return `#!/usr/bin/env sh\nexec node ${shellArg(target)} "$@"\n`;
|
||||
if (kind === "bun-script") return `#!/usr/bin/env sh\nexec bun ${shellArg(target)} "$@"\n`;
|
||||
if (kind === "sh-script") return `#!/usr/bin/env sh\nexec sh ${shellArg(target)} "$@"\n`;
|
||||
return `#!/usr/bin/env sh\nexec ${shellArg(target)} "$@"\n`;
|
||||
}
|
||||
|
||||
async function git(args: string[], cwd: string, options: { allowFailure?: boolean } = {}): Promise<{ stdout: string; stderr: string }> {
|
||||
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stdout = "";
|
||||
@@ -69,12 +95,20 @@ async function git(args: string[], cwd: string, options: { allowFailure?: boolea
|
||||
|
||||
function resolveWorkspacePath(checkoutPath: string, subdir: string | undefined): string {
|
||||
if (!subdir) return checkoutPath;
|
||||
const resolved = path.resolve(checkoutPath, subdir);
|
||||
return resolveBundlePath(checkoutPath, subdir, "resourceBundleRef.subdir");
|
||||
}
|
||||
|
||||
function resolveBundlePath(checkoutPath: string, relativePath: string, fieldName: string): string {
|
||||
const resolved = path.resolve(checkoutPath, relativePath);
|
||||
const root = path.resolve(checkoutPath);
|
||||
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) throw new AgentRunError("schema-invalid", "resource bundle subdir escaped checkout", { httpStatus: 400 });
|
||||
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) throw new AgentRunError("schema-invalid", `${fieldName} escaped checkout`, { httpStatus: 400 });
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function shellArg(value: string): string {
|
||||
return `'${value.replace(/'/gu, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function pathSummary(value: string): JsonRecord {
|
||||
const parts = value.split(/[\\/]+/u).filter(Boolean);
|
||||
return { absolute: path.isAbsolute(value), basename: parts.at(-1) ?? null, depth: parts.length, fingerprint: stableHash(value).slice(0, 16), valuePrinted: false };
|
||||
|
||||
+15
-1
@@ -65,6 +65,7 @@ export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
||||
const pollIntervalMs = normalizePollIntervalMs(options.pollIntervalMs);
|
||||
const commandResults: CommandExecutionResult[] = [];
|
||||
let workspacePath: string | undefined;
|
||||
let resourceEnv: NodeJS.ProcessEnv | undefined;
|
||||
let materializationAttempted = false;
|
||||
let materializationFailure: { failureKind: FailureKind; terminalStatus: TerminalStatus; message: string } | null = null;
|
||||
let backendSession: BackendSession | null = null;
|
||||
@@ -93,6 +94,7 @@ export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
||||
const materialized = await materializeResourceBundle(claimed.resourceBundleRef ?? null, options.env ?? process.env);
|
||||
if (materialized) {
|
||||
workspacePath = materialized.workspacePath;
|
||||
resourceEnv = materialized.binPath ? prependPath(options.env ?? process.env, materialized.binPath) : undefined;
|
||||
await api.appendEvent(options.runId, { type: "backend_status", payload: { ...materialized.event, commandId: command.id, attemptId, runnerId: runner.id } });
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -103,7 +105,7 @@ export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
||||
|
||||
const result = materializationFailure
|
||||
? await reportCommandFailure(api, options.runId, command.id, runner, attemptId, materializationFailure, "runner:resource-bundle")
|
||||
: await executeCommand(api, options, command, runner, attemptId, workspacePath, backendSession ?? (backendSession = createBackendSession(currentRun, options)));
|
||||
: await executeCommand(api, withResourceEnv(options, resourceEnv), command, runner, attemptId, workspacePath, backendSession ?? (backendSession = createBackendSession(currentRun, withResourceEnv(options, resourceEnv))));
|
||||
commandResults.push(result);
|
||||
if (options.oneShot === true) {
|
||||
const run = await api.reportStatus(options.runId, { terminalStatus: result.terminalStatus, failureKind: result.failureKind, failureMessage: null });
|
||||
@@ -130,6 +132,18 @@ export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
||||
}
|
||||
}
|
||||
|
||||
function withResourceEnv(options: RunnerOnceOptions, resourceEnv: NodeJS.ProcessEnv | undefined): RunnerOnceOptions {
|
||||
return resourceEnv ? { ...options, env: resourceEnv } : options;
|
||||
}
|
||||
|
||||
function prependPath(env: NodeJS.ProcessEnv, binPath: string): NodeJS.ProcessEnv {
|
||||
return { ...env, PATH: `${binPath}${pathDelimiter()}${env.PATH ?? process.env.PATH ?? ""}` };
|
||||
}
|
||||
|
||||
function pathDelimiter(): string {
|
||||
return process.platform === "win32" ? ";" : ":";
|
||||
}
|
||||
|
||||
async function executeCommand(api: RunnerManagerApi, options: RunnerOnceOptions, command: CommandRecord, runner: RunnerRecord, attemptId: string, workspacePath: string | undefined, backendSession: BackendSession | null): Promise<CommandExecutionResult> {
|
||||
await api.ackCommand(command.id);
|
||||
const acked = await api.getCommand(options.runId, command.id);
|
||||
|
||||
Reference in New Issue
Block a user