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
7 changes: 7 additions & 0 deletions .changeset/optional-plugin-version-and-meta-constant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"agent-bundle": minor
---

Make `plugin.version` optional and derive it from `package.json`, so a packaged plugin declares its release version once. A declared value that is not a nonempty string still reports `AB4001`, and a declared value that disagrees with `package.json` still reports the `AB4008` warning. Development keeps the labeled `0.0.0-dev` fallback, while `agent-bundle build` now refuses a project with no release version at all with the new `AB4013` error, so a development fallback can never reach a release artifact.

Add the `agent-bundle/meta` build-time identity module. Every compiled plugin surface — script, CLI, MCP entry, hook, and package bundles plus browser MCP App bundles — resolves it to the exact `{ name, version, packageName, packageVersion }` reported by artifact manifests, `inspect`, and dev status, so plugins can delete hand-written `src/lib/version.ts` shims. Types ship with the package export; outside Agent Bundle compilation the module throws instead of reporting a fabricated identity.
34 changes: 33 additions & 1 deletion docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ gate a build, a validation, or a dev rebuild.
| Family | Area |
| --- | --- |
| `AB30xx` | Skill documents: Markdown parsing (`AB3000`–`AB3002`: unreadable, missing or malformed frontmatter) and rendered-skill compilation (`AB3003`: module failed to load, `AB3004`: missing/invalid default component or `frontmatter` export, `AB3005`: content outside the supported Markdown element subset). |
| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`/`AB4001`: missing name/version; `AB4002`–`AB4007`: Skill fields; `AB4008`–`AB4011`: package identity; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project). |
| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`/`AB4001`: name/version; `AB4002`–`AB4007`: Skill fields; `AB4008`–`AB4011` and `AB4013`: release identity, see below; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project). |
| `AB41xx` | Normalized model invariants (unknown targets, duplicate IDs and outputs). |
| `AB42xx` | Hook configuration and native hook sources. |
| `AB43xx` | MCP server and MCP App configuration. |
Expand Down Expand Up @@ -62,6 +62,38 @@ its module does not export) are invisible to `tsc --noEmit`, so a green
`tsc --declaration --emitDeclarationOnly` over the lib entry source
directory.

## Release identity (`AB4001`, `AB4008`–`AB4011`, `AB4013`)

