|
|
|
@@ -81,7 +81,7 @@ interface GitSourceRef {
|
|
|
|
|
value: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface MaterializedGitBundle {
|
|
|
|
|
interface MaterializedGitBundle extends JsonRecord {
|
|
|
|
|
name: string | null;
|
|
|
|
|
repoUrl: string;
|
|
|
|
|
fetchRepoUrl: string;
|
|
|
|
@@ -96,6 +96,38 @@ interface MaterializedGitBundle {
|
|
|
|
|
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<MaterializedResourceBundle | null> {
|
|
|
|
|
if (!resourceBundleRef) return null;
|
|
|
|
|
assertPrimaryWorkspaceRef(workspaceRef, resourceBundleRef);
|
|
|
|
@@ -108,30 +140,64 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
|
|
|
|
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 } });
|
|
|
|
|
}
|
|
|
|
|
await rm(assemblyRoot, { recursive: true, force: true });
|
|
|
|
|
await mkdir(checkoutRoot, { recursive: true });
|
|
|
|
|
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);
|
|
|
|
|
const credentialEnv = await resourceCredentialEnv(resourceBundleRef, env);
|
|
|
|
|
const defaultSource = defaultGitBundleSource(resourceBundleRef, env, gitMirror, credentialEnv);
|
|
|
|
|
const checkoutCache = new Map<string, Promise<GitCheckout>>();
|
|
|
|
|
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);
|
|
|
|
|
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<string, Promise<GitCheckout>>();
|
|
|
|
|
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 stagedBundles = await materializeGitBundles(stagePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
|
|
|
|
|
const stagedProvenance = createPrimaryWorkspaceProvenance(resourceBundleRef, workspaceRef, defaultCheckout, stagedBundles);
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
return checkout;
|
|
|
|
|
};
|
|
|
|
|
const defaultCheckout = await checkoutFor(defaultSource, { kind: "resource-bundle", index: null, name: null });
|
|
|
|
|
await replacePrimaryWorkspace(defaultCheckout.checkoutPath, workspacePath, runScope);
|
|
|
|
|
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
|
|
|
|
|
const primaryWorkspace = await verifyPrimaryWorkspace(workspacePath, workspaceRef, defaultCheckout);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const primaryWorkspace = await verifyPrimaryWorkspace(workspacePath, workspaceRef, provenance, workspaceLifecycle, false);
|
|
|
|
|
const tools = await prepareGitBundleTools(workspacePath, env);
|
|
|
|
|
const skills = await discoverGitBundleSkills(workspacePath, materializedBundles);
|
|
|
|
|
const requiredSkills = materializeRequiredSkills(resourceBundleRef.requiredSkills ?? [], skills.items);
|
|
|
|
|
const prompts = await materializePromptRefs(defaultCheckout.checkoutPath, resourceBundleRef.promptRefs ?? []);
|
|
|
|
|
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 } : {}),
|
|
|
|
@@ -141,17 +207,19 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
|
|
|
|
phase: "resource-bundle-materialized",
|
|
|
|
|
kind: "gitbundle",
|
|
|
|
|
repoUrl: resourceBundleRef.repoUrl,
|
|
|
|
|
fetchRepoUrl: defaultCheckout.fetchRepoUrl,
|
|
|
|
|
mirrorUsed: defaultCheckout.mirrorUsed,
|
|
|
|
|
mirrorBaseUrl: defaultCheckout.mirrorBaseUrl ?? null,
|
|
|
|
|
gitMirror: gitMirror ? { enabled: true, baseUrl: gitMirror.baseUrl, valuesPrinted: false } : { enabled: false, baseUrl: null, valuesPrinted: false },
|
|
|
|
|
commitId: defaultCheckout.commitId,
|
|
|
|
|
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: defaultCheckout.requestedRef ?? null,
|
|
|
|
|
treeId: defaultCheckout.treeId,
|
|
|
|
|
checkoutPath: pathSummary(defaultCheckout.checkoutPath),
|
|
|
|
|
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,
|
|
|
|
|
gitTransport: gitTransportSummary(),
|
|
|
|
|
bundles: {
|
|
|
|
@@ -287,32 +355,260 @@ function localBranchFromRef(ref: string): string | 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 expectedBranch = optionalNonEmpty(workspaceRef.branch);
|
|
|
|
|
if (expectedBranch && resourceBundleRef.ref && normalizeBranch(expectedBranch) !== normalizeBranch(resourceBundleRef.ref)) {
|
|
|
|
|
throw new AgentRunError("schema-invalid", "workspaceRef.branch does not match gitbundle primary ref", { httpStatus: 400, details: { expectedBranch, requestedRef: resourceBundleRef.ref, valuesPrinted: false } });
|
|
|
|
|
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 } });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function replacePrimaryWorkspace(checkoutPath: string, workspacePath: string, runScope: string): Promise<void> {
|
|
|
|
|
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<PrimaryWorkspaceState> {
|
|
|
|
|
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<PrimaryWorkspaceProvenance> {
|
|
|
|
|
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<string> {
|
|
|
|
|
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)) return;
|
|
|
|
|
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 });
|
|
|
|
|
await rm(workspacePath, { recursive: true, force: true });
|
|
|
|
|
await rename(stage, workspacePath);
|
|
|
|
|
return stage;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await rm(stage, { recursive: true, force: true });
|
|
|
|
|
throw new AgentRunError("infra-failed", "primary workspace materialization failed", { httpStatus: 500, details: { stage: pathSummary(stage), workspacePath: pathSummary(workspacePath), error: fileErrorSummary(error), valuesPrinted: false } });
|
|
|
|
|
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 verifyPrimaryWorkspace(workspacePath: string, workspaceRef: WorkspaceRef, checkout: GitCheckout): Promise<JsonRecord> {
|
|
|
|
|
async function writePrimaryWorkspaceProvenance(workspacePath: string, provenance: PrimaryWorkspaceProvenance): Promise<void> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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 verifyPrimaryWorkspace(workspacePath: string, workspaceRef: WorkspaceRef, provenance: PrimaryWorkspaceProvenance, workspaceLifecycle: "first-materialization" | "continuation-reuse", requireExactSource: boolean): Promise<JsonRecord> {
|
|
|
|
|
let rootStat;
|
|
|
|
|
let gitStat;
|
|
|
|
|
try {
|
|
|
|
@@ -325,16 +621,27 @@ async function verifyPrimaryWorkspace(workspacePath: string, workspaceRef: Works
|
|
|
|
|
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();
|
|
|
|
|
if (actualCommit !== checkout.commitId) {
|
|
|
|
|
throw new AgentRunError("infra-failed", "primary workspace HEAD does not match materialized source commit", { httpStatus: 500, details: { expectedCommit: checkout.commitId, actualCommit, valuesPrinted: false } });
|
|
|
|
|
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;
|
|
|
|
|
if (checkout.branch && branch !== checkout.branch) {
|
|
|
|
|
throw new AgentRunError("infra-failed", "primary workspace branch does not match requested source ref", { httpStatus: 500, details: { expectedBranch: checkout.branch, actualBranch: branch, valuesPrinted: false } });
|
|
|
|
|
}
|
|
|
|
|
const origin = (await git(["remote", "get-url", "origin"], workspacePath)).stdout.trim();
|
|
|
|
|
if (origin !== checkout.repoUrl) {
|
|
|
|
|
throw new AgentRunError("infra-failed", "primary workspace origin does not match declared source authority", { httpStatus: 500, details: { declared: gitRemoteSummary(checkout.repoUrl), actual: gitRemoteSummary(origin), valuesPrinted: false } });
|
|
|
|
|
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 {
|
|
|
|
@@ -344,29 +651,36 @@ async function verifyPrimaryWorkspace(workspacePath: string, workspaceRef: Works
|
|
|
|
|
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: checkout.repoUrl,
|
|
|
|
|
fetchRepoUrl: checkout.fetchRepoUrl,
|
|
|
|
|
mirrorUsed: checkout.mirrorUsed,
|
|
|
|
|
requestedRef: checkout.requestedRef ?? null,
|
|
|
|
|
requestedCommitId: checkout.requestedCommitId ?? null,
|
|
|
|
|
sourceCommit: actualCommit,
|
|
|
|
|
treeId: checkout.treeId,
|
|
|
|
|
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: stableHash({ repoUrl: checkout.repoUrl, requestedRef: checkout.requestedRef ?? null, requestedCommitId: checkout.requestedCommitId ?? null, sourceCommit: actualCommit, treeId: checkout.treeId, targetPath: workspaceRef.path ?? null }),
|
|
|
|
|
provenanceHash: provenance.provenanceHash,
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeBranch(value: string): string {
|
|
|
|
|
return value.replace(/^refs\/heads\//u, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
@@ -741,7 +1055,7 @@ export function classifyGitStderr(stderr: string): string {
|
|
|
|
|
return text.length === 0 ? "stderr-empty" : "unclassified";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function git(args: string[], cwd: string, options: GitCommandOptions = {}): Promise<{ stdout: string; stderr: string }> {
|
|
|
|
|
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 = "";
|
|
|
|
@@ -816,7 +1130,7 @@ async function git(args: string[], cwd: string, options: GitCommandOptions = {})
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return { stdout, stderr };
|
|
|
|
|
return { stdout, stderr, exitCode: result.code };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function gitRemoteSummary(value: string | undefined): JsonRecord | null {
|
|
|
|
|