fix: 保留 Artificer session 工作区续跑状态

This commit is contained in:
root
2026-07-12 06:30:41 +02:00
parent a4a63ad0e8
commit b0233256d5
7 changed files with 488 additions and 104 deletions
+3 -29
View File
@@ -160,36 +160,10 @@ export function validatePrimaryWorkspaceContract(workspaceRef: WorkspaceRef | nu
if (workspaceRef.path !== ".") {
throw new AgentRunError("schema-invalid", "gitbundle primary workspace requires workspaceRef.path=. as its declared targetPath", { httpStatus: 400 });
}
const workspaceRepo = normalizedRepoIdentity(workspaceRef.repo);
const sourceRepo = normalizedRepoIdentity(resourceBundleRef.repoUrl);
if (workspaceRepo && sourceRepo && workspaceRepo !== sourceRepo) {
throw new AgentRunError("schema-invalid", "workspaceRef.repo does not match resourceBundleRef.repoUrl", { httpStatus: 400, details: { workspaceRepo, sourceRepo, 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 } });
}
const workspaceBranch = optionalString(workspaceRef.branch);
if (workspaceBranch && resourceBundleRef.ref && normalizeBranchRef(workspaceBranch) !== normalizeBranchRef(resourceBundleRef.ref)) {
throw new AgentRunError("schema-invalid", "workspaceRef.branch does not match resourceBundleRef.ref", { httpStatus: 400, details: { workspaceBranch, resourceRef: resourceBundleRef.ref, valuesPrinted: false } });
}
}
function normalizedRepoIdentity(value: unknown): string | null {
const raw = optionalString(value);
if (!raw) return null;
const plain = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?$/u.exec(raw);
if (plain) return `${plain[1]}/${plain[2]}`.toLowerCase();
const scp = /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/u.exec(raw);
if (scp) return `${scp[1]}/${scp[2]}`.toLowerCase();
try {
const parsed = new URL(raw);
if (parsed.hostname.toLowerCase() !== "github.com") return null;
const parts = parsed.pathname.replace(/^\/+|\/+$/gu, "").replace(/\.git$/u, "").split("/");
return parts.length === 2 ? `${parts[0]}/${parts[1]}`.toLowerCase() : null;
} catch {
return null;
}
}
function normalizeBranchRef(value: string): string {
return value.replace(/^refs\/heads\//u, "");
}
function validateCommitId(commitId: string, fieldName: string): void {
+371 -57
View File
@@ -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 {
@@ -179,7 +179,7 @@ process.exit(1);
const refResource = refEnvelope.resourceBundleRef as JsonRecord;
assert.equal(refResource.commitId, refBundle.latestCommitId, "result summary keeps the declared immutable source commit");
const refMaterialized = refResource.materialized as JsonRecord;
assert.equal(refMaterialized.commitId, refBundle.latestCommitId, "materialized bundle must resolve the current workspaceRef.branch commit");
assert.equal(refMaterialized.commitId, refBundle.latestCommitId, "first materialization must resolve the declared resource ref");
assert.equal(refMaterialized.requestedCommitId, refBundle.latestCommitId);
assert.equal(refMaterialized.requestedRef, refBundle.branch);
const refBundleCommits = (((refMaterialized.bundles as JsonRecord).items as JsonRecord[]).map((item) => item.commitId));
@@ -419,7 +419,7 @@ async function createHwlabRun(client: ManagerClient, context: SelfTestContext, b
const run = await client.post("/api/v1/runs", {
tenantId: "hwlab",
projectId: "pikasTech/HWLAB",
workspaceRef: { kind: "opaque", path: ".", ...(bundle.branch ? { branch: bundle.branch } : {}) },
workspaceRef: { kind: "opaque", path: "." },
sessionRef: { sessionId, conversationId: sessionId },
resourceBundleRef,
providerId: "G14",
@@ -238,7 +238,7 @@ function runPayload(context: SelfTestContext, backendProfile: BackendProfile, se
return {
tenantId: "hwlab",
projectId: "pikasTech/HWLAB",
workspaceRef: { kind: "opaque", repo: "pikasTech/HWLAB" },
workspaceRef: { kind: "opaque", path: "." },
sessionRef: { sessionId, conversationId: sessionId },
providerId: "G14",
backendProfile,
+7 -5
View File
@@ -5,7 +5,7 @@ import { ManagerClient } from "../../mgr/client.js";
import { startManagerServer } from "../../mgr/server.js";
import { MemoryAgentRunStore } from "../../mgr/store.js";
import type { JsonRecord } from "../../common/types.js";
import { validateResourceBundleRef } from "../../common/validation.js";
import { validatePrimaryWorkspaceContract, validateResourceBundleRef } from "../../common/validation.js";
import { classifyGitStderr, resolveGitBundleFetchSource } from "../../runner/resource-bundle.js";
import { assertNoSecretLeak, loadArtificerImageRef, type SelfTestCase } from "../harness.js";
@@ -40,10 +40,7 @@ const selfTest: SelfTestCase = async (context) => {
assert.equal(task.providerId, "G14");
assert.equal(task.idempotencyKey, "selftest-aipod-artificer");
const workspaceRef = task.workspaceRef as JsonRecord;
assert.equal(workspaceRef.kind, "opaque");
assert.equal(workspaceRef.path, ".");
assert.equal(workspaceRef.repo, "pikasTech/unidesk");
assert.equal(workspaceRef.branch, "master");
assert.deepEqual(workspaceRef, { kind: "opaque", path: "." });
const sessionRef = task.sessionRef as JsonRecord;
assert.match(String(sessionRef.sessionId), /^sess_artificer_[a-f0-9]{24}$/u, "Artificer queue task should default to a resumable session");
assert.equal(sessionRef.conversationId, sessionRef.sessionId, "default Artificer conversation should match the generated session");
@@ -92,6 +89,11 @@ const selfTest: SelfTestCase = async (context) => {
assert.throws(() => validateResourceBundleRef({ kind: "gitbundle", repoUrl: "git@github.com:pikasTech/unidesk.git", ref: "master", bundles: [{ repoUrl: "git@github.com:pikasTech/agentrun.git", subpath: "tools", targetPath: "tools" }] }), /different repoUrl and must declare its own ref or commitId/u);
assert.throws(() => validateResourceBundleRef({ kind: "gitbundle", repoUrl: "git@github.com:pikasTech/unidesk.git", ref: "master", bundles: [{ subpath: "tools", targetPath: "tools" }, { subpath: ".agents/skills", targetPath: "tools" }] }), /target_path conflicts/u);
assert.throws(() => validateResourceBundleRef({ kind: "gitbundle", repoUrl: "git@github.com:pikasTech/unidesk.git", ref: "master", bundles: [{ subpath: ".git", targetPath: ".git" }] }), /cannot replace primary workspace Git metadata/u);
const primaryContractBundle = validateResourceBundleRef({ kind: "gitbundle", repoUrl: "git@github.com:pikasTech/unidesk.git", ref: "master", bundles: [{ subpath: "tools", targetPath: "tools" }] });
assert.ok(primaryContractBundle);
assert.doesNotThrow(() => validatePrimaryWorkspaceContract({ kind: "opaque", path: "." }, primaryContractBundle));
assert.throws(() => validatePrimaryWorkspaceContract({ kind: "opaque", path: ".", repo: "pikasTech/unidesk" }, primaryContractBundle), /only supports kind and path/u);
assert.throws(() => validatePrimaryWorkspaceContract({ kind: "opaque", path: ".", branch: "master" }, primaryContractBundle), /only supports kind and path/u);
const submitPlan = await runCliJson(context, server.baseUrl, ["queue", "submit", "--aipod", "Artificer", "--prompt", "处理 pikasTech/unidesk#245", "--idempotency-key", "selftest-aipod-cli", "--dry-run"]);
assert.equal(submitPlan.ok, true);
+104 -8
View File
@@ -1,10 +1,11 @@
import assert from "node:assert/strict";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { access, mkdir, writeFile } from "node:fs/promises";
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path";
import { AgentRunError } from "../../common/errors.js";
import type { JsonRecord, ResourceBundleRef, WorkspaceRef } from "../../common/types.js";
import { stableHash } from "../../common/validation.js";
import { ManagerClient } from "../../mgr/client.js";
import { startManagerServer } from "../../mgr/server.js";
import { MemoryAgentRunStore } from "../../mgr/store.js";
@@ -22,7 +23,7 @@ const selfTest: SelfTestCase = async (context) => {
const addon = await createRepo(context, "addon-source", {
"capability/probe": "#!/usr/bin/env sh\necho capability\n",
});
const workspaceRef: WorkspaceRef = { kind: "opaque", path: ".", branch: primary.branch };
const workspaceRef: WorkspaceRef = { kind: "opaque", path: "." };
const resourceBundleRef: ResourceBundleRef = {
kind: "gitbundle",
repoUrl: primary.path,
@@ -33,6 +34,7 @@ const selfTest: SelfTestCase = async (context) => {
const workspacePath = path.join(context.tmp, "primary-workspace-materialized");
const env = { AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "primary-workspace-assembly"), AGENTRUN_WORKSPACE_PATH: workspacePath, AGENTRUN_RUN_ID: "run_primary_workspace_selftest" };
await mkdir(workspacePath, { recursive: true });
const first = await materializeResourceBundle(resourceBundleRef, workspaceRef, env);
assert.ok(first);
await access(path.join(workspacePath, ".git"));
@@ -44,23 +46,51 @@ const selfTest: SelfTestCase = async (context) => {
assert.equal((await execFile("git", ["remote", "get-url", "origin"], { cwd: workspacePath })).stdout.trim(), primary.path);
const firstPrimary = first.event.primaryWorkspace as JsonRecord;
assert.equal(firstPrimary.sourceCommit, primary.commitId);
assert.equal(firstPrimary.headCommit, primary.commitId);
assert.equal(firstPrimary.sourceContract, "resourceBundleRef");
assert.equal(firstPrimary.workspaceLifecycle, "first-materialization");
assert.equal(firstPrimary.resolutionAuthority, "declared-commit");
assert.equal(firstPrimary.writable, true);
const provenancePath = path.join(workspacePath, ".git", "agentrun-primary-workspace.json");
const persistedProvenance = JSON.parse(await readFile(provenancePath, "utf8")) as JsonRecord;
assert.equal(persistedProvenance.resolutionAuthority, "declared-commit");
assert.equal(((persistedProvenance.resolvedSource as JsonRecord).commitId), primary.commitId);
await writeFile(path.join(workspacePath, "stale-from-previous-attempt.txt"), "stale\n", "utf8");
await writeFile(path.join(workspacePath, "dirty-from-previous-attempt.txt"), "preserve me\n", "utf8");
const replay = await materializeResourceBundle(resourceBundleRef, workspaceRef, env);
assert.ok(replay);
await assert.rejects(access(path.join(workspacePath, "stale-from-previous-attempt.txt")));
assert.equal(await readFile(path.join(workspacePath, "dirty-from-previous-attempt.txt"), "utf8"), "preserve me\n");
assert.equal((replay.event.primaryWorkspace as JsonRecord).workspaceLifecycle, "continuation-reuse");
assert.equal((replay.event.primaryWorkspace as JsonRecord).provenanceHash, firstPrimary.provenanceHash);
await assert.rejects(
() => materializeResourceBundle({ ...resourceBundleRef, credentialRef: { name: "missing-resource-git", keys: ["id_ed25519", "known_hosts"] } }, workspaceRef, { ...env, AGENTRUN_RUN_ID: "run_missing_resource_credential", AGENTRUN_RESOURCE_CREDENTIAL_PATH: undefined }),
() => materializeResourceBundle({ ...resourceBundleRef, ref: "changed-source-ref" }, workspaceRef, { ...env, AGENTRUN_RUN_ID: "run_changed_source_contract" }),
(error) => error instanceof AgentRunError && error.failureKind === "schema-invalid" && /declared source conflicts/u.test(error.message),
);
await assert.rejects(
() => materializeResourceBundle({ ...resourceBundleRef, bundles: [{ ...resourceBundleRef.bundles[0]!, targetPath: "alternate-tools" }] }, workspaceRef, { ...env, AGENTRUN_RUN_ID: "run_changed_assembly_contract" }),
(error) => error instanceof AgentRunError && error.failureKind === "schema-invalid" && /assembly contract conflicts/u.test(error.message),
);
const tamperedProvenance = JSON.parse(await readFile(provenancePath, "utf8")) as JsonRecord;
(tamperedProvenance.resolvedSource as JsonRecord).treeId = "0000000000000000000000000000000000000000";
const tamperedBody = { ...tamperedProvenance };
delete tamperedBody.provenanceHash;
tamperedProvenance.provenanceHash = stableHash(tamperedBody);
await writeFile(provenancePath, `${JSON.stringify(tamperedProvenance, null, 2)}\n`, "utf8");
await assert.rejects(
() => materializeResourceBundle(resourceBundleRef, workspaceRef, { ...env, AGENTRUN_RUN_ID: "run_tampered_frozen_tree" }),
(error) => error instanceof AgentRunError && error.failureKind === "schema-invalid" && /frozen source tree conflicts/u.test(error.message),
);
const missingCredentialWorkspace = path.join(context.tmp, "primary-workspace-missing-credential");
await assert.rejects(
() => materializeResourceBundle({ ...resourceBundleRef, credentialRef: { name: "missing-resource-git", keys: ["id_ed25519", "known_hosts"] } }, workspaceRef, { ...env, AGENTRUN_WORKSPACE_PATH: missingCredentialWorkspace, AGENTRUN_RUN_ID: "run_missing_resource_credential", AGENTRUN_RESOURCE_CREDENTIAL_PATH: undefined }),
(error) => error instanceof AgentRunError && error.failureKind === "secret-unavailable" && /runtime projection is missing/u.test(error.message),
);
const missingRef: ResourceBundleRef = { kind: "gitbundle", repoUrl: primary.path, ref: "missing-primary-ref", bundles: resourceBundleRef.bundles };
await assert.rejects(
() => materializeResourceBundle(missingRef, { ...workspaceRef, branch: "missing-primary-ref" }, { ...env, AGENTRUN_RUN_ID: "run_missing_primary_ref" }),
() => materializeResourceBundle(missingRef, workspaceRef, { ...env, AGENTRUN_WORKSPACE_PATH: path.join(context.tmp, "primary-workspace-missing-ref"), AGENTRUN_RUN_ID: "run_missing_primary_ref" }),
(error) => {
if (!(error instanceof AgentRunError) || error.failureKind !== "infra-failed") return false;
const source = error.details?.source as JsonRecord | undefined;
@@ -70,6 +100,54 @@ const selfTest: SelfTestCase = async (context) => {
},
);
const mutablePrimary = await createRepo(context, "mutable-primary-source", {
"AGENTS.md": "# Frozen primary workspace\n",
"package.json": "{\"name\":\"mutable-primary-workspace-selftest\"}\n",
});
const mutableBundle: ResourceBundleRef = {
kind: "gitbundle",
repoUrl: mutablePrimary.path,
ref: mutablePrimary.branch,
bundles: [{ name: "addon", repoUrl: addon.path, commitId: addon.commitId, subpath: "capability", targetPath: "tools" }],
};
const mutableWorkspacePath = path.join(context.tmp, "mutable-primary-workspace");
const mutableEnv = { AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "mutable-primary-assembly"), AGENTRUN_WORKSPACE_PATH: mutableWorkspacePath, AGENTRUN_RUN_ID: "run_mutable_primary_first" };
const mutableFirst = await materializeResourceBundle(mutableBundle, workspaceRef, mutableEnv);
assert.ok(mutableFirst);
const mutableFirstPrimary = mutableFirst.event.primaryWorkspace as JsonRecord;
assert.equal(mutableFirstPrimary.sourceCommit, mutablePrimary.commitId);
assert.equal(mutableFirstPrimary.resolutionAuthority, "runner-first-materialization");
const workspaceSuccessorCommit = await commitFile(mutableWorkspacePath, "workspace-successor.txt", "workspace successor\n", "workspace successor");
await writeFile(path.join(mutableWorkspacePath, "AGENTS.md"), "# Dirty continuation state\n", "utf8");
await writeFile(path.join(mutableWorkspacePath, "tools", "probe"), "workspace-owned bundle target\n", "utf8");
const advancedUpstreamCommit = await commitFile(mutablePrimary.path, "upstream-advanced.txt", "upstream advanced\n", "upstream advances mutable ref");
assert.notEqual(advancedUpstreamCommit, mutablePrimary.commitId);
assert.notEqual(workspaceSuccessorCommit, advancedUpstreamCommit);
await rename(mutablePrimary.path, `${mutablePrimary.path}-unavailable-after-advance`);
const mutableReplay = await materializeResourceBundle(mutableBundle, workspaceRef, { ...mutableEnv, AGENTRUN_RUN_ID: "run_mutable_primary_continuation" });
assert.ok(mutableReplay);
const mutableReplayPrimary = mutableReplay.event.primaryWorkspace as JsonRecord;
assert.equal(mutableReplayPrimary.sourceCommit, mutablePrimary.commitId);
assert.equal(mutableReplayPrimary.headCommit, workspaceSuccessorCommit);
assert.equal(mutableReplayPrimary.headIsFrozenSource, false);
assert.equal(mutableReplayPrimary.workspaceLifecycle, "continuation-reuse");
assert.equal(mutableReplayPrimary.resolutionAuthority, "runner-first-materialization");
assert.equal((await execFile("git", ["rev-parse", "HEAD"], { cwd: mutableWorkspacePath })).stdout.trim(), workspaceSuccessorCommit);
assert.equal(await readFile(path.join(mutableWorkspacePath, "AGENTS.md"), "utf8"), "# Dirty continuation state\n");
assert.equal(await readFile(path.join(mutableWorkspacePath, "tools", "probe"), "utf8"), "workspace-owned bundle target\n");
assert.match((await execFile("git", ["status", "--porcelain"], { cwd: mutableWorkspacePath })).stdout, /AGENTS\.md/u);
const unownedWorkspace = path.join(context.tmp, "nonempty-workspace-without-provenance");
await mkdir(unownedWorkspace, { recursive: true });
await writeFile(path.join(unownedWorkspace, "user-file.txt"), "must not be deleted\n", "utf8");
await assert.rejects(
() => materializeResourceBundle(resourceBundleRef, workspaceRef, { ...env, AGENTRUN_WORKSPACE_PATH: unownedWorkspace, AGENTRUN_RUN_ID: "run_unowned_workspace" }),
(error) => error instanceof AgentRunError && error.failureKind === "schema-invalid" && /missing AgentRun provenance/u.test(error.message),
);
assert.equal(await readFile(path.join(unownedWorkspace, "user-file.txt"), "utf8"), "must not be deleted\n");
const store = new MemoryAgentRunStore();
const server = await startManagerServer({
port: 0,
@@ -91,7 +169,7 @@ const selfTest: SelfTestCase = async (context) => {
const run = await client.post("/api/v1/runs", {
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
workspaceRef: { kind: "opaque", path: ".", branch: "missing-primary-ref" },
workspaceRef: { kind: "opaque", path: "." },
sessionRef: null,
resourceBundleRef: missingRef,
providerId: "NC01",
@@ -126,7 +204,16 @@ const selfTest: SelfTestCase = async (context) => {
tests: [
"primary-workspace-exact-source-materialization",
"primary-workspace-provenance-and-declared-origin",
"primary-workspace-retry-idempotent-replacement",
"primary-workspace-empty-root-atomic-installation",
"primary-workspace-continuation-preserves-dirty-tree",
"primary-workspace-continuation-freezes-mutable-ref",
"primary-workspace-continuation-does-not-fetch",
"primary-workspace-continuation-preserves-successor-commit",
"primary-workspace-continuation-reuses-bundle-provenance",
"primary-workspace-source-contract-conflict-fails-closed",
"primary-workspace-assembly-contract-conflict-fails-closed",
"primary-workspace-frozen-tree-provenance-verified",
"primary-workspace-nonempty-unowned-root-fails-closed",
"primary-workspace-credential-missing-typed-blocker",
"primary-workspace-ref-not-found-first-error",
"partial-bundle-cannot-mask-primary-failure",
@@ -151,4 +238,13 @@ async function createRepo(context: SelfTestContext, name: string, files: Record<
return { path: repo, branch, commitId };
}
async function commitFile(repo: string, relativePath: string, text: string, message: string): Promise<string> {
const file = path.join(repo, relativePath);
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, text, "utf8");
await execFile("git", ["add", relativePath], { cwd: repo });
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", message], { cwd: repo });
return (await execFile("git", ["rev-parse", "HEAD"], { cwd: repo })).stdout.trim();
}
export default selfTest;