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/619-mcp-app-view-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Judge MCP App views from compiler evidence: the `ArtifactDependencyAuditPlugin` now sits in every view's Rsbuild compilation, and a module the compilation kept external — through a `tools.rspack` mutator or a function-form `externals`, whatever it was mapped to — fails the view with `AB6005` (`Compiled MCP App view "mcp-apps/<name>.html" keeps … external …`), since a browser document has no allowable external. (#627)
8 changes: 4 additions & 4 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`), 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; 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; `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 @@ -431,8 +431,8 @@ Validation happens at three moments, all fail-closed:

Compiled bundles reach this gate without bare imports: `prepack` builds before it packs, and the compiler
reports `AB6005` when a `dist` bundle or host-pack module keeps anything other than a Node built-in,
`pnpapi`, or an emitted sibling external. The emitted-module walk remains behind that compile-time check,
so the lexed `import` evidence below describes prebuilt payload modules and other packed scripts the
`pnpapi`, or an emitted sibling external, and when an MCP App view's compilation keeps anything external
at all. The emitted-module walk remains behind that compile-time check, so the lexed `import` evidence below describes prebuilt payload modules and other packed scripts the
framework did not compile. A `require`, `createRequire(…)(…)`, or `import.meta.resolve(…)` call the compiler
does not resolve is not a module dependency; its evidence is read from every packed file, compiled bundles
included. The dependency evidence is read from the packed bytes themselves: every `.js`/`.mjs`/`.cjs` file
Expand Down Expand Up @@ -1800,7 +1800,7 @@ names the host target namespace when the check is per target.
| `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. |
| `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: "<path>".` — 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 "<asset>" keeps "<request>" external (<external type>) from <issuer module>; a generated executable bundles everything but Node built-ins.` `<request>` is the run-time load target; when an object-map external redirected the authored specifier, `, imported as "<specifier>",` 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 <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. The emitted-module walk remains as defense in depth and reports `Generated JavaScript import from "<module>" <finding>.` for a residual unsupported or invalid specifier, unreadable module, invalid syntax, or non-literal dynamic import. Its relative-target findings remain `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 `dist` finding names `dist/<path>`. | Bundle every JavaScript dependency into the artifact, then rebuild it. |
| `AB6005` | error | Primary compile-time form: `Compiled module "<asset>" keeps "<request>" external (<external type>) from <issuer module>; a generated executable bundles everything but Node built-ins.` `<request>` is the run-time load target; when an object-map external redirected the authored specifier, `, imported as "<specifier>",` 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 <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. MCP App view form (the `, imported as` clause applies to both forms): `Compiled MCP App view "mcp-apps/<name>.html" keeps "<request>" external (<external type>) from <issuer module>; 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. The emitted-module walk remains as defense in depth and reports `Generated JavaScript import from "<module>" <finding>.` for a residual unsupported or invalid specifier, unreadable module, invalid syntax, or non-literal dynamic import. Its relative-target findings remain `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 `dist` finding names `dist/<path>`. | Bundle every JavaScript dependency into the artifact, then rebuild it. |
| `AB6006` | error | `Generated JSON cannot be parsed.` — a `.json` file in the artifact is not valid JSON (prebuilt payload files are exempt). Doctor's Claude document lane reports the same code inside an `AB7319` message for a Claude bundle document that is unreadable or not valid JSON. | Regenerate the affected JSON document as valid JSON, then rebuild the artifact. |
| `AB6007` | error | `MCP manifest references missing generated server "<path>".` — a root-level MCP manifest (pre-manifest pass) or a target's MCP manifest names a local server entry that the artifact does not contain. | Repair MCP manifest references to generated servers, then rebuild the artifact. |
| `AB6008` | error | `Artifact Agent Skills provenance does not match the pinned schema contract.` — the manifest's `agentSkills` schema SHA-256, source revision, or specification differs from the framework's pinned Agent Skills revision. | Rebuild the artifact with the pinned Agent Skills contract. |
Expand Down
11 changes: 7 additions & 4 deletions packages/agent-bundle/src/build/dependency-audit-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rspack, type Rspack } from '@rslib/core';
import type { Rspack } from '@rslib/core';

import { isRecord } from '../core/strict-json.ts';
import type { CompilationEvidence, CompilationExternal, CompilationModule } from './compile-result.ts';
Expand Down Expand Up @@ -61,14 +61,15 @@ const moduleResource = (module: Rspack.Module | null | undefined): string | unde
module?.nameForCondition() ?? module?.identifier();

const collectExternals = (compilation: Rspack.Compilation): readonly CompilationExternal[] => {
const { ExternalModule } = compilation.compiler.rspack;
const byRequest = new Map<string, {
readonly externalType: string;
readonly issuers: Set<string>;
readonly request: string;
readonly userRequest: string;
}>();
for (const module of compilation.modules) {
if (!(module instanceof rspack.ExternalModule)) continue;
if (!(module instanceof ExternalModule)) continue;
const { externalType, request } = runtimeRequest(module);
const key = [externalType, request, module.userRequest].join('\u0000');
const record = byRequest.get(key) ?? {
Expand Down Expand Up @@ -97,9 +98,10 @@ const collectExternals = (compilation: Rspack.Compilation): readonly Compilation
};

const collectModules = (compilation: Rspack.Compilation): readonly CompilationModule[] => {
const { NormalModule } = compilation.compiler.rspack;
const modules: CompilationModule[] = [];
for (const module of compilation.modules) {
if (!(module instanceof rspack.NormalModule)) continue;
if (!(module instanceof NormalModule)) continue;
const resource = module.nameForCondition();
modules.push(Object.freeze({ identifier: module.identifier(), ...(resource === undefined ? {} : { resource }) }));
}
Expand All @@ -109,7 +111,8 @@ const collectModules = (compilation: Rspack.Compilation): readonly CompilationMo
/**
* Records what one compilation resolved — externals with their issuers and
* bundled modules — once the module graph is final and before any asset is
* emitted.
* emitted. Module classes come from the compiler's own Rspack instance, so the
* plugin judges an Rslib and an Rsbuild compilation alike.
*/
export class ArtifactDependencyAuditPlugin {
readonly #record: (evidence: CompilationEvidence) => void;
Expand Down
30 changes: 26 additions & 4 deletions packages/agent-bundle/src/build/external-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { isBuiltin } from 'node:module';
import { posix } from 'node:path';

import type { Diagnostic } from '../core/diagnostics.ts';
import { posixRelativeWhenInside } from '../core/paths.ts';
import { isRecord } from '../core/strict-json.ts';
import { artifactDiagnostic } from './artifact-diagnostics.ts';
import type { CompileResult, ExternalIR, ExternalKind } from './compile-result.ts';
import type { CompilationEvidence, CompilationExternal, CompileResult, ExternalIR, ExternalKind } from './compile-result.ts';

/** A request a generated executable may load at run time: a Node built-in, or Yarn PnP's runtime API. */
export const isAllowedExternalRequest = (request: string): boolean => isBuiltin(request) || request === 'pnpapi';
Expand Down Expand Up @@ -47,12 +48,16 @@ export const classifyExternal = (
return 'package';
};

/** `keeps "<request>" external (<type>)[, imported as "<specifier>",][ from <issuers>]; ` */
const keptExternalClause = (external: CompilationExternal, issuers: readonly string[]): string =>
`keeps ${JSON.stringify(external.request)} external (${external.externalType})`
+ `${external.userRequest === external.request ? '' : `, imported as ${JSON.stringify(external.userRequest)},`}`
+ `${issuers.length === 0 ? '' : ` from ${issuers.join(', ')}`}; `;

const externalMessage = (external: ExternalIR): string => {
switch (external.kind) {
case 'package':
return `Compiled module ${JSON.stringify(external.asset)} keeps ${JSON.stringify(external.request)} external (${external.externalType})`
+ `${external.userRequest === external.request ? '' : `, imported as ${JSON.stringify(external.userRequest)},`}`
+ `${external.issuers.length === 0 ? '' : ` from ${external.issuers.join(', ')}`}; `
return `Compiled module ${JSON.stringify(external.asset)} ${keptExternalClause(external, external.issuers)}`
+ (!isModuleLoadingExternalType(external.externalType)
? `external type ${external.externalType} reads a variable instead of loading a module.`
: isRelativeRequest(external.request)
Expand All @@ -68,6 +73,23 @@ const externalMessage = (external: ExternalIR): string => {
}
};

/**
* A browser view has no allowable external: no Node built-ins, and no sibling
* module, since the document inlines every script.
*/
export const viewSelfContainmentDiagnostics = (
evidence: CompilationEvidence,
asset: string,
projectRoot: string,
): readonly Diagnostic[] =>
evidence.externals.map((external) => artifactDiagnostic(
'AB6005',
`Compiled MCP App view ${JSON.stringify(asset)} `
+ keptExternalClause(external, external.issuers.map((issuer) => posixRelativeWhenInside(projectRoot, issuer)))
+ 'a view inlines every module it loads.',
asset,
));

export const selfContainmentDiagnostics = (result: CompileResult): readonly Diagnostic[] =>
result.externals
.filter((external) => external.kind === 'package')
Expand Down
Loading
Loading