Skip to content

fix(build): byte-reproducible artifacts — root the generated-module namespace at the project root - #518

Merged
ScriptedAlchemy merged 2 commits into
mainfrom
fix/build-determinism
Sep 4, 2026
Merged

fix(build): byte-reproducible artifacts — root the generated-module namespace at the project root#518
ScriptedAlchemy merged 2 commits into
mainfrom
fix/build-determinism

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #503, which noted that two consecutive main builds of one source differed in the MCP entries' sha256 because of a per-build .artifact.stage-XXXXXX token. This PR removes that token from the emitted bytes at its source, proves byte-reproducibility on both examples with a double-build diff, and pins it with a build-pool test.

Root cause

Every compiled surface imports generated modules that are served from memory through Rspack's experiments.VirtualModulesPlugin: the wrapper entry, the route registry (agent-bundle/generated-route-server, agent-bundle/mcp-apps), and the identity module agent-bundle/meta. Their virtual paths were keyed under the staged output root: <project>/.<output>.stage-XXXXXX/<target>/.agent-bundle-virtual/<entry>-<n>.mjs (src/build/rslib.ts, src/build/meta.ts, src/build/mcp-apps.ts).

Rspack derives the readable identifier of a module from its path relative to the bundler context (the project root) and writes it into the bundle for every concatenated module that needs a namespace object — the // NAMESPACE OBJECT: ./… comment. The generated route registry of a routed MCP server is imported as a namespace by the generated wrapper, so every routed MCP entry contained

// NAMESPACE OBJECT: ./.artifact.stage-81CIRA/claude/.agent-bundle-virtual/mcp-host-test-0d229f1b-1.mjs

and that line changed with every mkdtemp. output.pathinfo is already off (no CONCATENATED MODULE/EXTERNAL MODULE comments are emitted); the namespace-object comment is emitted unconditionally by the concatenation codegen, so the fix has to be at the path, not at an Rspack option. No Rspack upgrade or option change was needed.

Fix

The reserved generated-module namespace now hangs off the project root (generatedModulesRoot(projectRoot) = <project>/.agent-bundle-virtual/, src/build/meta.ts), independent of the output root and of the staging directory. composeEntryLibConfig and composeMcpAppsRsbuildConfig take the project root (cwd) alongside the output root; assertExecutableConfig and the provenance ignoredSourcePaths follow. The emitted identifier is now ./.agent-bundle-virtual/mcp-host-test-0d229f1b-1.mjs — the same on every build, in every output directory, on every machine.

Nothing is ever written under that path: Rspack keeps one virtual file store per compiler (VFILES_BY_COMPILER WeakMap → the native store), so the multi-compiler's environments and the sequential per-target runs can share one namespace without interference, exactly as they already shared meta.mjs. inspect --bundler shows the same project-rooted paths in each entry's aliases and generated entry (it already reported absolute source paths; nothing new is redacted).

Proof (double build, byte-for-byte)

Local, pnpm exec agent-bundle build --output <dir> twice from the same unchanged source, each build into a different output directory, the first output moved out of the project between builds so the second build's source snapshot is identical to the first's (otherwise the first build's output joins the second's sourceInputs and shifts project.revision — a test-setup effect, not a build one):

Example Before (main e3473e61a) After
examples/host-test (4 targets) 46 files differ: agent-bundle.manifest.json, all 4 routed MCP entries (staging token in NAMESPACE OBJECT), and 41 hook wrappers (whose embedded artifactEpoch follows the manifest revision) diff -r of artifact/**: identical
examples/audiobook-curator (4 targets, MCP App, package build) diff -r of artifact/**: identical; artifact/** + dist/** with one output root built twice: identical

With two different output roots, dist/bin/audiobook-curator-install.js differs in exactly one line — artifactRoot: new URL("../../<output>/artifact/", import.meta.url) — which is the installer's relative pointer to the artifact it installs, a function of --output by design, not a leak.

Leak sweep over the emitted artifacts (grep -rE "/fast/|/tmp/|\.artifact\.stage|\.det-"): no absolute build-machine paths, no temp directories, no staging tokens. The remaining relative identifiers (./src/mcp/..., ../../node_modules/.pnpm/effect@4.0.0-rc.112/...) are Rslib's named module ids relative to the project root, stable for one lockfile and workspace layout; they were not changed here.

Test

packages/agent-bundle/tests/build-reproducibility.test.ts (added to the build integration pool in rstest.integration-tests.ts): builds a project with a routed MCP server, an event route for three hosts, a routed CLI, and a bundled script twice into two output directories through agent-bundle/api's build, and asserts identical agent-bundle.manifest.json text, identical per-file SHA-256 for every emitted file, that the manifest digests match the bytes, that no bundle names the project root, the parked output root, .artifact.stage-, or either build's .<output>.stage- token, and that the MCP entry's namespace comment reads ./.agent-bundle-virtual/mcp-harness-XXXXXXXX-N.mjs. Verified to fail against the unfixed src (manifests differ) and pass with the fix. Stubbed-Rslib tests in hooks.test.ts, target-stages.test.ts, and compose-layers.test.ts now key the expected aliases on the project root.

Locally: unit 3177/3177; integration build, build-reproducibility, hooks, mcp, package-build, cli-routes-build, layout-build, api, plugin-bundle, dev-package-build, cli, artifact-validator (280/280); pnpm lint, pnpm typecheck, pnpm docs:site:build green.

Docs

docs/framework-mode.md ("How a target compiles": reproducibility and the project-rooted namespace), website/docs/{en,zh}/guide/distribution/index.mdx (same, user-facing). Changeset: .changeset/build-reproducible-artifacts.md (patch).

Review status

Per the maintainer's instruction this PR carries no comments; review threads are answered here.

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b22943d

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

…o artifacts are byte-reproducible

Rspack writes module identifiers relative to the bundler context into the
emitted bundles (the // NAMESPACE OBJECT comments of concatenated modules).
The generated wrapper, route registry, and agent-bundle/meta modules were
served under the per-build staging root (.<output>.stage-XXXXXX/<target>/
.agent-bundle-virtual/), so every routed MCP entry carried the staging token
and two builds of one source differed in those files' sha256 and in the
manifest. The namespace now hangs off the project root, independent of the
output root, and a build-pool test builds one project twice into two output
directories and asserts identical manifests and bytes.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 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-04T03:44:02.228294Z 7377e52 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.

@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: 7377e52d05

ℹ️ 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 packages/agent-bundle/src/build/meta.ts
@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@518
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@518
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@518

commit: b22943d

The virtual paths under .agent-bundle-virtual/ are predictable, so an
authored file at one of them would be shadowed by the generated module it
names, or compile as an entry from generated source. Refuse to compile
while anything occupies the directory, as the fresh staging root once
made impossible.
@ScriptedAlchemy
ScriptedAlchemy merged commit 8f90c53 into main Sep 4, 2026
13 checks passed
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
…istry test

#518 made cwd required on composeEntryLibConfig and
composeMcpAppsRsbuildConfig; #509's framework-plugin-registration test
landed beside it without the argument, so main no longer typechecks.
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
…istry test (#523)

#518 made cwd required on composeEntryLibConfig and
composeMcpAppsRsbuildConfig; #509's framework-plugin-registration test
landed beside it without the argument, so main no longer typechecks.
@ScriptedAlchemy
ScriptedAlchemy deleted the fix/build-determinism branch September 4, 2026 20:02
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