Merge pull request #317 from pikasTech/fix/310-artificer-workspace-cleanliness
Pipelines as Code CI / agentrun-nc01-v02-ci-c38a90d6cd102f5ee4ceeaf257778166f6d1f1bf Success

修复 Artificer 受管能力覆盖污染源码视图
This commit is contained in:
Lyon
2026-07-12 15:22:50 +08:00
committed by GitHub
2 changed files with 141 additions and 18 deletions
+79 -16
View File
@@ -13,6 +13,7 @@ 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;
@@ -173,8 +174,10 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
checkoutPath = defaultCheckout.checkoutPath;
const stagePath = await stagePrimaryWorkspace(defaultCheckout.checkoutPath, workspacePath, runScope);
try {
const stagedBundles = await materializeGitBundles(stagePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
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);
@@ -192,8 +195,9 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
}
const primaryWorkspace = await verifyPrimaryWorkspace(workspacePath, workspaceRef, provenance, workspaceLifecycle, false);
const tools = await prepareGitBundleTools(workspacePath, env);
const skills = await discoverGitBundleSkills(workspacePath, materializedBundles);
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);
@@ -221,10 +225,18 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
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, valuesPrinted: false })),
items: materializedBundles.map((item) => ({ ...item, logicalTargetPath: item.targetPath, storageKind: "git-metadata-overlay", valuesPrinted: false })),
valuesPrinted: false,
},
tools: tools.event,
@@ -608,6 +620,57 @@ async function installPrimaryWorkspace(stagePath: string, workspacePath: string)
}
}
async function prepareManagedOverlay(workspacePath: string): Promise<string> {
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<string> {
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<void> {
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<JsonRecord> {
let rootStat;
let gitStat;
@@ -714,13 +777,13 @@ function cleanGithubPath(owner: string, repo: string): string | null {
return `${cleanOwner}/${cleanRepo}`;
}
async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource, sourceContext: GitBundleSourceContext) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
async function materializeGitBundles(overlayRoot: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource, sourceContext: GitBundleSourceContext) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
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 = resolveWorkspaceTargetPath(workspacePath, bundle.targetPath, `bundles[${index}].target_path`);
const target = resolveManagedOverlayTargetPath(overlayRoot, bundle.targetPath, `bundles[${index}].target_path`);
let sourceStat;
try {
sourceStat = await stat(source);
@@ -747,8 +810,8 @@ function installedToolShim(sourcePath: string): string {
return `#!/usr/bin/env sh\nexec ${shellSingleQuote(sourcePath)} "$@"\n`;
}
async function prepareGitBundleTools(workspacePath: string, env: NodeJS.ProcessEnv): Promise<{ binPath?: string; event: JsonRecord }> {
const sourceBinPath = path.join(workspacePath, "tools");
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;
@@ -862,8 +925,8 @@ async function materializePromptRefs(checkoutPath: string, refs: NonNullable<Res
};
}
async function discoverGitBundleSkills(workspacePath: string, bundles: MaterializedGitBundle[]): Promise<{ items: MaterializedSkillRef[]; skillsDir?: string; event: JsonRecord }> {
const skillsDir = path.join(workspacePath, ".agents", "skills");
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 });
@@ -984,8 +1047,8 @@ function assembleInitialPrompt(promptRefs: MaterializedPromptRef[], skills: Mate
if (skills.length > 0) {
const lines = [
"## Resource Skills",
"The following required runtime skills are mounted in the current workspace. Use these bundle skills instead of default model skill guesses.",
...skills.map((skill) => `- ${skill.name}: ${skill.summary || "No summary provided."} manifest=.agents/skills/${skill.aggregateAs}/SKILL.md source=${skill.path} required=${skill.required}`),
"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"));
}
@@ -1212,10 +1275,10 @@ function resolveBundlePath(checkoutPath: string, relativePath: string, fieldName
return resolved;
}
function resolveWorkspaceTargetPath(workspacePath: string, relativePath: string, fieldName: string): string {
const resolved = path.resolve(workspacePath, relativePath);
const root = path.resolve(workspacePath);
if (resolved === root || !resolved.startsWith(`${root}${path.sep}`)) throw new AgentRunError("schema-invalid", `${fieldName} escaped workspace`, { httpStatus: 400 });
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;
}
+62 -2
View File
@@ -19,9 +19,16 @@ const selfTest: SelfTestCase = async (context) => {
const primary = await createRepo(context, "primary-source", {
"AGENTS.md": "# Primary workspace\n",
"package.json": "{\"name\":\"primary-workspace-selftest\"}\n",
"tools/source-tool": "#!/usr/bin/env sh\necho primary-source-tool\n",
"tools/probe": "#!/usr/bin/env sh\necho primary-source-probe\n",
".agents/skills/source-skill/SKILL.md": "# Source-owned skill\n",
".agents/skills/dad-dev/SKILL.md": "# Source-owned dad-dev skill\n",
});
const addon = await createRepo(context, "addon-source", {
"capability/probe": "#!/usr/bin/env sh\necho capability\n",
"dad-dev/SKILL.md": "---\ndescription: Managed dad-dev capability\n---\n",
"git-spec/SKILL.md": "---\ndescription: Managed git-spec capability\n---\n",
"unidesk-gh/SKILL.md": "---\ndescription: Managed unidesk-gh capability\n---\n",
});
const workspaceRef: WorkspaceRef = { kind: "opaque", path: "." };
const resourceBundleRef: ResourceBundleRef = {
@@ -29,7 +36,14 @@ const selfTest: SelfTestCase = async (context) => {
repoUrl: primary.path,
ref: primary.branch,
commitId: primary.commitId,
bundles: [{ name: "addon", repoUrl: addon.path, commitId: addon.commitId, subpath: "capability", targetPath: "tools" }],
bundles: [
{ name: "primary-skills", subpath: ".agents/skills", targetPath: ".agents/skills" },
{ name: "addon-tools", repoUrl: addon.path, commitId: addon.commitId, subpath: "capability", targetPath: "tools" },
{ name: "dad-dev-skill", repoUrl: addon.path, commitId: addon.commitId, subpath: "dad-dev", targetPath: ".agents/skills/dad-dev" },
{ name: "git-spec-skill", repoUrl: addon.path, commitId: addon.commitId, subpath: "git-spec", targetPath: ".agents/skills/git-spec" },
{ name: "unidesk-gh-skill", repoUrl: addon.path, commitId: addon.commitId, subpath: "unidesk-gh", targetPath: ".agents/skills/unidesk-gh" },
],
requiredSkills: [{ name: "source-skill" }, { name: "dad-dev" }, { name: "git-spec" }, { name: "unidesk-gh" }],
};
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" };
@@ -40,7 +54,25 @@ const selfTest: SelfTestCase = async (context) => {
await access(path.join(workspacePath, ".git"));
await access(path.join(workspacePath, "AGENTS.md"));
await access(path.join(workspacePath, "package.json"));
await access(path.join(workspacePath, "tools", "probe"));
await access(path.join(workspacePath, "tools", "source-tool"));
await access(path.join(workspacePath, ".agents", "skills", "source-skill", "SKILL.md"));
assert.equal(await readFile(path.join(workspacePath, "tools", "probe"), "utf8"), "#!/usr/bin/env sh\necho primary-source-probe\n");
assert.equal(await readFile(path.join(workspacePath, ".agents", "skills", "dad-dev", "SKILL.md"), "utf8"), "# Source-owned dad-dev skill\n");
assert.ok(first.binPath);
assert.ok(first.skillsDir);
assert.match(first.binPath, /\.git\/agentrun-managed-overlay\/tools$/u);
assert.match(first.skillsDir, /\.git\/agentrun-managed-overlay\/\.agents\/skills$/u);
await access(path.join(first.binPath, "probe"));
await access(path.join(first.skillsDir, "source-skill", "SKILL.md"));
await access(path.join(first.skillsDir, "dad-dev", "SKILL.md"));
await access(path.join(first.skillsDir, "git-spec", "SKILL.md"));
await access(path.join(first.skillsDir, "unidesk-gh", "SKILL.md"));
assert.equal(await readFile(path.join(first.binPath, "probe"), "utf8"), "#!/usr/bin/env sh\necho capability\n");
assert.match(await readFile(path.join(first.skillsDir, "dad-dev", "SKILL.md"), "utf8"), /Managed dad-dev capability/u);
assert.match(first.initialPrompt?.text ?? "", /manifest=.*\.git\/agentrun-managed-overlay\/\.agents\/skills\/dad-dev\/SKILL\.md skillsDirEnv=AGENTRUN_SKILLS_DIRS/u);
assert.match(first.initialPrompt?.text ?? "", /manifest=.*\.git\/agentrun-managed-overlay\/\.agents\/skills\/git-spec\/SKILL\.md skillsDirEnv=AGENTRUN_SKILLS_DIRS/u);
assert.match(first.initialPrompt?.text ?? "", /manifest=.*\.git\/agentrun-managed-overlay\/\.agents\/skills\/unidesk-gh\/SKILL\.md skillsDirEnv=AGENTRUN_SKILLS_DIRS/u);
assert.equal((await execFile("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspacePath })).stdout, "");
assert.equal((await execFile("git", ["rev-parse", "HEAD"], { cwd: workspacePath })).stdout.trim(), primary.commitId);
assert.equal((await execFile("git", ["branch", "--show-current"], { cwd: workspacePath })).stdout.trim(), primary.branch);
assert.equal((await execFile("git", ["remote", "get-url", "origin"], { cwd: workspacePath })).stdout.trim(), primary.path);
@@ -51,17 +83,37 @@ const selfTest: SelfTestCase = async (context) => {
assert.equal(firstPrimary.workspaceLifecycle, "first-materialization");
assert.equal(firstPrimary.resolutionAuthority, "declared-commit");
assert.equal(firstPrimary.writable, true);
const managedOverlay = first.event.managedOverlay as JsonRecord;
assert.equal(managedOverlay.kind, "git-metadata");
assert.equal(managedOverlay.rootRelativeToWorkspace, ".git/agentrun-managed-overlay");
assert.equal(managedOverlay.sourceViewIsolated, true);
const bundleItems = ((first.event.bundles as JsonRecord).items as JsonRecord[]);
assert.deepEqual(bundleItems.map((item) => item.logicalTargetPath), [".agents/skills", "tools", ".agents/skills/dad-dev", ".agents/skills/git-spec", ".agents/skills/unidesk-gh"]);
assert.equal(bundleItems.every((item) => item.storageKind === "git-metadata-overlay"), true);
assert.deepEqual(((first.event.requiredSkills as JsonRecord).names), ["source-skill", "dad-dev", "git-spec", "unidesk-gh"]);
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, "dirty-from-previous-attempt.txt"), "preserve me\n", "utf8");
await writeFile(path.join(workspacePath, "tools", "source-tool"), "#!/usr/bin/env sh\necho source-modified\n", "utf8");
const dirtyStatus = (await execFile("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspacePath })).stdout;
assert.match(dirtyStatus, / M tools\/source-tool/u);
assert.match(dirtyStatus, /\?\? dirty-from-previous-attempt\.txt/u);
const replay = await materializeResourceBundle(resourceBundleRef, workspaceRef, env);
assert.ok(replay);
assert.equal(await readFile(path.join(workspacePath, "dirty-from-previous-attempt.txt"), "utf8"), "preserve me\n");
assert.equal(await readFile(path.join(workspacePath, "tools", "source-tool"), "utf8"), "#!/usr/bin/env sh\necho source-modified\n");
assert.equal(replay.binPath, first.binPath);
assert.equal(replay.skillsDir, first.skillsDir);
await access(path.join(replay.binPath!, "probe"));
await access(path.join(replay.skillsDir!, "dad-dev", "SKILL.md"));
assert.equal((replay.event.primaryWorkspace as JsonRecord).workspaceLifecycle, "continuation-reuse");
assert.equal((replay.event.primaryWorkspace as JsonRecord).provenanceHash, firstPrimary.provenanceHash);
const replayStatus = (await execFile("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspacePath })).stdout;
assert.match(replayStatus, / M tools\/source-tool/u);
assert.match(replayStatus, /\?\? dirty-from-previous-attempt\.txt/u);
await assert.rejects(
() => materializeResourceBundle({ ...resourceBundleRef, ref: "changed-source-ref" }, workspaceRef, { ...env, AGENTRUN_RUN_ID: "run_changed_source_contract" }),
@@ -103,6 +155,7 @@ 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",
"tools/source-tool": "#!/usr/bin/env sh\necho mutable-source-tool\n",
});
const mutableBundle: ResourceBundleRef = {
kind: "gitbundle",
@@ -117,6 +170,8 @@ const selfTest: SelfTestCase = async (context) => {
const mutableFirstPrimary = mutableFirst.event.primaryWorkspace as JsonRecord;
assert.equal(mutableFirstPrimary.sourceCommit, mutablePrimary.commitId);
assert.equal(mutableFirstPrimary.resolutionAuthority, "runner-first-materialization");
assert.ok(mutableFirst.binPath);
await access(path.join(mutableFirst.binPath, "probe"));
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");
@@ -125,6 +180,7 @@ const selfTest: SelfTestCase = async (context) => {
assert.notEqual(advancedUpstreamCommit, mutablePrimary.commitId);
assert.notEqual(workspaceSuccessorCommit, advancedUpstreamCommit);
await rename(mutablePrimary.path, `${mutablePrimary.path}-unavailable-after-advance`);
await rename(addon.path, `${addon.path}-unavailable-after-first-materialization`);
const mutableReplay = await materializeResourceBundle(mutableBundle, workspaceRef, { ...mutableEnv, AGENTRUN_RUN_ID: "run_mutable_primary_continuation" });
assert.ok(mutableReplay);
@@ -205,6 +261,10 @@ const selfTest: SelfTestCase = async (context) => {
"primary-workspace-exact-source-materialization",
"primary-workspace-provenance-and-declared-origin",
"primary-workspace-empty-root-atomic-installation",
"managed-capability-overlay-keeps-primary-source-view-clean",
"managed-capability-overlay-preserves-nested-targets",
"managed-capability-overlay-publishes-tools-and-skills",
"real-primary-source-modifications-remain-git-visible",
"primary-workspace-continuation-preserves-dirty-tree",
"primary-workspace-continuation-freezes-mutable-ref",
"primary-workspace-continuation-does-not-fetch",