From e0b2524a3e969b27896c551c663fdaa1c5ad81e9 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 10:47:58 +0000 Subject: [PATCH 1/5] feat(build): judge MCP App views from compile evidence (#619 step 5, views) --- .changeset/619-mcp-app-view-evidence.md | 5 +++ docs/diagnostics.md | 4 +- .../agent-bundle/src/build/external-policy.ts | 22 ++++++++++- packages/agent-bundle/src/build/mcp-apps.ts | 33 +++++++++++++++- packages/agent-bundle/src/build/rslib.ts | 7 +--- packages/agent-bundle/src/core/paths.ts | 4 ++ .../tests/mcp-apps-compile.test.ts | 38 +++++++++++++++++++ .../docs/en/guide/distribution/validation.mdx | 5 ++- .../docs/zh/guide/distribution/validation.mdx | 5 ++- 9 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 .changeset/619-mcp-app-view-evidence.md diff --git a/.changeset/619-mcp-app-view-evidence.md b/.changeset/619-mcp-app-view-evidence.md new file mode 100644 index 000000000..8364c9954 --- /dev/null +++ b/.changeset/619-mcp-app-view-evidence.md @@ -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/.html" keeps … external …`), since a browser document has no allowable external. (#PR) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 31e50397b..66180c492 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -431,7 +431,7 @@ 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, +`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 @@ -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: "".` — 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. The emitted-module walk remains as defense in depth and reports `Generated JavaScript import from "" .` 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/`. | Bundle every JavaScript dependency into the artifact, then rebuild it. | +| `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.` MCP App view form: `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 — 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 "" .` 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/`. | 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 "".` — 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. | diff --git a/packages/agent-bundle/src/build/external-policy.ts b/packages/agent-bundle/src/build/external-policy.ts index f3b5f0a2a..9c72ef900 100644 --- a/packages/agent-bundle/src/build/external-policy.ts +++ b/packages/agent-bundle/src/build/external-policy.ts @@ -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, 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'; @@ -68,6 +69,25 @@ const externalMessage = (external: ExternalIR): string => { } }; +/** + * A browser view has no allowable external at all: no Node built-ins, and no + * sibling module, since the document inlines every script. Every external the + * view's compilation kept is an `AB6005` against the emitted document. + */ +export const viewSelfContainmentDiagnostics = ( + evidence: CompilationEvidence, + asset: string, + projectRoot: string, +): readonly Diagnostic[] => + evidence.externals.map((external) => artifactDiagnostic( + 'AB6005', + `Compiled MCP App view ${JSON.stringify(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.map((issuer) => posixRelativeWhenInside(projectRoot, issuer)).join(', ')}`}; ` + + 'a view inlines every module it loads.', + asset, + )); + export const selfContainmentDiagnostics = (result: CompileResult): readonly Diagnostic[] => result.externals .filter((external) => external.kind === 'package') diff --git a/packages/agent-bundle/src/build/mcp-apps.ts b/packages/agent-bundle/src/build/mcp-apps.ts index 647aae0ff..49828deda 100644 --- a/packages/agent-bundle/src/build/mcp-apps.ts +++ b/packages/agent-bundle/src/build/mcp-apps.ts @@ -19,8 +19,11 @@ import { MAX_APP_HTML_BYTES } from '../core/mcp-app-limits.ts'; import { escapeRegExp } from '../core/strings.ts'; import type { AgentBundleMeta } from '../meta.ts'; import { appRuntimePath, appRuntimeSpecifier } from './app-runtime.ts'; +import type { CompilationEvidence } from './compile-result.ts'; import { composeToolsLayers, frameworkInvariantLayer } from './compose-layers.ts'; +import { ArtifactDependencyAuditPlugin } from './dependency-audit-plugin.ts'; import { listArtifactFiles, resolveArtifactDestination } from './emit.ts'; +import { viewSelfContainmentDiagnostics } from './external-policy.ts'; import { mcpAppBundlerFailureDiagnostic, mcpAppCompileErrorDiagnostics, @@ -344,6 +347,8 @@ export const composeMcpAppsRsbuildConfig = ( readonly meta: AgentBundleMeta; /** Defaults to `production`; see {@link McpAppCompileMode}. */ readonly mode?: McpAppCompileMode; + /** Receives each view compilation's externals and modules once its module graph is final; the audit plugin is composed either way, so `inspect --bundler` shows what runs. */ + readonly onCompilationEvidence?: (evidence: CompilationEvidence) => void; readonly outDir: string; readonly tools?: AgentBundleToolsConfig; }, @@ -406,13 +411,15 @@ export const composeMcpAppsRsbuildConfig = ( }, }; // Added after the hatch mutator (this hook is merged last), so a consumer - // cannot strip the generated identity module out of the compiler. + // cannot strip the generated identity module or the dependency audit out + // of the compiler. const VirtualModulesPlugin = rsbuildVirtualModulesPlugin(); config.plugins = [ ...(config.plugins ?? []), appRuntimeReplacement(runtimePath), metaModuleReplacement(metaModulePath), new VirtualModulesPlugin({ [metaModulePath]: generatedMetaModuleSource(options.meta) }), + new ArtifactDependencyAuditPlugin(options.onCompilationEvidence ?? (() => undefined)), ]; return config; }; @@ -428,6 +435,27 @@ export const composeMcpAppsRsbuildConfig = ( })); }; +/** + * Every view's compilation is judged from its own evidence: the Rsbuild + * environment is named after the App, so each record names the view it + * belongs to. A view without a record did not compile through the invariant + * layer, which is a framework fault, not a consumer one. + */ +const assertViewsSelfContained = ( + compiled: readonly PlannedMcpApp[], + evidence: readonly CompilationEvidence[], + projectRoot: string, +): void => { + const diagnostics = compiled.flatMap((app) => { + const records = evidence.filter((record) => record.compiler === app.name); + if (records.length !== 1) { + throw new Error(`Expected one compilation evidence record for MCP App ${JSON.stringify(app.name)}, found ${String(records.length)}.`); + } + return viewSelfContainmentDiagnostics(records[0]!, `mcp-apps/${app.name}.html`, projectRoot); + }); + if (diagnostics.length > 0) throw new DiagnosticError(diagnostics); +}; + export const compileMcpApps = async ( apps: readonly NormalizedMcpApp[], options: Readonly<{ @@ -455,12 +483,14 @@ export const compileMcpApps = async ( }); const mode: McpAppCompileMode = options.mode ?? 'production'; + const compilationEvidence: CompilationEvidence[] = []; const rsbuild = await createRsbuild({ cwd: options.cwd, config: composeMcpAppsRsbuildConfig(sources, { cwd: options.cwd, meta: options.meta, mode, + onCompilationEvidence: (evidence) => compilationEvidence.push(evidence), outDir: options.outDir, ...(options.tools === undefined ? {} : { tools: options.tools }), }), @@ -523,6 +553,7 @@ export const compileMcpApps = async ( } finally { await result?.close(); } + assertViewsSelfContained(compiled, compilationEvidence, options.cwd); const sizes = await assertSelfContainedViews(compiled, options.outDir); const compiledApps = Object.freeze(compiled.map((app): CompiledMcpApp => Object.freeze({ diff --git a/packages/agent-bundle/src/build/rslib.ts b/packages/agent-bundle/src/build/rslib.ts index 025ff15ba..71ca84514 100644 --- a/packages/agent-bundle/src/build/rslib.ts +++ b/packages/agent-bundle/src/build/rslib.ts @@ -9,7 +9,7 @@ import { dirname, join, resolve, sep } from 'node:path'; import { dependencyManifestPath } from '../core/dependency-manifest.ts'; import { sha256Hex } from '../core/digest.ts'; import { isErrno } from '../core/errors.ts'; -import { isInsideOrEqual, toPosixRelative } from '../core/paths.ts'; +import { isInsideOrEqual, posixRelativeWhenInside } from '../core/paths.ts'; import { isRecord } from '../core/strict-json.ts'; import type { AgentBundleToolsConfig } from '../core/types.ts'; import type { AgentBundleMeta } from '../meta.ts'; @@ -789,9 +789,6 @@ const moduleKindOf = ( return 'authored'; }; -const projectIssuer = (cwd: string, issuer: string): string => - isInsideOrEqual(cwd, issuer) ? toPosixRelative(cwd, issuer) : issuer; - /** * Lowers every surface's entries through one Rslib instance and returns the * compiler result per surface, in surface order. A surface without entries @@ -875,7 +872,7 @@ export const buildRslibSurfaces = async ( const externals = record.externals.map((external): ExternalIR => ({ asset: entry.outputRelativePath, externalType: external.externalType, - issuers: external.issuers.map((issuer) => projectIssuer(options.cwd, issuer)), + issuers: external.issuers.map((issuer) => posixRelativeWhenInside(options.cwd, issuer)), kind: classifyExternal(external, { asset: entry.outputRelativePath, emittedAssets }), request: external.request, userRequest: external.userRequest, diff --git a/packages/agent-bundle/src/core/paths.ts b/packages/agent-bundle/src/core/paths.ts index 14d3cd23a..65fc83a70 100644 --- a/packages/agent-bundle/src/core/paths.ts +++ b/packages/agent-bundle/src/core/paths.ts @@ -17,6 +17,10 @@ export const toPosixPath = (path: string): string => path.split(sep).join('/'); /** POSIX-form path of `path` relative to `root`; asserts nothing about containment. */ export const toPosixRelative = (root: string, path: string): string => toPosixPath(relative(root, path)); +/** `path` in POSIX form relative to `root` when it lies inside or at `root`; otherwise `path` unchanged. */ +export const posixRelativeWhenInside = (root: string, path: string): string => + (isInsideOrEqual(root, path) ? toPosixRelative(root, path) : path); + /** True when candidate resolves strictly inside root. */ export const isInside = (root: string, candidate: string): boolean => { const path = relative(root, candidate); diff --git a/packages/agent-bundle/tests/mcp-apps-compile.test.ts b/packages/agent-bundle/tests/mcp-apps-compile.test.ts index 98ea74884..925329046 100644 --- a/packages/agent-bundle/tests/mcp-apps-compile.test.ts +++ b/packages/agent-bundle/tests/mcp-apps-compile.test.ts @@ -2,9 +2,11 @@ import { mkdir, mkdtemp, readdir, readFile, realpath, rm, symlink, writeFile } f import { tmpdir } from 'node:os'; import { dirname, join, relative } from 'node:path'; +import { createRsbuild } from '@rsbuild/core'; import { afterEach, describe, expect, it } from '@rstest/core'; import { init, parse } from 'es-module-lexer'; +import type { CompilationEvidence } from '../src/build/compile-result.ts'; import { MCP_APP_HTML_ADVISORY_BYTES } from '../src/build/mcp-app-diagnostics.ts'; import { compileMcpApps, composeMcpAppsRsbuildConfig, type McpAppCompileMode } from '../src/build/mcp-apps.ts'; import { DiagnosticError, type Diagnostic } from '../src/core/diagnostics.ts'; @@ -197,6 +199,42 @@ describe('compileMcpApps', () => { expect(compiled!.size.gzipBytes).toBeLessThan(compiled!.size.bytes); }, 60_000); + it('fails a view with AB6005 when a tools.rspack mutator keeps a dependency external, judged from compile evidence', async () => { + const root = await createProject(reactView); + const diagnostics = await compileFailure(compile(root, [app(root)], { + tools: { + rspack: (config) => { + config.externals = { 'react-dom/client': 'ReactDOMClient' }; + }, + }, + })); + expect(diagnostics).toEqual([{ + code: 'AB6005', + generatedPath: 'mcp-apps/status.html', + message: 'Compiled MCP App view "mcp-apps/status.html" keeps "ReactDOMClient" external (var), imported as "react-dom/client", from views/status.ts; a view inlines every module it loads.', + recovery: 'Bundle every JavaScript dependency into the artifact, then rebuild it.', + severity: 'error', + }]); + }, 60_000); + + it('records no external for a self-contained view', async () => { + const root = await createProject(reactView); + const evidence: CompilationEvidence[] = []; + const config = composeMcpAppsRsbuildConfig([app(root)], { + cwd: root, + meta, + onCompilationEvidence: (record) => evidence.push(record), + outDir: join(root, 'dist'), + }); + const rsbuild = await createRsbuild({ cwd: root, config }); + const result = await rsbuild.build(); + await result.close(); + expect(evidence.map((record) => ({ compiler: record.compiler, externals: record.externals }))).toEqual([ + { compiler: 'status', externals: [] }, + ]); + expect(evidence[0]?.modules.some((module) => module.resource === join(root, 'views', 'StatusPanel.tsx'))).toBe(true); + }, 60_000); + it('reports a syntax error as one AB4770 naming the file and position', async () => { const root = await createProject({ 'views/status.ts': 'const x = ;\n' }); const diagnostics = await compileFailure(compile(root, [app(root)])); diff --git a/website/docs/en/guide/distribution/validation.mdx b/website/docs/en/guide/distribution/validation.mdx index 56e19fb3c..08cce7ceb 100644 --- a/website/docs/en/guide/distribution/validation.mdx +++ b/website/docs/en/guide/distribution/validation.mdx @@ -218,7 +218,10 @@ Generated executables — artifact scripts, the routed CLI, MCP entries, hook wr package build's JavaScript bundles — compile with Rslib's `autoExternal` disabled, `bundle: true`, and `splitChunks: false`, and the framework adds no `externals` of its own; Rslib's `node` target leaves only Node built-ins (`node:fs`, `path`, plus Yarn PnP's `pnpapi`) external. MCP App views -inline every script and style into one HTML file. `AB4725` rejects +inline every script and style into one HTML file, and the same audit plugin sits in their Rsbuild +compiler: a browser document has no allowable external, so any module a view's compilation kept +external — a `react` mapped to a global through the hatch, say — fails the view with `AB6005` +(`Compiled MCP App view "mcp-apps/.html" keeps … external …`). `AB4725` rejects `tools.rsbuild.output.autoExternal` other than `false` and statically visible string or object `externals` entries that name packages. RegExp, function-form, relative, and mutator externals are judged from the compilation's own evidence: `AB6005` fails a generated executable in a host pack diff --git a/website/docs/zh/guide/distribution/validation.mdx b/website/docs/zh/guide/distribution/validation.mdx index 03740179f..31fee23bd 100644 --- a/website/docs/zh/guide/distribution/validation.mdx +++ b/website/docs/zh/guide/distribution/validation.mdx @@ -188,7 +188,10 @@ bundle——都不能把裸包保持外部化:编译器会在 `prepack` 触及 CLI、MCP 入口、钩子包装层以及包构建的 JavaScript bundle——都以禁用 Rslib 的 `autoExternal`、`bundle: true` 与 `splitChunks: false` 的配置编译,框架自身也不添加任何 `externals`;Rslib 的 `node` target 只把 Node 内建模块(`node:fs`、`path`,以及 Yarn PnP 的 `pnpapi`)保持外部化。MCP App 视图则把每个脚本与样式都 -内联进同一个 HTML 文件。`AB4725` 会拒绝不为 `false` 的 `tools.rsbuild.output.autoExternal`,以及静态可见、 +内联进同一个 HTML 文件,同一个审计插件也位于它们的 Rsbuild 编译器中:浏览器文档不允许任何 external, +因此视图编译保持外部化的任何模块——比如通过逃生舱映射到全局变量的 `react`——都会让该视图以 `AB6005` +失败(`Compiled MCP App view "mcp-apps/.html" keeps … external …`)。`AB4725` 会拒绝不为 `false` +的 `tools.rsbuild.output.autoExternal`,以及静态可见、 点名某个包的字符串或对象 `externals` 条目。RegExp、函数形式、相对路径与 mutator 添加的 externals 则由编译过程 自身的证据判断:若 Rspack 在宿主包或 `dist` 的生成可执行文件中保持了内建模块、`pnpapi` 或已输出同级文件 之外的任何 external,`AB6005` 就会失败。输出模块遍历仍留在这项编译期检查之后。通过 From c56a0fce519460474af4118bdb02e6aa3d6303f0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 10:48:11 +0000 Subject: [PATCH 2/5] chore: changeset names #627 --- .changeset/619-mcp-app-view-evidence.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/619-mcp-app-view-evidence.md b/.changeset/619-mcp-app-view-evidence.md index 8364c9954..e197fa01c 100644 --- a/.changeset/619-mcp-app-view-evidence.md +++ b/.changeset/619-mcp-app-view-evidence.md @@ -2,4 +2,4 @@ "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/.html" keeps … external …`), since a browser document has no allowable external. (#PR) +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/.html" keeps … external …`), since a browser document has no allowable external. (#627) From e4f513d7e489d0c217d3769f95d0b6ade439f5cb Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 10:50:38 +0000 Subject: [PATCH 3/5] chore: deslop pass (cursor-grok-4.6-high-fast, 5 edits) --- packages/agent-bundle/src/build/external-policy.ts | 5 ++--- packages/agent-bundle/src/build/mcp-apps.ts | 14 +++++--------- .../agent-bundle/tests/mcp-apps-compile.test.ts | 4 +++- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/agent-bundle/src/build/external-policy.ts b/packages/agent-bundle/src/build/external-policy.ts index 9c72ef900..6122edcb3 100644 --- a/packages/agent-bundle/src/build/external-policy.ts +++ b/packages/agent-bundle/src/build/external-policy.ts @@ -70,9 +70,8 @@ const externalMessage = (external: ExternalIR): string => { }; /** - * A browser view has no allowable external at all: no Node built-ins, and no - * sibling module, since the document inlines every script. Every external the - * view's compilation kept is an `AB6005` against the emitted document. + * 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, diff --git a/packages/agent-bundle/src/build/mcp-apps.ts b/packages/agent-bundle/src/build/mcp-apps.ts index 49828deda..f190fa0ce 100644 --- a/packages/agent-bundle/src/build/mcp-apps.ts +++ b/packages/agent-bundle/src/build/mcp-apps.ts @@ -347,7 +347,7 @@ export const composeMcpAppsRsbuildConfig = ( readonly meta: AgentBundleMeta; /** Defaults to `production`; see {@link McpAppCompileMode}. */ readonly mode?: McpAppCompileMode; - /** Receives each view compilation's externals and modules once its module graph is final; the audit plugin is composed either way, so `inspect --bundler` shows what runs. */ + /** Called with each view compilation's evidence once its module graph is final. */ readonly onCompilationEvidence?: (evidence: CompilationEvidence) => void; readonly outDir: string; readonly tools?: AgentBundleToolsConfig; @@ -435,12 +435,7 @@ export const composeMcpAppsRsbuildConfig = ( })); }; -/** - * Every view's compilation is judged from its own evidence: the Rsbuild - * environment is named after the App, so each record names the view it - * belongs to. A view without a record did not compile through the invariant - * layer, which is a framework fault, not a consumer one. - */ +/** Missing or duplicate evidence is a framework fault: the invariant layer names each view's environment after the App. */ const assertViewsSelfContained = ( compiled: readonly PlannedMcpApp[], evidence: readonly CompilationEvidence[], @@ -448,10 +443,11 @@ const assertViewsSelfContained = ( ): void => { const diagnostics = compiled.flatMap((app) => { const records = evidence.filter((record) => record.compiler === app.name); - if (records.length !== 1) { + const [record] = records; + if (record === undefined || records.length !== 1) { throw new Error(`Expected one compilation evidence record for MCP App ${JSON.stringify(app.name)}, found ${String(records.length)}.`); } - return viewSelfContainmentDiagnostics(records[0]!, `mcp-apps/${app.name}.html`, projectRoot); + return viewSelfContainmentDiagnostics(record, `mcp-apps/${app.name}.html`, projectRoot); }); if (diagnostics.length > 0) throw new DiagnosticError(diagnostics); }; diff --git a/packages/agent-bundle/tests/mcp-apps-compile.test.ts b/packages/agent-bundle/tests/mcp-apps-compile.test.ts index 925329046..89d541ff0 100644 --- a/packages/agent-bundle/tests/mcp-apps-compile.test.ts +++ b/packages/agent-bundle/tests/mcp-apps-compile.test.ts @@ -232,7 +232,9 @@ describe('compileMcpApps', () => { expect(evidence.map((record) => ({ compiler: record.compiler, externals: record.externals }))).toEqual([ { compiler: 'status', externals: [] }, ]); - expect(evidence[0]?.modules.some((module) => module.resource === join(root, 'views', 'StatusPanel.tsx'))).toBe(true); + expect(evidence.some((record) => + record.modules.some((module) => module.resource === join(root, 'views', 'StatusPanel.tsx')), + )).toBe(true); }, 60_000); it('reports a syntax error as one AB4770 naming the file and position', async () => { From a7dffc87ab6d2e50a2735397a8450d291c20058c Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 10:54:41 +0000 Subject: [PATCH 4/5] fix(build): audit plugin uses the compiler's own Rspack module classes (#627) --- docs/diagnostics.md | 2 +- .../agent-bundle/src/build/dependency-audit-plugin.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 66180c492..db67a5d2e 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -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/`; `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/`; `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). | diff --git a/packages/agent-bundle/src/build/dependency-audit-plugin.ts b/packages/agent-bundle/src/build/dependency-audit-plugin.ts index 5a2abb7ff..b9ee1478d 100644 --- a/packages/agent-bundle/src/build/dependency-audit-plugin.ts +++ b/packages/agent-bundle/src/build/dependency-audit-plugin.ts @@ -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'; @@ -61,6 +61,7 @@ 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; @@ -68,7 +69,7 @@ const collectExternals = (compilation: Rspack.Compilation): readonly Compilation 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) ?? { @@ -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 }) })); } @@ -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; From 5e07ff0fb55c964b37ceac1ba63645a6659926aa Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 11:04:50 +0000 Subject: [PATCH 5/5] fix(build): pin view compiler names; share the kept-external clause; docs order (#627) --- docs/diagnostics.md | 6 +++--- .../agent-bundle/src/build/external-policy.ts | 17 ++++++++++------- packages/agent-bundle/src/build/mcp-apps.ts | 6 +++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index db67a5d2e..8beadb9cb 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -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, 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 +`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 @@ -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: "".` — 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.` MCP App view form: `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 — 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 "" .` 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/`. | Bundle every JavaScript dependency into the artifact, then rebuild it. | +| `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. 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. The emitted-module walk remains as defense in depth and reports `Generated JavaScript import from "" .` 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/`. | 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 "".` — 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. | diff --git a/packages/agent-bundle/src/build/external-policy.ts b/packages/agent-bundle/src/build/external-policy.ts index 6122edcb3..b544e1627 100644 --- a/packages/agent-bundle/src/build/external-policy.ts +++ b/packages/agent-bundle/src/build/external-policy.ts @@ -5,7 +5,7 @@ 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 { CompilationEvidence, 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'; @@ -48,12 +48,16 @@ export const classifyExternal = ( return 'package'; }; +/** `keeps "" external ()[, imported as "",][ from ]; ` */ +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) @@ -80,9 +84,8 @@ export const viewSelfContainmentDiagnostics = ( ): readonly Diagnostic[] => evidence.externals.map((external) => artifactDiagnostic( 'AB6005', - `Compiled MCP App view ${JSON.stringify(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.map((issuer) => posixRelativeWhenInside(projectRoot, issuer)).join(', ')}`}; ` + `Compiled MCP App view ${JSON.stringify(asset)} ` + + keptExternalClause(external, external.issuers.map((issuer) => posixRelativeWhenInside(projectRoot, issuer))) + 'a view inlines every module it loads.', asset, )); diff --git a/packages/agent-bundle/src/build/mcp-apps.ts b/packages/agent-bundle/src/build/mcp-apps.ts index f190fa0ce..2b156a722 100644 --- a/packages/agent-bundle/src/build/mcp-apps.ts +++ b/packages/agent-bundle/src/build/mcp-apps.ts @@ -220,6 +220,10 @@ const assertResolvedViewConfig = ( throw new Error('Rsbuild resolved an invalid self-contained MCP App configuration.'); } } + // The compiler name is how each view's compile evidence finds its App. + if (bundlers.map((bundler) => bundler.name).toSorted().join('\0') !== appNames.toSorted().join('\0')) { + throw new Error('Rsbuild resolved an invalid self-contained MCP App configuration.'); + } for (const bundler of bundlers) { if ( bundler.output?.asyncChunks !== false || @@ -435,7 +439,7 @@ export const composeMcpAppsRsbuildConfig = ( })); }; -/** Missing or duplicate evidence is a framework fault: the invariant layer names each view's environment after the App. */ +/** Missing or duplicate evidence is a framework fault: Rsbuild names each environment's compiler after its App, and `assertResolvedViewConfig` pins that name. */ const assertViewsSelfContained = ( compiled: readonly PlannedMcpApp[], evidence: readonly CompilationEvidence[],