Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/report-package-evidence-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": patch
---

Report `AB6039` package-only compile-evidence failures against `agent-bundle.package-compile-evidence.json` (#672).
20 changes: 11 additions & 9 deletions packages/agent-bundle/src/build/compile-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,8 @@ export const parseCompileEvidenceRecord = (bytes: string): CompileEvidenceRecord
/** MCP App views are the only compiled HTML documents (`mcp-apps/<name>.html`). */
const isViewAsset = (path: string): boolean => path.endsWith('.html');

const evidenceDiagnostic = (message: string): Diagnostic =>
artifactDiagnostic('AB6039', `Compile evidence ${message}`, compileEvidenceFileName);
const evidenceDiagnostic = (message: string, evidencePath: string): Diagnostic =>
artifactDiagnostic('AB6039', `Compile evidence ${message}`, evidencePath);

/**
* Checks a parsed record against the artifact's file table: every compiled
Expand All @@ -322,10 +322,12 @@ const evidenceDiagnostic = (message: string): Diagnostic =>
export const compileEvidenceDiagnostics = (
record: CompileEvidenceRecord,
files: ReadonlyMap<string, { readonly kind: string; readonly sha256: string }>,
evidencePath: string = compileEvidenceFileName,
): readonly Diagnostic[] => {
const diagnostics: Diagnostic[] = [];
const reportEvidence = (message: string): Diagnostic => evidenceDiagnostic(message, evidencePath);
if (record.policy.name !== externalPolicy.name || record.policy.revision !== externalPolicy.revision) {
diagnostics.push(evidenceDiagnostic(
diagnostics.push(reportEvidence(
`was judged under policy ${record.policy.name}@${String(record.policy.revision)}; `
+ `this validator applies ${externalPolicy.name}@${String(externalPolicy.revision)}.`,
));
Expand All @@ -334,18 +336,18 @@ export const compileEvidenceDiagnostics = (
const compiled = new Set([...files].filter(([, file]) => file.kind === 'bundle').map(([path]) => path));
for (const path of compiled) {
const asset = recorded.get(path);
if (asset === undefined) diagnostics.push(evidenceDiagnostic(`does not cover compiled file ${JSON.stringify(path)}.`));
else if (asset.sha256 !== files.get(path)!.sha256) diagnostics.push(evidenceDiagnostic(`for ${JSON.stringify(path)} describes different bytes.`));
if (asset === undefined) diagnostics.push(reportEvidence(`does not cover compiled file ${JSON.stringify(path)}.`));
else if (asset.sha256 !== files.get(path)!.sha256) diagnostics.push(reportEvidence(`for ${JSON.stringify(path)} describes different bytes.`));
}
// A view (an HTML document) inlines every module it loads; only node bundles may load a sibling, and only another node bundle.
const nodeBundles = new Set([...compiled].filter((path) => !isViewAsset(path)));
for (const asset of record.assets) {
if (!compiled.has(asset.path)) {
diagnostics.push(evidenceDiagnostic(`names ${JSON.stringify(asset.path)}, which the file table does not list as a compiled file.`));
diagnostics.push(reportEvidence(`names ${JSON.stringify(asset.path)}, which the file table does not list as a compiled file.`));
}
for (const external of asset.externals) {
if (isViewAsset(asset.path)) {
diagnostics.push(evidenceDiagnostic(
diagnostics.push(reportEvidence(
`for ${JSON.stringify(asset.path)} records ${JSON.stringify(external.request)} as an external; a view inlines every module it loads.`,
));
continue;
Expand All @@ -355,14 +357,14 @@ export const compileEvidenceDiagnostics = (
switch (external.kind) {
case 'builtin':
if (judged !== 'builtin') {
diagnostics.push(evidenceDiagnostic(
diagnostics.push(reportEvidence(
`for ${JSON.stringify(asset.path)} records ${JSON.stringify(external.request)} as a built-in; it is not one.`,
));
}
break;
case 'artifact-relative':
if (judged !== 'artifact-relative' || external.target !== posix.join(posix.dirname(asset.path), external.request)) {
diagnostics.push(evidenceDiagnostic(
diagnostics.push(reportEvidence(
`for ${JSON.stringify(asset.path)} records sibling ${JSON.stringify(external.request)}, which the artifact does not contain.`,
));
}
Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/src/build/pack-inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export const packInventoryDiagnostics = async (options: {
new Map(options.packageBuild.files
.filter((file) => file.kind === 'bundle' && !artifactPaths.has(file.path))
.map((file) => [file.path, { kind: file.kind, sha256: file.sha256 }])),
packageCompileEvidenceFileName,
));
} catch (error) {
diagnostics.push(diagnostic(
Expand Down
9 changes: 7 additions & 2 deletions packages/agent-bundle/tests/prepack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,13 @@ it('reports package-only compile evidence drift as AB6039', async () => {
const path = join(projectRoot, 'dist', packageCompileEvidenceFileName);
const original = await readFile(path, 'utf8');
try {
await writeFile(path, '{"assets":[]}\n');
expect(await diagnostics()).toContainEqual(expect.objectContaining({ code: 'AB6039' }));
const evidence = JSON.parse(original) as { policy: { revision: number } };
evidence.policy.revision += 1;
await writeFile(path, `${JSON.stringify(evidence)}\n`);
expect(await diagnostics()).toContainEqual(expect.objectContaining({
code: 'AB6039',
generatedPath: packageCompileEvidenceFileName,
}));
} finally {
await writeFile(path, original);
}
Expand Down
Loading