From bf3c2740a4855d265923f13ae2fa0bb7ce0c716c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Sep 2026 21:59:10 +0000 Subject: [PATCH 1/4] fix(dev): fail closed on missing artifact provenance; read Workbench file provenance from its own manifest record ArtifactInspectionService refuses (AB6200) a files[] row with no compiler.provenance row instead of inspecting it with an invented empty record. The Workbench Artifact file details carry each file's source inputs on its tree row from the file record itself, dropping the by-path lookup that defaulted to an em dash; an empty record reads 'No source inputs recorded'. Adds inspector-level missing, conflicting, and relocated provenance tests and a rendered-text test for the file details. Co-authored-by: Zack Jackson --- .changeset/682-inspector-provenance.md | 5 + docs/diagnostics.md | 2 +- .../artifacts/artifact-inspection-service.ts | 11 +- .../tests/artifact-inspection-service.test.ts | 102 +++++++++++++++++- .../src/artifacts/artifacts-model.ts | 30 +----- .../src/artifacts/artifacts-page.tsx | 29 ++--- .../workbench/tests/artifacts-model.test.ts | 22 ++-- .../workbench/tests/artifacts-page.test.ts | 15 +++ .../docs/en/guide/development/workbench.mdx | 5 +- .../docs/zh/guide/development/workbench.mdx | 4 +- 10 files changed, 168 insertions(+), 57 deletions(-) create mode 100644 .changeset/682-inspector-provenance.md diff --git a/.changeset/682-inspector-provenance.md b/.changeset/682-inspector-provenance.md new file mode 100644 index 000000000..e2e5b94c5 --- /dev/null +++ b/.changeset/682-inspector-provenance.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Make Workbench artifact provenance fail closed and read from one record: `ArtifactInspectionService` refuses an epoch (`AB6200`, `Artifact file has no manifest provenance record.`) instead of inspecting a `files[]` row with invented empty provenance when its `compiler.provenance` row is missing, and the Workbench Artifact file details render each file's source inputs from that file's own manifest record — a row with none reads `No source inputs recorded` — rather than a by-path lookup that defaulted to `—`. Adds inspector-level missing, conflicting, and relocated provenance tests (#PR) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index c5290dcd3..7a832f57c 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -1909,7 +1909,7 @@ diagnostics in the response body. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB6200` | error | `Artifact inspection could not validate the published artifact.` — the strict artifact validator threw over the epoch; `Artifact file provenance references an unknown project source input.` — an output's `sourceInputs` name an input the manifest project does not declare; `Artifact manifest project inputs are invalid.` — the manifest's project inputs are structurally invalid. An epoch whose validation merely reports diagnostics is refused with those diagnostics instead of this code. | Rebuild the epoch from a project whose artifact validates cleanly. | +| `AB6200` | error | `Artifact inspection could not validate the published artifact.` — the strict artifact validator threw over the epoch; `Artifact file has no manifest provenance record.` — a `files[]` row has no `compiler.provenance` row, so the inspector refuses rather than showing the file with empty provenance; `Artifact file provenance references an unknown project source input.` — an output's `sourceInputs` name an input the manifest project does not declare; `Artifact manifest project inputs are invalid.` — the manifest's project inputs are structurally invalid. An epoch whose validation merely reports diagnostics is refused with those diagnostics instead of this code. | Rebuild the epoch from a project whose artifact validates cleanly. | | `AB6201` | error | `Artifact inspection could not release every acquired epoch reference.` — releasing an epoch reference after an inspection or diff failed. | None in the project: the failure is internal to the development server's epoch bookkeeping. | | `AB6202` | error | Runtime metadata derived from the validated snapshot is unsafe: an MCP server's `entryPaths` name a file outside its target or absent from the manifest (`Validated MCP evidence references an unmanifested target file.`), or another runtime-evidence check named in the message failed. | Rebuild the epoch so its MCP runtime evidence references manifested target files. | diff --git a/packages/agent-bundle/src/dev/artifacts/artifact-inspection-service.ts b/packages/agent-bundle/src/dev/artifacts/artifact-inspection-service.ts index 8f3725bf2..c03427d52 100644 --- a/packages/agent-bundle/src/dev/artifacts/artifact-inspection-service.ts +++ b/packages/agent-bundle/src/dev/artifacts/artifact-inspection-service.ts @@ -176,7 +176,7 @@ export class ArtifactInspectionService { ); const files = Object.freeze(manifest.files - .map((file) => this.#file(file, sourceInputs, provenanceByPath.get(file.path) ?? [])) + .map((file) => this.#file(file, sourceInputs, provenanceByPath.get(file.path))) .sort(comparePaths)); const filesByPath = new Map(files.map((file) => [file.path, file])); const project = this.#project(manifest.compiler.project, sourceInputs); @@ -224,8 +224,15 @@ export class ArtifactInspectionService { #file( file: ArtifactManifest['files'][number], sourceInputs: ReadonlyMap, - provenanceInputs: readonly string[], + provenanceInputs: readonly string[] | undefined, ): ArtifactInspectionFile { + if (provenanceInputs === undefined) { + throw inspectionError( + 'ARTIFACT_INSPECTION_INVALID', + 'Artifact inspection requires a manifest provenance record for every output file.', + inspectionDiagnostic('AB6200', 'Artifact file has no manifest provenance record.', file.path), + ); + } const inputs = provenanceInputs.map((path) => sourceInputs.get(path)); if (inputs.some((input) => input === undefined)) { throw inspectionError( diff --git a/packages/agent-bundle/tests/artifact-inspection-service.test.ts b/packages/agent-bundle/tests/artifact-inspection-service.test.ts index 137372555..f5d1683b5 100644 --- a/packages/agent-bundle/tests/artifact-inspection-service.test.ts +++ b/packages/agent-bundle/tests/artifact-inspection-service.test.ts @@ -1,5 +1,5 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; -import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -10,11 +10,12 @@ import type { TargetAdapter, TargetAdapterMetadata } from '../src/adapters/types import { artifactCompilerRecordVersion, assembleArtifactManifest, + parseArtifactManifest, type ArtifactManifestFileKind, type ArtifactManifest, } from '../src/build/manifest.ts'; import { validateArtifact, validateArtifactWithSnapshot } from '../src/build/validate-artifact.ts'; -import { digest } from '../src/core/digest.ts'; +import { digest, stableJson } from '../src/core/digest.ts'; import { ArtifactInspectionService } from '../src/dev/index.ts'; import { EpochStore } from '../src/dev/epoch-store.ts'; import type { ArtifactEpoch } from '../src/dev/types.ts'; @@ -603,6 +604,103 @@ it('revalidates an epoch on each inspection so post-publication corruption is vi } }); +interface ProvenanceTamperCase { + readonly name: string; + readonly rule: string; + readonly tamper: (provenance: { path: string; sourceInputs: string[] }[]) => { path: string; sourceInputs: string[] }[]; +} + +const provenanceTamperCases: readonly ProvenanceTamperCase[] = [ + { + name: 'missing: an output file has no provenance row', + rule: 'compiler.provenance paths must exactly match files.', + tamper: (provenance) => provenance.filter((entry) => entry.path !== 'mcp/runner.mjs'), + }, + { + name: 'conflicting: a provenance row names an input the project never declared', + rule: 'compiler.provenance[mcp/runner.mjs].sourceInputs contains an undeclared project source input.', + tamper: (provenance) => provenance.map((entry) => entry.path === 'mcp/runner.mjs' + ? { ...entry, sourceInputs: ['src/elsewhere.ts'] } + : entry), + }, + { + name: 'conflicting: two provenance rows claim the same output file', + rule: 'compiler.provenance must be sorted with no duplicate entries.', + tamper: (provenance) => provenance.flatMap((entry) => entry.path === 'mcp/runner.mjs' + ? [entry, { ...entry, sourceInputs: [configPath] }] + : [entry]), + }, + { + name: 'relocated: a provenance row names an output the artifact does not carry', + rule: 'compiler.provenance paths must exactly match files.', + tamper: (provenance) => provenance.map((entry) => entry.path === 'mcp/runner.mjs' + ? { ...entry, path: 'mcp/moved/runner.mjs' } + : entry), + }, +]; + +it.each(provenanceTamperCases)('refuses to inspect an epoch whose provenance is $name', async ({ rule, tamper }) => { + // Every case is a manifest the compiler cannot emit (`createOutputProvenance` + // writes one row per output). A copy edited afterwards must fail closed at + // the manifest parser, so the inspector never renders a file with invented, + // empty, or borrowed source inputs. + const root = await mkdtemp(join(tmpdir(), 'agent-bundle-artifact-inspection-provenance-tamper-')); + const registry = runtimeRegistry(); + const store = new TrackingEpochStore({ projectRoot: root }); + const epochId = 'epoch-provenance-tamper'; + + try { + await publish({ files: runtimeFiles(), id: epochId, registry, root, store }); + const manifestPath = join(root, '.agent-bundle', 'epochs', epochId, 'agent-bundle.manifest.json'); + const published = JSON.parse(await readFile(manifestPath, 'utf8')) as { + compiler: { provenance: { path: string; sourceInputs: string[] }[] }; + }; + published.compiler.provenance = tamper(published.compiler.provenance); + const tampered = `${stableJson(published)}\n`; + expect(() => parseArtifactManifest(tampered)).toThrow(rule); + await writeFile(manifestPath, tampered); + + await expect(new ArtifactInspectionService(store, registry).inspect(epochId)).rejects.toMatchObject({ + code: 'ARTIFACT_INSPECTION_INVALID', + diagnostics: [expect.objectContaining({ code: 'AB6001', generatedPath: 'agent-bundle.manifest.json' })], + }); + expect(store).toMatchObject({ acquired: 1, closed: 1 }); + } finally { + await rm(root, { force: true, recursive: true }); + } +}); + +it('inspects identical root-relative provenance after the published epochs are relocated', async () => { + const root = await mkdtemp(join(tmpdir(), 'agent-bundle-artifact-inspection-relocated-origin-')); + const relocated = await mkdtemp(join(tmpdir(), 'agent-bundle-artifact-inspection-relocated-')); + const registry = runtimeRegistry(); + const epochId = 'epoch-relocated'; + + try { + await publish({ files: runtimeFiles(), id: epochId, registry, root, store: new EpochStore({ projectRoot: root }) }); + const before = await new ArtifactInspectionService(new EpochStore({ projectRoot: root }), registry).inspect(epochId); + + // The epoch directories move to another project root; the origin keeps its active-build pointer. + await mkdir(join(relocated, '.agent-bundle'), { recursive: true }); + await rename(join(root, '.agent-bundle', 'epochs'), join(relocated, '.agent-bundle', 'epochs')); + const store = new TrackingEpochStore({ projectRoot: relocated }); + const after = await new ArtifactInspectionService(store, registry).inspect(epochId); + + expect(after).toEqual(before); + expect(after.provenance).toContainEqual({ + outputPath: 'mcp/runner.mjs', + sourceInputs: [{ path: runnerSourcePath, sha256: fixtureInputs[1]!.sha256 }], + }); + const serialized = JSON.stringify(after); + expect(serialized).not.toContain(root); + expect(serialized).not.toContain(relocated); + expect(store).toMatchObject({ acquired: 1, closed: 1 }); + } finally { + await rm(root, { force: true, recursive: true }); + await rm(relocated, { force: true, recursive: true }); + } +}); + it('returns deeply frozen detached inspection records', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-artifact-inspection-immutable-')); const registry = runtimeRegistry(); diff --git a/packages/workbench/src/artifacts/artifacts-model.ts b/packages/workbench/src/artifacts/artifacts-model.ts index 22628726d..4b014dcaf 100644 --- a/packages/workbench/src/artifacts/artifacts-model.ts +++ b/packages/workbench/src/artifacts/artifacts-model.ts @@ -7,13 +7,10 @@ import type { ArtifactInspectionFile, ArtifactInspectionFileNode, ArtifactInspectionProjection, - ArtifactInspectionProvenance, - ArtifactInspectionSourceInput, ArtifactInspectionTreeNode, } from '../../../agent-bundle/src/contracts/artifacts.ts'; import { deepFreeze } from '../freeze.ts'; - export type ArtifactDiffChange = 'added' | 'changed' | 'removed' | 'unchanged'; export type ArtifactViewState = 'diagnostics' | 'empty' | 'no-epoch' | 'ready'; @@ -33,12 +30,8 @@ export interface ArtifactTreeRow { readonly name: string; readonly path: string; readonly sha256?: string; -} - -export interface ArtifactProvenanceRow { - readonly key: string; - readonly outputPath: string; - readonly sourceInputs: readonly ArtifactInspectionSourceInput[]; + /** The manifest's `compiler.provenance` source-input paths for a file row; a directory has none. */ + readonly sourceInputs?: readonly string[]; } export interface ArtifactDiffRow { @@ -86,7 +79,6 @@ export interface ArtifactView { readonly epochId: string | undefined; readonly identity: readonly ArtifactDetailRow[]; readonly projections: readonly ArtifactProjectionOption[]; - readonly provenance: readonly ArtifactProvenanceRow[]; readonly selected: ArtifactProjectionOption | undefined; readonly state: ArtifactViewState; readonly summary: string; @@ -95,8 +87,6 @@ export interface ArtifactView { const noDiagnostics: readonly Diagnostic[] = Object.freeze([]); -const noProvenance: readonly ArtifactProvenanceRow[] = Object.freeze([]); - const noRows: readonly ArtifactDetailRow[] = Object.freeze([]); const noProjections: readonly ArtifactProjectionOption[] = Object.freeze([]); @@ -128,6 +118,7 @@ const fileRow = (node: ArtifactInspectionFileNode, depth: number): ArtifactTreeR name: node.name, path: node.path, sha256: node.file.sha256, + sourceInputs: Object.freeze(node.file.sourceInputs.map((input) => input.path)), }); const directoryRow = (node: ArtifactInspectionDirectoryNode, depth: number): ArtifactTreeRow => Object.freeze({ @@ -171,20 +162,6 @@ export const artifactEpochIdentityRowsFor = (inspection: ArtifactInspection): re row('Emitted files', String(inspection.files.length)), ]); -export const artifactProvenanceRowsFor = ( - provenance: readonly ArtifactInspectionProvenance[], -): readonly ArtifactProvenanceRow[] => deepFreeze( - provenance - .map((entry): ArtifactProvenanceRow => ({ - key: entry.outputPath, - outputPath: entry.outputPath, - sourceInputs: Object.freeze( - [...entry.sourceInputs].sort((left, right) => left.path.localeCompare(right.path)), - ), - })) - .sort((left, right) => left.key.localeCompare(right.key)), -); - const diffGroup = ( change: ArtifactDiffChange, label: string, @@ -250,7 +227,6 @@ export const artifactViewFor = (options: ArtifactViewOptions): ArtifactView => { epochId: options.epochId, identity: inspection === undefined ? noRows : artifactEpochIdentityRowsFor(inspection), projections, - provenance: inspection === undefined ? noProvenance : artifactProvenanceRowsFor(inspection.provenance), selected, state, summary: summaryFor(state, inspection), diff --git a/packages/workbench/src/artifacts/artifacts-page.tsx b/packages/workbench/src/artifacts/artifacts-page.tsx index 728e7e39b..6f8ec043c 100644 --- a/packages/workbench/src/artifacts/artifacts-page.tsx +++ b/packages/workbench/src/artifacts/artifacts-page.tsx @@ -64,18 +64,29 @@ export const compareArtifactEpochs = async ( return client.diff(base, epochId); }; -const provenanceFor = (view: ArtifactView, path: string): readonly string[] => - view.provenance.find((entry) => entry.outputPath === path)?.sourceInputs.map((input) => input.path) ?? []; +/** + * An empty record is the manifest's own statement (a reindexed copy carries + * rows with no source inputs), so it reads as such rather than as an absent + * value. + */ +const provenanceLabel = (sourceInputs: readonly string[] | undefined): string => { + if (sourceInputs === undefined) return '—'; + return sourceInputs.length === 0 ? 'No source inputs recorded' : sourceInputs.join(', '); +}; + +export const ArtifactFileDetails = ({ row }: { readonly row: ArtifactTreeRow }) =>
+
SHA-256
{row.sha256 ?? '—'}
+
Mode
{row.mode ?? '—'}
+
Provenance
{provenanceLabel(row.sourceInputs)}
+
; const TreeRow = ({ detailsOpen, onToggle, - provenance, row, }: { readonly detailsOpen: boolean; readonly onToggle: () => void; - readonly provenance: readonly string[]; readonly row: ArtifactTreeRow; }) => { if (row.entry === 'directory') { @@ -108,14 +119,7 @@ const TreeRow = ({ {detailsOpen ? -
-
SHA-256
{row.sha256 ?? '—'}
-
Mode
{row.mode ?? '—'}
-
-
Provenance
-
{provenance.length === 0 ? '—' : provenance.join(', ')}
-
-
+ : undefined} @@ -144,7 +148,6 @@ const ArtifactTree = ({ view }: { readonly view: ArtifactView }) => { detailsOpen={openPaths.has(row.path)} key={row.key} onToggle={() => toggle(row.path)} - provenance={provenanceFor(view, row.path)} row={row} />)} } diff --git a/packages/workbench/tests/artifacts-model.test.ts b/packages/workbench/tests/artifacts-model.test.ts index db6507050..cfd0a4dcf 100644 --- a/packages/workbench/tests/artifacts-model.test.ts +++ b/packages/workbench/tests/artifacts-model.test.ts @@ -10,7 +10,6 @@ import type { import { artifactDiffViewFor, artifactEpochIdentityRowsFor, - artifactProvenanceRowsFor, artifactTreeRowsFor, artifactViewFor, } from '../src/artifacts/artifacts-model.ts'; @@ -167,6 +166,18 @@ it('flattens one projection tree into ordered directory and file rows', () => { expect(Object.isFrozen(rows)).toBe(true); }); +it('carries each file row\'s provenance from its own manifest file record', () => { + const rows = artifactTreeRowsFor(projection); + + expect(rows.map((row) => row.sourceInputs)).toEqual([ + undefined, + undefined, + ['hooks/session-start.ts'], + [], + ]); + expect(Object.isFrozen(rows[2]?.sourceInputs)).toBe(true); +}); + it('derives epoch identity rows from the inspection and its project context', () => { expect(artifactEpochIdentityRowsFor(inspection)).toEqual([ { label: 'Build ID', value: 'epoch-2' }, @@ -178,14 +189,6 @@ it('derives epoch identity rows from the inspection and its project context', () ]); }); -it('orders provenance rows by output path and keeps their declared source inputs', () => { - const rows = artifactProvenanceRowsFor(inspection.provenance); - - expect(rows.map((row) => row.outputPath)).toEqual(['AGENTS.md', 'hooks/session-start.mjs']); - expect(rows[0]?.sourceInputs).toEqual([]); - expect(rows[1]?.sourceInputs).toEqual([{ path: 'hooks/session-start.ts', sha256: 'b'.repeat(64) }]); -}); - it('groups an epoch diff into counted added, removed, changed, and unchanged rows', () => { const view = artifactDiffViewFor(diff); @@ -227,7 +230,6 @@ it('derives a ready view bound to the selected projection', () => { expect(view.application?.servers).toHaveLength(1); expect(view.application?.events).toHaveLength(1); expect(view.application?.hosts).toHaveLength(1); - expect(view.provenance).toHaveLength(2); expect(view.identity[0]).toEqual({ label: 'Build ID', value: 'epoch-2' }); expect(view.summary).toContain('fixture@1.2.3 build epoch-2'); expect(view.diagnostics).toEqual([]); diff --git a/packages/workbench/tests/artifacts-page.test.ts b/packages/workbench/tests/artifacts-page.test.ts index e54b9d286..62427aab9 100644 --- a/packages/workbench/tests/artifacts-page.test.ts +++ b/packages/workbench/tests/artifacts-page.test.ts @@ -13,6 +13,7 @@ import { ArtifactClient } from '../src/artifacts/artifact-client.ts'; import { ForegroundRouteClient } from '../src/mcp/mcp-route-client.ts'; import { ArtifactEpochDiffView, + ArtifactFileDetails, ArtifactInspectionView, ArtifactsPage, compareArtifactEpochs, @@ -188,6 +189,20 @@ it('renders the emitted file tree without runtime hook or MCP tables', () => { expect(markup).not.toContain('a'.repeat(64)); }); +it('renders each file\'s provenance from its own manifest record, never from a by-path lookup', () => { + // The fixture's flat `provenance` list has no AGENTS.md row; the file record is the only source. + const rows = new Map(readyView.tree.map((row) => [row.path, row])); + + const wrapperDetails = renderToStaticMarkup(createElement(ArtifactFileDetails, { row: rows.get('hooks/session-start.mjs')! })); + expect(wrapperDetails).toContain('
Provenance
hooks/session-start.ts
'); + expect(wrapperDetails).toContain('a'.repeat(64)); + expect(wrapperDetails).toContain('
Mode
0755
'); + + const agentsDetails = renderToStaticMarkup(createElement(ArtifactFileDetails, { row: rows.get('AGENTS.md')! })); + expect(agentsDetails).toContain('
Provenance
No source inputs recorded
'); + expect(agentsDetails).toContain('
Mode
'); +}); + it('renders artifact validation diagnostics as a visible alert', () => { const markup = renderToStaticMarkup(createElement(ArtifactInspectionView, { view: artifactViewFor({ diff --git a/website/docs/en/guide/development/workbench.mdx b/website/docs/en/guide/development/workbench.mdx index 141cac8bb..b0e830582 100644 --- a/website/docs/en/guide/development/workbench.mdx +++ b/website/docs/en/guide/development/workbench.mdx @@ -272,7 +272,10 @@ Repair a stale catalog in this order: - **Evals** has **Runs** and **Compare** views for admitting, cancelling, inspecting, and comparing eval runs. - **Artifact** defaults to a simple emitted-file tree. Select a file and enable its details to see - hashes, modes, and provenance. + hashes, modes, and provenance. Provenance is the file's own `compiler.provenance` row from + `agent-bundle.manifest.json`: the project source inputs it derives from. The compiler records at + least one input for every file it emits; a row with none reads **No source inputs recorded**. + Nothing is inferred from paths or file contents. - **Protocol** is the low-level MCP session inspector. It retains protocol traces, task controls, consent, restart/cancel actions, and the standalone [MCP Inspector](https://github.com/modelcontextprotocol/inspector) launcher. diff --git a/website/docs/zh/guide/development/workbench.mdx b/website/docs/zh/guide/development/workbench.mdx index 768e5817f..15ea89121 100644 --- a/website/docs/zh/guide/development/workbench.mdx +++ b/website/docs/zh/guide/development/workbench.mdx @@ -223,7 +223,9 @@ Problems 收集当前诊断。失败的构建不会发布新的 epoch,因此 - **Evals** 具有 **Runs** 与 **Compare** 视图,用于受理、取消、检视与对比 eval 运行。 - **Artifact** 默认是一棵简单的输出文件树。选中一个文件并启用其详情,即可看到哈希、模式与 - provenance。 + provenance。Provenance 就是该文件在 `agent-bundle.manifest.json` 中自己的 + `compiler.provenance` 行:它所派生自的项目源输入。编译器为每个输出文件至少记录一个源输入; + 没有任何源输入的行显示为 **No source inputs recorded**。不会从路径或文件内容推断任何内容。 - **Protocol** 是底层的 MCP 会话检查器。它保留协议轨迹、任务控件、同意、重启/取消操作,以及独立的 [MCP Inspector](https://github.com/modelcontextprotocol/inspector) 启动器。 - **Host diagnostics** 仅限于已安装状态、版本、路径、当前插件是否已附加、可操作的错误,以及一个 From 4fa7fa28a4647aed6deb116bd53bd7632e2ceaba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Sep 2026 22:00:39 +0000 Subject: [PATCH 2/4] changeset: reference #719 Co-authored-by: Zack Jackson --- .changeset/682-inspector-provenance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/682-inspector-provenance.md b/.changeset/682-inspector-provenance.md index e2e5b94c5..5fd9d4502 100644 --- a/.changeset/682-inspector-provenance.md +++ b/.changeset/682-inspector-provenance.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Make Workbench artifact provenance fail closed and read from one record: `ArtifactInspectionService` refuses an epoch (`AB6200`, `Artifact file has no manifest provenance record.`) instead of inspecting a `files[]` row with invented empty provenance when its `compiler.provenance` row is missing, and the Workbench Artifact file details render each file's source inputs from that file's own manifest record — a row with none reads `No source inputs recorded` — rather than a by-path lookup that defaulted to `—`. Adds inspector-level missing, conflicting, and relocated provenance tests (#PR) +Make Workbench artifact provenance fail closed and read from one record: `ArtifactInspectionService` refuses an epoch (`AB6200`, `Artifact file has no manifest provenance record.`) instead of inspecting a `files[]` row with invented empty provenance when its `compiler.provenance` row is missing, and the Workbench Artifact file details render each file's source inputs from that file's own manifest record — a row with none reads `No source inputs recorded` — rather than a by-path lookup that defaulted to `—`. Adds inspector-level missing, conflicting, and relocated provenance tests (#719) From e0afd10a43f369bb8ce5033b945aaa0f1c1de9e0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Sep 2026 22:09:52 +0000 Subject: [PATCH 3/4] test: explain what the relocated-epochs inspection depends on Co-authored-by: Zack Jackson --- .../agent-bundle/tests/artifact-inspection-service.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/agent-bundle/tests/artifact-inspection-service.test.ts b/packages/agent-bundle/tests/artifact-inspection-service.test.ts index f5d1683b5..808cbcb66 100644 --- a/packages/agent-bundle/tests/artifact-inspection-service.test.ts +++ b/packages/agent-bundle/tests/artifact-inspection-service.test.ts @@ -680,7 +680,10 @@ it('inspects identical root-relative provenance after the published epochs are r await publish({ files: runtimeFiles(), id: epochId, registry, root, store: new EpochStore({ projectRoot: root }) }); const before = await new ArtifactInspectionService(new EpochStore({ projectRoot: root }), registry).inspect(epochId); - // The epoch directories move to another project root; the origin keeps its active-build pointer. + // The epoch directories (bytes and store metadata) move to another project + // root; the origin keeps its active-build pointer. Acquiring an epoch by id + // reads only its directory and metadata id, so the inspection depends on + // nothing but the relocated manifest bytes. await mkdir(join(relocated, '.agent-bundle'), { recursive: true }); await rename(join(root, '.agent-bundle', 'epochs'), join(relocated, '.agent-bundle', 'epochs')); const store = new TrackingEpochStore({ projectRoot: relocated }); From ad95f878be47ba4e0de4f7049553a27808aec47c Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 6 Sep 2026 23:46:24 +0000 Subject: [PATCH 4/4] docs: report missing provenance as AB6001 --- .changeset/682-inspector-provenance.md | 2 +- docs/diagnostics.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/682-inspector-provenance.md b/.changeset/682-inspector-provenance.md index 5fd9d4502..ab18c7692 100644 --- a/.changeset/682-inspector-provenance.md +++ b/.changeset/682-inspector-provenance.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Make Workbench artifact provenance fail closed and read from one record: `ArtifactInspectionService` refuses an epoch (`AB6200`, `Artifact file has no manifest provenance record.`) instead of inspecting a `files[]` row with invented empty provenance when its `compiler.provenance` row is missing, and the Workbench Artifact file details render each file's source inputs from that file's own manifest record — a row with none reads `No source inputs recorded` — rather than a by-path lookup that defaulted to `—`. Adds inspector-level missing, conflicting, and relocated provenance tests (#719) +Make Workbench Artifact file details render each file's own `compiler.provenance` source inputs and show `No source inputs recorded` for an empty record. Add an internal `AB6200` missing-row guard without remapping malformed on-disk manifests, which continue to fail parsing as `AB6001`. (#719) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 7a832f57c..6adc1e698 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -33,7 +33,7 @@ even when no error diagnostic was reported. | `AB48xx`/`AB494x` | Route graph, state, layout (`AB4830`–`AB4832`), generated route declarations outside the TypeScript program (`AB4834`), route render budgets (`AB4835`), tool task support (`AB4836`), a route module that value-imports a compiler-carrying framework entry (`AB4837`), a CLI route `inputSchema` reference the static resolver cannot follow (`AB4838`) or that cycles (`AB4839`), an event route's `preflight` gate export (`AB4840`), an event route's declared provider keys (`AB4841`), a CLI surface projection of an MCP tool (`AB4843`–`AB4845`), and provider conventions (see below). | | `AB5000` | General CLI and adapter failures (see below). | | `AB60xx` | Built-artifact validation, including schema documents and referenced files (`AB6005`: the compiler finds a host-pack surface or package-build entry (`dist/bin/*.js`, the Flight workers, or the `lib` entry) that keeps something other than a Node built-in, `pnpapi`, or an emitted sibling external, or an MCP App view that keeps anything external; the emitted-module walk remains only for what the compiler cannot see — an expression `import()` in a compiled module, and the imports and syntax of JavaScript the framework did not compile or a `tools` hatch may have rewritten; a `dist` finding names `dist/`; `AB6011`/`AB6012`: a target's required pinned-schema document is missing or invalid; `AB6025`: a manifest-declared `logo` path is missing from the artifact or escapes the deploy tree; `AB6034`: emitted Skill Markdown has no instruction body; `AB6035`–`AB6038`: Agent Plugins portable validation, see below). | -| `AB6200`–`AB6202` | Workbench artifact inspection over published epochs: `AB6200` the epoch does not validate or its provenance is inconsistent, `AB6201` an epoch reference could not be released, `AB6202` unsafe runtime metadata (see below). | +| `AB6200`–`AB6202` | Workbench artifact inspection over published epochs: `AB6200` the validator threw or an internal post-validation invariant failed, `AB6201` an epoch reference could not be released, `AB6202` unsafe runtime metadata. Artifact-validation diagnostics such as `AB6001` retain their original codes (see below). | | `AB700x` | Host installation and uninstallation: bundle identity, host availability, scope, command failure, and collision checks (`AB7000`–`AB7004`: unsupported host, unreadable bundle identity, missing host, scope or mode refusal, host command failure — the same five codes are also the development project service's preparation failures; `AB7001` in detail: the composite root at `--from` cannot be resolved for the host from its `agent-bundle.manifest.json` — the manifest is missing or not canonical, has no `projections[]` row for the host, the row has no host plugin manifest pointer or the pointed file is missing, a `files[]` row is missing or its bytes, size, digest, or executable state differ from the row after npm normalization, `claude`/`codex` have no marketplace identity, or the `cursor` plugin name is not a safe local plugin name; `install`, `uninstall`, and `doctor` never probe `.claude-plugin/plugin.json` or look under `/`; `AB7005`: version collision, pre-receipt content collision, or foreign install; `AB7006`: the host lists the installed copy with load errors; see below), plus the `uninstall` refusals `AB7007`–`AB7009` (ownership or content mismatch, unconfirmed data purge, missing receipt; see below). | | `AB7010`–`AB7015` | npm prepack inventory, artifact freshness, package bin targets, release-version agreement, and installed-dependency hygiene (`AB7014`: a dependency no consumer-runtime evidence requires; `AB7015`: a git, remote-tarball, path, or unrewritten workspace-protocol dependency specifier). | | `AB7200`–`AB7202`, `AB7210`–`AB7211` | Development rebuilds and live host surfaces: rebuild admission and phase failures, development host install sync, and the dev-epoch contract gate (see below). | @@ -1876,7 +1876,7 @@ therefore does not prove the absence of such a load. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | | `AB6000` | error | `Artifact root is not a readable directory.` — the artifact root cannot be walked; `Artifact manifest is missing or cannot be read.` — the tree could not be inspected, or `agent-bundle.manifest.json` is absent, is not a regular file, or could not be read (the manifest is read between two identity checks, so a manifest replaced mid-read reports here too). Validation stops at this code. | Restore a readable artifact root and canonical manifest, then rebuild the artifact. | -| `AB6001` | error | `Artifact manifest is not a strict canonical manifest.` — `agent-bundle.manifest.json` does not parse as a strict canonical artifact manifest. `Artifact manifest changed during validation.` — its bytes or identity differ between the first read and the re-read after validation. | Regenerate the strict canonical manifest without concurrent writes, then rerun validation. | +| `AB6001` | error | `Artifact manifest is not a strict canonical manifest.` — `agent-bundle.manifest.json` does not parse as a strict canonical artifact manifest. This includes a `files[]` row with no matching `compiler.provenance` row: the parser's exact-path rule rejects it before inspection, and the diagnostic keeps this generic message. `Artifact manifest changed during validation.` — its bytes or identity differ between the first read and the re-read after validation. | Regenerate the strict canonical manifest without concurrent writes, then rerun validation. | | `AB6002`–`AB6003` | error | Reserved: both codes are declared in the artifact diagnostic registry, but no validator emits either today. | `AB6002`: Rebuild the artifact from complete project source, then rerun validation. `AB6003`: Rebuild the artifact with canonical generated output, then rerun validation. | | `AB6004` | error | `Artifact files do not match the manifest.` — the regular files on disk differ from the manifest file table (a path, byte length, mode, or SHA-256; a missing or unmanifested file). `Artifact file changed during validation: "".` — a file differed between the initial and final inspection, or between a validated staging tree and its re-check after `build` renamed it into place. `Artifact file table changed during validation.` — the final inspection could not be taken. | Rebuild the artifact so its file table and contents match the manifest. | | `AB6005` | error | Primary compile-time form: `Compiled module "" keeps "" external () from ; a generated executable bundles everything but Node built-ins.` `` is the run-time load target; when an object-map external redirected the authored specifier, `, imported as "",` follows the type, a relative target that names no emitted asset of the artifact (or escapes it) ends `; it names no module emitted by this artifact.` instead, and a request kept under an external type that does not load a module (`var`, `global`, `this`, `window`, `assign`, `umd`, `amd`, `system`, `jsonp`, `promise`, `script`, …) — even a Node built-in — ends `; external type reads a variable instead of loading a module.` — the compiler service lowered a host-pack surface or package-build entry (`dist/bin/*.js`, the Flight workers, or the `lib` entry) and Rspack kept something other than a Node built-in, `pnpapi`, or an emitted sibling of that artifact external, whatever spelling Rspack emitted (`import`, `require`, or its `createRequire` shim); `generatedPath` names the asset. An expression request (`import(expr)`, `require(expr)`) is outside the compiler's view: Rslib's profile leaves it verbatim without parsing it, so it is neither bundled nor external; so is a literal import marked `rspackIgnore`/`webpackIgnore`, which Rspack leaves verbatim with no module, external, or warning. The emitted-module walk reports both in a compiled module the record proves: `Generated JavaScript import from "" has a non-literal dynamic import.` and `Generated JavaScript import from "" loads "", which the compiler neither bundled nor recorded as an external; an import the build ignored is a run-time load outside the artifact.` (a literal request that is neither a Node built-in nor one of the record's externals for that file). MCP App view form (the `, imported as` clause applies to both forms): `Compiled MCP App view "mcp-apps/.html" keeps "" external () from ; a view inlines every module it loads.` — a browser document has no allowable external, so the view's Rsbuild compilation (which carries the same audit plugin) fails on any `ExternalModule`, whatever the hatch mapped it to. Residual walk forms, `Generated JavaScript import from "" .`, reported only for what the compiler cannot see: `has a non-literal dynamic import` for an expression `import()` in any emitted module, compiled ones included, since Rslib's profile leaves that form verbatim; and, for a module the compile evidence record does not prove — JavaScript the framework did not compile (`install.mjs`, a copied script), every module of an artifact without a record, and every module of a build whose `tools` hatch may have rewritten the emitted bytes (`coverage.rewritable`) — `has invalid syntax` from a full parse, `uses unsupported specifier` or `uses invalid specifier` for a bare or malformed import, `cannot be read`, and the relative-target findings `is missing`, `resolves outside the artifact root`, `is not listed in the artifact manifest`, `does not resolve to a regular file`, `references invalid JSON`, and `uses unsupported target`. A compiled module the record covers with the same bytes is lexed for syntax and its literal imports are not resolved again — the compiler resolved them (bundled, built-in, or an emitted sibling). Prebuilt payloads are not walked; a `dist` finding names `dist/`. | Bundle every JavaScript dependency into the artifact, then rebuild it. | @@ -1909,7 +1909,7 @@ diagnostics in the response body. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB6200` | error | `Artifact inspection could not validate the published artifact.` — the strict artifact validator threw over the epoch; `Artifact file has no manifest provenance record.` — a `files[]` row has no `compiler.provenance` row, so the inspector refuses rather than showing the file with empty provenance; `Artifact file provenance references an unknown project source input.` — an output's `sourceInputs` name an input the manifest project does not declare; `Artifact manifest project inputs are invalid.` — the manifest's project inputs are structurally invalid. An epoch whose validation merely reports diagnostics is refused with those diagnostics instead of this code. | Rebuild the epoch from a project whose artifact validates cleanly. | +| `AB6200` | error | `Artifact inspection could not validate the published artifact.` — the strict artifact validator threw over the epoch; `Artifact file has no manifest provenance record.` — an internal defense-in-depth invariant failed after parsing (an on-disk missing row is rejected first as `AB6001`); `Artifact file provenance references an unknown project source input.` — an output's `sourceInputs` name an input the manifest project does not declare; `Artifact manifest project inputs are invalid.` — the manifest's project inputs are structurally invalid. An epoch whose validation merely reports diagnostics is refused with those diagnostics instead of this code. | Rebuild the epoch from a project whose artifact validates cleanly. | | `AB6201` | error | `Artifact inspection could not release every acquired epoch reference.` — releasing an epoch reference after an inspection or diff failed. | None in the project: the failure is internal to the development server's epoch bookkeeping. | | `AB6202` | error | Runtime metadata derived from the validated snapshot is unsafe: an MCP server's `entryPaths` name a file outside its target or absent from the manifest (`Validated MCP evidence references an unmanifested target file.`), or another runtime-evidence check named in the message failed. | Rebuild the epoch so its MCP runtime evidence references manifested target files. |