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
8 changes: 8 additions & 0 deletions .changeset/619-compile-evidence-record.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'agent-bundle': patch
---

Record compile evidence beside the emitted files: `agent-bundle build`
writes `agent-bundle.compile-evidence.json` at the artifact root;
`agent-bundle validate --artifact` verifies it against the manifest
file table (`AB6039`). (#638)
26 changes: 24 additions & 2 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ even when no error diagnostic was reported.
| `AB490x`/`AB492x` | Conventional host components (#100 stage 2): rules `src/rules/*.mdc` (`AB4900`–`AB4908`) and commands `src/commands/*.md` (`AB4920`–`AB4928`), including per-host feature-set enforcement (`AB4907`/`AB4908`, `AB4927`/`AB4928`); see below. |
| `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 behind that compile-time check and reports residual import, syntax, and relative-target findings; a `dist` finding names `dist/<path>`; `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). |
| `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 behind that compile-time check and reports residual import, syntax, and relative-target findings; a `dist` finding names `dist/<path>`; `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; `AB6039`: the compile evidence record does not match the manifest file table; `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). |
| `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; `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 packed file references; `AB7015`: a git, remote-tarball, path, or unrewritten workspace-protocol dependency specifier). |
Expand Down Expand Up @@ -1801,7 +1801,7 @@ placeholders itself.
| --- | --- | --- | --- |
| `AB7326` | info / warning / error | Info (`launch.state = expanded`): the receipt's expansion still describes the installed copy — same plugin root, existing data directory, no placeholder left, absolute `cwd` and plugin-root `command`/`args` paths that exist, `PLUGIN_ROOT` / `PLUGIN_DATA` equal to the recorded values. Warning (`unexpanded`): an Agent Plugins install without a recorded expansion whose stdio servers still rely on the spec forms Cursor does not resolve (the message lists the forms per server); Cursor reports `spawn … ENOENT` / `MODULE_NOT_FOUND` for them. Error (`drifted`, entry `corrupt`): the installed `mcp.json` is not byte-identical to the expansion Doctor recomputes from the recorded document (edited, replaced, or removed after install), the recorded expansion names another plugin root (the copy was moved or duplicated), the data directory or an expanded path no longer exists, or the environment no longer carries the recorded values. Only a byte-identical copy has its recorded document validated by `AB7320`; a drifted copy is validated as the bytes on disk. Packages without stdio servers, and copies already carrying absolute paths with the §9.1 variables, produce no finding. | Reinstall with the bundle's emitted `install.mjs` at the copy's current location; the Cursor-target (`.cursor-plugin/plugin.json`) bundle is never rewritten and is not subject to this check. |

## Built-artifact validation (`AB6000`–`AB6018`, `AB6023`–`AB6025`)
## Built-artifact validation (`AB6000`–`AB6018`, `AB6023`–`AB6025`, `AB6039`)

`agent-bundle build` validates the staged tree before it writes the manifest
(`validateArtifactFiles`: filesystem entries, generated JSON documents, and
Expand All @@ -1816,6 +1816,27 @@ whose `recovery` is fixed per code in the artifact diagnostic registry
file (`agent-bundle.manifest.json` for manifest-level findings) and `target`
names the host target namespace when the check is per target.

`agent-bundle build` writes `agent-bundle.compile-evidence.json` at the
artifact root and lists it in `agent-bundle.manifest.json` as a `generated`
file. The record is what the compiler service reported about each file it
emitted, bound to those bytes: one `assets[]` entry per compiled file
(`bundle` kind — `bin/*.mjs`, `scripts/*.mjs`, `hooks/*.mjs`, `mcp/*.mjs`,
Flight workers, `mcp-apps/*.html`) holds `path`, `sha256`, the kept
`externals` (`kind` `artifact-relative` or `builtin`, `externalType`,
`issuers`, `request`, `userRequest`, and `target` for a sibling), and the
inlined `packages`. Record-level fields are policy `closed-world-externals@1`,
producer `{ name: 'agent-bundle', rspack, version }`, `coverage.rewritable`
(true when a `tools.rspack` or `tools.rsbuild` hatch took part, so emitted
bytes may differ from the module graph), and `coverage.unobserved`.
`agent-bundle validate --artifact` re-checks a listed record against the
manifest file table without reading JavaScript (`AB6039`). The package build
returns the same record in process (`PackageBuildResult.evidence`, paths
`dist/bin/…`) and writes nothing to disk. `coverage.unobserved` lists the load forms Rslib leaves verbatim in
the bundle, so the compiler neither bundles nor records them:
`import(<expression>)`, `require(<expression>)`, `require.resolve(…)`,
`createRequire(…)(…)`, `import.meta.resolve(…)`. No externals recorded
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. |
Expand All @@ -1839,6 +1860,7 @@ names the host target namespace when the check is per target.
| `AB6023` | error | `Artifact is missing required install surface "INSTALL.md".` — the selection includes a built-in host (`claude`, `codex`, `cursor`, `portable`, judged by adapter identity, so an advanced registry's own adapter named like one requires nothing) but the composite root has no `INSTALL.md`; the surface is emitted once at the root, never per target. | Rebuild the artifact so the root carries its generated `INSTALL.md`. |
| `AB6024` | error | `Artifact is missing required install surface "install.mjs".` — the selection includes the shipped `cursor` or `portable` adapter (judged by adapter identity, like `AB6023`) but the composite root has no `install.mjs` (a root selecting only `claude` and/or `codex` requires none). | Rebuild the artifact so the root carries its generated `install.mjs`. |
| `AB6025` | error | `Plugin logo "<logo>" escapes the artifact for target "<name>".` or `Plugin logo "<logo>" references missing artifact file "<path>".` — a `plugin.json` `logo` string resolves outside the target directory or to a file the artifact does not contain. | Rebuild the artifact so every manifest-declared logo path copies into the deploy tree. |
| `AB6039` | error | `Compile evidence record <reason>.` — the listed `agent-bundle.compile-evidence.json` failed the strict parser (`is not valid JSON`, `<location> has unexpected keys: …`, `assets must be sorted by path with no duplicates`, …); `Compile evidence record cannot be read.` — it is listed but unreadable. `Compile evidence was judged under policy <name>@<revision>; this validator applies closed-world-externals@1.` — the record's `policy` is not this validator's. `Compile evidence does not cover compiled file "<path>".` — a manifest `bundle` file has no matching asset. `Compile evidence for "<path>" describes different bytes.` — the recorded `sha256` does not match the file table. `Compile evidence names "<path>", which the manifest does not list as a compiled file.` — a recorded path is absent or not `bundle`. `Compile evidence for "<path>" records "<request>" as a built-in; it is not one.` — re-judged with the build's policy, the external is not a Node built-in loaded through a module-loading external type. `Compile evidence for "<path>" records sibling "<request>", which the artifact does not contain.` — the request is not relative, does not resolve from the asset to the recorded `target`, or the target is not another compiled node bundle in the file table. `Compile evidence for "<path>" records "<request>" as an external; a view inlines every module it loads.` — an MCP App view (`mcp-apps/<name>.html`) recorded any external at all. | Rebuild the artifact so its compile evidence record describes the emitted files. |

## Workbench artifact inspection (`AB6200`–`AB6202`)

Expand Down
8 changes: 6 additions & 2 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1311,8 +1311,12 @@ lowers every host-pack surface and package-build entry. The framework-owned
Rspack kept external, and the service reads that evidence before trusting an
asset. `AB6005` rejects anything Rspack kept external except a Node built-in,
`pnpapi`, or an emitted sibling of the same artifact, whatever spelling the
bundle uses. The emitted-module walk remains behind that check as defense in
depth. A `require`,
bundle uses. `agent-bundle build` writes that evidence as
`agent-bundle.compile-evidence.json` at the artifact root (listed in
`agent-bundle.manifest.json` as a `generated` file); `agent-bundle validate
--artifact` re-checks a listed record against the file table without reading
JavaScript (`AB6039`). The emitted-module walk remains behind that check as
defense in depth. A `require`,
`createRequire(…)(…)`, or `import.meta.resolve(…)` call the compiler does not
resolve is not a module dependency; content the compiler did not compile is
opaque and must declare what it needs. Run-time path references are kept the
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-bundle/src/build/artifact-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export type ArtifactDiagnosticCode =
| 'AB6023'
| 'AB6024'
| 'AB6025'
| 'AB6034';
| 'AB6034'
| 'AB6039';

export const artifactDiagnosticRecoveries: Readonly<Record<ArtifactDiagnosticCode, string>> = Object.freeze({
AB6000: 'Restore a readable artifact root and canonical manifest, then rebuild the artifact.',
Expand Down Expand Up @@ -57,6 +58,7 @@ export const artifactDiagnosticRecoveries: Readonly<Record<ArtifactDiagnosticCod
AB6024: 'Rebuild the Cursor-compatible artifact so it includes its generated install.mjs.',
AB6025: 'Rebuild the artifact so every manifest-declared logo path copies into the deploy tree.',
AB6034: 'Add Markdown instructions after the Skill frontmatter, then rebuild the artifact.',
AB6039: 'Rebuild the artifact so its compile evidence record describes the emitted files.',
});

const isArtifactDiagnosticCode = (code: string): code is ArtifactDiagnosticCode =>
Expand Down
29 changes: 28 additions & 1 deletion packages/agent-bundle/src/build/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
import { basename, dirname, join, relative, resolve } from 'node:path';

import { rspack } from '@rslib/core';

import packageManifest from '../../package.json' with { type: 'json' };

import type { TargetRegistry } from '../adapters/registry.ts';
Expand Down Expand Up @@ -32,6 +34,12 @@ import {
} from './mcp-apps.ts';
import { bundleSyntaxCheckFor } from './module-imports.ts';
import { compileRslibSurfaces, settledRslibSurface } from './compiler.ts';
import {
compileEvidenceFileName,
createCompileEvidenceRecord,
type CompileEvidenceRecord,
} from './compile-evidence.ts';
import type { CompileResult } from './compile-result.ts';
import { planCompileStages } from './compile-stages.ts';
import {
assertUniqueArtifactDestinations,
Expand All @@ -41,6 +49,7 @@ import {
listArtifactFiles,
publishArtifact,
resolveArtifactDestination,
writeCompileEvidence,
writeHookIndex,
writeManifest,
} from './emit.ts';
Expand All @@ -62,6 +71,7 @@ export interface BuildResult {
readonly compiledHooks: readonly CompiledHookEntry[];
readonly compiledMcpApps: readonly CompiledMcpApp[];
readonly compiledMcpEntries: readonly CompiledMcpEntry[];
readonly compileEvidence: CompileEvidenceRecord;
/**
* Non-fatal compiler findings the artifact survived — MCP App view compile
* warnings and size advisories. Errors never reach here: a failing compile
Expand Down Expand Up @@ -283,6 +293,11 @@ const outputCandidatesFor = (options: {
path: resolveArtifactDestination(options.artifactRoot, artifactHookIndexName),
sourceInputs: hookIndexSourceInputs(options.model, options.compiledHooks),
},
{
kind: 'generated' as const,
path: resolveArtifactDestination(options.artifactRoot, compileEvidenceFileName),
sourceInputs: [options.model.metadata.provenance.sourcePath],
},
];

const assertOutputProvenanceSources = (options: {
Expand Down Expand Up @@ -430,6 +445,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
const compiledHooks: CompiledHookEntry[] = [];
const compiledMcpApps: CompiledMcpApp[] = [];
const compiledMcpEntries: CompiledMcpEntry[] = [];
const compileResults: CompileResult[] = [];
const compileDiagnostics: Diagnostic[] = [];
const tools = options.tools === undefined ? {} : { tools: options.tools };
// The resolved `notices.retention`; generated ledgers fall back to the runtime defaults without it.
Expand Down Expand Up @@ -462,14 +478,15 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
});
stagedMcpApps = views.apps;
compiledMcpApps.push(...views.apps);
compileResults.push(...views.compileResults);
compileDiagnostics.push(...views.diagnostics);
}
break;
case 'node-surfaces': {
await emitPlanEntries({ entries: composite.entries, root: stageRoot });
// Every agent-host surface of the root lowers through one Rslib
// instance; each surface keeps its own evidence and result.
const [cliBins, scripts, hooks, mcpEntries] = await compileRslibSurfaces(
const compiled = await compileRslibSurfaces(
{ cwd: options.projectRoot, meta, outputRoot: stageRoot, ...tools },
[
composite.cliBin
Expand Down Expand Up @@ -513,6 +530,8 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
}),
],
);
const [cliBins, scripts, hooks, mcpEntries] = compiled.results;
compileResults.push(...compiled.compileResults);
compiledCliBins.push(...cliBins);
compiledEntries.push(...scripts);
compiledHooks.push(...hooks);
Expand Down Expand Up @@ -545,6 +564,13 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
...(entry.timeout === undefined ? {} : { timeout: entry.timeout }),
})),
});
const compileEvidence = await createCompileEvidenceRecord({
results: compileResults,
rewritable: options.tools?.rspack !== undefined || options.tools?.rsbuild !== undefined,
root: stageRoot,
rspackVersion: rspack.rspackVersion,
});
await writeCompileEvidence({ artifactRoot: stageRoot, evidence: compileEvidence });
const outputProvenance = createOutputProvenance({
artifactRoot: stageRoot,
outputs: outputCandidatesFor({
Expand Down Expand Up @@ -615,6 +641,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
output: publishedOutput(entry),
...(entry.workerOutput === undefined ? {} : { workerOutput: publishedOutput({ output: entry.workerOutput }) }),
}))),
compileEvidence,
diagnostics: deepFreeze(deduplicateDiagnostics(compileDiagnostics)),
manifest,
outputProvenance,
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-bundle/src/build/cli-bins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ export const planCliBinsSurface = (
const planned = planCompiledCliBins(model, options);
return {
entries: planned.length === 0 ? [] : cliBinRslibEntries(planned, model),
finish: async (evidence) => {
const evidenceByPath = new Map(evidence.map((entry) => [entry.path, entry.sourceInputs]));
finish: async (result) => {
const evidenceByPath = new Map(result.assets.map((entry) => [entry.path, entry.sourceInputs]));
const bundledInputs = (path: string, label: string): readonly string[] => {
const inputs = evidenceByPath.get(path);
if (inputs === undefined) throw new Error(`Missing bundled routed CLI ${label} evidence for ${JSON.stringify(path)}.`);
Expand Down
Loading
Loading