Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/typed-routes-by-default-and-plugin-collision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"agent-bundle": patch
"create-agent-bundle": patch
---

Make the generated `.agent-bundle/routes.d.ts` part of the TypeScript program by default and reject duplicated framework plugins. `create-agent-bundle` templates list `".agent-bundle/routes.d.ts"` in `tsconfig.json` `include` (the file stays gitignored), so `renderRoute` / `renderRouteEvents` type-check route ids, `input`, and `result` from the first build instead of degrading to `string` / `unknown` until the include is discovered in the docs; `agent-bundle validate` warns with `AB4834` when a project that compiles routes or providers has a root `tsconfig.json` whose program (resolved like `tsc -p`, including `extends` and one level of project `references`) leaves the published declaration out. `agent-bundle validate` (and every diagnostic-gated command) rejects a `tools.rsbuild.plugins` entry whose `name` matches a plugin the framework already registers (`rsbuild:react` from `@rsbuild/plugin-react`) with `AB4724`, because `plugins` arrays concatenate and Rsbuild never dedupes plugins by name, so the plugin would otherwise run twice. (#497)
52 changes: 48 additions & 4 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ even when no error diagnostic was reported.
| `AB46xx` | Assets and the generated-runtime floor. |
| `AB470x` | Package build `bin` configuration (`AB4706`: artifact output overlaps `dist`; `AB4707`–`AB4709`: `output.distPath` shape, root escape, reserved namespace). |
| `AB471x` | Package build `lib` configuration (`AB4710`–`AB4715`) and declaration generation (`AB4716`; see below). |
| `AB472x` | The `tools.rsbuild` / `tools.rspack` escape hatch. |
| `AB472x` | The `tools.rsbuild` / `tools.rspack` escape hatch (`AB4720`–`AB4723`: shape; `AB4724`: a framework-owned Rsbuild plugin re-added through `tools.rsbuild.plugins`; see below). |
| `AB473x` | Migration nudges (informational; see below). |
| `AB474x`/`AB4750` | Prebuilt payloads and prebuilt entries (see below). |
| `AB4760` | The published `agent-bundle/meta` identity module evaluated outside every compiled surface and outside the Rstest presets (see below). |
| `AB4765`–`AB4766` | Artifact-hosted routed CLI: a target without the `cli` capability omits `bin/<name>.mjs`; a host-emitted file collides with it (see below). |
| `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`), and provider conventions (see below). |
| `AB48xx`/`AB494x` | Route graph, state, layout (`AB4830`–`AB4832`), generated route declarations outside the TypeScript program (`AB4834`), 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). |
| `AB700x` | Host installation: 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). |
Expand Down Expand Up @@ -521,7 +521,38 @@ above, never per feature. Skills keep their own closed per-host schemas
| `AB4927` | error | A command explicitly targets a host that supports commands but whose `commands.<field>` row for a frontmatter field the command uses is `degraded`, `unavailable`, or `prohibited` (the message carries the host's reason). Cursor's pinned commands surface is frontmatter-free Markdown, so every field row is unavailable there. | Remove the field or drop that host from the command's `targets`. |
| `AB4928` | warning | An implicitly selected host supports commands but cannot express a frontmatter field the command uses; the command ships there without it (Cursor receives the prompt body only). | Accept the omission, restrict the command's `targets` to hosts that support the field, or remove the field. |

## Route graph, state, layout, and provider conventions (`AB4800`–`AB4833`, `AB4940`–`AB4942`)
## The bundler escape hatch (`AB4720`–`AB4724`)

`tools.rsbuild` and `tools.rspack` are validated with the rest of the config
source, so a malformed or colliding hatch is an **error** before any bundler
runs — in `validate`, `build`, `inspect`, and `dev` alike. `AB4720`–`AB4723`
check the shape: `tools` must be an object whose only keys are `rsbuild`
(an Rsbuild environment-config object) and `rspack` (an Rspack config
object, a mutator function, or an array of both).

`AB4724` checks `tools.rsbuild.plugins` against the Rsbuild plugins the
framework registers itself — currently `@rsbuild/plugin-react`
(`rsbuild:react`), which every synthesized Rslib entry and every React-syntax
MCP App view carries. The hatch merges *beside* the framework profile
(`mergeRslibConfig` / `mergeRsbuildConfig` concatenate `plugins` arrays), and
Rsbuild's plugin manager appends every plugin it is handed without deduping by
name, so re-adding `pluginReact()` would register it twice. The check is
static: plugin objects are matched by `name`, nested arrays are flattened the
way Rsbuild flattens them, `false`/`null`/`undefined` holes are skipped, and a
plugin supplied as a Promise is not inspected. It is an error rather than a
warning for the same reason as its siblings: a config problem with one
deterministic fix, reported once at the source, so no build ever runs a
framework-owned plugin twice by accident.

| Code | Severity | Trigger | Recovery |
| --- | --- | --- | --- |
| `AB4720` | error | `tools` is not an object. | Declare `tools: { rsbuild?, rspack? }`. |
| `AB4721` | error | `tools` carries a key other than `rsbuild` or `rspack`. | Remove the key; the hatch has exactly two fragments. |
| `AB4722` | error | `tools.rsbuild` is not an Rsbuild environment-config object. | Declare an object fragment. |
| `AB4723` | error | `tools.rspack` is not an Rspack config object, a mutator function, or an array of both. | Use one of the three Rslib `tools.rspack` forms. |
| `AB4724` | error | `tools.rsbuild.plugins` supplies a plugin whose `name` matches a framework-owned registration (`rsbuild:react` from `@rsbuild/plugin-react`). The message names the plugin and its package. | Remove the plugin from `tools.rsbuild.plugins`; agent-bundle registers it in every config it synthesizes. |

## Route graph, state, layout, and provider conventions (`AB4800`–`AB4834`, `AB4940`–`AB4942`)

The route-graph compiler discovers conventional route modules
(`src/mcp/<server>/{tools,resources,prompts,apps}/*`, `src/events/*/*`,
Expand Down Expand Up @@ -606,7 +637,19 @@ order — and augments `@agent-bundle/runtime`'s `AgentProviderValues` so
`(await agent()).providers.<key>` observes that type in projects whose
TypeScript program includes the file. Provider-free graphs emit no
augmentation, so the declaration never references a module the project has no
reason to depend on.
reason to depend on. The file is only as good as the program that compiles
it: `create-agent-bundle` templates and the `examples/*` projects list
`".agent-bundle/routes.d.ts"` in `tsconfig.json` `include` (a literal entry,
because `**/*` never descends into dot-directories), while the file itself
stays gitignored. After publishing the declaration, `agent-bundle validate`
resolves the root `tsconfig.json` program the way `tsc -p` does — `extends`,
`files`, `include`, `exclude`, and one level of `references` for a
solution-style root — and reports `AB4834` (a **warning**, surfaced by
`validate` only) when the published file is not among its root files. A
project with no root `tsconfig.json`, no published declaration (route-free
and provider-free), or a `tsconfig.json` TypeScript cannot parse gets no
diagnostic: there is no program to be missing from, or `tsc` already
reports the parse failure itself.

Conventional `src/scripts/` routes ship through the same pipeline as
explicit `scripts` entries (#102 stage 1): a plain module directly under
Expand Down Expand Up @@ -717,6 +760,7 @@ schema constants), unions, nested objects, transforms, coercions — raises
| `AB4831` | error | Two layout modules declare one layout scope (for example `src/layout.ts` beside `src/layout.tsx`). Keep exactly one module per scope. |
| `AB4832` | error | A server layout (`src/mcp/<server>/layout.*`) names an MCP server that declares no tool, resource, or prompt route modules — the server directory is missing or holds only `apps/` routes, which never take a layout. Add routes under that server directory, move the layout, or rename it `_layout.*` to opt out. A server pinned to `custom`, `command`, or `remote` via `routes.servers.<server>` is skipped entirely: its layout is neither validated (`AB4830`) nor retained, because no generated worker composes it. |
| `AB4833` | error | `notices.retention` is malformed: `notices` or `retention` is not an object, carries an unknown key, `terminalTtl` is not a positive integer of milliseconds or a duration such as `"7d"`, `"12h"`, `"30m"`, or `"90s"`, `maxTerminal` / `maxJournalBytes` is not a positive integer — or the policy is declared by a project without a conventional `src/state.ts`, which has no co-mounted notice ledger to retain. Omit a field to keep the runtime default (`7d`, `500`, `16777216`). |
| `AB4834` | warning | `agent-bundle validate` published `.agent-bundle/routes.d.ts` (the project compiles routes or providers) but the root `tsconfig.json` program — resolved like `tsc -p`, including `extends` and one level of project `references` — does not compile it, so `renderRoute` / `renderRouteEvents` type-check route ids as `string` and `input` / `result` as `unknown`. Reported on `tsconfig.json`; never for a project without one. | Add `".agent-bundle/routes.d.ts"` to `tsconfig.json` `include` (not `files`: an `include` entry is inert until the first build publishes the file, while a missing `files` entry is a `tsc` error); `build`, `dev`, and `validate` keep the file current and it stays gitignored. |
| `AB4940` | error | A conventional provider module has no default export or its default export is not a function. Default-export a factory receiving `{ invocation, signal }`. |
| `AB4941` | error | Two provider filenames derive the same camel-cased provider key. Rename one file so every provider key is unique. |
| `AB4942` | error | A provider filename derives the reserved `processLifetime` key. Rename the file so its camel-cased key does not collide with the framework-owned provider. |
Expand Down
10 changes: 10 additions & 0 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,16 @@ and through a post-build scan of the emitted bundle for function-form
`externals` — because generated executables must stay self-contained. The
hatch customizes *how code compiles*, never *what the artifact promises*.

The hatch merges *beside* the framework profile, not over it: `plugins`
arrays concatenate, and Rsbuild's plugin manager appends every plugin it is
handed without deduping by name. So a `tools.rsbuild.plugins` entry that
re-adds a plugin the framework already registers — `@rsbuild/plugin-react`
(`rsbuild:react`), carried by every synthesized Rslib entry and every
React-syntax MCP App view — would run it twice. `agent-bundle validate`
reports that as `AB4724` (an error, like the other `tools` shape checks) with
the plugin and package name; remove the entry, the framework already
registers it.

The hatch executes under two different bundler engine copies. Artifact
scripts, MCP entries, hook wrappers, and the package build compile through
Rslib, which runs the Rsbuild/Rspack versions nested inside `@rslib/core`
Expand Down
8 changes: 5 additions & 3 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,11 @@ The generated `.agent-bundle/routes.d.ts` declares `AgentBundleProviders`
(`ProviderKey`, `ProviderValue<Key>`) from each factory's resolved return type
and augments `@agent-bundle/runtime`'s `AgentProviderValues`, so
`(await agent()).providers.library` is a `LibraryContext` with no cast once
the file is part of the project's TypeScript program (add
`".agent-bundle/routes.d.ts"` to `tsconfig.json` `include`). Undeclared keys
stay `unknown`. The `agent-bundle/test` harness (`renderRoute`, `invokeCli`,
the file is part of the project's TypeScript program. `create-agent-bundle`
templates include it by default (`".agent-bundle/routes.d.ts"` in
`tsconfig.json` `include`; the file stays gitignored), and `agent-bundle
validate` warns with `AB4834` when a project that compiles routes or
providers leaves it out. Undeclared keys stay `unknown`. The `agent-bundle/test` harness (`renderRoute`, `invokeCli`,
the in-memory MCP helpers) mounts the project's providers automatically, in the
same order and with the same fail-closed semantics as the generated request
scopes, so a route-unit test observes what the artifact would mount — including
Expand Down
7 changes: 6 additions & 1 deletion examples/audiobook-curator/tests/route-unit/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AgentProviderValues } from '@agent-bundle/runtime';
import { expect, it } from '@rstest/core';
import { expectDocument, renderRoute, testManifest } from 'agent-bundle/test';

Expand Down Expand Up @@ -40,8 +41,12 @@ it('renders the catalog from injected library context with its contents envelope
it('renders an honest degraded catalog when library context is absent', async () => {
// The harness mounts `src/providers/library.ts` automatically, so the
// degraded path needs an explicit empty provider map to keep it absent.
// The generated `.agent-bundle/routes.d.ts` (in this project's tsconfig
// program) makes the declared `library` key required, so this deliberate
// contract violation is spelled out as a cast rather than left implicit.
const absentProviders = {} as unknown as AgentProviderValues;
const rendered = await renderRoute('resource:curator/catalog', {
context: { providers: {} },
context: { providers: absentProviders },
input: { uri: 'audiobook-curator://catalog' },
});
const value = rendered.document.value as {
Expand Down
1 change: 1 addition & 0 deletions examples/audiobook-curator/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
},
"include": [
"agent-bundle.config.ts",
".agent-bundle/routes.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"tests/**/*.ts",
Expand Down
1 change: 1 addition & 0 deletions examples/host-test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
},
"include": [
"agent-bundle.config.ts",
".agent-bundle/routes.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"tests/**/*.ts",
Expand Down
1 change: 1 addition & 0 deletions examples/mcp-app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"jsx": "react-jsx"
},
"include": [
".agent-bundle/routes.d.ts",
"agent-bundle.config.ts",
"evals/**/*.ts",
"src/**/*.ts",
Expand Down
1 change: 1 addition & 0 deletions examples/rsc-agent-runtime/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
},
"include": [
"agent-bundle.config.ts",
".agent-bundle/routes.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"tests/**/*.ts",
Expand Down
16 changes: 13 additions & 3 deletions examples/worktree-proximity/tests/route-unit/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createSqliteStateDriver } from '@agent-bundle/runtime/state/sqlite';
import { expectDocument, renderRoute, testManifest } from 'agent-bundle/test';

import BeforeTool from '../../src/events/tool/before.js';
import agentTopologyProvider from '../../src/providers/agent-topology.js';
import {
topologyStateDefinition,
type TopologyEvents,
Expand Down Expand Up @@ -40,6 +41,15 @@ const provider = (root: string) => ({
state: 'available' as const,
});

// The generated `.agent-bundle/routes.d.ts` (in this project's tsconfig program)
// makes every declared provider key required on an explicit map, so the fixture
// carries `agentTopology` too; its factory is pure and reports the same honest
// unavailable value the harness would mount.
const providers = (root: string) => ({
agentTopology: agentTopologyProvider(),
gitWorktree: provider(root),
});

const eventInput = (
event: 'agent/start' | 'session/start' | 'stop' | 'tool/after' | 'tool/before',
native: Record<string, unknown>,
Expand Down Expand Up @@ -80,7 +90,7 @@ const renderEventInput = async (
},
...(lineage === undefined ? {} : { lineage }),
noticeLedger: bindings.noticeLedger,
providers: { gitWorktree: provider(worktreeRoot) },
providers: providers(worktreeRoot),
session: available({ sessionId: 'root-session' }, 'native'),
state: bindings.state,
workspace: available({ root: worktreeRoot }, 'native'),
Expand Down Expand Up @@ -438,7 +448,7 @@ describe('worktree proximity journeys', () => {
rendered = await renderRoute('tool:coordinator/status', {
context: {
noticeLedger: bindings.noticeLedger,
providers: { gitWorktree: provider(worktrees.root) },
providers: providers(worktrees.root),
state: bindings.state,
},
input: {},
Expand All @@ -465,7 +475,7 @@ describe('worktree proximity journeys', () => {
const rendered = await renderRoute({ default: BeforeTool }, {
context: {
actor: available({ id: 'agent-a' }, 'native'),
providers: { gitWorktree: provider(worktrees.a) },
providers: providers(worktrees.a),
},
input: eventInput(
'tool/before',
Expand Down
1 change: 1 addition & 0 deletions examples/worktree-proximity/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
},
"include": [
"agent-bundle.config.ts",
".agent-bundle/routes.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"tests/**/*.ts",
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,9 @@ kind, and the module provenance.

The same generated `.agent-bundle/routes.d.ts` registers the route contracts on
`@agent-bundle/runtime`'s `Register` interface. With that file in the project's
TypeScript program (add it to `tsconfig.json` `include`), a string-literal route
TypeScript program (`create-agent-bundle` templates list it in `tsconfig.json`
`include` by default; `agent-bundle validate` warns with `AB4834` when a routed
project's tsconfig leaves it out), a string-literal route
id is checked against the compiled ids, `input` is typed from the route's
`inputSchema`, and `result` from its `resultSchema` (an event route's `input`
is its `{ canonical, native }` payload and its `result` `undefined`);
Expand Down
45 changes: 45 additions & 0 deletions packages/agent-bundle/src/build/framework-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* The Rsbuild plugins agent-bundle registers itself in the configs it
* synthesizes, keyed by plugin `name`. Rsbuild's plugin manager appends every
* plugin it is handed and never dedupes by name, and both engines the layers
* from `composeToolsLayers` are handed to (`mergeRslibConfig` for artifact
* scripts, MCP entries, hooks, and the package build; `mergeRsbuildConfig` for
* MCP App views) concatenate `plugins` arrays, so a consumer who adds one of
* these through `tools.rsbuild.plugins` registers it twice. `validateTools`
* reports that as AB4724 instead.
*
* The names are literals rather than instances so the validator does not
* load a bundler plugin to read a string; `framework-plugins.test.ts` pins
* each literal to the plugin it names.
*/
export const frameworkOwnedRsbuildPlugins: ReadonlyMap<string, string> = new Map([
// `pluginReact()` from rslib.ts (every synthesized entry) and mcp-apps.ts
// (every React-syntax view): automatic JSX runtime, fast refresh off.
['rsbuild:react', '@rsbuild/plugin-react'],
]);

const hasPluginName = (value: unknown): value is { readonly name: string } =>
typeof value === 'object' && value !== null && typeof (value as { readonly name?: unknown }).name === 'string';

/**
* The plugin names a `tools.rsbuild.plugins` value supplies that collide with
* a framework-owned registration, in authored order and deduplicated. Only
* statically visible plugin objects are inspected: nested arrays are
* flattened the way Rsbuild flattens them, while `false`/`null`/`undefined`
* holes and plugins supplied as Promises (which Rsbuild also accepts) carry
* no name to compare until the build awaits them.
*/
export const frameworkOwnedPluginCollisions = (plugins: unknown): readonly string[] => {
const collisions: string[] = [];
const visit = (value: unknown): void => {
if (Array.isArray(value)) {
for (const item of value) visit(item);
return;
}
if (hasPluginName(value) && frameworkOwnedRsbuildPlugins.has(value.name) && !collisions.includes(value.name)) {
collisions.push(value.name);
}
};
visit(plugins);
return collisions;
};
Loading
Loading