`package.json` is authoritative for release identity (issue #94): its `name`
and `version` become the `packageName` and `packageVersion` axes carried on
the project context, artifact manifests, `inspect` output, and dev status.
`plugin.name` stays the host-native slug and is never derived from the npm
package name.

`plugin.version` is **optional**. When it is omitted, the version every
surface reports — manifests, host projections, dev status, and the
`agent-bundle/meta` constant compiled into plugin code — is the `package.json`
version. When it is declared, the declared value still wins so a legacy
config never changes meaning mid-migration, and a disagreement reports the
`AB4008` **warning**. Declaring it as anything but a nonempty string is an
`AB4001` error.

A project with neither an authored `plugin.version` nor a valid `package.json`
version has no release identity. Development commands (`dev`, `inspect`,
`validate`) keep running on the labeled `0.0.0-dev.<short-revision>` fallback,
because an unpackaged scratch project is a normal development state. A
development-only fallback can never produce a release artifact, so
`agent-bundle build` alone refuses it with `AB4013`.

| Code | Severity | Trigger |
| --- | --- | --- |
| `AB4001` | error | `plugin.version` is declared as something other than a nonempty string. Omit the field to derive the version from `package.json`. |
| `AB4008` | warning | A declared `plugin.version` differs from the `package.json` version. Align the two, or drop `plugin.version`. |
| `AB4009` | warning | `package.json` `name` is not a valid npm package name; the `packageName` axis is withheld. |
| `AB4010` | warning | `package.json` `version` is not a valid semantic version; the `packageVersion` axis is withheld. |
| `AB4011` | warning | `package.json` is unusable — unparsable, not a JSON object, or symlinked outside the project root. |
| `AB4013` | error (build) | `agent-bundle build` refuses a project with no release version: `plugin.version` is omitted and `package.json` declares no valid semantic version. |

## Migration nudges (`AB4730`–`AB4735`)

The entry conventions and the framework-owned stdio lifecycle shell (RFC #50)
Expand Down
33 changes: 31 additions & 2 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,35 @@ Export detection is a static scan of the entry source (comment-, string-, and
template-safe). The generated shells re-verify the export shape at runtime
with a clear error.

## `agent-bundle/meta` — build-time release identity

Plugin code reads its own identity from the framework instead of maintaining
a hand-written `src/lib/version.ts`:

```ts
import meta, { name, packageName, packageVersion, version } from 'agent-bundle/meta';
```

`version` is the resolved plugin version: the authored `plugin.version` when
declared, otherwise the `package.json` version. `name` is the host-native
plugin slug — never the npm package name. `packageName` and `packageVersion`
are the validated npm axes, `undefined` for an unpackaged development
project. Every value is exactly what artifact manifests, `inspect`, and dev
status report for the same build.

The compiler replaces the specifier in **every** compiled surface: artifact
scripts, the routed CLI, MCP entries, hook wrappers, and the package build
(all through Rslib), plus browser MCP App view bundles (through Rsbuild). The
module is a reserved specifier, so the `tools` hatch cannot externalize it,
and no emitted bundle can still carry an unresolved import of it.

Types ship with the package export, so no generated declaration file is
involved. Outside Agent Bundle compilation the published module throws rather
than reporting a fabricated identity — a plugin slug exists only in the
config, and a runtime guess at it would silently disagree with the artifact.
A release build refuses a project with no release version at all (`AB4013`),
so a compiled artifact never carries the development fallback.

## Prebuilt payloads — package what you compiled yourself

Some projects legitimately own their compilation — a coordinated
Expand Down Expand Up @@ -359,8 +388,8 @@ A hatch value that breaks an artifact contract (async chunks, output roots,
self-containment) fails the build with a hard diagnostic instead of silently
overriding the contract. Reserved module specifiers are protected the same
way: a hatch that externalizes `agent-bundle/mcp-entry` or a generated
registry specifier (such as `agent-bundle/mcp-apps`) fails the build with a
hard diagnostic — at config inspection for statically visible `externals`,
module specifier (`agent-bundle/meta`, or a registry specifier such as
`agent-bundle/mcp-apps`) fails the build with a hard diagnostic — at config inspection for statically visible `externals`,
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*.
Expand Down
10 changes: 3 additions & 7 deletions examples/audiobook-curator/agent-bundle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@ export default defineConfig({
// package name (`@agent-bundle-example/audiobook-curator`); scoped npm
// names never become slugs.
name: 'audiobook-curator',
// Release identity is derived from package.json: `packageName` and
// `packageVersion` flow into the project context, artifact manifests,
// inspect output, and dev status. This declared version must match the
// package.json version — a mismatch reports the AB4008 warning. The
// package.json version is the single version source; this field only
// restates it until plugin.version becomes optional (issue #94 stage 3).
version: '1.0.0',
// No `version` field: package.json is the single version source. It flows
// into the project context, artifact manifests, inspect output, dev
// status, and the `agent-bundle/meta` constant this plugin imports.
},
runtime: { node: '22.19.0' },
// No `scripts` or `bin` fields needed: the routed `src/cli/` commands
Expand Down
5 changes: 4 additions & 1 deletion examples/mcp-app/src/mcp/status.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { McpServer } from '@modelcontextprotocol/server';
import apps from 'agent-bundle/mcp-apps';
import { name, version } from 'agent-bundle/meta';
import { z } from 'zod';

import { healthyCompilerStatus } from '../compiler-status-contract.ts';
Expand All @@ -21,7 +22,9 @@ const serviceCatalog = Object.freeze({
});

export const createStatusServer = (): McpServer => {
const server = new McpServer({ name: 'mcp-app-example', version: '1.0.0' });
// The compiler stamps this project's identity into `agent-bundle/meta`, so
// the wire identity cannot drift from the config or package.json.
const server = new McpServer({ name, version });

server.registerResource(app.name, app.resourceUri, {
_meta: { ui: { resourceUri: app.resourceUri } },
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"types": "./dist/mcp-apps.d.ts",
"import": "./dist/mcp-apps.js"
},
"./meta": {
"types": "./dist/meta.d.ts",
"import": "./dist/meta.js"
},
"./mcp-entry": {
"types": "./dist/mcp-entry.d.ts",
"import": "./dist/mcp-entry.js"
Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default defineConfig({
index: './src/index.ts',
'mcp-apps': './src/mcp-apps.ts',
'mcp-entry': './src/mcp-entry.ts',
meta: './src/meta.ts',
'mcp-server-runtime': './src/mcp-server-runtime.ts',
rstest: './src/rstest/index.ts',
test: './src/test/index.ts',
Expand Down
8 changes: 8 additions & 0 deletions packages/agent-bundle/src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type CompiledHookEntry,
type CompiledMcpEntry,
} from './entries.ts';
import { projectMeta } from './meta.ts';
import { compileMcpApps, planCompiledMcpApps, type CompiledMcpApp } from './mcp-apps.ts';
import {
assertUniqueArtifactDestinations,
Expand Down Expand Up @@ -346,9 +347,13 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
const compiledMcpApps: CompiledMcpApp[] = [];
const compiledMcpEntries: CompiledMcpEntry[] = [];
const tools = options.tools === undefined ? {} : { tools: options.tools };
// One identity feeds every compiled surface, exactly the identity the
// manifest, `inspect`, and dev status report (issue #237).
const meta = projectMeta(options.model.metadata);
for (const target of stagedTargets) {
const targetMcpApps = await compileMcpApps(options.model.mcpApps ?? [], {
cwd: options.projectRoot,
meta,
outDir: target.root,
target: target.name,
...tools,
Expand All @@ -360,6 +365,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
options.model.scripts.filter((script) => script.targets.includes(target.name)),
{
cwd: options.projectRoot,
meta,
outDir: target.root,
...(options.model.state === undefined ? {} : { state: options.model.state }),
...tools,
Expand All @@ -369,6 +375,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
compiledHooks.push(...(await compileHooks(target.hookEntries, {
artifactEpoch: options.projectContext.revision,
cwd: options.projectRoot,
meta,
outDir: target.root,
...tools,
})));
Expand All @@ -379,6 +386,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
eventHooks: target.hookEntries
.filter((entry) => entry.hook.eventRoute !== undefined)
.map((entry) => entry.hook),
meta,
outDir: target.root,
plugin: { name: options.model.metadata.name, version: options.model.metadata.version },
providers: options.model.providers ?? [],
Expand Down
7 changes: 7 additions & 0 deletions packages/agent-bundle/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
NormalizedScript,
NormalizedStateDefinition,
} from '../core/types.ts';
import type { AgentBundleMeta } from '../meta.ts';
import { mcpEntryAliasPattern } from '../config/normalize.ts';
import { stableJson } from '../core/digest.ts';
import { emitPlanEntries, resolveArtifactDestination } from './emit.ts';
Expand Down Expand Up @@ -156,6 +157,7 @@ export const compileEntries = async (
entries: readonly NormalizedScript[],
options: {
readonly cwd: string;
readonly meta: AgentBundleMeta;
readonly outDir: string;
readonly state?: NormalizedStateDefinition;
readonly tools?: AgentBundleToolsConfig;
Expand Down Expand Up @@ -226,6 +228,7 @@ export const compileEntries = async (
})()];
})),
...(cliRuntimeShell === undefined ? {} : { ignoredSourcePaths: [runtimeIgnoredRoot(cliRuntimeShell)] }),
meta: options.meta,
outputRoot: options.outDir,
...(options.tools === undefined ? {} : { tools: options.tools }),
});
Expand Down Expand Up @@ -307,6 +310,7 @@ export const compileMcpEntries = async (
readonly artifactEpoch: string;
readonly cwd: string;
readonly eventHooks: readonly NormalizedHook[];
readonly meta: AgentBundleMeta;
readonly outDir: string;
readonly plugin: { readonly name: string; readonly version: string };
readonly providers?: readonly CompiledProvider[];
Expand Down Expand Up @@ -450,6 +454,7 @@ export const compileMcpEntries = async (
],
}),
logLevel: 'error',
meta: options.meta,
outputRoot: options.outDir,
...(options.tools === undefined ? {} : { tools: options.tools }),
});
Expand Down Expand Up @@ -484,6 +489,7 @@ export const compileHooks = async (
options: {
readonly artifactEpoch: string;
readonly cwd: string;
readonly meta: AgentBundleMeta;
readonly outDir: string;
readonly tools?: AgentBundleToolsConfig;
},
Expand Down Expand Up @@ -520,6 +526,7 @@ export const compileHooks = async (
: {
ignoredSourcePaths: [runtimeIgnoredRoot(eventIpcRuntime)],
}),
meta: options.meta,
outputRoot: options.outDir,
...(options.tools === undefined ? {} : { tools: options.tools }),
});
Expand Down
17 changes: 16 additions & 1 deletion packages/agent-bundle/src/build/inspect-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import {
} from './entry-shell.ts';
import { planCompiledMcpEntries } from './entries.ts';
import { composeMcpAppsRsbuildConfig, planCompiledMcpApps } from './mcp-apps.ts';
import { projectMeta } from './meta.ts';
import { planPackageEntries } from './package-build.ts';
import { composeEntryLibConfig, type RslibEntry } from './rslib.ts';
import { deepFreeze } from '../core/freeze.ts';
import type { AgentBundleMeta } from '../meta.ts';


/**
Expand Down Expand Up @@ -97,6 +99,7 @@ const renderConfigValue = (value: unknown, ancestors = new Set<object>()): unkno
const rslibInspectionEntry = (options: {
readonly entry: RslibEntry;
readonly kind: BundlerInspectionEntry['kind'];
readonly meta: AgentBundleMeta;
readonly name: string;
readonly outputPath: string;
readonly outputRoot: string;
Expand All @@ -106,6 +109,7 @@ const rslibInspectionEntry = (options: {
}): BundlerInspectionEntry => Object.freeze({
bundler: 'rslib',
config: renderConfigValue(composeEntryLibConfig(options.entry, {
meta: options.meta,
outputRoot: options.outputRoot,
...(options.tools === undefined ? {} : { tools: options.tools }),
})),
Expand All @@ -122,6 +126,7 @@ const scriptEntries = async (
target: string,
tools: AgentBundleToolsConfig | undefined,
): Promise<readonly BundlerInspectionEntry[]> => {
const meta = projectMeta(model.metadata);
const outputRoot = artifactOutputToken(target);
const scripts = model.scripts.filter((script) =>
script.mode === 'bundle' && script.targets.includes(target));
Expand All @@ -143,6 +148,7 @@ const scriptEntries = async (
: {}),
},
kind: 'script',
meta,
name: script.name,
outputPath: `${target}/scripts/${script.name}.mjs`,
outputRoot,
Expand All @@ -158,6 +164,7 @@ const mcpEntryEntries = async (
target: string,
tools: AgentBundleToolsConfig | undefined,
): Promise<readonly BundlerInspectionEntry[]> => {
const meta = projectMeta(model.metadata);
const outputRoot = artifactOutputToken(target);
const planned = planCompiledMcpEntries(model.mcpServers, { outDir: outputRoot, target });
const entries: BundlerInspectionEntry[] = [];
Expand Down Expand Up @@ -204,6 +211,7 @@ const mcpEntryEntries = async (
],
},
kind: 'mcp-entry',
meta,
name: serverName,
outputPath: `${target}/mcp/${entry.name}.mjs`,
outputRoot,
Expand All @@ -229,6 +237,7 @@ const mcpEntryEntries = async (
}),
},
kind: 'mcp-entry',
meta,
name: `${serverName}:flight`,
outputPath: `${target}/mcp/${workerFile}`,
outputRoot,
Expand All @@ -243,6 +252,7 @@ const mcpEntryEntries = async (

const hookEntries = (
entries: readonly TargetHookEntry[],
meta: AgentBundleMeta,
target: string,
tools: AgentBundleToolsConfig | undefined,
): readonly BundlerInspectionEntry[] => {
Expand All @@ -256,6 +266,7 @@ const hookEntries = (
virtualSource: entry.virtualSource,
},
kind: 'hook',
meta,
name: entry.hook.name,
outputPath: `${target}/${entry.relativePath}`,
outputRoot,
Expand Down Expand Up @@ -284,6 +295,7 @@ const mcpAppsEntry = (
return [Object.freeze({
bundler: 'rsbuild' as const,
config: renderConfigValue(composeMcpAppsRsbuildConfig(sources, {
meta: projectMeta(model.metadata),
outDir: outputRoot,
...(tools === undefined ? {} : { tools }),
})),
Expand All @@ -302,11 +314,13 @@ const packageBuildEntries = async (
if (packageBuild === undefined) return [];
const dtsTsconfig = packageBuild.lib?.dts === true ? generatedDtsTsconfigToken : undefined;
const planned = await planPackageEntries(model, dtsTsconfig);
const meta = projectMeta(model.metadata);
return planned.map((entry) => {
const bin = entry.executable;
return rslibInspectionEntry({
entry,
kind: bin ? 'bin' : 'lib',
meta,
name: bin ? entry.name.replace(/^bin-/u, '') : entry.name,
outputPath: `${packageBuild.outputDir}/${entry.outputRelativePath}`,
outputRoot: packageBuild.outputDir,
Expand All @@ -327,11 +341,12 @@ export const composeBundlerInspection = async (options: {
readonly tools?: AgentBundleToolsConfig;
}): Promise<BundlerInspection> => {
const entries: BundlerInspectionEntry[] = [];
const meta = projectMeta(options.model.metadata);
for (const target of options.targets) {
entries.push(
...(await scriptEntries(options.model, target.name, options.tools)),
...(await mcpEntryEntries(options.model, target.name, options.tools)),
...hookEntries(target.hookEntries, target.name, options.tools),
...hookEntries(target.hookEntries, meta, target.name, options.tools),
...mcpAppsEntry(options.model, target.name, options.tools),
);
}
Expand Down
Loading
Loading