import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { chmod, cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { AgentRunError } from "../common/errors.js"; import { redactText } from "../common/redaction.js"; import type { InitialPromptAssembly, JsonRecord, ResourceBundleRef, WorkspaceRef } from "../common/types.js"; import { defaultGitDirectHosts, defaultGitHttpVersion, defaultGitLowSpeedLimitBytes, defaultGitLowSpeedTimeSeconds, defaultGitOperationTimeoutMs, gitTransportSummary } from "../common/git-transport.js"; import { stableHash } from "../common/validation.js"; import { bundledWorkReadyTools } from "../common/work-ready.js"; const maxPromptRefBytes = 16 * 1024; const maxInitialPromptBytes = 64 * 1024; const skillSummaryChars = 600; const runtimeBundledToolNames = Object.freeze(bundledWorkReadyTools.map((tool) => tool.name)); const managedOverlayDirectory = "agentrun-managed-overlay"; export interface MaterializedResourceBundle { workspacePath: string; binPath?: string; skillsDir?: string; initialPrompt?: InitialPromptAssembly; event: JsonRecord; } interface MaterializedPromptRef { name: string; path: string; inject: "thread-start"; required: boolean; text: string; bytes: number; sha256: string; } interface MaterializedSkillRef { name: string; path: string; aggregateAs: string; required: boolean; registryPath: string; manifestBytes: number; manifestSha256: string; summary: string; sourceBundle: JsonRecord | null; } interface GitCheckout { repoUrl: string; fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl?: string; commitId: string; requestedCommitId?: string; requestedRef?: string; checkoutPath: string; treeId: string; branch: string | null; } interface GitMirrorConfig { baseUrl: string; } interface GitBundleSource { repoUrl: string; commitId?: string; ref?: string; gitMirror?: GitMirrorConfig; credentialRefPresent: boolean; credentialEnv?: NodeJS.ProcessEnv; } interface GitBundleSourceContext extends JsonRecord { kind: "resource-bundle" | "bundle"; index: number | null; name: string | null; } interface GitSourceRef { kind: "ref" | "commit"; value: string; } interface MaterializedGitBundle extends JsonRecord { name: string | null; repoUrl: string; fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl: string | null; commitId: string; requestedCommitId: string | null; requestedRef: string | null; subpath: string; targetPath: string; sourceKind: "file" | "directory"; sourceBytes: number | null; } interface PrimarySourceContract extends JsonRecord { repoUrl: string; requestedRef: string | null; requestedCommitId: string | null; } interface ResolvedPrimarySource extends JsonRecord { commitId: string; treeId: string; branch: string | null; fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl: string | null; } interface PrimaryWorkspaceProvenance extends JsonRecord { schemaVersion: 1; declaredSource: PrimarySourceContract; resolvedSource: ResolvedPrimarySource; resolutionAuthority: "runner-first-materialization" | "declared-commit"; assemblyContractHash: string; bundleContractHash: string; bundleProvenance: MaterializedGitBundle[]; provenanceHash: string; } type PrimaryWorkspaceState = | { kind: "uninitialized" } | { kind: "continuation"; provenance: PrimaryWorkspaceProvenance }; const primaryWorkspaceProvenanceFile = "agentrun-primary-workspace.json"; export async function materializeResourceBundle(resourceBundleRef: ResourceBundleRef | null | undefined, workspaceRef: WorkspaceRef, env: NodeJS.ProcessEnv = process.env): Promise { if (!resourceBundleRef) return null; assertPrimaryWorkspaceRef(workspaceRef, resourceBundleRef); const workspaceRoot = path.resolve(env.AGENTRUN_WORKSPACE_ROOT ?? "/home/agentrun/workspaces"); const runScope = env.AGENTRUN_RUN_ID ?? env.AGENTRUN_ATTEMPT_ID ?? "standalone"; const assemblyRoot = path.join(workspaceRoot, `gitbundle-${stableHash({ runScope, resourceBundleRef }).slice(0, 16)}`); const checkoutRoot = path.join(assemblyRoot, "checkouts"); const explicitWorkspacePath = optionalNonEmpty(env.AGENTRUN_WORKSPACE_PATH); const workspacePath = explicitWorkspacePath ? path.resolve(explicitWorkspacePath) : path.join(assemblyRoot, "workspace"); if (explicitWorkspacePath && isSameOrChildPath(workspacePath, assemblyRoot)) { throw new AgentRunError("schema-invalid", "AGENTRUN_WORKSPACE_PATH must not be inside transient resource assembly root", { httpStatus: 400, details: { workspacePath: pathSummary(workspacePath), assemblyRoot: pathSummary(assemblyRoot), valuesPrinted: false } }); } const declaredSource = primarySourceContract(resourceBundleRef); const assemblyContractHash = primaryAssemblyContractHash(resourceBundleRef, workspaceRef); const bundleContractHash = stableHash(resourceBundleRef.bundles); const initialState = await inspectPrimaryWorkspace(workspacePath, declaredSource, assemblyContractHash, bundleContractHash); const gitMirror = gitMirrorConfig(resourceBundleRef, env); let provenance: PrimaryWorkspaceProvenance; let materializedBundles: MaterializedGitBundle[]; let checkoutPath: string | null = null; let workspaceLifecycle: "first-materialization" | "continuation-reuse"; if (initialState.kind === "continuation") { provenance = initialState.provenance; materializedBundles = provenance.bundleProvenance; workspaceLifecycle = "continuation-reuse"; } else { await rm(checkoutRoot, { recursive: true, force: true }); await mkdir(checkoutRoot, { recursive: true }); const credentialEnv = await resourceCredentialEnv(resourceBundleRef, env); const defaultSource = defaultGitBundleSource(resourceBundleRef, env, gitMirror, credentialEnv); const checkoutCache = new Map>(); const checkoutFor = (source: GitBundleSource, sourceContext: GitBundleSourceContext) => { const key = stableHash(gitSourceIdentity(source)); let checkout = checkoutCache.get(key); if (!checkout) { checkout = checkoutGitSource(checkoutRoot, source, sourceContext); checkoutCache.set(key, checkout); } return checkout; }; const defaultCheckout = await checkoutFor(defaultSource, { kind: "resource-bundle", index: null, name: null }); checkoutPath = defaultCheckout.checkoutPath; const stagePath = await stagePrimaryWorkspace(defaultCheckout.checkoutPath, workspacePath, runScope); try { const stageOverlayRoot = await prepareManagedOverlay(stagePath); const stagedBundles = await materializeGitBundles(stageOverlayRoot, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor); const stagedProvenance = createPrimaryWorkspaceProvenance(resourceBundleRef, workspaceRef, defaultCheckout, stagedBundles); await assertCleanPrimarySourceView(stagePath); await verifyPrimaryWorkspace(stagePath, workspaceRef, stagedProvenance, "first-materialization", true); await writePrimaryWorkspaceProvenance(stagePath, stagedProvenance); const installed = await installPrimaryWorkspace(stagePath, workspacePath); const installedState = await inspectPrimaryWorkspace(workspacePath, declaredSource, assemblyContractHash, bundleContractHash); if (installedState.kind !== "continuation") { throw new AgentRunError("infra-failed", "primary workspace installation did not produce durable provenance", { httpStatus: 500, details: { workspacePath: pathSummary(workspacePath), valuesPrinted: false } }); } provenance = installedState.provenance; materializedBundles = provenance.bundleProvenance; workspaceLifecycle = installed ? "first-materialization" : "continuation-reuse"; if (!installed) checkoutPath = null; } finally { await rm(stagePath, { recursive: true, force: true }); } } const primaryWorkspace = await verifyPrimaryWorkspace(workspacePath, workspaceRef, provenance, workspaceLifecycle, false); const managedOverlayRoot = await verifyManagedOverlay(workspacePath, materializedBundles); const tools = await prepareGitBundleTools(managedOverlayRoot, env); const skills = await discoverGitBundleSkills(managedOverlayRoot, materializedBundles); const requiredSkills = materializeRequiredSkills(resourceBundleRef.requiredSkills ?? [], skills.items); const prompts = await materializePromptRefs(checkoutPath ?? workspacePath, resourceBundleRef.promptRefs ?? []); const initialPrompt = assembleInitialPrompt(prompts.items, skills.items); const resolvedSource = provenance.resolvedSource; return { workspacePath, ...(tools.binPath ? { binPath: tools.binPath } : {}), ...(skills.skillsDir ? { skillsDir: skills.skillsDir } : {}), ...(initialPrompt ? { initialPrompt } : {}), event: { phase: "resource-bundle-materialized", kind: "gitbundle", repoUrl: resourceBundleRef.repoUrl, fetchRepoUrl: resolvedSource.fetchRepoUrl, mirrorUsed: resolvedSource.mirrorUsed, mirrorBaseUrl: resolvedSource.mirrorBaseUrl, gitMirror: resolvedSource.mirrorUsed ? { enabled: true, baseUrl: resolvedSource.mirrorBaseUrl, valuesPrinted: false } : { enabled: false, baseUrl: null, valuesPrinted: false }, commitId: resolvedSource.commitId, requestedCommitId: resourceBundleRef.commitId ?? null, requestedRef: resourceBundleRef.ref ?? null, treeId: resolvedSource.treeId, checkoutPath: checkoutPath ? pathSummary(checkoutPath) : null, workspacePath: pathSummary(workspacePath), workspacePersistence: explicitWorkspacePath ? { mode: "session", path: pathSummary(workspacePath), valuesPrinted: false } : { mode: "run", path: pathSummary(workspacePath), valuesPrinted: false }, workspaceLifecycle, resolutionAuthority: provenance.resolutionAuthority, primaryWorkspace, managedOverlay: { kind: "git-metadata", root: pathSummary(managedOverlayRoot), rootRelativeToWorkspace: `.git/${managedOverlayDirectory}`, sourceViewIsolated: true, targetPathAuthority: "resourceBundleRef.bundles[].targetPath", valuesPrinted: false, }, gitTransport: gitTransportSummary({ scope: "resource-bundle" }), bundles: { count: materializedBundles.length, items: materializedBundles.map((item) => ({ ...item, logicalTargetPath: item.targetPath, storageKind: "git-metadata-overlay", valuesPrinted: false })), valuesPrinted: false, }, tools: tools.event, skillDirs: skills.event, requiredSkills: requiredSkills.event, promptRefs: prompts.event, initialPrompt: initialPrompt?.summary ?? { available: false, bytes: 0, sha256: null, promptRefCount: prompts.items.length, skillCount: skills.items.length, valuesPrinted: false }, valuesPrinted: false, }, }; } function isSameOrChildPath(candidate: string, parent: string): boolean { const relative = path.relative(parent, candidate); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } function gitMirrorConfig(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): GitMirrorConfig | undefined { void resourceBundleRef; return defaultGitMirrorConfig(env); } function defaultGitMirrorConfig(env: NodeJS.ProcessEnv): GitMirrorConfig { const baseUrl = optionalNonEmpty(env.AGENTRUN_GIT_MIRROR_BASE_URL) ?? "http://git-mirror-http.devops-infra.svc.cluster.local"; return { baseUrl: normalizeMirrorBaseUrl(baseUrl) }; } function defaultGitBundleSource(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv, gitMirror?: GitMirrorConfig, credentialEnv?: NodeJS.ProcessEnv): GitBundleSource { void env; const ref = optionalNonEmpty(resourceBundleRef.ref); const commitId = optionalNonEmpty(resourceBundleRef.commitId); const credentialRefPresent = resourceBundleRef.credentialRef !== undefined; if (!ref && !commitId) throw new AgentRunError("schema-invalid", "gitbundle primary source must declare ref or commitId", { httpStatus: 400 }); return { repoUrl: resourceBundleRef.repoUrl, ...(ref ? { ref } : {}), ...(commitId ? { commitId } : {}), credentialRefPresent, ...(gitMirror ? { gitMirror } : {}), ...(credentialEnv ? { credentialEnv } : {}) }; } function bundleGitSource(bundle: ResourceBundleRef["bundles"][number], resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource): GitBundleSource { const repoUrl = bundle.repoUrl ?? resourceBundleRef.repoUrl; const ref = optionalNonEmpty(bundle.ref); const mirror = defaultSource.gitMirror; const credentialRefPresent = defaultSource.credentialRefPresent; const credentialEnv = defaultSource.credentialEnv; const commitId = optionalNonEmpty(bundle.commitId); if (ref || commitId) return { repoUrl, ...(ref ? { ref } : {}), ...(commitId ? { commitId } : {}), credentialRefPresent, ...(mirror ? { gitMirror: mirror } : {}), ...(credentialEnv ? { credentialEnv } : {}) }; if (repoUrl === defaultSource.repoUrl) return defaultSource; throw new AgentRunError("schema-invalid", "cross-repository bundle must declare its own ref or commitId", { httpStatus: 400 }); } async function checkoutGitSource(checkoutRoot: string, source: GitBundleSource, sourceContext: GitBundleSourceContext): Promise { const checkoutPath = path.join(checkoutRoot, stableHash(gitSourceIdentity(source)).slice(0, 16)); const fetch = gitFetchSource(source); const sourceRef: GitSourceRef = source.ref ? { kind: "ref", value: source.ref } : { kind: "commit", value: source.commitId ?? "" }; const fetchOptions: GitCommandOptions = { operation: "fetch", repoUrl: source.repoUrl, fetchRepoUrl: fetch.fetchRepoUrl, mirrorUsed: fetch.mirrorUsed, sourceRef, sourceContext, credentialRefPresent: source.credentialRefPresent, ...(source.credentialEnv ? { env: source.credentialEnv } : {}), }; await mkdir(checkoutPath, { recursive: true }); await git(["init"], checkoutPath); await git(["remote", "remove", "origin"], checkoutPath, { allowFailure: true }); await git(["remote", "add", "origin", fetch.fetchRepoUrl], checkoutPath); let branch: string | null = null; if (source.ref) { await git(["fetch", "--depth", "1", "origin", source.ref], checkoutPath, fetchOptions); branch = localBranchFromRef(source.ref); if (branch) await git(["checkout", "-B", branch, "FETCH_HEAD"], checkoutPath); else await git(["checkout", "--detach", "FETCH_HEAD"], checkoutPath); } else if (source.commitId) { await git(["fetch", "--depth", "1", "origin", source.commitId], checkoutPath, fetchOptions); await git(["checkout", "--detach", source.commitId], checkoutPath); } else { throw new AgentRunError("schema-invalid", "gitbundle source must include repo ref or commit", { httpStatus: 400 }); } const actualCommit = (await git(["rev-parse", "HEAD"], checkoutPath)).stdout.trim(); if (source.commitId && actualCommit !== source.commitId) throw new AgentRunError("infra-failed", "gitbundle checkout did not land on requested commit", { httpStatus: 500, details: { expectedCommit: source.commitId, actualCommit } }); const treeId = (await git(["rev-parse", "HEAD^{tree}"], checkoutPath)).stdout.trim(); await git(["remote", "set-url", "origin", source.repoUrl], checkoutPath); if (branch) { await git(["config", `branch.${branch}.remote`, "origin"], checkoutPath); await git(["config", `branch.${branch}.merge`, `refs/heads/${branch}`], checkoutPath); } return { repoUrl: source.repoUrl, fetchRepoUrl: fetch.fetchRepoUrl, mirrorUsed: fetch.mirrorUsed, ...(fetch.mirrorBaseUrl ? { mirrorBaseUrl: fetch.mirrorBaseUrl } : {}), commitId: actualCommit, ...(source.commitId ? { requestedCommitId: source.commitId } : {}), ...(source.ref ? { requestedRef: source.ref } : {}), checkoutPath, treeId, branch }; } function gitSourceIdentity(source: GitBundleSource): JsonRecord { return { repoUrl: source.repoUrl, commitId: source.commitId ?? null, ref: source.ref ?? null, gitMirror: source.gitMirror ? { baseUrl: source.gitMirror.baseUrl } : null }; } async function resourceCredentialEnv(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): Promise { const credentialRef = resourceBundleRef.credentialRef; if (!credentialRef) return undefined; const credentialPath = optionalNonEmpty(env.AGENTRUN_RESOURCE_CREDENTIAL_PATH); if (!credentialPath) { throw new AgentRunError("secret-unavailable", "resource bundle credentialRef was declared but its runtime projection is missing", { httpStatus: 503, details: { credentialRef: { name: credentialRef.name, namespace: credentialRef.namespace ?? null, keys: credentialRef.keys ?? [], valuesPrinted: false }, valuesPrinted: false } }); } const absolutePath = path.resolve(credentialPath); try { const info = await stat(absolutePath); if (!info.isDirectory()) throw new Error("credential projection is not a directory"); for (const key of credentialRef.keys ?? []) await stat(path.join(absolutePath, key)); } catch (error) { throw new AgentRunError("secret-unavailable", "resource bundle credential projection is incomplete", { httpStatus: 503, details: { credentialRef: { name: credentialRef.name, namespace: credentialRef.namespace ?? null, keys: credentialRef.keys ?? [], valuesPrinted: false }, projectionPath: pathSummary(absolutePath), error: fileErrorSummary(error), valuesPrinted: false } }); } const keys = new Set(credentialRef.keys ?? []); if (keys.has("id_ed25519") && keys.has("known_hosts")) { const args = [ "ssh", ...(keys.has("config") ? ["-F", `${absolutePath}/config`] : []), "-i", `${absolutePath}/id_ed25519`, "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=yes", "-o", `UserKnownHostsFile=${absolutePath}/known_hosts`, ]; return { GIT_SSH_COMMAND: args.join(" ") }; } return {}; } function localBranchFromRef(ref: string): string | null { const normalized = ref.replace(/^refs\/heads\//u, ""); if (normalized !== ref || (!ref.startsWith("refs/") && !/^[0-9a-f]{40}$/u.test(ref))) return normalized; return null; } function assertPrimaryWorkspaceRef(workspaceRef: WorkspaceRef, resourceBundleRef: ResourceBundleRef): void { void resourceBundleRef; if (workspaceRef.kind !== "opaque" || workspaceRef.path !== ".") { throw new AgentRunError("schema-invalid", "gitbundle primary workspace requires workspaceRef.kind=opaque and workspaceRef.path=.", { httpStatus: 400 }); } const rejectedKeys = Object.keys(workspaceRef).filter((key) => key !== "kind" && key !== "path"); if (rejectedKeys.length > 0) { throw new AgentRunError("schema-invalid", "gitbundle primary workspaceRef only supports kind and path; resourceBundleRef is the source authority", { httpStatus: 400, details: { rejectedKeys: rejectedKeys.sort(), valuesPrinted: false } }); } } function primarySourceContract(resourceBundleRef: ResourceBundleRef): PrimarySourceContract { return { repoUrl: resourceBundleRef.repoUrl, requestedRef: resourceBundleRef.ref ?? null, requestedCommitId: resourceBundleRef.commitId ?? null, }; } function primaryAssemblyContractHash(resourceBundleRef: ResourceBundleRef, workspaceRef: WorkspaceRef): string { return stableHash({ schemaVersion: 1, workspaceRef: { kind: workspaceRef.kind, path: workspaceRef.path ?? null }, bundles: resourceBundleRef.bundles, promptRefs: resourceBundleRef.promptRefs ?? [], requiredSkills: resourceBundleRef.requiredSkills ?? [], submodules: resourceBundleRef.submodules ?? false, lfs: resourceBundleRef.lfs ?? false, }); } function createPrimaryWorkspaceProvenance(resourceBundleRef: ResourceBundleRef, workspaceRef: WorkspaceRef, checkout: GitCheckout, bundles: MaterializedGitBundle[]): PrimaryWorkspaceProvenance { const body: JsonRecord = { schemaVersion: 1, declaredSource: primarySourceContract(resourceBundleRef), resolvedSource: { commitId: checkout.commitId, treeId: checkout.treeId, branch: checkout.branch, fetchRepoUrl: checkout.fetchRepoUrl, mirrorUsed: checkout.mirrorUsed, mirrorBaseUrl: checkout.mirrorBaseUrl ?? null, }, resolutionAuthority: resourceBundleRef.commitId ? "declared-commit" : "runner-first-materialization", assemblyContractHash: primaryAssemblyContractHash(resourceBundleRef, workspaceRef), bundleContractHash: stableHash(resourceBundleRef.bundles), bundleProvenance: bundles, }; return { ...body, provenanceHash: stableHash(body) } as PrimaryWorkspaceProvenance; } async function inspectPrimaryWorkspace(workspacePath: string, declaredSource: PrimarySourceContract, assemblyContractHash: string, bundleContractHash: string): Promise { let root; try { root = await stat(workspacePath); } catch (error) { if (hasErrorCode(error, "ENOENT")) return { kind: "uninitialized" }; throw new AgentRunError("infra-failed", "primary workspace cannot be inspected", { httpStatus: 500, details: { workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } }); } if (!root.isDirectory()) { throw new AgentRunError("schema-invalid", "primary workspace path exists but is not a directory", { httpStatus: 400, details: { workspacePath: pathSummary(workspacePath), valuesPrinted: false } }); } let entries; try { entries = await readdir(workspacePath); } catch (error) { throw new AgentRunError("infra-failed", "primary workspace directory cannot be read", { httpStatus: 500, details: { workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } }); } if (entries.length === 0) return { kind: "uninitialized" }; const provenance = await readPrimaryWorkspaceProvenance(workspacePath); assertPrimaryWorkspaceProvenanceContract(provenance, declaredSource, assemblyContractHash, bundleContractHash); return { kind: "continuation", provenance }; } async function readPrimaryWorkspaceProvenance(workspacePath: string): Promise { const file = path.join(workspacePath, ".git", primaryWorkspaceProvenanceFile); let text: string; try { text = await readFile(file, "utf8"); } catch (error) { if (hasErrorCode(error, "ENOENT")) { throw new AgentRunError("schema-invalid", "non-empty primary workspace is missing AgentRun provenance", { httpStatus: 400, details: { workspacePath: pathSummary(workspacePath), provenancePath: pathSummary(file), valuesPrinted: false } }); } throw new AgentRunError("infra-failed", "primary workspace provenance cannot be read", { httpStatus: 500, details: { provenancePath: pathSummary(file), error: fileErrorSummary(error), valuesPrinted: false } }); } let parsed: unknown; try { parsed = JSON.parse(text); } catch (error) { throw new AgentRunError("schema-invalid", "primary workspace provenance is not valid JSON", { httpStatus: 400, details: { provenancePath: pathSummary(file), error: fileErrorSummary(error), valuesPrinted: false } }); } if (!isJsonObject(parsed)) { throw new AgentRunError("schema-invalid", "primary workspace provenance must be an object", { httpStatus: 400, details: { provenancePath: pathSummary(file), valuesPrinted: false } }); } const provenanceHash = requiredProvenanceString(parsed, "provenanceHash"); const hashBody = { ...parsed }; delete hashBody.provenanceHash; if (stableHash(hashBody) !== provenanceHash) { throw new AgentRunError("schema-invalid", "primary workspace provenance hash does not match its content", { httpStatus: 400, details: { provenancePath: pathSummary(file), valuesPrinted: false } }); } if (parsed.schemaVersion !== 1) { throw new AgentRunError("schema-invalid", "primary workspace provenance schemaVersion is unsupported", { httpStatus: 400, details: { schemaVersion: parsed.schemaVersion ?? null, valuesPrinted: false } }); } const declaredSourceRecord = requiredProvenanceObject(parsed, "declaredSource"); const resolvedSourceRecord = requiredProvenanceObject(parsed, "resolvedSource"); const resolutionAuthority = parsed.resolutionAuthority; if (resolutionAuthority !== "runner-first-materialization" && resolutionAuthority !== "declared-commit") { throw new AgentRunError("schema-invalid", "primary workspace provenance resolutionAuthority is invalid", { httpStatus: 400 }); } if (!Array.isArray(parsed.bundleProvenance)) { throw new AgentRunError("schema-invalid", "primary workspace bundle provenance must be an array", { httpStatus: 400 }); } const provenance: PrimaryWorkspaceProvenance = { schemaVersion: 1, declaredSource: { repoUrl: requiredProvenanceString(declaredSourceRecord, "repoUrl"), requestedRef: nullableProvenanceString(declaredSourceRecord, "requestedRef"), requestedCommitId: nullableProvenanceString(declaredSourceRecord, "requestedCommitId"), }, resolvedSource: { commitId: requiredProvenanceString(resolvedSourceRecord, "commitId"), treeId: requiredProvenanceString(resolvedSourceRecord, "treeId"), branch: nullableProvenanceString(resolvedSourceRecord, "branch"), fetchRepoUrl: requiredProvenanceString(resolvedSourceRecord, "fetchRepoUrl"), mirrorUsed: requiredProvenanceBoolean(resolvedSourceRecord, "mirrorUsed"), mirrorBaseUrl: nullableProvenanceString(resolvedSourceRecord, "mirrorBaseUrl"), }, resolutionAuthority, assemblyContractHash: requiredProvenanceString(parsed, "assemblyContractHash"), bundleContractHash: requiredProvenanceString(parsed, "bundleContractHash"), bundleProvenance: parsed.bundleProvenance.map((item, index) => parseMaterializedBundleProvenance(item, index)), provenanceHash, }; return provenance; } function assertPrimaryWorkspaceProvenanceContract(provenance: PrimaryWorkspaceProvenance, declaredSource: PrimarySourceContract, assemblyContractHash: string, bundleContractHash: string): void { if (stableHash(provenance.declaredSource) !== stableHash(declaredSource)) { throw new AgentRunError("schema-invalid", "primary workspace declared source conflicts with frozen provenance", { httpStatus: 400, details: { expectedSourceHash: stableHash(declaredSource), actualSourceHash: stableHash(provenance.declaredSource), valuesPrinted: false } }); } if (provenance.assemblyContractHash !== assemblyContractHash || provenance.bundleContractHash !== bundleContractHash) { throw new AgentRunError("schema-invalid", "primary workspace assembly contract conflicts with frozen provenance", { httpStatus: 400, details: { expectedAssemblyContractHash: assemblyContractHash, actualAssemblyContractHash: provenance.assemblyContractHash, expectedBundleContractHash: bundleContractHash, actualBundleContractHash: provenance.bundleContractHash, valuesPrinted: false } }); } const expectedAuthority = declaredSource.requestedCommitId ? "declared-commit" : "runner-first-materialization"; if (provenance.resolutionAuthority !== expectedAuthority) { throw new AgentRunError("schema-invalid", "primary workspace resolution authority conflicts with declared source", { httpStatus: 400, details: { expectedAuthority, actualAuthority: provenance.resolutionAuthority, valuesPrinted: false } }); } if (declaredSource.requestedCommitId && provenance.resolvedSource.commitId !== declaredSource.requestedCommitId) { throw new AgentRunError("schema-invalid", "primary workspace frozen commit conflicts with declared commit", { httpStatus: 400, details: { declaredCommit: declaredSource.requestedCommitId, frozenCommit: provenance.resolvedSource.commitId, valuesPrinted: false } }); } } function parseMaterializedBundleProvenance(value: unknown, index: number): MaterializedGitBundle { if (!isJsonObject(value)) throw new AgentRunError("schema-invalid", `primary workspace bundle provenance[${index}] must be an object`, { httpStatus: 400 }); const sourceKind = value.sourceKind; if (sourceKind !== "file" && sourceKind !== "directory") throw new AgentRunError("schema-invalid", `primary workspace bundle provenance[${index}].sourceKind is invalid`, { httpStatus: 400 }); const sourceBytes = value.sourceBytes; if (sourceBytes !== null && (typeof sourceBytes !== "number" || !Number.isFinite(sourceBytes) || sourceBytes < 0)) { throw new AgentRunError("schema-invalid", `primary workspace bundle provenance[${index}].sourceBytes is invalid`, { httpStatus: 400 }); } return { name: nullableProvenanceString(value, "name"), repoUrl: requiredProvenanceString(value, "repoUrl"), fetchRepoUrl: requiredProvenanceString(value, "fetchRepoUrl"), mirrorUsed: requiredProvenanceBoolean(value, "mirrorUsed"), mirrorBaseUrl: nullableProvenanceString(value, "mirrorBaseUrl"), commitId: requiredProvenanceString(value, "commitId"), requestedCommitId: nullableProvenanceString(value, "requestedCommitId"), requestedRef: nullableProvenanceString(value, "requestedRef"), subpath: requiredProvenanceString(value, "subpath"), targetPath: requiredProvenanceString(value, "targetPath"), sourceKind, sourceBytes, }; } function isJsonObject(value: unknown): value is JsonRecord { return typeof value === "object" && value !== null && !Array.isArray(value); } function requiredProvenanceObject(record: JsonRecord, key: string): JsonRecord { const value = record[key]; if (!isJsonObject(value)) throw new AgentRunError("schema-invalid", `primary workspace provenance ${key} must be an object`, { httpStatus: 400 }); return value; } function requiredProvenanceString(record: JsonRecord, key: string): string { const value = record[key]; if (typeof value !== "string" || value.trim().length === 0) throw new AgentRunError("schema-invalid", `primary workspace provenance ${key} must be a non-empty string`, { httpStatus: 400 }); return value; } function nullableProvenanceString(record: JsonRecord, key: string): string | null { const value = record[key]; if (value === null) return null; if (typeof value !== "string" || value.trim().length === 0) throw new AgentRunError("schema-invalid", `primary workspace provenance ${key} must be a string or null`, { httpStatus: 400 }); return value; } function requiredProvenanceBoolean(record: JsonRecord, key: string): boolean { const value = record[key]; if (typeof value !== "boolean") throw new AgentRunError("schema-invalid", `primary workspace provenance ${key} must be boolean`, { httpStatus: 400 }); return value; } function hasErrorCode(error: unknown, code: string): boolean { return typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === code; } async function stagePrimaryWorkspace(checkoutPath: string, workspacePath: string, runScope: string): Promise { const parent = path.dirname(workspacePath); const stage = path.join(parent, `.${path.basename(workspacePath)}.agentrun-stage-${stableHash({ runScope, workspacePath }).slice(0, 12)}`); if (path.resolve(checkoutPath) === path.resolve(workspacePath)) { throw new AgentRunError("schema-invalid", "primary checkout cannot be the durable workspace path", { httpStatus: 400 }); } await mkdir(parent, { recursive: true }); await rm(stage, { recursive: true, force: true }); try { await cp(checkoutPath, stage, { recursive: true, force: true, dereference: false }); return stage; } catch (error) { await rm(stage, { recursive: true, force: true }); throw new AgentRunError("infra-failed", "primary workspace staging failed", { httpStatus: 500, details: { stage: pathSummary(stage), workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } }); } } async function writePrimaryWorkspaceProvenance(workspacePath: string, provenance: PrimaryWorkspaceProvenance): Promise { const file = path.join(workspacePath, ".git", primaryWorkspaceProvenanceFile); try { await writeFile(file, `${JSON.stringify(provenance, null, 2)}\n`, { encoding: "utf8", flag: "wx" }); } catch (error) { throw new AgentRunError("infra-failed", "primary workspace provenance could not be persisted", { httpStatus: 500, details: { provenancePath: pathSummary(file), error: fileErrorSummary(error), valuesPrinted: false } }); } } async function installPrimaryWorkspace(stagePath: string, workspacePath: string): Promise { let targetIsInstallable = false; try { const root = await stat(workspacePath); targetIsInstallable = root.isDirectory() && (await readdir(workspacePath)).length === 0; } catch (error) { if (hasErrorCode(error, "ENOENT")) targetIsInstallable = true; else throw new AgentRunError("infra-failed", "primary workspace install target cannot be inspected", { httpStatus: 500, details: { workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } }); } if (!targetIsInstallable) return false; try { await rename(stagePath, workspacePath); return true; } catch (error) { if (hasErrorCode(error, "EEXIST") || hasErrorCode(error, "ENOTEMPTY")) return false; throw new AgentRunError("infra-failed", "primary workspace atomic installation failed", { httpStatus: 500, details: { stage: pathSummary(stagePath), workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } }); } } async function prepareManagedOverlay(workspacePath: string): Promise { const overlayRoot = managedOverlayRoot(workspacePath); await rm(overlayRoot, { recursive: true, force: true }); await mkdir(overlayRoot, { recursive: true }); return overlayRoot; } async function verifyManagedOverlay(workspacePath: string, bundles: MaterializedGitBundle[]): Promise { const overlayRoot = managedOverlayRoot(workspacePath); let root; try { root = await stat(overlayRoot); } catch (error) { throw new AgentRunError("schema-invalid", "primary workspace managed capability overlay is missing", { httpStatus: 400, details: { overlayRoot: pathSummary(overlayRoot), error: fileErrorSummary(error), valuesPrinted: false } }); } if (!root.isDirectory()) { throw new AgentRunError("schema-invalid", "primary workspace managed capability overlay is not a directory", { httpStatus: 400, details: { overlayRoot: pathSummary(overlayRoot), valuesPrinted: false } }); } for (const [index, bundle] of bundles.entries()) { const target = resolveManagedOverlayTargetPath(overlayRoot, bundle.targetPath, `bundleProvenance[${index}].targetPath`); let targetStat; try { targetStat = await stat(target); } catch (error) { throw new AgentRunError("schema-invalid", "primary workspace managed capability target is missing", { httpStatus: 400, details: { index, targetPath: bundle.targetPath, overlayRoot: pathSummary(overlayRoot), error: fileErrorSummary(error), valuesPrinted: false } }); } const actualKind = targetStat.isDirectory() ? "directory" : targetStat.isFile() ? "file" : "unsupported"; if (actualKind !== bundle.sourceKind) { throw new AgentRunError("schema-invalid", "primary workspace managed capability target kind conflicts with frozen provenance", { httpStatus: 400, details: { index, targetPath: bundle.targetPath, expectedKind: bundle.sourceKind, actualKind, valuesPrinted: false } }); } } return overlayRoot; } function managedOverlayRoot(workspacePath: string): string { return path.join(workspacePath, ".git", managedOverlayDirectory); } async function assertCleanPrimarySourceView(workspacePath: string): Promise { const status = await git(["status", "--porcelain=v1", "--untracked-files=all"], workspacePath); if (status.stdout.trim().length > 0) { throw new AgentRunError("infra-failed", "managed capability assembly polluted the staged primary Git source view", { httpStatus: 500, details: { changedEntries: status.stdout.split(/\r?\n/u).filter((line) => line.trim().length > 0).length, valuesPrinted: false, }, }); } } async function verifyPrimaryWorkspace(workspacePath: string, workspaceRef: WorkspaceRef, provenance: PrimaryWorkspaceProvenance, workspaceLifecycle: "first-materialization" | "continuation-reuse", requireExactSource: boolean): Promise { let rootStat; let gitStat; try { rootStat = await stat(workspacePath); gitStat = await stat(path.join(workspacePath, ".git")); } catch (error) { throw new AgentRunError("infra-failed", "primary workspace is missing its Git root", { httpStatus: 500, details: { workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } }); } if (!rootStat.isDirectory() || (!gitStat.isDirectory() && !gitStat.isFile())) { throw new AgentRunError("infra-failed", "primary workspace is not a materialized Git directory", { httpStatus: 500, details: { workspacePath: pathSummary(workspacePath), valuesPrinted: false } }); } const actualCommit = (await git(["rev-parse", "HEAD"], workspacePath)).stdout.trim(); const frozenCommit = provenance.resolvedSource.commitId; if (requireExactSource && actualCommit !== frozenCommit) { throw new AgentRunError("infra-failed", "staged primary workspace HEAD does not match resolved source commit", { httpStatus: 500, details: { frozenCommit, actualCommit, valuesPrinted: false } }); } if (!requireExactSource) { const ancestry = await git(["merge-base", "--is-ancestor", frozenCommit, actualCommit], workspacePath, { allowFailure: true }); if (ancestry.exitCode === 1) { throw new AgentRunError("schema-invalid", "primary workspace HEAD is not a successor of its frozen source commit", { httpStatus: 400, details: { frozenCommit, actualCommit, valuesPrinted: false } }); } if (ancestry.exitCode !== 0) { throw new AgentRunError("infra-failed", "primary workspace ancestry could not be verified", { httpStatus: 500, details: { frozenCommit, actualCommit, exitCode: ancestry.exitCode, valuesPrinted: false } }); } } const frozenTreeId = (await git(["rev-parse", `${frozenCommit}^{tree}`], workspacePath)).stdout.trim(); if (frozenTreeId !== provenance.resolvedSource.treeId) { throw new AgentRunError("schema-invalid", "primary workspace frozen source tree conflicts with provenance", { httpStatus: 400, details: { frozenCommit, expectedTreeId: provenance.resolvedSource.treeId, actualTreeId: frozenTreeId, valuesPrinted: false } }); } const branch = (await git(["branch", "--show-current"], workspacePath)).stdout.trim() || null; const origin = (await git(["remote", "get-url", "origin"], workspacePath)).stdout.trim(); if (origin !== provenance.declaredSource.repoUrl) { throw new AgentRunError("schema-invalid", "primary workspace origin conflicts with declared source authority", { httpStatus: 400, details: { declared: gitRemoteSummary(provenance.declaredSource.repoUrl), actual: gitRemoteSummary(origin), valuesPrinted: false } }); } const probe = path.join(workspacePath, `.agentrun-write-probe-${stableHash({ workspacePath, actualCommit }).slice(0, 12)}`); try { await writeFile(probe, "", { flag: "wx" }); await rm(probe, { force: true }); } catch (error) { await rm(probe, { force: true }); throw new AgentRunError("infra-failed", "primary workspace is not writable", { httpStatus: 500, details: { workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } }); } const headTreeId = (await git(["rev-parse", "HEAD^{tree}"], workspacePath)).stdout.trim(); return { status: "materialized", sourceContract: "resourceBundleRef", repoUrl: provenance.declaredSource.repoUrl, fetchRepoUrl: provenance.resolvedSource.fetchRepoUrl, mirrorUsed: provenance.resolvedSource.mirrorUsed, requestedRef: provenance.declaredSource.requestedRef, requestedCommitId: provenance.declaredSource.requestedCommitId, sourceCommit: frozenCommit, headCommit: actualCommit, headIsFrozenSource: actualCommit === frozenCommit, treeId: provenance.resolvedSource.treeId, headTreeId, sourceBranch: provenance.resolvedSource.branch, branch, targetPath: workspaceRef.path ?? null, workspaceRoot: pathSummary(workspacePath), workspaceLifecycle, resolutionAuthority: provenance.resolutionAuthority, assemblyContractHash: provenance.assemblyContractHash, bundleContractHash: provenance.bundleContractHash, bundleProvenanceHash: stableHash(provenance.bundleProvenance), provenancePath: pathSummary(path.join(workspacePath, ".git", primaryWorkspaceProvenanceFile)), writable: true, provenanceHash: provenance.provenanceHash, valuesPrinted: false, }; } export function resolveGitBundleFetchSource(repoUrl: string, gitMirror?: GitMirrorConfig, env: NodeJS.ProcessEnv = process.env): { fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl?: string } { const mirror = gitMirror ? { baseUrl: normalizeMirrorBaseUrl(gitMirror.baseUrl) } : defaultGitMirrorConfig(env); const githubPath = githubRepoPath(repoUrl); if (!githubPath) return { fetchRepoUrl: repoUrl, mirrorUsed: false }; return { fetchRepoUrl: `${mirror.baseUrl}/${githubPath}.git`, mirrorUsed: true, mirrorBaseUrl: mirror.baseUrl }; } function normalizeMirrorBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/+$/u, ""); } function gitFetchSource(source: GitBundleSource): { fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl?: string } { return resolveGitBundleFetchSource(source.repoUrl, source.gitMirror); } function githubRepoPath(repoUrl: string): string | null { const raw = repoUrl.trim(); const scp = /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/u.exec(raw); if (scp) return cleanGithubPath(scp[1] ?? "", scp[2] ?? ""); const ssh = /^ssh:\/\/git@(?:github\.com|ssh\.github\.com)(?::\d+)?\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/u.exec(raw); if (ssh) return cleanGithubPath(ssh[1] ?? "", ssh[2] ?? ""); const http = /^https?:\/\/github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?\/?$/u.exec(raw); if (http) return cleanGithubPath(http[1] ?? "", http[2] ?? ""); return null; } function cleanGithubPath(owner: string, repo: string): string | null { const cleanOwner = owner.trim(); const cleanRepo = repo.trim().replace(/\.git$/u, ""); if (!/^[A-Za-z0-9_.-]+$/u.test(cleanOwner) || !/^[A-Za-z0-9_.-]+$/u.test(cleanRepo)) return null; return `${cleanOwner}/${cleanRepo}`; } async function materializeGitBundles(overlayRoot: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource, sourceContext: GitBundleSourceContext) => Promise): Promise { const items: MaterializedGitBundle[] = []; for (const [index, bundle] of resourceBundleRef.bundles.entries()) { const gitSource = bundleGitSource(bundle, resourceBundleRef, defaultSource); const checkout = gitSource === defaultSource ? defaultCheckout : await checkoutFor(gitSource, { kind: "bundle", index, name: bundle.name ?? null }); const source = resolveBundlePath(checkout.checkoutPath, bundle.subpath, `bundles[${index}].subpath`); const target = resolveManagedOverlayTargetPath(overlayRoot, bundle.targetPath, `bundles[${index}].target_path`); let sourceStat; try { sourceStat = await stat(source); } catch (error) { throw new AgentRunError("schema-invalid", `gitbundle subpath ${bundle.subpath} is not readable`, { httpStatus: 400, details: { index, subpath: bundle.subpath, error: fileErrorSummary(error), valuesPrinted: false } }); } await mkdir(path.dirname(target), { recursive: true }); await rm(target, { recursive: true, force: true }); await cp(source, target, { recursive: true, force: true, dereference: false }); items.push({ name: bundle.name ?? null, repoUrl: checkout.repoUrl, fetchRepoUrl: checkout.fetchRepoUrl, mirrorUsed: checkout.mirrorUsed, mirrorBaseUrl: checkout.mirrorBaseUrl ?? null, commitId: checkout.commitId, requestedCommitId: bundle.commitId ?? resourceBundleRef.commitId ?? null, requestedRef: checkout.requestedRef ?? null, subpath: bundle.subpath, targetPath: bundle.targetPath, sourceKind: sourceStat.isDirectory() ? "directory" : "file", sourceBytes: sourceStat.isFile() ? sourceStat.size : null }); } return items; } function optionalNonEmpty(value: unknown): string | undefined { return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; } function shellSingleQuote(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } function installedToolShim(sourcePath: string): string { return `#!/usr/bin/env sh\nexec ${shellSingleQuote(sourcePath)} "$@"\n`; } async function prepareGitBundleTools(overlayRoot: string, env: NodeJS.ProcessEnv): Promise<{ binPath?: string; event: JsonRecord }> { const sourceBinPath = path.join(overlayRoot, "tools"); const installedBinPath = optionalNonEmpty(env.AGENTRUN_RESOURCE_BIN_PATH); const runtimeBinPath = installedBinPath ?? sourceBinPath; 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") sourceToolsAvailable = false; else throw error; } const names: string[] = []; const items: JsonRecord[] = []; 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 }); } } const runtimeTools = await installRuntimeBundledTools(runtimeToolTargetBinPath, names); const hasInstalledTools = names.length > 0 || runtimeTools.names.length > 0; return { ...(hasInstalledTools ? { binPath: runtimeBinPath } : {}), event: { count: names.length, names, items, runtimeTools: runtimeTools.event, binPath: hasInstalledTools ? pathSummary(runtimeBinPath) : null, sourceBinPath: sourceToolsAvailable ? pathSummary(sourceBinPath) : null, installedBinPath: installedBinPath ? pathSummary(installedBinPath) : null, 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): Promise<{ items: MaterializedPromptRef[]; event: JsonRecord }> { const items: MaterializedPromptRef[] = []; const eventItems: JsonRecord[] = []; let totalBytes = 0; for (const ref of refs) { const promptPath = resolveBundlePath(checkoutPath, ref.path, `promptRefs.${ref.name}.path`); const required = ref.required === true; let text: string; try { text = await readFile(promptPath, "utf8"); } catch (error) { if (required) throw new AgentRunError("prompt-unavailable", `required resource prompt ${ref.name} is not readable`, { httpStatus: 400, details: { name: ref.name, path: ref.path, error: fileErrorSummary(error), valuesPrinted: false } }); eventItems.push({ name: ref.name, path: ref.path, inject: "thread-start", required, status: "missing", valuesPrinted: false }); continue; } const bytes = Buffer.byteLength(text, "utf8"); if (bytes > maxPromptRefBytes) throw new AgentRunError("prompt-too-large", `resource prompt ${ref.name} exceeds the per-file size limit`, { httpStatus: 400, details: { name: ref.name, path: ref.path, bytes, maxPromptRefBytes, valuesPrinted: false } }); totalBytes += bytes; if (totalBytes > maxInitialPromptBytes) throw new AgentRunError("prompt-too-large", "assembled resource prompt exceeds the total size limit", { httpStatus: 400, details: { totalBytes, maxInitialPromptBytes, valuesPrinted: false } }); const sha = sha256Text(text); items.push({ name: ref.name, path: ref.path, inject: "thread-start", required, text, bytes, sha256: sha }); eventItems.push({ name: ref.name, path: ref.path, inject: "thread-start", required, status: "materialized", sha256: sha, bytes, valuesPrinted: false }); } return { items, event: { count: refs.length, materializedCount: items.length, names: items.map((item) => item.name), items: eventItems, totalBytes, valuesPrinted: false, }, }; } async function discoverGitBundleSkills(overlayRoot: string, bundles: MaterializedGitBundle[]): Promise<{ items: MaterializedSkillRef[]; skillsDir?: string; event: JsonRecord }> { const skillsDir = path.join(overlayRoot, ".agents", "skills"); let entries; try { entries = await readdir(skillsDir, { withFileTypes: true }); } catch (error) { if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") return { items: [], event: { count: 0, materializedCount: 0, names: [], skillsDir: null, items: [], valuesPrinted: false } }; throw error; } const items: MaterializedSkillRef[] = []; const eventItems: JsonRecord[] = []; for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) { if (!entry.isDirectory()) continue; const aggregateAs = entry.name; const relativeManifestPath = `.agents/skills/${aggregateAs}/SKILL.md`; const manifestPath = path.join(skillsDir, aggregateAs, "SKILL.md"); const sourceBundle = skillSourceBundle(relativeManifestPath, bundles); let manifestText: string; try { manifestText = await readFile(manifestPath, "utf8"); } catch (error) { eventItems.push({ name: aggregateAs, path: relativeManifestPath, required: true, aggregateAs, status: "missing", error: fileErrorSummary(error), sourceBundle, valuesPrinted: false }); continue; } const bytes = Buffer.byteLength(manifestText, "utf8"); const sha = sha256Text(manifestText); const summary = skillSummary(manifestText); items.push({ name: aggregateAs, path: relativeManifestPath, aggregateAs, required: true, registryPath: manifestPath, manifestBytes: bytes, manifestSha256: sha, summary, sourceBundle }); eventItems.push({ name: aggregateAs, path: relativeManifestPath, aggregateAs, required: true, status: "materialized", manifestSha256: sha, manifestBytes: bytes, registryPath: pathSummary(manifestPath), sourceBundle, summary, valuesPrinted: false }); } return { items, skillsDir, event: { count: entries.filter((entry) => entry.isDirectory()).length, materializedCount: items.length, names: items.map((item) => item.name), skillsDir: pathSummary(skillsDir), items: eventItems, valuesPrinted: false, }, }; } function materializeRequiredSkills(refs: NonNullable, skills: MaterializedSkillRef[]): { items: MaterializedSkillRef[]; event: JsonRecord } { if (refs.length === 0) return { items: [], event: { count: 0, materializedCount: 0, names: [], items: [], valuesPrinted: false } }; const byName = new Map(skills.map((skill) => [skill.name, skill])); const items: MaterializedSkillRef[] = []; const eventItems: JsonRecord[] = []; const missing: JsonRecord[] = []; const missingNames: string[] = []; for (const ref of refs) { const skill = byName.get(ref.name); if (!skill) { const item = { name: ref.name, path: `.agents/skills/${ref.name}/SKILL.md`, status: "missing", valuesPrinted: false }; missingNames.push(ref.name); missing.push(item); eventItems.push(item); continue; } items.push(skill); eventItems.push({ name: skill.name, path: skill.path, aggregateAs: skill.aggregateAs, status: "materialized", manifestSha256: skill.manifestSha256, manifestBytes: skill.manifestBytes, sourceBundle: skill.sourceBundle, summary: skill.summary, valuesPrinted: false }); } if (missing.length > 0) { throw new AgentRunError("required-skill-unavailable", `required resource skill ${missingNames.join(", ")} is not materialized`, { httpStatus: 400, details: { required: refs.map((ref) => ref.name), missing, available: skills.map((skill) => ({ name: skill.name, path: skill.path, manifestSha256: skill.manifestSha256, manifestBytes: skill.manifestBytes, sourceBundle: skill.sourceBundle, valuesPrinted: false })), valuesPrinted: false, }, }); } return { items, event: { count: refs.length, materializedCount: items.length, names: items.map((item) => item.name), items: eventItems, valuesPrinted: false, }, }; } function skillSourceBundle(relativePath: string, bundles: MaterializedGitBundle[]): JsonRecord | null { const match = bundles .filter((bundle) => relativePathMatchesTarget(relativePath, bundle.targetPath)) .sort((left, right) => right.targetPath.length - left.targetPath.length)[0]; if (!match) return null; return { name: match.name, repoUrl: match.repoUrl, fetchRepoUrl: match.fetchRepoUrl, mirrorUsed: match.mirrorUsed, mirrorBaseUrl: match.mirrorBaseUrl, commitId: match.commitId, requestedCommitId: match.requestedCommitId, requestedRef: match.requestedRef, subpath: match.subpath, targetPath: match.targetPath, valuesPrinted: false, }; } function relativePathMatchesTarget(relativePath: string, targetPath: string): boolean { const normalizedTarget = targetPath.replace(/\/+$/u, ""); return relativePath === normalizedTarget || relativePath.startsWith(`${normalizedTarget}/`); } function assembleInitialPrompt(promptRefs: MaterializedPromptRef[], skills: MaterializedSkillRef[]): InitialPromptAssembly | undefined { if (promptRefs.length === 0 && skills.length === 0) return undefined; const sections: string[] = [ "AgentRun initial runtime instructions. These instructions are assembled from ResourceBundleRef promptRefs and gitbundle skill directories for the first thread-start turn only.", ]; for (const prompt of promptRefs) { sections.push([`## Resource Prompt: ${prompt.name}`, `path: ${prompt.path}`, prompt.text].join("\n")); } if (skills.length > 0) { const lines = [ "## Resource Skills", "The following required runtime skills are published through the runtime-managed AGENTRUN_SKILLS_DIRS directory outside the Git source view. Use these bundle skills instead of default model skill guesses.", ...skills.map((skill) => `- ${skill.name}: ${skill.summary || "No summary provided."} manifest=${skill.registryPath} skillsDirEnv=AGENTRUN_SKILLS_DIRS source=${skill.path} required=${skill.required}`), ]; sections.push(lines.join("\n")); } const text = sections.join("\n\n"); const bytes = Buffer.byteLength(text, "utf8"); if (bytes > maxInitialPromptBytes) throw new AgentRunError("prompt-too-large", "assembled initial prompt exceeds the total size limit", { httpStatus: 400, details: { bytes, maxInitialPromptBytes, promptRefCount: promptRefs.length, skillCount: skills.length, valuesPrinted: false } }); return { text, summary: { available: true, bytes, sha256: sha256Text(text), promptRefCount: promptRefs.length, promptRefNames: promptRefs.map((item) => item.name), skillCount: skills.length, skillNames: skills.map((item) => item.name), valuesPrinted: false, }, }; } function skillSummary(text: string): string { const frontmatter = /^---\s*\n([\s\S]*?)\n---\s*/u.exec(text); if (frontmatter) { const descriptionLine = frontmatter[1]?.split(/\r?\n/u).find((line) => /^description\s*:/iu.test(line)); if (descriptionLine) return trimSummary(descriptionLine.replace(/^description\s*:\s*/iu, "").trim().replace(/^['"]|['"]$/gu, "")); } const line = text.split(/\r?\n/u).map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#") && entry !== "---"); return trimSummary(line ?? ""); } function trimSummary(value: string): string { const normalized = value.replace(/\s+/gu, " ").trim(); return normalized.length > skillSummaryChars ? `${normalized.slice(0, skillSummaryChars)}...` : normalized; } function sha256Text(text: string): string { return createHash("sha256").update(text, "utf8").digest("hex"); } function fileErrorSummary(error: unknown): JsonRecord { const record = typeof error === "object" && error !== null ? error as { code?: unknown; message?: unknown } : {}; return { code: typeof record.code === "string" ? record.code : null, message: typeof record.message === "string" ? redactText(record.message).slice(0, 300) : null }; } interface GitCommandOptions { allowFailure?: boolean; operation?: string; repoUrl?: string; fetchRepoUrl?: string; mirrorUsed?: boolean; sourceRef?: GitSourceRef; sourceContext?: GitBundleSourceContext; credentialRefPresent?: boolean; env?: NodeJS.ProcessEnv; } export function classifyGitStderr(stderr: string): string { const text = stderr.trim(); if (/repository\s+['"].+['"]\s+not found|repository not found/iu.test(text)) return "repository-not-found"; if (/couldn't find remote ref|remote ref .+ not found|not our ref|unadvertised object/iu.test(text)) return "source-ref-not-found"; if (/authentication failed|permission denied|could not read username|access denied|http[^\n]*\b(?:401|403)\b/iu.test(text)) return "authentication-failed"; if (/could not resolve host|name or service not known|temporary failure in name resolution/iu.test(text)) return "dns-failed"; if (/failed to connect|connection refused|connection timed out|network is unreachable|no route to host/iu.test(text)) return "connection-failed"; if (/certificate|\bssl\b|\btls\b/iu.test(text)) return "tls-failed"; if (/early eof|rpc failed|remote end hung up|http[^\n]*\b5\d\d\b/iu.test(text)) return "transport-failed"; return text.length === 0 ? "stderr-empty" : "unclassified"; } async function git(args: string[], cwd: string, options: GitCommandOptions = {}): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { const env = gitCommandEnv({ ...process.env, ...(options.env ?? {}) }); const child = spawn("git", gitArgs(args), { cwd, stdio: ["ignore", "pipe", "pipe"], env }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => { stdout += String(chunk); }); child.stderr.on("data", (chunk) => { stderr += String(chunk); }); const timeoutMs = gitTimeoutMs(process.env); const startedAt = Date.now(); let timedOut = false; let closed = false; const timeout = setTimeout(() => { timedOut = true; child.kill("SIGTERM"); setTimeout(() => { if (!closed) child.kill("SIGKILL"); }, 2_000).unref(); }, timeoutMs); const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => { child.on("error", reject); child.on("close", (code, signal) => { closed = true; resolve({ code, signal }); }); }).catch((error: unknown) => { clearTimeout(timeout); throw new AgentRunError("infra-failed", `failed to start git: ${error instanceof Error ? error.message : String(error)}`, { httpStatus: 503 }); }); clearTimeout(timeout); if (result.code !== 0 && !options.allowFailure) { const failureKind = timedOut ? "backend-timeout" : "infra-failed"; const stderrCategory = timedOut ? "operation-timeout" : classifyGitStderr(stderr); throw new AgentRunError(failureKind, `git ${args[0] ?? "command"} ${timedOut ? `timed out after ${timeoutMs}ms` : `failed with code ${result.code} (${stderrCategory})`}`, { httpStatus: timedOut ? 504 : 502, details: { operation: options.operation ?? args[0] ?? "git", stage: options.operation === "fetch" ? "git-fetch" : `git-${options.operation ?? args[0] ?? "command"}`, source: { bundle: options.sourceContext ?? null, sourceRef: options.sourceRef ? { present: true, kind: options.sourceRef.kind, value: options.sourceRef.value, valuesPrinted: false } : { present: false, kind: null, value: null, valuesPrinted: false }, credentialRefPresent: options.credentialRefPresent === true, valuesPrinted: false, }, remote: { declared: gitRemoteSummary(options.repoUrl), fetch: gitRemoteSummary(options.fetchRepoUrl), mirrorUsed: options.mirrorUsed === true, valuesPrinted: false, }, stderr: { category: stderrCategory, firstLineCategory: classifyGitStderr(firstNonEmptyLine(stderr)), bytes: Buffer.byteLength(stderr, "utf8"), lines: nonEmptyLineCount(stderr), truncated: stderr.length > 4000, valuesPrinted: false, }, stdout: { bytes: Buffer.byteLength(stdout, "utf8"), truncated: stdout.length > 1000, valuesPrinted: false, }, timeoutMs, elapsedMs: Date.now() - startedAt, protocol: defaultGitHttpVersion, terminalPrompt: false, credentialHelper: "gh-auth-setup-git", proxyDecision: proxyDecisionForUrl(options.fetchRepoUrl ?? options.repoUrl ?? ""), signal: timedOut ? "SIGTERM" : result.signal, valuesPrinted: false, }, }); } return { stdout, stderr, exitCode: result.code }; } function gitRemoteSummary(value: string | undefined): JsonRecord | null { const remote = optionalNonEmpty(value); if (!remote) return null; const scp = /^(?:[^@\s]+@)?([^:/\s]+):/u.exec(remote); let scheme = scp ? "ssh" : "file"; let host: string | null = scp?.[1] ?? null; try { const parsed = new URL(remote); scheme = parsed.protocol.replace(/:$/u, "") || scheme; host = parsed.hostname || null; } catch { // Local paths and SCP-style SSH remotes are summarized without parsing their path. } return { scheme, host, fingerprint: sha256Text(remote), valuesPrinted: false }; } function firstNonEmptyLine(value: string): string { return value.split(/\r?\n/u).find((line) => line.trim().length > 0)?.trim() ?? ""; } function nonEmptyLineCount(value: string): number { return value.split(/\r?\n/u).filter((line) => line.trim().length > 0).length; } function gitArgs(args: string[]): string[] { return [ "-c", `http.version=${defaultGitHttpVersion}`, "-c", `http.lowSpeedLimit=${process.env.GIT_HTTP_LOW_SPEED_LIMIT ?? String(defaultGitLowSpeedLimitBytes)}`, "-c", `http.lowSpeedTime=${process.env.GIT_HTTP_LOW_SPEED_TIME ?? String(defaultGitLowSpeedTimeSeconds)}`, ...args, ]; } function gitCommandEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const next: NodeJS.ProcessEnv = { ...env, GIT_TERMINAL_PROMPT: "0", GIT_HTTP_VERSION: env.GIT_HTTP_VERSION ?? defaultGitHttpVersion, }; const noProxy = new Set(String(next.NO_PROXY || next.no_proxy || "").split(",").map((item) => item.trim()).filter(Boolean)); for (const host of gitDirectHosts(env)) noProxy.add(host); next.NO_PROXY = [...noProxy].join(","); next.no_proxy = next.NO_PROXY; return next; } function gitTimeoutMs(env: NodeJS.ProcessEnv): number { const value = Number(env.AGENTRUN_GIT_DEFAULT_TIMEOUT_MS); return Number.isFinite(value) && value > 0 ? Math.trunc(value) : defaultGitOperationTimeoutMs; } function proxyDecisionForUrl(url: string): JsonRecord { let host: string | null = null; try { host = new URL(url).hostname; } catch { if (/^git@github\.com:/u.test(url.trim())) host = "github.com"; } const directHosts = new Set(gitDirectHosts(process.env)); return { host, mode: host && directHosts.has(host) ? "direct-preferred" : "env-proxy", valuesPrinted: false, }; } function gitDirectHosts(env: NodeJS.ProcessEnv): string[] { const raw = env.AGENTRUN_GIT_DIRECT_HOSTS; if (!raw || raw.trim().length === 0) return [...defaultGitDirectHosts]; return raw.split(",").map((item) => item.trim()).filter(Boolean); } 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", `${fieldName} escaped checkout`, { httpStatus: 400 }); return resolved; } function resolveManagedOverlayTargetPath(overlayRoot: string, relativePath: string, fieldName: string): string { const resolved = path.resolve(overlayRoot, relativePath); const root = path.resolve(overlayRoot); if (resolved === root || !resolved.startsWith(`${root}${path.sep}`)) throw new AgentRunError("schema-invalid", `${fieldName} escaped managed overlay`, { httpStatus: 400 }); return resolved; } 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 }; }