Skip to content

fix(mcp-apps): React plugin on every view, AB4770–AB4772 compile diagnostics, size report, html defaults, reserved-specifier precedence, readable dev output; rsc-agent-runtime AB8206 detail (#572) - #585

Merged
ScriptedAlchemy merged 8 commits into
mainfrom
fix/572-mcp-apps-compiler
Sep 5, 2026

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Resolves the MCP Apps compiler path, MCP Apps dev loop, and the two rsc-agent-runtime P2 findings of #572 (Rsbuild audit). Out of scope and left for their own PRs: the Workbench HMR proxy (§3), docsite buildCache, the @rspack/core pin, and Rsbuild/Rslib version bumps.

Plugin builds stay self-contained: every App view is still one HTML document with everything inlined — no externals, no autoExternal — and the rsc-agent-runtime app environment now asserts the same invariants.

Per-finding resolution

P1 — pluginReact keyed on the entry extension (mcp-apps.ts:49,189)

@rsbuild/plugin-react is registered for every App view regardless of entry extension (packages/agent-bundle/src/build/mcp-apps.ts:344, pluginReact({ fastRefresh: false })). A .ts entry importing a .tsx component now compiles JSX with the automatic runtime.

Evidence — fixture views/status.tsviews/StatusPanel.tsx, built with agent-bundle build:

free React.createElement in emitted HTML host result
before (main dist) 1 ReferenceError: React is not defined
after 0 renders panel-ready

Regression test: tests/mcp-apps-compile.test.tscompiles a .ts entry that imports a .tsx component to the automatic JSX runtime and measures the view (asserts no [^.\w]React\.createElement, the component text is present, and runs through the same compileMcpApps path the App test harness uses).

P1 — logLevel: 'silent' and nobody reads stats.errors/warnings (mcp-apps.ts:196)

An onAfterEnvironmentCompile collector keeps each environment's stats.toJson(...) (mcp-apps.ts:106-118); after the build the errors map to one AB4770 per Rspack error — project-relative file, line:column, module request, and the bundler message with the miette frame flattened to one line (packages/agent-bundle/src/build/mcp-app-diagnostics.tsstatsDiagnostic, on the shared parser packages/agent-bundle/src/build/rspack-stats-errors.tsrspackStatsErrors, describeRspackStatsError, formatRspackStatsError, exported from agent-bundle/api so the rsc-agent-runtime example renders the same shape without a private import). At most 20 diagnostics per App (MCP_APP_COMPILE_ERROR_CAP, mcp-app-diagnostics.ts:46): past the cap the last one counts the remaining errors. Warnings surface as AB4771 at warning severity unless matched by ignoredMcpAppCompileWarnings (mcp-app-diagnostics.ts:58) — the list is empty and documented as such in docs/diagnostics.md so every future ignore has to be written down. logLevel stays silent: the diagnostics are the channel, not the console.

Before → after, agent-bundle build on the same fixtures:

# syntax error (`const x = ;`)
before: [{"code":"AB5000","message":"Rspack build failed.","severity":"error"}]
after:  [{"code":"AB4770","message":"MCP App \"status\" failed to compile: views/status.ts:1:10: Module build failed (from builtin:swc-loader): Syntax Error: Expression expected","recovery":"Fix the reported error in the named file and rebuild; run `agent-bundle build` for the full message.","severity":"error","sourcePath":"/tmp/i572-evidence/proj/views/status.ts"}]

# unresolved import (`import { thing } from './missing'`)
before: [{"code":"AB5000","message":"Rspack build failed.","severity":"error"}]
after:  [{"code":"AB4770","message":"MCP App \"status\" failed to compile: views/status.ts:1:1: Module not found: Can't resolve './missing' in '/tmp/i572-evidence/proj/views'","recovery":"…","severity":"error","sourcePath":"/tmp/i572-evidence/proj/views/status.ts"}]

# broken tsconfig.json (truncated JSON)
before: [{"code":"AB5000","message":"Rspack build failed.","severity":"error"}]
after:  [{"code":"AB4770","message":"MCP App \"status\" failed to compile: JSON parse error: EOF while parsing a value at line 2 column 0 in '/tmp/i572-evidence/proj/tsconfig.json'","recovery":"…","severity":"error","sourcePath":"/tmp/i572-evidence/proj/views/status.ts"}]

Tests: tests/mcp-apps-compile.test.ts — syntax error, unresolved import, broken tsconfig.json (each asserting the file is named), the error cap, warning surfacing and the ignore list; tests/rspack-stats-errors.test.tsloc forms (4:1-27, 12:5), filename-bearing and marker-only miette frames, loader queries, match resources, errors without a module; tests/mcp-app-diagnostics.test.ts — the AB4770AB4772 mapping.

P1 — dev shows AB7100 "Unable to compile the build: Rspack build failed."; AB7100 undocumented (artifact-service.ts:80-85)

ArtifactService now compiles with mode: 'development' and merges the compiler's diagnostics into the epoch (packages/agent-bundle/src/dev/artifacts/artifact-service.ts:215,239). A failed compile reaches the Workbench as the compiler's own AB4770 rows (file as sourcePath, line:column in the message), not a wrapped AB7100; advisories (AB4771/AB4772) ride the published epoch as warnings. The Overview Diagnostics table already renders {code, severity, message, sourcePath} rows, so no Workbench change was needed — verified by packages/workbench/tests/overview.e2e.test.ts and the dev-artifact-service.test.ts cases reports a failed MCP App compile as the compiler's own AB4770 diagnostics, not AB7100 / carries MCP App compile advisories onto the published epoch / keeps AB7100 for compiler throws that carry no diagnostics.

AB7100AB7102 are documented (docs/diagnostics.md § Development rebuild compilation and publication), and the AB7xxx family row names them; the reference pages render from that file.

P2 — rsc-agent-runtime AB8206 swallows stats errors

The compile observer in examples/rsc-agent-runtime/rsbuild.config.ts throws an RscRuntimeCompileError carrying the Rspack stats; the session formats it with describeRspackCompileErrors (src/dev/compile-diagnostics.ts, a thin wrapper over the agent-bundle/api formatters above) as file:line:col: message with the frame flattened, so its AB8206 reads RSC runtime source build failed: RSC runtime compile reported N error(s):\nsrc/rsc/worker.tsx:177:7: Module build failed (from builtin:swc-loader): Syntax Error: Unexpected token … (src/dev/rsbuild-runtime-session.ts:569). Covered by tests/compile-diagnostics.test.ts (new), the dev-provider.integration.test.ts source-build cases, and the Workbench runtime-playground-hmr.e2e.test.ts assertion on the rendered AB8206. Development sessions run Rsbuild at logLevel: 'silent' so the diagnostic is the only channel (production builds keep error for the console); README documents both.

Known pre-existing limit, unchanged here: the Workbench Logs page redacts absolute paths in diagnostic messages, so there the row shows the code and source path; the Overview shows the full message.

P2 — silent size

compileMcpApps measures every emitted document (assertSelfContainedViews returns raw and gzip bytes, mcp-apps.ts:500); agent-bundle build prints one line per view after Built … and --json carries build.compiledMcpApps[].size (packages/agent-bundle/src/cli.ts:371). AB4772 (mcp-app-diagnostics.ts:306) warns when a production view reaches 1 MiB raw (MCP_APP_HTML_ADVISORY_BYTES) or any view exceeds the 2 MiB MAX_APP_HTML_BYTES the Workbench/serve-app hosts accept, naming the five largest modules from stats. The stats are recorded with orphanModules: true (mcp-apps.tsmcpAppStatsOptions): the parts of a concatenated module are orphans of the chunk graph and Rspack otherwise collapses them into one nameless aggregate, so the advisory could name a CommonJS dependency but never the author's own ESM source — largestModules ranks each part once through the module that absorbed it and skips top-level orphans (tested end to end: names the largest authored modules in a production advisory, concatenated or not). 1 MiB is a fixed threshold — there is no existing App-level config home, and it sits ~2.4× the audit's measured baseline and at half the hard host bound.

Evidence — examples/mcp-app with the branch's CLI reproduces the audit's measurement exactly:

Built mcp-app-example to …/examples/mcp-app/artifact-evidence
MCP App status (portable): mcp-apps/status.html 437.3 KiB (104.4 KiB gzip)

P2 — html defaults

An api.modifyHTML plugin adds lang="en" to a root element without one and a <title> equal to the App name when the template has none (mcp-apps.ts:125-156); html.mountId stays root. Before: <html> <title>Rsbuild App</title>; after: <html lang="en"> <title>status</title> <div id="root"></div>. A template that sets its own lang/title is left alone (tested). The #root mount contract is documented in website/docs/{en,zh}/guide/authoring/mcp.mdx § The compiled document.

P2 — resolve.aliasStrategy lets a paths entry shadow agent-bundle/meta

Implemented the "otherwise guarantee reserved specifiers win" branch rather than aliasStrategy: 'prefer-alias': prefer-alias makes Rsbuild drop tsconfig.json paths entirely, which would break @/components/*-style imports in every view. Instead a NormalModuleReplacementPlugin rewrites the exact agent-bundle/meta request to the generated identity module before resolution (mcp-apps.ts:168-172), so a shadowing paths entry cannot intercept it while other paths keep resolving. Tests: a shadowing paths entry still yields the real identity module; a non-shadowing paths entry resolves; assertResolvedViewConfig rejects a config without the replacement plugin.

P2 — dev preview uses the production profile; inline-source-map hatch undocumented

Measured first: with inline-source-map the ext-apps example views grow to 4.2 MiB, past the 2 MiB host bound, and even inline-nosources-source-map overshoots. So agent-bundle dev compiles views unminified with no source maps (mcp-apps.ts:371-372) — readable module boundaries (// CONCATENATED MODULE: markers), still one self-contained document — and when the readable output would exceed MAX_APP_HTML_BYTES that App is recompiled with the production profile (mcp-apps.tscompileMcpApps, fallback block). Each substituted view gets exactly one AB4772: the substitution notice (readable size, production size, largest modules) when the production document fits the hosts; the plain over-bound advisory for the production bytes when it does not, since a notice claiming the preview renders it would be false. The production compile's own size advisory is folded into that one row; its AB4771 warnings are the same module graph's and are not repeated. Production is unchanged (minified, no maps). tools.rsbuild opting into inline-source-map is now documented as the hatch for views small enough to carry it; external source-map files are still rejected as stray assets. Documented in mcp.mdx § Development vs production builds (en+zh) and workbench.mdx.

P2 — rsc-agent-runtime app env lacks the self-contained invariants

selfContainedAppPlugin (examples/rsc-agent-runtime/rsbuild.config.ts:166) asserts inlineScripts/inlineStyles: true, dataUriLimit: Number.MAX_SAFE_INTEGER, splitChunks: false, asyncChunks: false, filenameHash: false, legalComments: 'inline' on the app environment before the compiler is created and reports stray non-HTML assets through the session's AB8206; the environment config sets those values (rsbuild.config.ts:394). Covered by tests/host-artifacts.test.ts.

Docs and changeset

  • docs/diagnostics.md: new AB477x family + § MCP App view compilation (AB4770–AB4772); § Development rebuild compilation and publication (AB7100–AB7102).
  • website/docs/{en,zh}/guide/authoring/mcp.mdx, reference/configuration.mdx, guide/development/workbench.mdx updated in both locales; pnpm docs:site:build green (language parity passed).
  • Changeset .changeset/572-mcp-apps-compiler.md (agent-bundle: patch).

Gates (run on the final branch state, merged with main at #582 / Rslib 1.0 / Rsbuild 2.2.3)

pnpm install --frozen-lockfile · pnpm build · pnpm typecheck · pnpm lint · pnpm test:unit (3780) · pnpm docs:site:build (language parity passed) · integration subset mcp-apps-compile, mcp, build, cli, dev-artifact-service, api, public-api, prepack, serve-app (221) · examples/rsc-agent-runtime: tsc --noEmit + full rstest suite (172; dev-invocation.integrationreleases an exact historical lease when four active workers reject its admission failed once under four concurrent suites — the capacity workers' 1 s lifetime elapsed on the loaded host, as that test's own comment anticipates — and passes in isolation, 26/26). Every added module has a production importer (git grep check per AGENTS.md: rspack-stats-errorsapi.ts, mcp-app-diagnostics.ts; mcp-app-diagnosticsmcp-apps.ts; mcp-app-limitsmcp-apps.ts, mcp-app-bridge.ts, browser-setup-module.ts).

Self-review

Reviewer: change-risk-reviewer on gpt-5.6-sol-medium, against the branch diff vs origin/main. Four should-fix findings, all fixed; second pass (same reviewer and model, against the branch merged with origin/main at Rsbuild 2.2.3): no should-fix or nice-to-have findings — each prior item confirmed resolved in code and tests, the orphanModules traversal checked for byte loss and double counting, changeset and en/zh API rows and AB4772 prose checked against the implementation, no conflicting Rsbuild 2.2 interaction.

# Finding Disposition
1 Production fallback can remain above the 2 MiB host bound yet is copied and reported as renderable (mcp-apps.ts fallback block). Fixed. The replacement's measured size is checked; over the bound it is still what lands (it is the smaller document) but the diagnostic is the plain over-bound AB4772 naming the production bytes — no "the preview renders" claim. Test: does not claim a substitution when the production build would not render in the hosts either.
2 A substituted view can receive two AB4772s: the substitution notice plus the production compile's own 1 MiB advisory. Fixed. The production compile's diagnostics are not merged; a substituted view gets exactly one AB4772, and the notice now carries the largest-modules clause the advisory would have. Test: reports one AB4772 for a substituted view whose production build itself draws the advisory (readable > 2 MiB, production between 1 and 2 MiB).
3 examples/rsc-agent-runtime/src/dev/compile-diagnostics.ts re-implemented the Rspack file/location/message parser from mcp-app-diagnostics.ts, and the copies had already diverged. Fixed. One parser, packages/agent-bundle/src/build/rspack-stats-errors.ts, consumed by mcp-app-diagnostics.ts and exported from agent-bundle/api (rspackStatsErrors, describeRspackStatsError, formatRspackStatsError); the example's module is a wrapper over it and rsbuild.config.ts throws RscRuntimeCompileError with the raw stats instead of pre-formatted text. The reviewer's case list is tests/rspack-stats-errors.test.ts; the API reference row and the changeset name the exports.
4 README says errors land only in diagnostics while logLevel: 'error' also prints them to the console. Fixed. logLevel is silent for development sessions and error otherwise; README states both.

Found while fixing 2 (not raised by the reviewer): with the stats options as merged, the AB4772 largest-modules clause could never name an authored ESM module — the parts of a concatenated module are chunk-graph orphans and Rspack collapses them into a nameless aggregate unless orphanModules is on. Fixed in mcpAppStatsOptions + largestModules (rank each part once, skip top-level orphans), with a unit fixture in the real shape and an end-to-end production advisory test.

… measured sizes; accept a compile mode (#572 seam)
…nostics with file:line, size report, html defaults, reserved-specifier precedence, readable dev output; rsc-agent-runtime AB8206 detail and self-contained app env (#572)
@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f01459b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T02:51:47.543857Z d063eaf PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@585
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@585
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@585
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@585

commit: f01459b

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d063eaf646

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .changeset/572-mcp-apps-compiler.md Outdated
…fallback over the host bound, shared Rspack stats parser on agent-bundle/api, orphan-aware largest-modules ranking; rsc-agent-runtime silent dev log level (#585)
…iler

# Conflicts:
#	packages/agent-bundle/src/cli.ts
#	website/docs/en/reference/api.mdx
#	website/docs/zh/reference/api.mdx
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 5, 2026 04:07
@ScriptedAlchemy
ScriptedAlchemy merged commit 97a5bfa into main Sep 5, 2026
16 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the fix/572-mcp-apps-compiler branch September 5, 2026 04:28
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
… root; mcp-apps-compile test selects the portable projection
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
…e selection identity (codex+portable), matching planCompiledMcpApps after the #585 merge
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
…ections; remove the plugin target (#555 W1) (#578)

* feat(targets): remove the plugin target from the public target model (#555 S1)

- config/normalize.ts no longer synthesizes a plugin lowering target or a
  shared plugin skill document; loweringHosts is a pure isSkillHost filter.
- adapters/registry.ts registers portable, codex, claude, cursor only;
  adapters/plugin.ts is deleted together with the composite-only capability
  helpers (intersect/union/mergeCapabilityEvidence, capabilityBooleanView).
- targets: ['plugin'] and --target plugin fail with the existing AB4100.
- plugin branches removed from api.ts, pack-inventory, package-build,
  entry-shell, events/projection, lifecycle replay, install/surface,
  install/install, create-agent-bundle options/scaffold, capability JSON,
  and the CLI help text.
- Tests: plugin-bundle.test.ts deleted; plugin rows dropped from the
  adapter, install-surface, normalization and lifecycle suites; acceptance 3
  covered by normalization.test.ts and cli.test.ts.

* feat(build): plan the selected projections into one composite root (#555 S2)

build/build.ts stages every selected host projection into one root at
artifactRoot; build/compose.ts merges byte-identical entries once, fails
same-path collisions with AB4103 in host-name/path order, and refuses
host-scoped components another selected host would discover conventionally
with AB4105. Codex and Cursor hook/MCP documents move beside their manifests;
shared hooks compile to hooks/<name>.<host>.mjs; compiled surfaces are
attributed to the sorted composite identity; the artifact manifest records
only the selected projections. Install, doctor, dev services, eval harnesses,
Workbench, examples, and tests follow the composite root.

* feat(build): AB4106 for mixed advanced-registry selections; validate/inspect judge the composite root; port #569 consumer and docs fixes

- config/validate.ts: AB4106 when an adapter registered on an advanced
  TargetRegistry is selected beside any other target; the built-in host list
  moves to adapters/composite-layout.ts and install/surface.ts imports it.
- build/compose.ts: planComposite returns the plan beside its diagnostics;
  dev/project-service.ts prepare uses it so validate, inspect, dev report
  AB4103/AB4105 exactly where build refuses (caught by the composite-rules
  parity test ported from #569).
- Ported from #569 (superseded): tests/support/mcp-conformance.ts pluginRoot,
  tests/skill-document-service.test.ts decoy, examples/audiobook-curator and
  examples/hooks-and-scripts READMEs, scripts/measure-hook-cold-start.mjs.
- tests/composite-rules.test.ts: layout-independent rules from #569 (four-host
  root, commands/ dialect collision, INSTALL.md sections, validate parity).
- Stale per-host wording: Cursor capability evidence, doctor/types/cursor
  comments, create-agent-bundle README, docs/diagnostics.md, canvas sample.

* test(packed): read the composite root's bin/ and single mcp/ entry in the packed pool

* fix(inspect): describe the same event allowed-targets set the build bakes into a generated-route server; AB4105 trigger names skills

* build: carry #585's MCP App compile diagnostics through the composite root; mcp-apps-compile test selects the portable projection

* test(cli): MCP App compile report attributes the view to the composite selection identity (codex+portable), matching planCompiledMcpApps after the #585 merge

* build: identify the event runtime by the artifact alone; port #569's nested-root and shared-root tests (#592 boundary)

The generated MCP entry and every hook wrapper derived the event runtime's
endpoint id from `<epoch>:<selection identity>:<root>`, so the composite
selection (`claude+codex`) had become runtime identity, and the runtime
took `events.target` as the tool-call lineage host fallback, reading the
selection as a host. Both are projection selection leaking into runtime
identity (#592 §2). The endpoint is now `<epoch>:<root>` on both sides
(entry-shell, hook-contract, `agent-bundle/test` installed harness), the
`target` field leaves `GeneratedRouteMcpEntryOptions` and
`GeneratedEventRuntimeBinding`, and the lineage fallback is the one host a
single-projection root serves, or none for a composite root; `entries.ts`
requires the selection instead of defaulting it from the composite name.

`AB4105` never fired for a skill: normalization gives every skill every
selected target and per-host frontmatter extensions collide as `AB4103`
instead, so the dead skill branch leaves `compose.ts` and the diagnostics,
reference, and framework-mode prose say so. `AGENT_BUNDLE_HOOK_HOST` leaves
the runtime-environment reference (en/zh) and the test env fixtures; `api.ts`
reuses `isBuiltInHost`; `compose.ts` reuses `sortedProjections`; stale
`<target>/…` doc comments in `test/packed.ts` and `routes/graph.ts` name the
root layout.

Ports from #569: install refuses `--from` naming a directory above the plugin
root for all three hosts (`AB7001`, no host CLI runs), doctor lists Claude
plugins from the root `--from` names and never from a nested `claude/`, and
the Codex validator judges only `.codex-plugin/*` in a root shared with
Claude's `.mcp.json` and `hooks/hooks.json`. Lane C's docs parity pass:
tree drawings gain `commands/` and `rules/`, the folder-discovery shield
names all three guarded paths, over-wide code samples re-padded to 90
columns, `AB4808`/`AB4809` prose and the rsc-agent-runtime README describe
one composite root.

* test(prepack): #588's prebuilt payload lands in the composite root (host-packs/runtime/…), not under a target partition

* changeset: the event runtime endpoint is the artifact's alone (#592 boundary)

* review: lineage fallback is the one host whose MCP document lists the server; document AB7001's composite-root trigger

Self-review pass 1 read the fallback as inferring the root's cardinality
from `allowedTargets`. It is `server.targets ∩ selected` — the hosts whose
MCP documents list the server, so the hosts that can have spawned it — and a
Claude-only server in a Claude+Codex root is correctly assumed to be Claude's.
The comment and the two test names now say so. `docs/diagnostics.md` names
`AB7001` in the `AB700x` family row: the host manifest sits directly under
`--from`, never under `<from>/<host>`.

* build: host the composite root's event runtime per selected host's first generated server; judge built-in hosts by adapter identity for the install surface (#578 review)

- planMcpEntriesSurface no longer attaches every event route to the first
  generated-route server: eventRuntimeHosting hosts the runtime in the first
  generated server each selected host's MCP document lists (one process when
  they agree, one per host otherwise) and every hosting server accepts the
  same allowed set; a Claude-only server in a Claude+Codex root no longer
  refuses Codex wrappers while Codex's own server hosts nothing.
- The generated entry carries `hosts` (the selected hosts whose documents
  list the server) separately from `allowedTargets`; the lineage fallback
  reads `hosts`, so a Claude-only server hosting a two-host runtime still
  assumes Claude for an anonymous MCP client (#592).
- TargetRegistry.builtInHost()/builtInHosts() judge the four shipped adapters
  by identity; compose and artifact validation use it for INSTALL.md and
  install.mjs, so an advanced registry's adapter named `portable` earns no
  install surface it never asked for. The pack inventory (manifest names
  only) keeps the name-based requirement it had on main.

* test(browser): mount each MCP App as one host of the composite selection, never as the selection identity

The browser pool compiles every app once for the project's whole selection
(as the build stages it, #555) but the registry's `target` — the preview
profile and the binding's `target` the page sees — is the host the app
mounts as: the override, or the app's first declared target the project
selects, as before. `claude+codex+portable` had leaked into the binding
(#592) and failed examples/mcp-app's browser-app suite in CI.

* docs: fold #599's per-code diagnostics rows into the composite root — AB4100 lists the four hosts, AB6023/AB6024 and AB7001 describe the one root

* review: judge built-in hosts by adapter identity in AB4106 and --host-validation; the pack inventory expects exactly the manifested files

- NormalizationTargetRegistry.builtInHost? lets config/validate.ts refuse a
  custom adapter registered under a built-in host's name beside other targets
  (AB4106) the way compose and validate-artifact already judge it; registries
  that cannot tell still judge by name.
- validate --host-validation and build --host-validation select the shipped
  validators through registry.builtInHosts(), so a custom `claude` or
  `portable` adapter is held to no shipped host's contract.
- pack-inventory.ts no longer re-derives the install surface from manifest
  target names: every emitted file is manifested and AB6023/AB6024 already
  judged the surface by identity, so the pack expects what the manifest lists.
- Changeset names the browser pool's `target` option semantics.

* docs: AB4106 judges the shipped adapters by identity

* test: exercise host validation by adapter identity through validate --artifact and build --host-validation

The identity test validated the project root, which never enters host
validation; validate the built custom root as an artifact instead, and
build a custom adapter named claude with a runner spy that must not be
spawned. AB6024's row names the shipped cursor/portable adapters by
identity, as the validator judges.

* test: validate --artifact takes the project root too

* test: a custom adapter under the portable name owes no install surface (AB6023/AB6024 by identity)
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
…rs read the composite root through the manifest (#592 step 3, #555 W2/S3) (#604)

* feat(targets): remove the plugin target from the public target model (#555 S1)

- config/normalize.ts no longer synthesizes a plugin lowering target or a
  shared plugin skill document; loweringHosts is a pure isSkillHost filter.
- adapters/registry.ts registers portable, codex, claude, cursor only;
  adapters/plugin.ts is deleted together with the composite-only capability
  helpers (intersect/union/mergeCapabilityEvidence, capabilityBooleanView).
- targets: ['plugin'] and --target plugin fail with the existing AB4100.
- plugin branches removed from api.ts, pack-inventory, package-build,
  entry-shell, events/projection, lifecycle replay, install/surface,
  install/install, create-agent-bundle options/scaffold, capability JSON,
  and the CLI help text.
- Tests: plugin-bundle.test.ts deleted; plugin rows dropped from the
  adapter, install-surface, normalization and lifecycle suites; acceptance 3
  covered by normalization.test.ts and cli.test.ts.

* feat(build): plan the selected projections into one composite root (#555 S2)

build/build.ts stages every selected host projection into one root at
artifactRoot; build/compose.ts merges byte-identical entries once, fails
same-path collisions with AB4103 in host-name/path order, and refuses
host-scoped components another selected host would discover conventionally
with AB4105. Codex and Cursor hook/MCP documents move beside their manifests;
shared hooks compile to hooks/<name>.<host>.mjs; compiled surfaces are
attributed to the sorted composite identity; the artifact manifest records
only the selected projections. Install, doctor, dev services, eval harnesses,
Workbench, examples, and tests follow the composite root.

* feat(build): AB4106 for mixed advanced-registry selections; validate/inspect judge the composite root; port #569 consumer and docs fixes

- config/validate.ts: AB4106 when an adapter registered on an advanced
  TargetRegistry is selected beside any other target; the built-in host list
  moves to adapters/composite-layout.ts and install/surface.ts imports it.
- build/compose.ts: planComposite returns the plan beside its diagnostics;
  dev/project-service.ts prepare uses it so validate, inspect, dev report
  AB4103/AB4105 exactly where build refuses (caught by the composite-rules
  parity test ported from #569).
- Ported from #569 (superseded): tests/support/mcp-conformance.ts pluginRoot,
  tests/skill-document-service.test.ts decoy, examples/audiobook-curator and
  examples/hooks-and-scripts READMEs, scripts/measure-hook-cold-start.mjs.
- tests/composite-rules.test.ts: layout-independent rules from #569 (four-host
  root, commands/ dialect collision, INSTALL.md sections, validate parity).
- Stale per-host wording: Cursor capability evidence, doctor/types/cursor
  comments, create-agent-bundle README, docs/diagnostics.md, canvas sample.

* test(packed): read the composite root's bin/ and single mcp/ entry in the packed pool

* fix(inspect): describe the same event allowed-targets set the build bakes into a generated-route server; AB4105 trigger names skills

* build: carry #585's MCP App compile diagnostics through the composite root; mcp-apps-compile test selects the portable projection

* test(cli): MCP App compile report attributes the view to the composite selection identity (codex+portable), matching planCompiledMcpApps after the #585 merge

* build: identify the event runtime by the artifact alone; port #569's nested-root and shared-root tests (#592 boundary)

The generated MCP entry and every hook wrapper derived the event runtime's
endpoint id from `<epoch>:<selection identity>:<root>`, so the composite
selection (`claude+codex`) had become runtime identity, and the runtime
took `events.target` as the tool-call lineage host fallback, reading the
selection as a host. Both are projection selection leaking into runtime
identity (#592 §2). The endpoint is now `<epoch>:<root>` on both sides
(entry-shell, hook-contract, `agent-bundle/test` installed harness), the
`target` field leaves `GeneratedRouteMcpEntryOptions` and
`GeneratedEventRuntimeBinding`, and the lineage fallback is the one host a
single-projection root serves, or none for a composite root; `entries.ts`
requires the selection instead of defaulting it from the composite name.

`AB4105` never fired for a skill: normalization gives every skill every
selected target and per-host frontmatter extensions collide as `AB4103`
instead, so the dead skill branch leaves `compose.ts` and the diagnostics,
reference, and framework-mode prose say so. `AGENT_BUNDLE_HOOK_HOST` leaves
the runtime-environment reference (en/zh) and the test env fixtures; `api.ts`
reuses `isBuiltInHost`; `compose.ts` reuses `sortedProjections`; stale
`<target>/…` doc comments in `test/packed.ts` and `routes/graph.ts` name the
root layout.

Ports from #569: install refuses `--from` naming a directory above the plugin
root for all three hosts (`AB7001`, no host CLI runs), doctor lists Claude
plugins from the root `--from` names and never from a nested `claude/`, and
the Codex validator judges only `.codex-plugin/*` in a root shared with
Claude's `.mcp.json` and `hooks/hooks.json`. Lane C's docs parity pass:
tree drawings gain `commands/` and `rules/`, the folder-discovery shield
names all three guarded paths, over-wide code samples re-padded to 90
columns, `AB4808`/`AB4809` prose and the rsc-agent-runtime README describe
one composite root.

* test(prepack): #588's prebuilt payload lands in the composite root (host-packs/runtime/…), not under a target partition

* changeset: the event runtime endpoint is the artifact's alone (#592 boundary)

* review: lineage fallback is the one host whose MCP document lists the server; document AB7001's composite-root trigger

Self-review pass 1 read the fallback as inferring the root's cardinality
from `allowedTargets`. It is `server.targets ∩ selected` — the hosts whose
MCP documents list the server, so the hosts that can have spawned it — and a
Claude-only server in a Claude+Codex root is correctly assumed to be Claude's.
The comment and the two test names now say so. `docs/diagnostics.md` names
`AB7001` in the `AB700x` family row: the host manifest sits directly under
`--from`, never under `<from>/<host>`.

* build: host the composite root's event runtime per selected host's first generated server; judge built-in hosts by adapter identity for the install surface (#578 review)

- planMcpEntriesSurface no longer attaches every event route to the first
  generated-route server: eventRuntimeHosting hosts the runtime in the first
  generated server each selected host's MCP document lists (one process when
  they agree, one per host otherwise) and every hosting server accepts the
  same allowed set; a Claude-only server in a Claude+Codex root no longer
  refuses Codex wrappers while Codex's own server hosts nothing.
- The generated entry carries `hosts` (the selected hosts whose documents
  list the server) separately from `allowedTargets`; the lineage fallback
  reads `hosts`, so a Claude-only server hosting a two-host runtime still
  assumes Claude for an anonymous MCP client (#592).
- TargetRegistry.builtInHost()/builtInHosts() judge the four shipped adapters
  by identity; compose and artifact validation use it for INSTALL.md and
  install.mjs, so an advanced registry's adapter named `portable` earns no
  install surface it never asked for. The pack inventory (manifest names
  only) keeps the name-based requirement it had on main.

* wip(manifest): artifact manifest v2 — projections, routes, executables, distribution; hook index folded into executables.hooks (#592 step 3)

* test(browser): mount each MCP App as one host of the composite selection, never as the selection identity

The browser pool compiles every app once for the project's whole selection
(as the build stages it, #555) but the registry's `target` — the preview
profile and the binding's `target` the page sees — is the host the app
mounts as: the override, or the app's first declared target the project
selects, as before. `claude+codex+portable` had leaked into the binding
(#592) and failed examples/mcp-app's browser-app suite in CI.

* test(manifest): retarget hook and MCP suites at the v2 artifact manifest

The hook-index sidecar is gone; these tests now assert executables.hooks
on agent-bundle.manifest.json and pass the required route graph into build().

* test: migrate manifest coverage to v2

* refactor: read install identity from artifact manifest

* feat: inspect authoritative artifact manifest

* docs: fold #599's per-code diagnostics rows into the composite root — AB4100 lists the four hosts, AB6023/AB6024 and AB7001 describe the one root

* feat(cli): resolve MCP --target from the artifact manifest

serve-app and mcp list|invoke|run default to the only projection that
runs the named server, and inspect reports a built-manifest summary
when one exists at the project's artifact output.

* manifest: routes.cli.routes may hold projected MCP tool routes (routes.mcpCommands)

* test(build-compose): TargetRegistry is constructed, keep the value import

* review: judge built-in hosts by adapter identity in AB4106 and --host-validation; the pack inventory expects exactly the manifested files

- NormalizationTargetRegistry.builtInHost? lets config/validate.ts refuse a
  custom adapter registered under a built-in host's name beside other targets
  (AB4106) the way compose and validate-artifact already judge it; registries
  that cannot tell still judge by name.
- validate --host-validation and build --host-validation select the shipped
  validators through registry.builtInHosts(), so a custom `claude` or
  `portable` adapter is held to no shipped host's contract.
- pack-inventory.ts no longer re-derives the install surface from manifest
  target names: every emitted file is manifested and AB6023/AB6024 already
  judged the surface by identity, so the pack expects what the manifest lists.
- Changeset names the browser pool's `target` option semantics.

* docs: AB4106 judges the shipped adapters by identity

* manifest: hooks[].routeId for event-route wrappers; cross-check CLI command route ids; inspect reads the CLI build root; drop unused imports

* workbench: hook playground rows are manifest hook rows (host, kind, routeId)

* test(workbench): hook client fixture carries manifest hook rows

* docs(manifest): agent-bundle.manifest.json v2 reference; consumers read the composite root through the manifest (#592 step 3, #555 W2/S3)

- New reference page website/docs/{en,zh}/reference/artifact-manifest.mdx
  (+ _meta.json): every section and field of the v2 manifest, who writes it,
  who reads it, the shipped JSON Schema and public reader exports, reserved
  keys not yet emitted, the versioning rule, one abbreviated example.
- install / uninstall / doctor: --from is the composite root; identity and the
  host plugin document come from application + projections[host]; AB7001
  reworded (cli.mdx, installation.mdx, package README).
- serve-app / mcp: --target optional, ambiguity names the choices; inspect
  --json gains output.manifest (cli.mdx, mcp.mdx).
- docs/diagnostics.md: AB7001 and AB60xx family rows; new "Artifact manifest
  coherence (AB6039–AB6040)" section; AB6018 reads executables.hooks[].
- Stale statements: agent-bundle.hooks.json sidecar removed from artifact
  trees and prose (index, project-structure, targets-artifacts, hooks,
  entry-conventions); targets rows -> projection rows.
- Changeset .changeset/592-authoritative-manifest.md (agent-bundle minor).

* feat(validate): AB6039/AB6040 manifest coherence lane over the v2 artifact manifest (#592 step 3)

New `src/build/validate-artifact-manifest.ts`, wired into `validateArtifact`
beside the hook and MCP coherence validators and gated on a parsed manifest
whose file table verified (no AB6004), so neither code fires on top of
AB6000/AB6001/AB6004 noise.

AB6039 (error) — manifest section coherence, what the parser cannot know
without the adapter registry or the tree:
- executables.bins[].path|worker, scripts[].path|worker,
  mcpServers[].entry.path|worker, mcpServers[].apps[].path must be direct
  files of the row's host layout (cliBin, scripts, mcpEntries, mcpApps);
  hooks[] stays with AB6018, which already holds it to hookWrappers.
- a route-generated server (routes.servers[] mode 'generated' with routes)
  whose executables.mcpServers[] row is not 'compiled' or carries another name.
- projections[host].documents.mcp / .hooks must name the document the host's
  runtime / hook contract reads; a row listing a host without an MCP runtime
  or without an MCP document.
- the host MCP document and the rows listing that host declare the same
  server names, each with the transport its row records.

AB6040 (error) — host document disagrees with the manifest identity:
- documents.plugin name/version vs application.name/version.
- documents.marketplace name vs projections[host].marketplace.name; a
  marketplace document the projection does not record (the parser already
  refuses the reverse).
- a host document that is not a strict JSON object cannot be proven.

Both are error severity because a consumer acting on the manifest would
install or launch something the tree does not contain.

Tests: tests/artifact-manifest-coherence.test.ts builds one composite root
(claude, codex, cursor, portable; compiled + command + remote MCP servers;
three marketplaces) and forges one disagreement per case with the manifest
re-serialized and the file table re-hashed, asserting exactly one code.

* fix: resolve MCP documents from artifact manifest

* docs(manifest): hooks[].routeId and projected CLI tool routes (en, zh)

* test: exercise host validation by adapter identity through validate --artifact and build --host-validation

The identity test validated the project root, which never enters host
validation; validate the built custom root as an artifact instead, and
build a custom adapter named claude with a runner spy that must not be
spawned. AB6024's row names the shipped cursor/portable adapters by
identity, as the validator judges.

* Ship a JSON Schema for agent-bundle.manifest.json (#592 step 3)

Add schemas/agent-bundle.manifest.schema.json (draft 2020-12, closed keys
at every level, $defs per manifest shape, "present exactly when" rules as
if/then/else) and publish it through package.json `files` and the
`./schemas/agent-bundle.manifest.schema.json` export.

src/build/manifest-schema.ts imports the JSON, deep-freezes it as
`artifactManifestSchema`, and compiles it once with Ajv 2020 strict mode
(strictRequired relaxed for the conditionals) behind
`validateArtifactManifestSchema(value): readonly string[]`; both are
exported from src/api.ts and src/index.ts. The Rslib bundle inlines the
JSON, so dist needs nothing from the packed schemas directory.

src/schemas/ajv-issues.ts now installs ajv-formats, accepts Ajv options,
and owns the shared `compareSchemaIssues` comparator (lifted from
agent-skills/contract.ts) plus `formatSchemaIssue`.

scripts/dist-freshness.mjs counts the schemas directory as an agent-bundle
build input, since the JSON is compiled into dist.

tests/manifest-schema.test.ts checks a populated and a minimal hand-built
manifest against both validators, sweeps every object for delete /
unknown-key / retype mutations asserting parser-schema agreement with a
two-entry documented allowlist, pins the parser-only rules (sorted arrays,
cross-references, digests, runtime floor, npm name and semver validity)
as accepted-by-schema, pins the schema-encoded rules as rejected by both,
and asserts $id, $schema, manifestVersion const, freezing, and the
package.json wiring.

* test: validate --artifact takes the project root too

* validate: manifest coherence judges MCP rows against the MCP lane's single document read; fixtures follow host layouts

* schema: hooks[].routeId, projected CLI tool routes; writer output validates against the shipped schema; changeset names #604

* doctor: AB7306 recovery names the composite root and manifest projection

* review fixes: reciprocal contract binding, ENOENT-only missing manifest, marketplace pointer existence, projection document pointers judged by the host contract, doctor proof over the built root, stale AB6018/AB7001 prose

* validator: judge document→row MCP coherence by built-in host identity; integration expectations for AB6039/AB6040 and the mcp run refusal

* manifest: projections[].builtInHost records the shipped adapter identity; install/doctor and the installed harness key on it, the validator cross-checks it (AB6039), inspect reports it

* test: mcp-probe fixture records the claude adapter identity

* fixtures record built-in identity; advanced-adapter MCP documents own servers the manifest never rowed; inspection projections in the workbench proof

* validator: the coherence lane reuses the contracts the target-contract lane fetched; no registry re-entry after evidence snapshots

* docs: architecture page describes manifest v2 (#597 follow-up); export ArtifactManifestRouteContract types; cli test imports at top

* feat(workbench): project manifest application explorer

* Prove artifact-manifest paths stay relocatable when the composite root moves.

The writer already emitted root-relative POSIX paths; the parser now also
rejects a Windows drive-letter prefix, and a real build proves the bytes
never encode the machine and every reader still works after rename.

* paths: one relocatable-path rule (core/paths isRelocatablePosixPath) shared by the manifest parser and the writer

* refactor(build): serialize manifest from compiler plans

* docs: describe authoritative manifest generation

* manifest: split operational compiler facts into compiler.recordVersion 1

Keep timings, cache keys, adapter revisions, source-input hashes, and other
run metadata out of the public artifact contract so a compiler refactor never
forces a manifestVersion bump.

* readers: consume manifest.compiler for operational facts

Move validator, pack inventory, eval, inspection, and the installed harness
onto compiler.project / provenance / adapters / agentSkills so consumer
surfaces keep reading only the public contract.

* docs: document the public contract vs compiler record

Restructure the artifact-manifest pages and the targets outline around
manifestVersion and compiler.recordVersion, and mention routes.contracts[]
in the authoritative-manifest changeset.

* test: read provenance and compiler.project after the manifest split

Leftover fixture helpers still typed adapter facts onto projections and
asserted files[].sourceInputs / manifest.project on the public contract.

* docs(architecture): no separate coherence pass; AB6010 carries adapter identity

* test: explorer fixture follows the compiler record split

* rstest: drop the deleted coherence test from the integration list

* docs(architecture): public contract vs compiler record; explorer projection

* Add inspect --artifact so a copied composite root is read through the manifest alone.

* fix: trust manifest inventory for installs

* docs: describe authoritative install inventory

* manifest: MCP/hooks pointers come from the adapter runtime and hook contracts; explorer tolerates route-less servers; tests follow the compiler split

* deslop: reuse errorMessage from core/errors, drop dead projectionFor export, fix orphaned doc comment

- manifest-file.ts: replace private describe helper with the existing
  errorMessage from core/errors.ts (helper-before-writing rule)
- manifest.ts: delete projectionFor, exported with no importer anywhere
  (the coherence test defines its own throwing variant)
- manifest-routes.ts: move the artifactRoutesFor doc comment off
  artifactRouteContractFor, where it was stranded as a second docblock

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

* fix: reindex derived artifact variants

* docs: explain variant manifest reindexing

* deslop: drop type-bypassing casts, restating comments, and repetitive changeset prose

* manifest: carry the web section through v2 — schema, referenced-path rule, doctor reads it from the parsed manifest

* tests: preflight artifact graph reads hook rows from the manifest, not the removed sidecar

* tests: compiler-evidence fixture supplies the route graph the manifest writer requires

* review: doctor --from keeps AB7001 from the identity reader; mcp run launches the host document's own line, cross-checked against the manifest entry; relocatable proof covers web.apps[].entry

* tests(packed): provenance lives on compiler.provenance; the packed MCP fixture declares its portable projection identity and server row

* Address second self-review: keep AB7001 in doctor, launch host MCP lines verbatim, reject drive paths in schema, cover web entries in relocation proof

* feat(manifest): serialize definePrebuilt payloads as distribution.payloads[] (#630 absorbed)

* chore: deslop merge delta (gpt-5.6-sol-medium, 1 edit)

* Address merge-delta review: payload host-filtering proof, transport on inspection MCP rows, index-based payload location, diagnostics prose

* web-host: read declared projections from manifest v2 projections[].host (dev /web route 404 after #628 merge)

* feat(manifest): record event route execution

* Pin a strict closed-key inventory for manifest v2 and surface payload runtimeDependencies on inspect.

An old closed reader rejects any unknown key, so adding an optional public field is not compatible — the inventory fixture fails until the version bumps, and artifact-only consumers read packages from distribution.payloads[].runtimeDependencies.

* test: add combined manifest proof

* manifest-key-inventory: merge narrowed row properties over the shared $def; regenerate v2 inventory with routes.events[].execution

* combined proof: assert routes.events[].execution from the built manifest

* docs: readers refuse any other manifestVersion in either direction

* manifest: one launch record for compiled MCP servers

`executables.mcpServers[]` rows of kind `compiled` carry `launch`
({ args, entry, env, worker? }) in place of `entry`; `args[]` records the
author's declaration as `artifact` root-relative paths (plugin-root-anchored)
or `literal` values whose tokens the launcher expands. `web.apps[]` drops its
copied `entry`/`args`/`env` and names the compiled server instead; the parser
cross-checks the reference, the files[] rows, and artifact arguments.

The launch types live in `web-host/manifest.ts` (bundled into every plugin
bin) and `build/manifest.ts` imports them; `readWebManifestDocument` returns
`{ hosts, launches, web? }` so `<plugin> web` resolves the App's launch
through the record. Schema, docs (en+zh), changeset clause, and the packed,
relocatable, and unit proofs follow.

* combined proof: assert executables.mcpServers[].launch; bare plugin-data token

* manifest: prebuilt MCP servers carry the same launch record (kind 'prebuilt'), so web Apps on definePrebuilt servers keep working through one record

* deslop: 5 edits

* Honor manifest launch records in mcp run fallback; web reader requires manifestVersion 2; pin proof wording; prebuilt args/env launch coverage

* Rewire the read-only state-root proof to the manifest launch record

* Remove the prebuilt launch test's temporary home

* Anchor the manifest-only mcp run fallback on the durable plugin root; document the lean web reader and optional web key

* Document the lean web reader as the one non-parser consumer

* Locate the declared state root through the installed manifest's MCP pointer; admit the compile evidence record

* Deslop the state-root rewire; state the inherited state-root fallback

* fix(manifest): lean web reader rejects unsupported versions, duplicate server identities, malformed launch and projection rows; reindex refuses compiled files and compile evidence

* test(workbench): avoid terminal close locator race

* test(manifest): combined proof checks compile evidence survives reindex and install

* chore: deslop pass over the reader/reindex delta

* fix(manifest): launch records name indexed bytes only in both readers; copies re-measure to the verified inventory

* test(manifest): match server-name diagnostics

* fix(manifest): the runtime-owned state root is reserved everywhere — AB4741 for payloads, files[] parser and schema, installers

* fix(manifest): one files[] path rule for both readers — never the manifest, the runtime state root, or the install receipt entry

* chore: deslop the post-review reader/evidence delta

---------

Co-authored-by: Ubuntu <zack@ubuntu-main.local>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant