Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/package-build-ab6005.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Hold the npm package build's `dist` JavaScript to the same self-containment rule as the host packs: `agent-bundle build` and `agent-bundle prepack` now walk every emitted `dist/**/*.js` and `*.mjs` bundle (`dist/bin/<name>.js`, its rendered-route Flight worker, the generated install bin, and the `lib` entry) as an ES module and fail with `AB6005` on any bare import specifier that is not a Node built-in — including an import kept external through the `tools` escape hatch, which previously survived in `dist` and was judged only by the prepack gate. The diagnostic names the file as `dist/<path>` and the specifier; a literal dynamic `import()` counts like a static import; `node:` and bare built-in imports, relative imports of emitted files, `import.meta`, and `createRequire` of packed files stay valid, and `.d.ts` output is not walked. A dependency that only a compiled bundle imports is therefore an `AB6005` build failure before the pack inventory runs, never an `AB7014` finding; `AB7014`'s recovery now names what still keeps a runtime dependency — a prebuilt payload module that imports it, a packed file that requires or resolves it, a packed declaration that references it, or an install script or packed file that runs it. (#588)
41 changes: 25 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,37 @@
every dependency of a generated executable — `output.autoExternal: false`,
`bundle: true`, `splitChunks: false`, no `externals`. Rslib's `node` target
leaves only Node built-ins (and `pnpapi`) external, and the only bare
specifiers `AB6005` accepts in a host-pack module are Node built-ins, so a
generated executable in a host pack loads nothing else from outside the
artifact. MCP App views
(`src/build/mcp-apps.ts`) inline every script and style into one HTML file.
The framework never adds `externals` to a plugin build; the `externals`
handling in `rslib.ts` (`reservedExternalsViolation`,
import specifiers `AB6005` accepts in a host-pack module are Node built-ins. The
package build's `dist` bundles are walked by the same `AB6005` rule
(`src/build/package-build.ts` reuses `validateJavaScriptModules` from
`src/build/validate-artifact-modules.ts`), so a generated executable in a
host pack or in `dist` imports nothing but Node built-ins from outside its
tree. The walk reads import specifiers, static and literal dynamic; a
`createRequire(…)(…)` or `import.meta.resolve(…)` call is not an import and
is outside `AB6005` in either output — the prepack gate reads those calls
as dependency evidence. MCP App views (`src/build/mcp-apps.ts`) inline every script and style
into one HTML file. The framework never adds `externals` to a plugin build;
the `externals` handling in `rslib.ts` (`reservedExternalsViolation`,
`guardReservedExternals`) only rejects reserved specifiers in the resolved
externals, which come from the author's `tools` hatch and Rslib's built-in
list, never from the profile.
- No refactor, toolchain upgrade, or "leaner install" change may enable
`autoExternal` or externalize a dependency on the author's behalf. A package
a consumer must install is the author's explicit decision — an import kept
external through the `tools` hatch, a packed declaration reference, an
install script — and the prepack gate judges it: `AB7014` demands
packed-file evidence of use, `AB7015` a specifier a consumer's npm can
install.
a consumer must install is the author's explicit decision, and an import
kept external through the `tools` hatch is not a way to make it anywhere:
`AB6005` fails such an import in a host pack and in `dist` alike. What
legitimately puts a package under `dependencies` is a packed declaration
reference, a prebuilt payload module that imports it, an install script,
or a `bin` command packed JavaScript runs — and the prepack gate judges
those: `AB7014` demands that evidence, `AB7015` a specifier a consumer's
npm can install.
- Proof is bytes and processes, not config: every artifact build walks the
compiled host-pack modules (`AB6005` fails a bare package specifier there;
the package build's `dist` is judged by the prepack gate instead), and the
packed pool (`pnpm test:packed`) installs the packed tarball into a clean
consumer, builds, removes the project source, and spawns the generated
entry as a real process (`packed-deleted-source`).
compiled host-pack modules and every package build walks its emitted `dist`
bundles (`AB6005` fails a bare package specifier in either), the prepack
gate then judges what remains declared, and the packed pool
(`pnpm test:packed`) installs the packed tarball into a clean consumer,
builds, removes the project source, and spawns the generated entry as a
real process (`packed-deleted-source`).

## Documentation site

Expand Down
11 changes: 8 additions & 3 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ even when no error diagnostic was reported.
| `AB490x`/`AB492x` | Conventional host components (#100 stage 2): rules `src/rules/*.mdc` (`AB4900`–`AB4908`) and commands `src/commands/*.md` (`AB4920`–`AB4928`), including per-host feature-set enforcement (`AB4907`/`AB4908`, `AB4927`/`AB4928`); see below. |
| `AB48xx`/`AB494x` | Route graph, state, layout (`AB4830`–`AB4832`), generated route declarations outside the TypeScript program (`AB4834`), route render budgets (`AB4835`), tool task support (`AB4836`), a route module that value-imports a compiler-carrying framework entry (`AB4837`), and provider conventions (see below). |
| `AB5000` | General CLI and adapter failures. |
| `AB60xx` | Built-artifact validation, including schema documents and referenced files (`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`: an emitted JavaScript module — a host-pack module or a package build `dist` bundle (`dist/bin/*.js`, the Flight workers, the `lib` entry), prebuilt payloads excepted — has an import that is neither a Node built-in nor a relative or `file:` specifier resolving to a listed regular file inside its tree, or a non-literal dynamic import; a `dist` finding names `dist/<path>`; `AB6011`/`AB6012`: a target's required pinned-schema document is missing or invalid; `AB6025`: a manifest-declared `logo` path is missing from the artifact or escapes the deploy tree; `AB6034`: emitted Skill Markdown has no instruction body; `AB6035`–`AB6038`: Agent Plugins portable validation, see below). |
| `AB700x` | Host installation and uninstallation: bundle identity, host availability, scope, command failure, and collision checks (`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). |
| `AB7200`–`AB7202`, `AB7210`–`AB7211` | Development rebuilds and live host surfaces: rebuild admission and phase failures, development host install sync, and the dev-epoch contract gate (see below). |
Expand Down Expand Up @@ -187,10 +187,15 @@ Validation happens at three moments, all fail-closed:
| `AB7011` | An on-disk artifact file no longer matches its manifest SHA-256. Rebuild and do not modify generated host packs. |
| `AB7012` | A `package.json` bin points outside the packed `dist` output (including `src/`) or names a file npm omitted. Point it at the generated `dist/bin` file. |
| `AB7013` | `package.json`, normalized plugin metadata, a host manifest, or artifact provenance reports a different release version. Make every release identity agree. |
| `AB7014` | A `package.json` `dependencies`, `optionalDependencies`, or `peerDependencies` field names packages nothing in the pack uses: no packed JavaScript imports, requires, or resolves them, or runs one of their `bin` commands, no packed declaration file references them, no `#subpath` import reaches them through the manifest's `imports` map, and no consumer-side install script (or script it delegates to) runs them (one diagnostic per field; the full evidence list follows this table). Peers `peerDependenciesMeta` marks optional are never installed and are not inspected here (their specifier is still checked by `AB7015`), and a name under both `dependencies` and `optionalDependencies` is judged by its optional entry, which npm lets override. The build inlines every dependency into `dist/bin` and the host packs, so such an entry only makes every consumer's `npm install` fetch build-time packages. Move them to `devDependencies`, or import the package from a packed module if a consumer really needs it at runtime. For `peerDependencies` the diagnostic is a warning: a required peer nothing imports may be a deliberate compatibility contract with the host that loads the package, though npm 7+ still installs it for every consumer — keep it, mark it optional in `peerDependenciesMeta`, or move a build-only package to `devDependencies`. |
| `AB7014` | A `package.json` `dependencies`, `optionalDependencies`, or `peerDependencies` field names packages nothing in the pack uses: no packed JavaScript imports, requires, or resolves them, or runs one of their `bin` commands, no packed declaration file references them, no `#subpath` import reaches them through the manifest's `imports` map, and no consumer-side install script (or script it delegates to) runs them (one diagnostic per field; the full evidence list follows this table). Peers `peerDependenciesMeta` marks optional are never installed and are not inspected here (their specifier is still checked by `AB7015`), and a name under both `dependencies` and `optionalDependencies` is judged by its optional entry, which npm lets override. The build inlines every dependency into `dist` and the host packs, and `AB6005` fails a compiled bundle that imports a bare specifier other than a Node built-in (`prepack` runs that build before this inventory), so a compiled bundle can never supply `import` evidence here and such an entry only makes every consumer's `npm install` fetch build-time packages; the packed-JavaScript `import` evidence class is for modules the framework copied rather than compiled — prebuilt payload modules and other packed scripts — while `require`, `createRequire`, and `import.meta.resolve` calls, which `AB6005` does not walk, count from any packed file. Move build-only packages to `devDependencies`; keep a runtime dependency only for what a prebuilt payload module imports, a packed file requires or resolves, a packed declaration references, a `#subpath` import reaches, or an install script or packed file runs. For `peerDependencies` the diagnostic is a warning: a required peer nothing imports may be a deliberate compatibility contract with the host that loads the package, though npm 7+ still installs it for every consumer — keep it, mark it optional in `peerDependenciesMeta`, or move a build-only package to `devDependencies`. |
| `AB7015` | A `package.json` `dependencies`, `optionalDependencies`, or `peerDependencies` entry that a consumer's npm cannot resolve through a registry. Each entry — name and specifier together, the value exactly as written (a leading space makes `" npm:bar@1"` an invalid dist-tag, not an alias) — is read with `npm-package-arg`, the parser npm, Arborist, and pacote share, so the verdict is npm's own rather than an imitation of its grammar: **registry** (a version, range, or dist-tag, or an `npm:` alias of one — the only kind a published package can rely on), **fetched** (parseable, but a `git`/`github:`/`gitlab:`/`bitbucket:`/`gist:` source or `owner/repo` shorthand, an `http(s):` tarball, or a `file:`/relative/bare path or tarball filename — npm 12 refuses git and remote fetches by default (`allow-git=none`, `allow-remote=none`) and a path never exists on the consumer's disk), or **unparseable** (npm rejects the manifest before fetching anything: `EINVALIDPACKAGENAME` for a name such as `bad name`, `.hidden`, or `node_modules`; `EUNSUPPORTEDPROTOCOL` for `link:`, `portal:`, `jsr:`, a `git+` transport npm lacks, or a typo; `EINVALIDTAGNAME` for a selector that is neither a range nor a URL-safe dist-tag, such as `"not a valid spec"`; an `npm:` alias without a name or with a non-registry target, since aliases only work for registry dependencies; or an invalid URL such as `http:%zz`). A fetched specifier is reported on installed entries only; an unparseable one is reported on every entry, even an optional peer npm would never install, because the manifest read itself fails. A peer that `dependencies` or `optionalDependencies` also names is judged by that concrete entry alone: npm resolves the concrete declaration and never reads the duplicate peer's selector. For an `optionalDependencies` entry that is fetched, the diagnostic is a warning, not an error (`agent-bundle prepack` prints it and exits 0): npm continues an install without such a dependency, but every consumer still tries and fails to fetch it. It stays an error when the entry is unparseable, or when a consumer-side install script needs the skipped package — runs one of its `bin` commands in command position (`setup-tool --init`, `npx setup-tool`, `cross-env CI=1 setup-tool`, `./node_modules/.bin/setup-tool`; a mention elsewhere, `echo setup-tool`, proves nothing), runs one of its files (`node node_modules/setup-tool/install.js`), loads it from an inline program (`node -e "require('setup-tool')"`, `node --input-type=module -e "await import('setup-tool')"`, also `-p`, `-pe`, `--eval=…`, `--print=…`; the program is read as a packed file is — `require`, `createRequire`, and `import()` — and a computed load there, or a program the lexer rejects, may need any declared package), preloads it (`node -r setup-tool/register install.js`; `-r`/`--require`, `--import`, `--loader`/`--experimental-loader`, with a space or `=` before the module — read as Node does, `node [options] script [arguments]`: options end at the first positional (the script, or an argument when `-e`/`-p` supply the program) or a `--`, valued options such as `--conditions x` or `--env-file x` taking their word with them, so `node install.js --require x` passes `--require x` to `install.js` and preloads nothing; a `NODE_OPTIONS` assignment on the same command — `NODE_OPTIONS=--require=setup-tool/register node install.js`, `cross-env NODE_OPTIONS="-r setup-tool/register" node .` — supplies options Node applies before the command line's, while one `export`ed by an earlier command is not read), or runs a packed file (`node install.cjs`, `node scripts/install` resolving `scripts/install.js`, `node "scripts/my install.cjs"`, `node install.js&&echo done`, `node .` or `node ./` running the root `main`, `node --import ./setup.mjs .` running a packed preload) that imports it — every word of the script that names a packed JavaScript file counts as run, deliberately, so that runners this gate does not model (`tsx`, `ts-node`, `zx`, `bun`, `deno run`, `npx <runner>`) still have the dependencies their file loads traced; the cost is a rare escalation for a word that names a packed file without running it (`echo install.js`), which the diagnostic makes visible by naming the file — directly, through relative imports inside the tarball (`require("./lib")` following `lib/package.json`'s `main` before `lib/index.js`, as Node does), or through the `imports` map resolved as Node does (`"#setup": "./setup.js"`; `#setup/foo` through `"#setup/*": "./scripts/*.js"`, a preloaded `#setup` included): npm continues past the failed fetch, then the script fails on the missing command or module. Each command of a script is read on its own: after a shell operator (`&&`, `;`, and the rest) or a newline — the second line of a script, and each lifecycle script after the first, starts a new command — and Node's options belong to `node` alone (`rm -r dist` preloads nothing, `npm --prefix . run setup` runs no `main`). Depend on a published registry version, or bundle the package and declare it under `devDependencies`. Entries the tarball itself carries are never reported, since a consumer does not fetch them: `bundleDependencies` (by name or `true`; never a peer, which npm cannot bundle; only when the pack inventory contains `node_modules/<name>/package.json`, since npm silently packs nothing for a bundled name absent from `node_modules`), and a `file:` or bare path inside the package (`file:vendor/foo`, `file:vendor/foo.tgz`) whose packed source npm can install from — a directory whose packed `package.json` parses to an object, or a packed tarball (gzipped or plain tar, ustar headers with valid checksums and payloads inside the archive) whose `<dir>/package.json` entry parses to an object — since npm installs it from the consumer's own copy. A path that escapes the package (`file:../sibling`), whose source is not packed, or whose packed source is not installable (a `.tgz` that is not an archive, or is malformed or truncated, fails the consumer's install with `TAR_BAD_ARCHIVE`; a manifest that does not parse, on disk or inside the archive, fails it with `EJSONPARSE`) is reported. `workspace:` and `catalog:` count as registry specifiers only when the `prepack` lifecycle runs under pnpm, Yarn, or Bun (`npm_config_user_agent`), which rewrite them in the tarball they pack; `npm publish` publishes them verbatim and consumers fail with `EUNSUPPORTEDPROTOCOL`, so under npm — or when `agent-bundle prepack` runs outside any package-manager lifecycle — they are reported. The `npm pack --dry-run` that `prepack` itself spawns is only the file inventory; the tarball consumers receive is the lifecycle's packer's, which is what the user agent identifies. |

The dependency evidence is read from the packed bytes themselves: every `.js`/`.mjs`/`.cjs` file
Compiled bundles reach this gate without bare imports: `prepack` builds before it packs, and `AB6005`
fails any `dist` bundle or host-pack module whose import is neither a Node built-in nor a listed file inside
its tree, so the lexed `import` evidence below describes prebuilt payload modules and other packed scripts
the framework did not compile. `AB6005` walks import specifiers only; a `require`, `createRequire(…)(…)`, or
`import.meta.resolve(…)` call is not an import, so that 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
`npm pack --dry-run` lists is lexed for static and dynamic `import` specifiers and scanned for
literal `require("…")` and `.resolve("…")` calls (`require.resolve`, `createRequire(…).resolve`,
`import.meta.resolve`: a package located only to find an asset is still a runtime dependency; a
Expand Down
Loading
Loading