From 4de793f110697c94ae236c606a60bdc5afbb7c60 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 09:10:40 +0000 Subject: [PATCH 01/11] refactor: remove legacy API compatibility paths --- .changeset/remove-legacy-apis.md | 7 + docs/diagnostics.md | 39 +- docs/entry-conventions.md | 15 +- docs/framework-mode.md | 6 +- examples/hooks-and-scripts/package.json | 1 + examples/mcp-app/package.json | 1 + examples/skills-starter/package.json | 1 + .../agent-bundle.config.ts | 2 +- .../canonical-collision/package.json | 6 + .../comprehensive/agent-bundle.config.ts | 1 - .../integration/comprehensive/package.json | 3 +- .../micro-eval/agent-bundle.config.ts | 1 - fixtures/integration/micro-eval/package.json | 6 + .../packed-release/agent-bundle.config.ts | 1 - .../integration/packed-release/package.json | 3 +- .../skills-only/agent-bundle.config.ts | 1 - fixtures/integration/skills-only/package.json | 6 + .../agent-bundle.config.ts | 2 +- .../unsupported-capability/package.json | 6 + packages/agent-bundle/README.md | 2 +- .../agent-bundle.config.ts | 1 - .../fixtures/durable-web-surface/package.json | 3 +- .../src/mcp/journal/tools/record.tsx | 33 +- .../meta-consumer/agent-bundle.config.ts | 7 +- .../src/mcp/consumer/tools/identity.ts | 27 +- .../route-harness/agent-bundle.config.ts | 1 - .../fixtures/route-harness/package.json | 5 + .../src/mcp/harness/tools/catalog.tsx | 37 +- .../src/mcp/harness/tools/context.tsx | 39 +- .../src/mcp/harness/tools/echo.tsx | 35 +- .../src/mcp/harness/tools/fault.tsx | 45 +- .../src/mcp/harness/tools/journal.tsx | 33 +- .../src/mcp/harness/tools/layout-probe.tsx | 33 +- .../src/mcp/harness/tools/lifecycle.tsx | 87 +-- .../src/mcp/harness/tools/mutation-probe.tsx | 33 +- .../src/mcp/harness/tools/plugin-root.tsx | 27 +- .../src/mcp/harness/tools/publish-notice.tsx | 63 +-- .../src/mcp/harness/tools/select.tsx | 17 +- .../src/mcp/harness/tools/strict-report.tsx | 35 +- .../src/mcp/harness/tools/submit.tsx | 85 +-- .../src/mcp/harness/tools/ticket.tsx | 63 +-- .../src/mcp/harness/tools/tooling.tsx | 41 +- .../src/mcp/harness/tools/unavailable.tsx | 25 +- .../src/mcp/harness/tools/wait.tsx | 49 +- .../web-surface/agent-bundle.config.ts | 1 - packages/agent-bundle/src/api.ts | 2 +- packages/agent-bundle/src/cli-entry.ts | 23 +- packages/agent-bundle/src/config/normalize.ts | 11 +- .../src/config/plugin-identity.ts | 23 +- packages/agent-bundle/src/config/validate.ts | 53 +- .../agent-bundle/src/core/project-context.ts | 7 +- packages/agent-bundle/src/core/types.ts | 12 - .../src/dev/mcp-app-profile-descriptors.ts | 3 - .../src/dev/mcp-apps/mcp-app-host-profiles.ts | 8 +- .../src/dev/playground/playground-store.ts | 1 - .../agent-bundle/src/dev/workbench-server.ts | 2 +- packages/agent-bundle/src/install/commands.ts | 4 +- packages/agent-bundle/src/install/surface.ts | 15 +- packages/agent-bundle/src/meta.ts | 7 +- .../agent-bundle/src/routes/app-template.ts | 42 +- packages/agent-bundle/src/routes/contract.ts | 12 +- packages/agent-bundle/src/routes/graph.ts | 17 +- packages/agent-bundle/src/routes/public.ts | 8 +- packages/agent-bundle/src/test/manifest.ts | 4 +- .../tests/adapter-contract.test.ts | 6 +- .../agent-bundle/tests/amp-adapter.test.ts | 2 +- packages/agent-bundle/tests/api.test.ts | 77 +-- .../tests/artifact-cli-bin.test.ts | 6 +- .../tests/browser-stdio-bridge-spike.test.ts | 2 +- .../agent-bundle/tests/build-compose.test.ts | 4 +- .../tests/build-reproducibility.test.ts | 25 +- .../tests/build-source-publication.test.ts | 2 +- .../tests/claude-hooks-schema.test.ts | 2 +- .../agent-bundle/tests/cli-projection.test.ts | 25 +- .../tests/cli-routes-build.test.ts | 36 +- .../agent-bundle/tests/cli-routes.test.ts | 67 ++- packages/agent-bundle/tests/cli.test.ts | 8 +- .../agent-bundle/tests/command-config.test.ts | 6 +- .../tests/compiler-evidence.test.ts | 2 +- .../tests/composite-rules.test.ts | 2 +- packages/agent-bundle/tests/config.test.ts | 45 +- .../tests/dev-artifact-service.test.ts | 7 +- .../tests/dev-contract-config.test.ts | 2 +- .../tests/dev-coordinator.test.ts | 6 +- .../tests/dev-package-build.test.ts | 2 +- .../agent-bundle/tests/dev-services.test.ts | 27 +- .../tests/dev-web-host-routes.test.ts | 2 +- .../tests/dev-workbench-packaging.test.ts | 6 +- .../agent-bundle/tests/dev-workbench.test.ts | 30 +- .../event-handler-artifact-graph.test.ts | 4 +- .../tests/examples-contract.test.ts | 4 +- .../agent-bundle.config.ts | 1 - .../agent-bundle.config.ts | 1 - .../host-install/agent-bundle.config.ts | 1 - .../agent-bundle.config.ts | 1 - .../fixtures/packed-native-smoke/package.json | 3 +- .../tests/function-authoring-build.test.ts | 6 +- .../tests/function-authoring.test.ts | 4 +- .../tests/generated-route-server.test.ts | 498 ++++++++++-------- .../tests/helpers/project-fixture.ts | 20 +- .../tests/hook-playground-service.test.ts | 2 +- .../tests/hook-receipt-pipe.test.ts | 2 +- packages/agent-bundle/tests/hooks.test.ts | 10 +- .../tests/host-discovery-dev-server.test.ts | 2 +- .../agent-bundle/tests/host-mcp-proxy.test.ts | 2 +- .../tests/inspect-bundler.test.ts | 6 +- .../agent-bundle/tests/inspect-state.test.ts | 8 +- .../agent-bundle/tests/install-cli.test.ts | 2 +- .../tests/install-surface.test.ts | 6 +- packages/agent-bundle/tests/install.test.ts | 2 +- .../agent-bundle/tests/layout-build.test.ts | 2 +- .../tests/lifecycle-replay-dev-server.test.ts | 2 +- .../tests/manifest-combined-proof.test.ts | 8 +- .../tests/mcp-app-host-profiles.test.ts | 2 +- .../tests/mcp-probe-dev-server.test.ts | 4 +- .../tests/mcp-session-service.test.ts | 4 +- packages/agent-bundle/tests/mcp.test.ts | 46 +- .../agent-bundle/tests/normalization.test.ts | 112 ++-- .../tests/notice-retention-config.test.ts | 4 +- .../agent-bundle/tests/package-build.test.ts | 44 +- .../tests/package-conventions.test.ts | 30 +- .../tests/package-identity.test.ts | 65 +-- .../tests/packed-consumer.test.ts | 4 +- .../tests/path-token-resolver.test.ts | 2 +- .../playground-orchestration-service.test.ts | 4 +- .../tests/playground-service.test.ts | 2 +- .../agent-bundle/tests/plugin-logo.test.ts | 6 - .../tests/prebuilt-payload.test.ts | 2 +- .../tests/project-context-walk-bound.test.ts | 3 +- .../tests/projection/script-dispatch.test.ts | 4 +- .../tests/provider-typegen.test.ts | 17 +- .../tests/public-api-packed.test.ts | 4 +- .../agent-bundle/tests/public-api.test.ts | 4 +- .../tests/rendered-skills.test.ts | 8 +- .../tests/route-caller-input-types.test.ts | 2 +- .../tests/route-contract-imports.test.ts | 6 +- .../agent-bundle/tests/route-graph.test.ts | 361 +++++-------- .../tests/route-invocation-dev-server.test.ts | 366 +++++++------ .../tests/route-invocation-service.test.ts | 58 +- .../tests/route-register-typegen.test.ts | 47 +- .../tests/route-task-support.test.ts | 23 +- .../route-unit/load-route-module.test.ts | 17 +- .../workbench-surface-rendered-skill.test.ts | 6 +- .../agent-bundle/tests/rule-config.test.ts | 6 +- .../tests/skill-document-service.test.ts | 2 +- packages/agent-bundle/tests/skill-ir.test.ts | 2 +- .../tests/support/dev-contract-project.ts | 2 +- .../tests/support/eval-project.ts | 2 +- .../tests/support/mcp-conformance.ts | 2 +- .../tests/target-hook-contract.test.ts | 2 +- .../tests/trace-dev-server.test.ts | 27 +- .../agent-bundle/tests/web-config.test.ts | 4 +- .../workbench-surface-dev-server.test.ts | 31 +- .../tests/workbench-surface.test.ts | 12 +- packages/rsc-markdown-stream/src/resolve.js | 27 +- packages/rsc-runtime/README.md | 24 +- packages/rsc-runtime/src/agent-document.ts | 8 +- packages/rsc-runtime/src/elements.ts | 56 -- packages/rsc-runtime/src/index.ts | 12 +- packages/rsc-runtime/src/lower-hook.ts | 77 --- packages/rsc-runtime/src/lower-mcp.ts | 144 +---- packages/rsc-runtime/src/notices/contract.ts | 6 +- packages/rsc-runtime/src/notices/redaction.ts | 18 +- packages/rsc-runtime/src/notices/state.ts | 3 +- packages/rsc-runtime/src/request-context.ts | 22 - .../rsc-runtime/tests/docs-contract.test.ts | 11 - .../rsc-runtime/tests/mcp-projector.test.ts | 14 + .../tests/notices-redaction.test.ts | 6 +- packages/rsc-runtime/tests/runtime.test.ts | 134 ----- packages/workbench/src/main.tsx | 7 +- packages/workbench/src/mcp/mcp-page.tsx | 8 +- packages/workbench/src/project-client.ts | 10 +- .../workbench/src/workbench-capabilities.ts | 41 +- .../tests/build-status-model.test.ts | 4 +- .../workbench/tests/project-client.test.ts | 61 +-- .../workbench/tests/support/workbench-e2e.ts | 39 +- .../tests/workbench-capabilities.test.ts | 20 +- .../docs/en/examples/audiobook-curator.mdx | 4 +- website/docs/en/guide/authoring/index.mdx | 6 +- website/docs/en/guide/authoring/mcp.mdx | 57 +- .../docs/en/guide/concepts/architecture.mdx | 7 +- .../en/guide/distribution/installation.mdx | 2 +- .../docs/en/guide/distribution/validation.mdx | 6 +- website/docs/en/reference/cli.mdx | 2 +- website/docs/en/reference/configuration.mdx | 11 +- .../docs/en/reference/targets-artifacts.mdx | 2 +- .../docs/zh/examples/audiobook-curator.mdx | 2 +- website/docs/zh/guide/authoring/index.mdx | 5 +- website/docs/zh/guide/authoring/mcp.mdx | 54 +- .../docs/zh/guide/concepts/architecture.mdx | 6 +- .../zh/guide/distribution/installation.mdx | 2 +- .../docs/zh/guide/distribution/validation.mdx | 5 +- website/docs/zh/reference/cli.mdx | 2 +- website/docs/zh/reference/configuration.mdx | 8 +- .../docs/zh/reference/targets-artifacts.mdx | 2 +- 195 files changed, 1853 insertions(+), 2616 deletions(-) create mode 100644 .changeset/remove-legacy-apis.md create mode 100644 fixtures/integration/canonical-collision/package.json create mode 100644 fixtures/integration/micro-eval/package.json create mode 100644 fixtures/integration/skills-only/package.json create mode 100644 fixtures/integration/unsupported-capability/package.json create mode 100644 packages/agent-bundle/fixtures/route-harness/package.json delete mode 100644 packages/rsc-runtime/src/lower-hook.ts delete mode 100644 packages/rsc-runtime/src/request-context.ts delete mode 100644 packages/rsc-runtime/tests/docs-contract.test.ts delete mode 100644 packages/rsc-runtime/tests/runtime.test.ts diff --git a/.changeset/remove-legacy-apis.md b/.changeset/remove-legacy-apis.md new file mode 100644 index 000000000..d60946133 --- /dev/null +++ b/.changeset/remove-legacy-apis.md @@ -0,0 +1,7 @@ +--- +"agent-bundle": minor +"@agent-bundle/runtime": minor +"rsc-markdown-stream": patch +--- + +Remove `plugin.version` and diagnostics AB4001/AB4008, the install `--force` alias, split tool exports, `ServedApp`, generated CLI `isTty`, and legacy MCP App template resolution; remove runtime `Hook`/`Mcp` lowerers, `createRscRequestContext`, and `AgentDocumentSnapshot`; require React 19 element and context contracts (#PR) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index e85de5c0c..77d36451e 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -14,7 +14,7 @@ even when no error diagnostic was reported. | Family | Area | | --- | --- | | `AB30xx` | Skill documents: Markdown parsing (`AB3000`–`AB3002`: unreadable, missing or malformed frontmatter), rendered-skill compilation (`AB3003`: module failed to load, `AB3004`: missing/invalid default component or `frontmatter` export, `AB3005`: content outside the supported Markdown element subset), and the Skill IR (`AB3006`: unknown frontmatter field; `AB3008`–`AB3010`: per-host lowering of tokens and frontmatter); see below. | -| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`/`AB4001`: name/version; `AB4002`–`AB4007`: Skill fields; `AB4008`–`AB4011` and `AB4013`: release identity; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project; `AB4014`/`AB4015`: the shared descriptive metadata every host projection reads); see below. | +| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`: name; `AB4002`–`AB4007`: Skill fields; `AB4009`–`AB4011` and `AB4013`: release identity; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project; `AB4014`/`AB4015`: the shared descriptive metadata every host projection reads); see below. | | `AB41xx` | Normalized model invariants (`AB4100`–`AB4102`: unknown targets — the retired `plugin` name included — duplicate IDs and outputs; `AB4103`, `AB4105`, `AB4106`: the composite-root checks — same path with different bytes across selected projections, a host-scoped component leaking through conventional discovery, an advanced-registry adapter selected beside another target; see below). | | `AB42xx` | Hook configuration and native hook sources (`AB4200`–`AB4212`; see below). | | `AB43xx` | MCP server and MCP App configuration (`AB4300`–`AB4339`, see below; `AB4340`: a declaration for a route-generated server redeclares `entry`/`command`/`url`; `AB4341`: the `web` exposure/policy key; see below). | @@ -97,7 +97,7 @@ is an optional project-relative image. Each discovered skill is checked against the pinned Agent Skills frontmatter schema (`schemas/agent-skills/frontmatter.schema.json`: `name` and `description` required, closed portable field shapes), its directory, its resources, and -the other skills. `AB4001`, `AB4008`–`AB4011`, and `AB4013` are the release +the other skills. `AB4009`–`AB4011` and `AB4013` are the release identity codes (see "Release identity" below). Every row is reported on the config file (`AB4000`, `AB4012`) or the skill source. @@ -588,7 +588,7 @@ readable output. | `AB4771` | warning | One Rspack warning while compiling an App view that the framework's ignore list does not cover; `MCP App "" produced a warning while compiling: ::: `. | Address the warning in the named file; a warning that is bundler noise inside the framework's own dependency graph belongs on the documented ignore list. | | `AB4772` | warning | The emitted App HTML is 1 MiB or larger in a production build, or larger than 2 MiB in any build; `MCP App "" compiled to ( gzip), above the … bound; largest modules: …`. In `agent-bundle dev`, a view whose readable output would exceed 2 MiB was recompiled with the production profile for the preview and that production build fits: `MCP App "" readable development output compiled to , above the 2 MiB bound …; the preview renders the production build (…) instead; largest modules: …` — the only size advisory that view receives; a production build that is itself over 2 MiB gets the ordinary over-bound message instead. | Trim the largest modules the message names — usually a dependency imported whole; a view over 2 MiB does not render in the Workbench or `serve-app` and must shrink before it ships. The development substitution costs only the readable source in the preview. | -## Release identity (`AB4001`, `AB4008`–`AB4011`, `AB4013`) +## Release identity (`AB4009`–`AB4011`, `AB4013`) `package.json` is authoritative for release identity (issue #94): its `name` and `version` become the `packageName` and `packageVersion` axes carried on @@ -596,18 +596,8 @@ 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 **deprecated and optional**. New projects declare the -release version only in `package.json`; removal of the compatibility field -follows the normal breaking-change policy rather than a fixed window. 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`, +A project with no valid `package.json` version has no release identity. +Development commands (`dev`, `inspect`, `validate`) keep running on the labeled `0.0.0-dev.` fallback, because an unpackaged scratch project is a normal development state. A development-only fallback can never produce a release artifact, so @@ -615,12 +605,10 @@ development-only fallback can never produce a release artifact, so | 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. | +| `AB4013` | error (build) | `agent-bundle build` refuses a project whose `package.json` declares no valid semantic version. | | `AB4014` | error | A `plugin.metadata` field is not the shape the shared descriptive layer accepts, or the block declares a field beyond `author`, `homepage`, `keywords`, `license`, and `repository`. The config declared it, so it is an error rather than a withheld value — a blank string or empty array included, where `null` is how a field is opted out. | | `AB4015` | warning | A `package.json` descriptive field cannot be shared with any host manifest — a `homepage`, `repository`, or `author.url` the pinned host schemas' `uri` format refuses, an `author.email` their `email` format refuses, or a `repository` in a form this compiler will not convert (`owner/repo` and `github:` shorthands, `git@`/`git://`/`git+ssh`/`git+http` URLs; only `http(s)` and the `git+https://…` URL npm writes, with or without a trailing `.git`, are read). An `author` with any malformed part is withheld whole. The field is withheld rather than guessed at; declare `plugin.metadata.` to share an explicit value. A field the config already overrides is not reported. | @@ -1033,12 +1021,9 @@ forms. A module without a `config` export compiles silently with an empty config. An MCP App route's `config.template` resolves **relative to the route -module**, the way its imports do (`template: './dashboard.html'`). The older -project-root-relative form (`'./src/mcp//apps/dashboard.html'`) is -still accepted, without a diagnostic, while it is the only interpretation -that names an existing file. When both interpretations name different -existing files, or neither exists, `AB4827` names both candidate paths; the -fix is to make the path route-relative. The IR keeps the authored path (so the +module**, the way its imports do (`template: './dashboard.html'`). When that +path names no existing file, `AB4827` names the resolved candidate. The IR +keeps the authored path (so the graph digest stays machine-independent) and the normalized model carries the resolved absolute file. Config-declared Apps (`mcp.servers..apps`) keep resolving `entry` and `template` from the project root, where the config @@ -1183,7 +1168,7 @@ projections; no static provider subset declaration is required. | `AB4807` | retired | The stage-1 rendered-script gate. Rendered script routes ship through the Agent renderer pipeline since #102 stage 3; the code is never reused. | | `AB4808` | error | A conventional `src/scripts/` route nests below the scripts root; conventional scripts ship as direct children only. Move it up, prefix a path segment with `_`, or declare it under `scripts` in config with a flat name. | | `AB4809` | error | A conventional `src/scripts/` route and a configured `scripts` entry share one script identity through different files. Point the config entry at the module to claim it, or rename one of the two. | -| `AB4810` | error | A generated MCP route is missing named `inputSchema`/`resultSchema` declarations or its default export, an event route lacks a default export, or a helper definition is not a supported direct declaration. The bundler resolves explicit re-exports; the runtime checks callability. Sync and async handlers are accepted. | +| `AB4810` | error | A tool route does not default-export a direct `defineTool({ inputSchema, resultSchema, ... }, handler)` call, another generated MCP route is missing named `inputSchema`/`resultSchema` declarations or its default export, an event route lacks a default export, or a helper definition is not a supported direct declaration. | | `AB4811` | error | A generated MCP route exports `execute` or `render`; route mode accepts only the default Server Component contract. | | `AB4812` | error | A generated MCP App route has no non-empty static `config.resourceUri`. | | `AB4813` | error | The command graph collides: a route is both a command module and a command group, an alias collides with a sibling command, group, or alias, an alias is unsafe or duplicated, or an explicit `bin` entry claims the generated CLI executable's name. | @@ -1200,7 +1185,7 @@ projections; no static provider subset declaration is required. | `AB4824` | error | An event route selects an unknown target, requires an event the selected target does not support, or declares a capability row in `config.requires` that no selected host supports. An unmet requirement names the row and every selected host considered. | | `AB4825` | error | An event route declares both `config.targets` and `config.requires`, or either selector is not a nonempty array of nonempty target names or capability row ids. | | `AB4826` | error | A route's static `config` calls `appResourceUri('')` with a reference that matches no App route of the route's own generated server with a static `config.resourceUri`: an unknown name, another server's App (a generated server registers only its own Apps), or a reference from a non-MCP route. The message names the cause and lists the server's known App route ids; reference the App as `''`, `'/'`, `'app:/'`, or a relative module path. | -| `AB4827` | error | An MCP App route's `config.template` is ambiguous or missing: both the route-relative and the project-root-relative interpretation name different existing files, or neither exists. The message names both candidate paths; templates resolve relative to the route module, so rewrite the path as `'./.html'` beside the route. | +| `AB4827` | error | An MCP App route's route-relative `config.template` names no existing file. The message names the resolved candidate; point it at `'./.html'` beside the route. | | `AB4828` | error | A generated MCP route advertises `_meta.ui.resourceUri` of an App on its server (through `appResourceUri()` or a literal) that is not built for every target the server ships to, because the App's `config.targets` (or a config-declared App's `targets`) is narrower. Widen the App's targets or restrict `mcp.servers..targets`. | | `AB4829` | error | Two distinct MCP App routes of one generated server declare the same static `config.resourceUri`. The message names both route files and the server; a generated server registers one App per resource URI and never picks a side. The same URI on App routes of *different* servers is not a collision — each server registers only its own Apps. Give each App route of the server a distinct `config.resourceUri`, or remove the duplicate module. | | `AB4830` | error | A conventional layout module (`src/layout.*`, `src/mcp//layout.*`) does not satisfy the layout contract: it has no default export, it exports the route-only `config`/`inputSchema`/`resultSchema`, or it exports `execute`/`render`. Default-export one component receiving `{ children, route, signal }` that renders `Agent.Result` around `children`. | @@ -1373,7 +1358,7 @@ installed plugin from it; `AB7303` is emitted only when that listing is unusable the host owns those copies, so replacement runs `claude plugin uninstall --keep-data` + `install` or `codex plugin remove` + `add`. -| Installed copy | `install` | `install --replace` (alias `--force`) | Doctor | +| Installed copy | `install` | `install --replace` | Doctor | | --- | --- | --- | --- | | Identical content (receipt / host-managed) | `already-installed` no-op | `already-installed` no-op | `current` | | Identical content (legacy) | `already-installed` no-op | `adopted` — receipt written, no plugin file changes | `current` | diff --git a/docs/entry-conventions.md b/docs/entry-conventions.md index bbf503ea5..84240e923 100644 --- a/docs/entry-conventions.md +++ b/docs/entry-conventions.md @@ -71,8 +71,8 @@ entries carry `provenance.kind: 'conventional'` in the normalized model. | `src/cli.ts` | Package bin named after `plugin.name` (skipped when the name is not a safe output name). | `bin: false` | | `src/index.ts` | Library output with declarations. | `lib: false` | | `src/mcp/.ts` | Stdio entry for the declared MCP server `` that names no `entry`, `command`, or `url`. | Declare `entry` explicitly | -| `src/mcp//{tools,resources,prompts}/*.{ts,tsx}` | Generated MCP server routes; path supplies identity and each executable module supplies static `config`, schemas, and one default Server Component. | Set `routes.servers.` to `custom`, `command`, or `remote` | -| `src/mcp//apps/*.{ts,tsx}` | Browser MCP App entry compiled to self-contained HTML and registered on the generated server; static `config.resourceUri` is required (`AB4812`), and two App routes of one server sharing a URI are `AB4829` (the same URI on different servers is not a collision). An optional `config.template` HTML shell resolves relative to the route module like its imports (`'./dashboard.html'`); the legacy project-root-relative form is accepted only while unambiguous (`AB4827` otherwise). Tools, resources, and prompts reference the App from their own static `config` with `appResourceUri('')` from `agent-bundle/routes` or a shared `const` string literal instead of repeating the `ui://` literal. The view talks to its host through `createAppClient()` from the browser-safe `agent-bundle/app` (see [`agent-bundle/app`](#agent-bundleapp--the-app-side-bridge-client)), typed by the generated `AppRegister` augmentation. | Use a custom server or prefix the file with `_` | +| `src/mcp//{tools,resources,prompts}/*.{ts,tsx}` | Generated MCP server routes; path supplies identity. Tool modules default-export a direct `defineTool({ inputSchema, resultSchema, ... }, handler)` call; resources and prompts supply their static `config`, schemas, and default Server Component. | Set `routes.servers.` to `custom`, `command`, or `remote` | +| `src/mcp//apps/*.{ts,tsx}` | Browser MCP App entry compiled to self-contained HTML and registered on the generated server; static `config.resourceUri` is required (`AB4812`), and two App routes of one server sharing a URI are `AB4829` (the same URI on different servers is not a collision). An optional `config.template` HTML shell resolves relative to the route module like its imports (`'./dashboard.html'`) and reports `AB4827` when missing. Tools, resources, and prompts reference the App from their own static `config` with `appResourceUri('')` from `agent-bundle/routes` or a shared `const` string literal instead of repeating the `ui://` literal. The view talks to its host through `createAppClient()` from the browser-safe `agent-bundle/app` (see [`agent-bundle/app`](#agent-bundleapp--the-app-side-bridge-client)), typed by the generated `AppRegister` augmentation. | Use a custom server or prefix the file with `_` | | `src/scripts/.ts` | Plain script compiled once to `scripts/.mjs` in the plugin root, shared by every selected host — the same pipeline explicit `scripts` entries use, with ordinary Node stdout/stderr semantics. A `scripts` entry that references the file claims it. Nested modules are hard errors (`AB4808`). A `bin` entry that references the file does **not** claim it: the module ships as both the npm bin and the artifact script (see [Which config keys claim a conventional module](#which-config-keys-claim-a-conventional-module)); export `main` or make the module self-executing, because a `default`-only module would run as the bin but ship as an inert script (`AB4738`). | Prefix a path segment with `_`, or claim the file with an explicit `scripts` entry | | `src/scripts/.tsx` | Rendered script: the default component receives `{ argv, signal }` and renders through the Agent renderer with the CLI output contract (`--json`, `--ndjson`, TTY progress, piped Markdown). Compiles to `scripts/.mjs` plus a `scripts/-flight.mjs` react-server worker. The extension is the explicit, visible contract — plain `.ts` scripts are never wrapped in React behavior, and explicit `scripts` config entries stay plain regardless of extension. A `bin` entry that references a rendered script is `AB4737` unless the module exports both the default component (for the script) and a named `main` (for the bin envelope); with both, the module serves both surfaces. | Rename to `.ts`, prefix a path segment with `_`, or claim the file with an explicit `scripts` entry | | `src/cli/**/*.{ts,tsx}` | Routed CLI commands compiled into one collision-checked command graph and one generated package executable named after `plugin.name` (superseding the `src/cli.ts` bin convention for the project), plus the same executable as `bin/.mjs` in the plugin root whenever a selected host publishes the `cli` capability (all built-in hosts do). Nesting is identity: `src/cli/library/audit.ts` runs as ` library audit`. Plain `.ts` commands execute directly and print one canonical JSON line; `.tsx` commands render through the dispatcher with the four output modes. | `bin: false`, `routes.cli: 'conventional'`, or prefix a path segment with `_` | @@ -1127,12 +1127,11 @@ a hand-written `src/lib/version.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. +`version` is 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 diff --git a/docs/framework-mode.md b/docs/framework-mode.md index d5aecb4f5..94e41ae91 100644 --- a/docs/framework-mode.md +++ b/docs/framework-mode.md @@ -10,8 +10,8 @@ Agent Bundle has one newcomer model: static resources, and `agent-bundle.config.ts` stays at the project root. 2. **One small flat config.** `agent-bundle.config.ts` holds project identity, targets, and policy that no route file can own. The release version is - not repeated there: `package.json` is the single version source - (`plugin.version` is deprecated; see [Diagnostics](diagnostics.md#release-identity-ab4001-ab4008ab4011-ab4013)). + not repeated there: `package.json` is the single version source; see + [Diagnostics](diagnostics.md#release-identity-ab4009ab4011-ab4013). 3. **JSX = rendering.** An executable route is one async default Server Component. It does the work and returns `Agent.*`; there is no public `execute`/`render` split. @@ -627,7 +627,7 @@ content, records an install receipt (`.agent-bundle-install.json`: plugin, version, content hash, owned files and directories), replaces a same-version stale copy of its own plugin in place (owned files only; legacy `state/` survives, while current builds keep framework state outside the plugin root), and accepts -`--replace` (alias `--force`) to replace a different installed version or adopt +`--replace` to replace a different installed version or adopt a pre-receipt copy. Foreign directories are refused with a content-hash comparison. It never invokes sudo or changes PATH. `agent-bundle install [--replace]` applies the same policy for every host, and `agent-bundle doctor diff --git a/examples/hooks-and-scripts/package.json b/examples/hooks-and-scripts/package.json index fecc60079..423ab33c5 100644 --- a/examples/hooks-and-scripts/package.json +++ b/examples/hooks-and-scripts/package.json @@ -4,6 +4,7 @@ "private": true, "license": "Apache-2.0", "type": "module", + "version": "1.0.0", "scripts": { "build": "agent-bundle build", "check": "pnpm validate && pnpm build && pnpm typecheck", diff --git a/examples/mcp-app/package.json b/examples/mcp-app/package.json index 86c2f02f8..70da99ec5 100644 --- a/examples/mcp-app/package.json +++ b/examples/mcp-app/package.json @@ -4,6 +4,7 @@ "private": true, "license": "Apache-2.0", "type": "module", + "version": "1.0.0", "scripts": { "build": "agent-bundle build", "check": "pnpm validate && pnpm build && pnpm typecheck", diff --git a/examples/skills-starter/package.json b/examples/skills-starter/package.json index 2e25b3396..a9ae28098 100644 --- a/examples/skills-starter/package.json +++ b/examples/skills-starter/package.json @@ -4,6 +4,7 @@ "private": true, "license": "Apache-2.0", "type": "module", + "version": "1.0.0", "scripts": { "build": "agent-bundle build", "check": "pnpm validate && pnpm build", diff --git a/fixtures/integration/canonical-collision/agent-bundle.config.ts b/fixtures/integration/canonical-collision/agent-bundle.config.ts index 54835b641..f145ae0eb 100644 --- a/fixtures/integration/canonical-collision/agent-bundle.config.ts +++ b/fixtures/integration/canonical-collision/agent-bundle.config.ts @@ -1,5 +1,5 @@ export default { - plugin: { name: 'canonical-collision', version: '1.0.0' }, + plugin: { name: 'canonical-collision' }, scripts: { bundle: './src/bundle.ts', 'dir/../bundle': './src/bundle.ts', diff --git a/fixtures/integration/canonical-collision/package.json b/fixtures/integration/canonical-collision/package.json new file mode 100644 index 000000000..3e72c21ef --- /dev/null +++ b/fixtures/integration/canonical-collision/package.json @@ -0,0 +1,6 @@ +{ + "name": "agent-bundle-canonical-collision-fixture", + "private": true, + "type": "module", + "version": "1.0.0" +} diff --git a/fixtures/integration/comprehensive/agent-bundle.config.ts b/fixtures/integration/comprehensive/agent-bundle.config.ts index f28282847..1b5f91a55 100644 --- a/fixtures/integration/comprehensive/agent-bundle.config.ts +++ b/fixtures/integration/comprehensive/agent-bundle.config.ts @@ -29,7 +29,6 @@ export default { plugin: { description: 'A complete Agent Bundle compiler integration fixture.', name: 'integration-fixture', - version: '1.0.0', }, scripts: { bundle: './src/bundle.ts', diff --git a/fixtures/integration/comprehensive/package.json b/fixtures/integration/comprehensive/package.json index b73084d85..e453ea48c 100644 --- a/fixtures/integration/comprehensive/package.json +++ b/fixtures/integration/comprehensive/package.json @@ -4,5 +4,6 @@ "@modelcontextprotocol/server": "2.0.0" }, "private": true, - "type": "module" + "type": "module", + "version": "1.0.0" } diff --git a/fixtures/integration/micro-eval/agent-bundle.config.ts b/fixtures/integration/micro-eval/agent-bundle.config.ts index 15c8de473..367d5dc0e 100644 --- a/fixtures/integration/micro-eval/agent-bundle.config.ts +++ b/fixtures/integration/micro-eval/agent-bundle.config.ts @@ -2,7 +2,6 @@ export default { plugin: { description: 'A minimal end-to-end spot-check fixture with one skill and one deterministic eval.', name: 'micro-eval', - version: '1.0.0', }, targets: ['portable'], }; diff --git a/fixtures/integration/micro-eval/package.json b/fixtures/integration/micro-eval/package.json new file mode 100644 index 000000000..48d147b7e --- /dev/null +++ b/fixtures/integration/micro-eval/package.json @@ -0,0 +1,6 @@ +{ + "name": "agent-bundle-micro-eval-fixture", + "private": true, + "type": "module", + "version": "1.0.0" +} diff --git a/fixtures/integration/packed-release/agent-bundle.config.ts b/fixtures/integration/packed-release/agent-bundle.config.ts index 9e586ab78..58d35554d 100644 --- a/fixtures/integration/packed-release/agent-bundle.config.ts +++ b/fixtures/integration/packed-release/agent-bundle.config.ts @@ -20,7 +20,6 @@ export default { plugin: { description: 'Exercises an installed Agent Bundle release without workspace sources.', name: 'packed-release-fixture', - version: '1.0.0', }, scripts: { review: './src/review.ts', diff --git a/fixtures/integration/packed-release/package.json b/fixtures/integration/packed-release/package.json index 55292deb3..aec7266dd 100644 --- a/fixtures/integration/packed-release/package.json +++ b/fixtures/integration/packed-release/package.json @@ -1,5 +1,6 @@ { "name": "agent-bundle-packed-release-fixture", "private": true, - "type": "module" + "type": "module", + "version": "1.0.0" } diff --git a/fixtures/integration/skills-only/agent-bundle.config.ts b/fixtures/integration/skills-only/agent-bundle.config.ts index 01a3a25ff..20ac5c95c 100644 --- a/fixtures/integration/skills-only/agent-bundle.config.ts +++ b/fixtures/integration/skills-only/agent-bundle.config.ts @@ -2,7 +2,6 @@ export default { plugin: { description: 'A portable skills-only fixture.', name: 'portable-skills-only', - version: '1.0.0', }, targets: ['portable'], }; diff --git a/fixtures/integration/skills-only/package.json b/fixtures/integration/skills-only/package.json new file mode 100644 index 000000000..eb553b49a --- /dev/null +++ b/fixtures/integration/skills-only/package.json @@ -0,0 +1,6 @@ +{ + "name": "agent-bundle-skills-only-fixture", + "private": true, + "type": "module", + "version": "1.0.0" +} diff --git a/fixtures/integration/unsupported-capability/agent-bundle.config.ts b/fixtures/integration/unsupported-capability/agent-bundle.config.ts index 629d72405..af4a97b66 100644 --- a/fixtures/integration/unsupported-capability/agent-bundle.config.ts +++ b/fixtures/integration/unsupported-capability/agent-bundle.config.ts @@ -2,6 +2,6 @@ export default { hooks: { sessionStart: { handler: './src/hook.ts', targets: ['portable'] }, }, - plugin: { name: 'unsupported-capability', version: '1.0.0' }, + plugin: { name: 'unsupported-capability' }, targets: ['portable'], }; diff --git a/fixtures/integration/unsupported-capability/package.json b/fixtures/integration/unsupported-capability/package.json new file mode 100644 index 000000000..d4f1a212f --- /dev/null +++ b/fixtures/integration/unsupported-capability/package.json @@ -0,0 +1,6 @@ +{ + "name": "agent-bundle-unsupported-capability-fixture", + "private": true, + "type": "module", + "version": "1.0.0" +} diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index 3bf5db550..2240303de 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -270,7 +270,7 @@ every host treats it differently. `agent-bundle install` and the emitted version-gated and a plain reinstall reports "already installed" while the cache stays stale. Codex replacement runs `codex plugin remove` before `marketplace add` + `add`, so files a rebuild removed do not linger. -- **`--replace` (alias `--force`).** Also replaces an agent-bundle install of +- **`--replace`.** Also replaces an agent-bundle install of the same plugin at a *different* version, and adopts a Cursor copy that was installed before receipts existed (recognised by its emitted `INSTALL.md` + `install.mjs` and matching manifest name). A legacy copy has no owned-file diff --git a/packages/agent-bundle/fixtures/durable-web-surface/agent-bundle.config.ts b/packages/agent-bundle/fixtures/durable-web-surface/agent-bundle.config.ts index 67bec4d46..2828089e1 100644 --- a/packages/agent-bundle/fixtures/durable-web-surface/agent-bundle.config.ts +++ b/packages/agent-bundle/fixtures/durable-web-surface/agent-bundle.config.ts @@ -21,7 +21,6 @@ export default { plugin: { description: 'A workspace-durable plugin whose MCP App is exposed through web.apps and whose CLI reads the same state.', name: 'durable-web-surface-fixture', - version: '1.0.0', }, targets: ['portable'], web: { apps: [{ allow: ['call-tool'], app: 'journal/status' }] }, diff --git a/packages/agent-bundle/fixtures/durable-web-surface/package.json b/packages/agent-bundle/fixtures/durable-web-surface/package.json index a5879a15f..9f61fdfc9 100644 --- a/packages/agent-bundle/fixtures/durable-web-surface/package.json +++ b/packages/agent-bundle/fixtures/durable-web-surface/package.json @@ -1,5 +1,6 @@ { "name": "durable-web-surface-fixture", "private": true, - "type": "module" + "type": "module", + "version": "1.0.0" } diff --git a/packages/agent-bundle/fixtures/durable-web-surface/src/mcp/journal/tools/record.tsx b/packages/agent-bundle/fixtures/durable-web-surface/src/mcp/journal/tools/record.tsx index 3e4b87998..f2564c5bd 100644 --- a/packages/agent-bundle/fixtures/durable-web-surface/src/mcp/journal/tools/record.tsx +++ b/packages/agent-bundle/fixtures/durable-web-surface/src/mcp/journal/tools/record.tsx @@ -1,7 +1,19 @@ import { Agent, agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; import { z } from 'zod'; -export const config = { +export const inputSchema = z.object({ note: z.string().min(1) }).strict(); + +export const resultSchema = z.object({ + entries: z.array(z.object({ note: z.string() }).strict()), + revision: z.number().int().nonnegative(), +}).strict(); + +interface JournalState { + readonly entries: readonly { readonly note: string }[]; +} + +export default defineTool({ inputJsonSchema: { "additionalProperties": false, "properties": { @@ -16,21 +28,10 @@ export const config = { }, _meta: { ui: { resourceUri: 'ui://durable-web-surface-fixture/status.html' } }, description: 'Appends one note to the durable journal and reports every entry.', + inputSchema, + resultSchema, title: 'Record', -}; - -export const inputSchema = z.object({ note: z.string().min(1) }).strict(); - -export const resultSchema = z.object({ - entries: z.array(z.object({ note: z.string() }).strict()), - revision: z.number().int().nonnegative(), -}).strict(); - -interface JournalState { - readonly entries: readonly { readonly note: string }[]; -} - -export default async function Record({ input }: { readonly input: z.infer }) { +}, async (input) => { const context = await agent(); if (context.state === undefined) throw new TypeError('Journal state is unavailable.'); // The note is the idempotency key: a replayed note is recorded once. @@ -43,4 +44,4 @@ export default async function Record({ input }: { readonly input: z.infer{`recorded ${String(state.entries.length)} note(s)`} ); -} +}); diff --git a/packages/agent-bundle/fixtures/meta-consumer/agent-bundle.config.ts b/packages/agent-bundle/fixtures/meta-consumer/agent-bundle.config.ts index 15d382976..d208aa314 100644 --- a/packages/agent-bundle/fixtures/meta-consumer/agent-bundle.config.ts +++ b/packages/agent-bundle/fixtures/meta-consumer/agent-bundle.config.ts @@ -1,8 +1,7 @@ // A project whose source imports `agent-bundle/meta` (issue #386). Plain -// object export, like every other repository fixture. `plugin.version` is -// omitted on purpose: the resolved version, `packageName`, and -// `packageVersion` all derive from the sibling package.json, which is the -// identity the Rstest presets must hand to source under test. +// object export, like every other repository fixture. The resolved version, +// `packageName`, and `packageVersion` all derive from the sibling package.json, +// which is the identity the Rstest presets must hand to source under test. export default { plugin: { description: 'Source that reads its own release identity from agent-bundle/meta.', diff --git a/packages/agent-bundle/fixtures/meta-consumer/src/mcp/consumer/tools/identity.ts b/packages/agent-bundle/fixtures/meta-consumer/src/mcp/consumer/tools/identity.ts index d465b0cf7..d00b84bf0 100644 --- a/packages/agent-bundle/fixtures/meta-consumer/src/mcp/consumer/tools/identity.ts +++ b/packages/agent-bundle/fixtures/meta-consumer/src/mcp/consumer/tools/identity.ts @@ -1,20 +1,10 @@ import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; import { createElement } from 'react'; import { z } from 'zod'; import { banner, identity } from '../../../lib/identity.ts'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": {}, - "type": "object" - }, - annotations: { readOnlyHint: true }, - description: 'Reports the identity agent-bundle/meta resolved to.', - title: 'Identity', -}; - export const inputSchema = z.object({}); export const resultSchema = z.object({ @@ -25,7 +15,18 @@ export const resultSchema = z.object({ version: z.string(), }); -export default async function Identity() { +export default defineTool({ + inputJsonSchema: { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + annotations: { readOnlyHint: true }, + description: 'Reports the identity agent-bundle/meta resolved to.', + title: 'Identity', + inputSchema, + resultSchema, +}, async () => { // The document value is JSON; the optional npm axes are omitted when absent // rather than carried as `undefined`. const value = { @@ -36,4 +37,4 @@ export default async function Identity() { version: identity.version, }; return createElement(Agent.Result, { value }, createElement(Agent.Text, null, banner)); -} +}); diff --git a/packages/agent-bundle/fixtures/route-harness/agent-bundle.config.ts b/packages/agent-bundle/fixtures/route-harness/agent-bundle.config.ts index d9bb09fcf..b0c66f126 100644 --- a/packages/agent-bundle/fixtures/route-harness/agent-bundle.config.ts +++ b/packages/agent-bundle/fixtures/route-harness/agent-bundle.config.ts @@ -5,7 +5,6 @@ export default { plugin: { description: 'Conventional route modules for the route-unit test harness.', name: 'route-harness', - version: '1.0.0', }, routes: { mcpCommands: true, diff --git a/packages/agent-bundle/fixtures/route-harness/package.json b/packages/agent-bundle/fixtures/route-harness/package.json new file mode 100644 index 000000000..a0e955605 --- /dev/null +++ b/packages/agent-bundle/fixtures/route-harness/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "type": "module", + "version": "1.0.0" +} diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/catalog.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/catalog.tsx index 186707400..082717132 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/catalog.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/catalog.tsx @@ -1,23 +1,8 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent } from '@agent-bundle/runtime'; import { Suspense } from 'react'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "genre": { - "type": "string" - } - }, - "type": "object" - }, - description: 'Streams the harness catalog behind one Suspense boundary.', - // Its streamed Agent.Progress fallback is what a task reports through tasks/get (#369). - execution: { taskSupport: 'optional' }, - title: 'Catalog', -}; - export const inputSchema = z.object({ genre: z.string().optional() }); export const resultSchema = z.object({ genre: z.string(), titles: z.array(z.string()) }); @@ -32,7 +17,7 @@ const Titles = async ({ genre }: { readonly genre: string }) => { return {`## ${genre}\n\n${titles.map((title) => `- ${title}`).join('\n')}`}; }; -export default async function Catalog({ input }: { readonly input: z.infer }) { +async function Catalog({ input }: { readonly input: z.infer }) { const genre = input.genre ?? 'all'; return ( @@ -43,3 +28,21 @@ export default async function Catalog({ input }: { readonly input: z.infer ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "genre": { + "type": "string" + } + }, + "type": "object" + }, + description: 'Streams the harness catalog behind one Suspense boundary.', + // Its streamed Agent.Progress fallback is what a task reports through tasks/get (#369). + execution: { taskSupport: 'optional' }, + title: 'Catalog', + inputSchema, + resultSchema, +}, async (input) => Catalog({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/context.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/context.tsx index 0b6f1914a..81e9ae9ee 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/context.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/context.tsx @@ -1,23 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent, type JsonValue } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "session": { - "type": "string" - } - }, - "type": "object" - }, - description: 'Returns the request identity axes observed by this route.', - title: 'Context', -}; - export const inputSchema = z.object({ host: z.string().optional(), session: z.string().optional(), @@ -32,7 +16,7 @@ export const resultSchema = z.object({ workspace: z.unknown(), }).strict(); -export default async function Context() { +async function Context() { const context = await agent(); const actor: JsonValue = context.actor.state === 'available' ? { source: context.actor.source, state: context.actor.state, value: { id: context.actor.value.id } } @@ -66,3 +50,22 @@ export default async function Context() { ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "session": { + "type": "string" + } + }, + "type": "object" + }, + description: 'Returns the request identity axes observed by this route.', + title: 'Context', + inputSchema, + resultSchema, +}, async () => Context()); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/echo.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/echo.tsx index 75ea40ac4..83cc01010 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/echo.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/echo.tsx @@ -1,21 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "message": { - "type": "string" - } - }, - "type": "object" - }, - annotations: { readOnlyHint: true }, - description: 'Echoes one message back with the observed workspace root.', - title: 'Echo', -}; - export const inputSchema = z.object({ message: z.string().optional() }); export const resultSchema = z.object({ @@ -24,7 +10,7 @@ export const resultSchema = z.object({ workspace: z.string().nullable(), }); -export default async function Echo({ input }: { readonly input: z.infer }) { +async function Echo({ input }: { readonly input: z.infer }) { const context = await agent(); await context.progress.report({ completed: 1, message: 'echoing', total: 1 }); const workspace = context.workspace.state === 'available' ? context.workspace.value.root : null; @@ -36,3 +22,20 @@ export default async function Echo({ input }: { readonly input: z.infer ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + }, + annotations: { readOnlyHint: true }, + description: 'Echoes one message back with the observed workspace root.', + title: 'Echo', + inputSchema, + resultSchema, +}, async (input) => Echo({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/fault.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/fault.tsx index 13e72e828..59d507f61 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/fault.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/fault.tsx @@ -1,27 +1,8 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent } from '@agent-bundle/runtime'; import { Suspense } from 'react'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "mode": { - "enum": [ - "ok", - "throw", - "reject-boundary" - ], - "type": "string", - "default": "ok" - } - }, - "type": "object" - }, - annotations: { readOnlyHint: true }, - description: 'Throws from the route or from a nested Suspense boundary, for thrown-error projection proof.', -}; - export const inputSchema = z.object({ mode: z.enum(['ok', 'throw', 'reject-boundary']).default('ok'), }); @@ -42,7 +23,7 @@ const Rejecting = async () => { * `reject-boundary` streams a shell whose nested Suspense child rejects. * Neither renders `Agent.Error` — that represented path is `unavailable.tsx`. */ -export default async function Fault({ input }: { readonly input: z.infer }) { +async function Fault({ input }: { readonly input: z.infer }) { if (input.mode === 'throw') throw new Error('fault: route threw'); return ( @@ -57,3 +38,25 @@ export default async function Fault({ input }: { readonly input: z.infer ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "mode": { + "enum": [ + "ok", + "throw", + "reject-boundary" + ], + "type": "string", + "default": "ok" + } + }, + "type": "object" + }, + annotations: { readOnlyHint: true }, + description: 'Throws from the route or from a nested Suspense boundary, for thrown-error projection proof.', + inputSchema, + resultSchema, +}, async (input) => Fault({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/journal.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/journal.tsx index 890208561..074fb83ee 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/journal.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/journal.tsx @@ -1,20 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "note": { - "type": "string" - } - }, - "type": "object" - }, - description: 'Records and reads durable route-harness journal entries.', - title: 'Journal', -}; - export const inputSchema = z.object({ note: z.string().optional() }).strict(); export const resultSchema = z.object({ @@ -26,7 +13,7 @@ interface JournalState { readonly entries: readonly { readonly note: string }[]; } -export default async function Journal({ input }: { readonly input: z.infer }) { +async function Journal({ input }: { readonly input: z.infer }) { const context = await agent(); if (context.state === undefined) throw new TypeError('Journal state is unavailable.'); if (input.note !== undefined) { @@ -47,3 +34,19 @@ export default async function Journal({ input }: { readonly input: z.infer ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "note": { + "type": "string" + } + }, + "type": "object" + }, + description: 'Records and reads durable route-harness journal entries.', + title: 'Journal', + inputSchema, + resultSchema, +}, async (input) => Journal({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/layout-probe.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/layout-probe.tsx index a7c6d59fc..a705e99ba 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/layout-probe.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/layout-probe.tsx @@ -1,8 +1,21 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { +export const inputSchema = z.object({ label: z.string().default('probe') }); + +export const resultSchema = z.object({ label: z.string() }); + +async function LayoutProbe({ input }: { readonly input: z.infer }) { + return ( + + {`probe: ${input.label}`} + + ); +} + +export default defineTool({ +inputJsonSchema: { "additionalProperties": false, "properties": { "label": { @@ -15,16 +28,6 @@ export const config = { annotations: { readOnlyHint: true }, description: 'Renders a bare valued result so the layout chain around it is observable.', title: 'Layout probe', -}; - -export const inputSchema = z.object({ label: z.string().default('probe') }); - -export const resultSchema = z.object({ label: z.string() }); - -export default async function LayoutProbe({ input }: { readonly input: z.infer }) { - return ( - - {`probe: ${input.label}`} - - ); -} + inputSchema, + resultSchema, +}, async (input) => LayoutProbe({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/lifecycle.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/lifecycle.tsx index d32afaebf..ea339480f 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/lifecycle.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/lifecycle.tsx @@ -1,3 +1,4 @@ +import { defineTool } from 'agent-bundle/routes'; import { setTimeout as delay } from 'node:timers/promises'; import { Agent, agent } from '@agent-bundle/runtime'; @@ -12,47 +13,6 @@ const lifecyclePhaseSchema = z.enum([ 'terminal', ]); -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "exceed-budget", - "observe", - "transition" - ], - "type": "string" - }, - "emitProgress": { - "type": "boolean" - }, - "idempotencyKey": { - "type": "string" - }, - "payload": { - "type": "string" - }, - "phase": { - "enum": [ - "queued", - "running", - "first-progress", - "repeated-progress", - "terminal" - ], - "type": "string" - } - }, - "required": [ - "action" - ], - "type": "object" - }, - description: 'Replays a deterministic durable lifecycle through mounted state.', - title: 'Lifecycle', -}; - export const inputSchema = z.object({ action: z.enum(['exceed-budget', 'observe', 'transition']), emitProgress: z.boolean().optional(), @@ -79,7 +39,7 @@ interface LifecycleState { }; } -export default async function Lifecycle({ input }: { readonly input: z.infer }) { +async function Lifecycle({ input }: { readonly input: z.infer }) { const context = await agent(); if (context.state === undefined) throw new TypeError('Lifecycle state is unavailable.'); @@ -174,3 +134,46 @@ export default async function Lifecycle({ input }: { readonly input: z.infer ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "action": { + "enum": [ + "exceed-budget", + "observe", + "transition" + ], + "type": "string" + }, + "emitProgress": { + "type": "boolean" + }, + "idempotencyKey": { + "type": "string" + }, + "payload": { + "type": "string" + }, + "phase": { + "enum": [ + "queued", + "running", + "first-progress", + "repeated-progress", + "terminal" + ], + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + description: 'Replays a deterministic durable lifecycle through mounted state.', + title: 'Lifecycle', + inputSchema, + resultSchema, +}, async (input) => Lifecycle({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/mutation-probe.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/mutation-probe.tsx index f07fafced..00e844191 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/mutation-probe.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/mutation-probe.tsx @@ -1,22 +1,9 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent } from '@agent-bundle/runtime'; import { z } from 'zod'; let executions = 0; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "marker": { - "type": "string" - } - }, - "type": "object" - }, - description: 'Records how many times the mutation probe executed.', - title: 'Mutation probe', -}; - export const inputSchema = z.object({ marker: z.string().optional(), }).strict(); @@ -28,7 +15,7 @@ export const resultSchema = z.object({ operationId: z.string(), }).strict(); -export default async function MutationProbe({ +async function MutationProbe({ input, }: { readonly input: z.infer; @@ -47,3 +34,19 @@ export default async function MutationProbe({ ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "marker": { + "type": "string" + } + }, + "type": "object" + }, + description: 'Records how many times the mutation probe executed.', + title: 'Mutation probe', + inputSchema, + resultSchema, +}, async (input) => MutationProbe({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/plugin-root.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/plugin-root.tsx index c0fc62bca..ab7ebb9dd 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/plugin-root.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/plugin-root.tsx @@ -1,17 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent, type JsonValue } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": {}, - "type": "object" - }, - annotations: { readOnlyHint: true }, - description: 'Reports the plugin root and durable-state anchor this route observes.', - title: 'Plugin root', -}; - export const inputSchema = z.object({}).strict(); export const resultSchema = z.object({ @@ -23,7 +13,7 @@ export const resultSchema = z.object({ * level can assert the anchor a generated scope resolved from * `AGENT_BUNDLE_PLUGIN_ROOT` (or its fallback) reached the request. */ -export default async function PluginRoot() { +async function PluginRoot() { const { plugin } = await agent(); const observed: JsonValue = plugin.state === 'available' ? { source: plugin.source, state: plugin.state, value: { root: plugin.value.root, stateRoot: plugin.value.stateRoot } } @@ -34,3 +24,16 @@ export default async function PluginRoot() { ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + annotations: { readOnlyHint: true }, + description: 'Reports the plugin root and durable-state anchor this route observes.', + title: 'Plugin root', + inputSchema, + resultSchema, +}, async () => PluginRoot()); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/publish-notice.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/publish-notice.tsx index 9837a61df..6995746db 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/publish-notice.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/publish-notice.tsx @@ -1,36 +1,8 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent } from '@agent-bundle/runtime'; import type { AgentNoticePublishInput } from '@agent-bundle/runtime/notices'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "message": { - "type": "string" - }, - "recipientSession": { - "type": "string" - }, - "sensitivity": { - "enum": [ - "public", - "internal", - "secret" - ], - "type": "string" - } - }, - "required": [ - "message", - "recipientSession" - ], - "type": "object" - }, - description: 'Publishes a durable notice for a later session event.', - title: 'Publish notice', -}; - export const inputSchema = z.object({ message: z.string(), recipientSession: z.string(), @@ -44,7 +16,7 @@ export const resultSchema = z.object({ state: z.literal('pending'), }).strict(); -export default async function PublishNotice({ input }: { readonly input: z.infer }) { +async function PublishNotice({ input }: { readonly input: z.infer }) { const context = await agent(); if (context.notices === undefined) throw new TypeError('Notice publishing is unavailable.'); // `satisfies` pins the publish API surface: a vocabulary change on @@ -75,3 +47,34 @@ export default async function PublishNotice({ input }: { readonly input: z.infer ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + }, + "recipientSession": { + "type": "string" + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "secret" + ], + "type": "string" + } + }, + "required": [ + "message", + "recipientSession" + ], + "type": "object" + }, + description: 'Publishes a durable notice for a later session event.', + title: 'Publish notice', + inputSchema, + resultSchema, +}, async (input) => PublishNotice({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/select.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/select.tsx index 9ed1964d2..81b146718 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/select.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/select.tsx @@ -1,12 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - annotations: { readOnlyHint: false }, - description: 'Selects catalog entries by id or by query, with nested filters no flag grammar can spell.', - title: 'Select', -}; - // A discriminated union under a nested object: the bounded argv grammar has // no flag form for it, so the tool's CLI projection takes canonical JSON. export const inputSchema = z.object({ @@ -29,7 +24,7 @@ export const resultSchema = z.object({ ]), }); -export default async function Select({ input }: { readonly input: z.infer }) { +async function Select({ input }: { readonly input: z.infer }) { const context = await agent(); const value = { filters: input.filters, invocation: context.invocation.kind as 'cli' | 'tool', selection: input.selection }; return ( @@ -38,3 +33,11 @@ export default async function Select({ input }: { readonly input: z.infer ); } + +export default defineTool({ +annotations: { readOnlyHint: false }, + description: 'Selects catalog entries by id or by query, with nested filters no flag grammar can spell.', + title: 'Select', + inputSchema, + resultSchema, +}, async (input) => Select({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/strict-report.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/strict-report.tsx index 46d8703ab..8b6dad53a 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/strict-report.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/strict-report.tsx @@ -1,21 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "reportId": { - "type": "string" - } - }, - "type": "object" - }, - _meta: { ui: { resourceUri: 'ui://route-harness/panel.html' } }, - description: 'Returns a closed-object report that rejects unknown serialized keys.', - title: 'Strict report', -}; - export const inputSchema = z.object({ reportId: z.string().optional() }); export const resultSchema = z.strictObject({ @@ -23,7 +9,7 @@ export const resultSchema = z.strictObject({ summary: z.string(), }); -export default async function StrictReport({ input }: { readonly input: z.infer }) { +async function StrictReport({ input }: { readonly input: z.infer }) { const reportId = input.reportId ?? 'report-1'; const value = { reportId, summary: `summary for ${reportId}` }; // The MCP Apps convention stamps the App resource on every result as well @@ -34,3 +20,20 @@ export default async function StrictReport({ input }: { readonly input: z.infer< ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "reportId": { + "type": "string" + } + }, + "type": "object" + }, + _meta: { ui: { resourceUri: 'ui://route-harness/panel.html' } }, + description: 'Returns a closed-object report that rejects unknown serialized keys.', + title: 'Strict report', + inputSchema, + resultSchema, +}, async (input) => StrictReport({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/submit.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/submit.tsx index 045f27bf2..a1ef48206 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/submit.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/submit.tsx @@ -1,8 +1,47 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { +export const inputSchema = z.object({ + argv: z.array(z.string()).min(1).describe('The command line to run.'), + cwd: z.string().min(1).describe('Working directory of the command.'), + laneKey: z.string().min(1).optional().describe('Lane the work is queued under.'), + regions: z.array(z.enum(['eu', 'us'])).optional().describe('Regions the work may run in.'), + tags: z.array(z.string()).optional().describe('Tags attached to the request.'), +}); + +export const resultSchema = z.object({ + argv: z.array(z.string()).min(1), + cwd: z.string().min(1), + laneKey: z.string().optional(), + operation: z.literal('submit'), + regions: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), +}); + +async function Submit({ input }: { readonly input: z.infer }) { + const context = await agent(); + const value = { + argv: input.argv, + cwd: input.cwd, + ...(input.laneKey === undefined ? {} : { laneKey: input.laneKey }), + operation: 'submit' as const, + ...(input.regions === undefined ? {} : { regions: input.regions }), + ...(input.tags === undefined ? {} : { tags: input.tags }), + }; + const { invocation } = context; + const libraryTooling = await context.provider('libraryTooling'); + return ( + + {`submit: ${input.argv.join(' ')}`} + {`invocation: ${invocation.kind} ${invocation.operationId ?? '(no operation)'} ${invocation.surface ?? '(no surface)'}`} + {`provider: ${JSON.stringify(libraryTooling)}`} + + ); +} + +export default defineTool({ +inputJsonSchema: { "additionalProperties": false, "properties": { "argv": { @@ -48,42 +87,6 @@ export const config = { annotations: { readOnlyHint: false }, description: 'Submits one command line as lane work and echoes the accepted request.', title: 'Submit', -}; - -export const inputSchema = z.object({ - argv: z.array(z.string()).min(1).describe('The command line to run.'), - cwd: z.string().min(1).describe('Working directory of the command.'), - laneKey: z.string().min(1).optional().describe('Lane the work is queued under.'), - regions: z.array(z.enum(['eu', 'us'])).optional().describe('Regions the work may run in.'), - tags: z.array(z.string()).optional().describe('Tags attached to the request.'), -}); - -export const resultSchema = z.object({ - argv: z.array(z.string()).min(1), - cwd: z.string().min(1), - laneKey: z.string().optional(), - operation: z.literal('submit'), - regions: z.array(z.string()).optional(), - tags: z.array(z.string()).optional(), -}); - -export default async function Submit({ input }: { readonly input: z.infer }) { - const context = await agent(); - const value = { - argv: input.argv, - cwd: input.cwd, - ...(input.laneKey === undefined ? {} : { laneKey: input.laneKey }), - operation: 'submit' as const, - ...(input.regions === undefined ? {} : { regions: input.regions }), - ...(input.tags === undefined ? {} : { tags: input.tags }), - }; - const { invocation } = context; - const libraryTooling = await context.provider('libraryTooling'); - return ( - - {`submit: ${input.argv.join(' ')}`} - {`invocation: ${invocation.kind} ${invocation.operationId ?? '(no operation)'} ${invocation.surface ?? '(no surface)'}`} - {`provider: ${JSON.stringify(libraryTooling)}`} - - ); -} + inputSchema, + resultSchema, +}, async (input) => Submit({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/ticket.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/ticket.tsx index 460968b8e..3a3d296ad 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/ticket.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/ticket.tsx @@ -1,35 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "includeDiagnostics": { - "type": "boolean" - }, - "includeExecArgv": { - "type": "boolean" - }, - "includeTail": { - "type": "boolean" - }, - "status": { - "enum": [ - "pending", - "running", - "completed", - "failed" - ], - "type": "string" - } - }, - "type": "object" - }, - description: 'Returns a cargo-conductor-shaped ticket status with optional diagnostics fields.', - title: 'Ticket', -}; - export const inputSchema = z.object({ includeDiagnostics: z.boolean().optional(), includeExecArgv: z.boolean().optional(), @@ -44,7 +16,7 @@ export const resultSchema = z.object({ tail: z.string().optional(), }); -export default async function Ticket({ input }: { readonly input: z.infer }) { +async function Ticket({ input }: { readonly input: z.infer }) { const status = input.status ?? 'completed'; const value = { status, @@ -58,3 +30,34 @@ export default async function Ticket({ input }: { readonly input: z.infer ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "includeDiagnostics": { + "type": "boolean" + }, + "includeExecArgv": { + "type": "boolean" + }, + "includeTail": { + "type": "boolean" + }, + "status": { + "enum": [ + "pending", + "running", + "completed", + "failed" + ], + "type": "string" + } + }, + "type": "object" + }, + description: 'Returns a cargo-conductor-shaped ticket status with optional diagnostics fields.', + title: 'Ticket', + inputSchema, + resultSchema, +}, async (input) => Ticket({ input })); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/tooling.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/tooling.tsx index 93a6b7ebb..932317917 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/tooling.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/tooling.tsx @@ -1,24 +1,7 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent, type JsonValue } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "failProvider": { - "type": "boolean" - }, - "inbox": { - "type": "boolean" - } - }, - "type": "object" - }, - annotations: { readOnlyHint: true }, - description: 'Reports the request providers an MCP tool observes.', - title: 'Tooling', -}; - export const inputSchema = z.object({ /** Makes the `library-tooling` provider throw, to prove the request fails closed. */ failProvider: z.boolean().optional(), @@ -33,7 +16,7 @@ export const resultSchema = z.object({ requestView: z.unknown().optional(), }).strict(); -export default async function Tooling() { +async function Tooling() { const context = await agent(); const providers = { processLifetime: context.process, @@ -53,3 +36,23 @@ export default async function Tooling() { ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "failProvider": { + "type": "boolean" + }, + "inbox": { + "type": "boolean" + } + }, + "type": "object" + }, + annotations: { readOnlyHint: true }, + description: 'Reports the request providers an MCP tool observes.', + title: 'Tooling', + inputSchema, + resultSchema, +}, async () => Tooling()); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/unavailable.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/unavailable.tsx index a6afb784f..8cf1f63f3 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/unavailable.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/unavailable.tsx @@ -1,24 +1,27 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent } from '@agent-bundle/runtime'; import { z } from 'zod'; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": {}, - "type": "object" - }, - description: 'Returns a typed unavailable result for projection checks.', - title: 'Unavailable', -}; - export const inputSchema = z.object({}); export const resultSchema = z.object({ available: z.literal(false) }); -export default async function Unavailable() { +async function Unavailable() { return ( The harness fixture represents this capability as unavailable. ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + description: 'Returns a typed unavailable result for projection checks.', + title: 'Unavailable', + inputSchema, + resultSchema, +}, async () => Unavailable()); diff --git a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/wait.tsx b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/wait.tsx index f42179aa3..af949fdca 100644 --- a/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/wait.tsx +++ b/packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/wait.tsx @@ -1,30 +1,9 @@ +import { defineTool } from 'agent-bundle/routes'; import { Agent, agent } from '@agent-bundle/runtime'; import { z } from 'zod'; const maxHoldMs = 5000; -export const config = { - inputJsonSchema: { - "additionalProperties": false, - "properties": { - "holdMs": { - "type": "number" - }, - "tickMs": { - "type": "number" - } - }, - "type": "object" - }, - description: 'Waits until aborted or holdMs elapses, for cancellation contract proof.', - // A long wait a task-aware client may run behind a task (#369) and poll. - execution: { taskSupport: 'optional' }, - // The long-poll shape of #454: a route whose legitimate wait outlives the - // runtime's default render session declares its own budget. - render: { maxElapsedMs: 120_000 }, - title: 'Wait', -}; - export const inputSchema = z.object({ holdMs: z.number().int().positive().optional(), /** Report progress every `tickMs` while holding; omitted means no progress reports. */ @@ -48,7 +27,7 @@ const waitForAbortOrTimeout = async ( }, { once: true }); }); -export default async function Wait({ +async function Wait({ input, signal, }: { @@ -79,3 +58,27 @@ export default async function Wait({ ); } + +export default defineTool({ +inputJsonSchema: { + "additionalProperties": false, + "properties": { + "holdMs": { + "type": "number" + }, + "tickMs": { + "type": "number" + } + }, + "type": "object" + }, + description: 'Waits until aborted or holdMs elapses, for cancellation contract proof.', + // A long wait a task-aware client may run behind a task (#369) and poll. + execution: { taskSupport: 'optional' }, + // The long-poll shape of #454: a route whose legitimate wait outlives the + // runtime's default render session declares its own budget. + render: { maxElapsedMs: 120_000 }, + title: 'Wait', + inputSchema, + resultSchema, +}, async (input, context) => Wait({ input, signal: context.signal })); diff --git a/packages/agent-bundle/fixtures/web-surface/agent-bundle.config.ts b/packages/agent-bundle/fixtures/web-surface/agent-bundle.config.ts index 82ca22d4c..b564bf700 100644 --- a/packages/agent-bundle/fixtures/web-surface/agent-bundle.config.ts +++ b/packages/agent-bundle/fixtures/web-surface/agent-bundle.config.ts @@ -24,7 +24,6 @@ export default { plugin: { description: 'A plugin whose MCP App is exposed through web.apps and opened with web.', name: 'web-surface-fixture', - version: '1.0.0', }, targets: ['portable'], // Omitting `tool` proves unique live-server resolution. diff --git a/packages/agent-bundle/src/api.ts b/packages/agent-bundle/src/api.ts index d253cd063..d5d02c743 100644 --- a/packages/agent-bundle/src/api.ts +++ b/packages/agent-bundle/src/api.ts @@ -41,7 +41,7 @@ import { mcpServerStateDirectory, runMcpForeground } from './services/mcp-run.ts import { serveMcpApp } from './serve-app/serve-mcp-app.ts'; import type { ServedMcpApp, ServeMcpAppPublicOptions } from './serve-app/types.ts'; import { parseAppSelector } from './web-host/select-app.ts'; -export type { McpAppConsentCapability, ServedMcpApp as ServedApp } from './serve-app/types.ts'; +export type { McpAppConsentCapability, ServedMcpApp } from './serve-app/types.ts'; export type { OpenBrowser } from './dev/mcp-apps/mcp-app-preview-host.ts'; export type { McpAppProfileId } from './dev/mcp-app-profile-descriptors.ts'; import { deepFreeze } from './core/freeze.ts'; diff --git a/packages/agent-bundle/src/cli-entry.ts b/packages/agent-bundle/src/cli-entry.ts index edf618872..a8604ae17 100644 --- a/packages/agent-bundle/src/cli-entry.ts +++ b/packages/agent-bundle/src/cli-entry.ts @@ -5,7 +5,6 @@ import { detectProcessTerminal, type AgentTerminal, type ProbedTerminalSurface, - type TerminalStreamProbe, } from './terminal-capability.ts'; export type { AgentTerminal } from './terminal-capability.ts'; @@ -317,24 +316,12 @@ export interface GeneratedCliRenderContext { /** * The terminal capability one shell invocation reports (#511): an explicit * value wins (the in-process harness supplies one), otherwise the process's - * own streams are probed, with the legacy `isTty` knob standing in for - * stdout's TTY-ness so callers that only override that still see a - * consistent capability and output mode. + * own streams are probed. */ const resolveTerminal = ( hostSurface: ProbedTerminalSurface, - options: { readonly isTty?: () => boolean; readonly terminal?: AgentTerminal }, -): AgentTerminal => { - if (options.terminal !== undefined) return options.terminal; - if (options.isTty === undefined) return detectProcessTerminal(hostSurface); - const stdout: TerminalStreamProbe = { - columns: process.stdout.columns, - fd: 1, - isTTY: options.isTty(), - rows: process.stdout.rows, - }; - return detectProcessTerminal(hostSurface, { stdout }); -}; + options: { readonly terminal?: AgentTerminal }, +): AgentTerminal => options.terminal ?? detectProcessTerminal(hostSurface); export interface RunGeneratedCliOptions { readonly argv: readonly string[]; @@ -346,8 +333,6 @@ export interface RunGeneratedCliOptions { input: Readonly>, context: GeneratedCliExecuteContext, ) => Promise; - /** Overrides stdout's TTY-ness only; rendered commands then update progress in place. Prefer `terminal`. */ - readonly isTty?: () => boolean; readonly name: string; /** Opens one rendered run for a resolved `.tsx` command with parsed input. */ readonly render?: ( @@ -1149,8 +1134,6 @@ export interface RunGeneratedRenderedScriptOptions { argv: readonly string[], context: { readonly signal: AbortSignal; readonly terminal: AgentTerminal }, ) => GeneratedCliRenderSession; - /** Overrides stdout's TTY-ness only. Prefer `terminal`. */ - readonly isTty?: () => boolean; readonly name: string; readonly signal?: AbortSignal; /** The terminal capability to report and select the output mode from (#511); probed from the process when omitted. */ diff --git a/packages/agent-bundle/src/config/normalize.ts b/packages/agent-bundle/src/config/normalize.ts index d3e2fdece..df98caab8 100644 --- a/packages/agent-bundle/src/config/normalize.ts +++ b/packages/agent-bundle/src/config/normalize.ts @@ -890,10 +890,10 @@ const normalizeMcpApps = ( : server.targets; const metadata = route.config['_meta']; const template = route.config['template']; - // Route-relative first, legacy project-root-relative when unambiguous; - // the route-graph compiler already reported AB4827 for the other cases. + // The route-graph compiler reports AB4827 when the route-relative + // template does not exist. const templatePath = typeof template === 'string' - ? appRouteTemplatePath(resolveAppRouteTemplate(loaded.context.projectRoot, route.source, template)) + ? appRouteTemplatePath(resolveAppRouteTemplate(route.source, template)) : undefined; apps.push({ ...(isRecord(metadata) ? { _meta: structuredClone(metadata) } : {}), @@ -1299,9 +1299,8 @@ export const normalizeProject = async ( const description = loaded.config.plugin.description; const logo = normalizePluginLogo(loaded); // The npm package axes are derived, never authored in config: package.json - // is authoritative for release identity (issue #94), while plugin.version - // remains the host-facing declared version during the migration. The same - // derivation serves `agent-bundle/meta` to rendered skills at discovery. + // is authoritative for release identity (issue #94). The same derivation + // serves `agent-bundle/meta` to rendered skills at discovery. const identity = pluginIdentity(loaded.context.projectRoot, loaded.config); // The descriptive layer every host projection shares (issue #753): resolved // once here so `author`, `homepage`, `keywords`, `license`, and `repository` diff --git a/packages/agent-bundle/src/config/plugin-identity.ts b/packages/agent-bundle/src/config/plugin-identity.ts index f805870fc..cc824d866 100644 --- a/packages/agent-bundle/src/config/plugin-identity.ts +++ b/packages/agent-bundle/src/config/plugin-identity.ts @@ -9,22 +9,6 @@ import { import { isRecord } from '../core/strict-json.ts'; import type { AgentBundleConfig } from '../core/types.ts'; -/** - * The one plugin version every surface agrees on (issue #94 stage 3): an - * authored `plugin.version` still wins so a legacy declaration never changes - * meaning mid-migration (a disagreement with package.json is the AB4008 - * warning), an omitted one derives the release version from package.json, - * and a project with neither carries the development fallback that - * `agent-bundle build` refuses to package (AB4013). - */ -export const resolvePluginVersion = ( - authored: unknown, - packageVersion: string | undefined, -): string => - (typeof authored === 'string' && authored.trim().length > 0 ? authored : undefined) - ?? packageVersion - ?? developmentFallbackVersion; - /** * The plugin identity axes a project carries before normalization: the * host-native slug from `plugin.name`, the npm axes derived from @@ -43,7 +27,7 @@ export const pluginIdentity = ( name: config.plugin.name, packageName: packageIdentity.packageName, packageVersion: packageIdentity.packageVersion, - version: resolvePluginVersion(config.plugin.version, packageIdentity.packageVersion), + version: packageIdentity.packageVersion ?? developmentFallbackVersion, }); }; @@ -110,8 +94,5 @@ export const declaredPluginIdentity = ( ): ProjectMetaSource | undefined => { const plugin = config.plugin; if (!isRecord(plugin) || typeof plugin.name !== 'string' || plugin.name.trim().length === 0) return undefined; - // `resolvePluginVersion` already treats a non-string `version` as absent. - return pluginIdentity(projectRoot, { - plugin: { name: plugin.name, ...(typeof plugin.version === 'string' ? { version: plugin.version } : {}) }, - }); + return pluginIdentity(projectRoot, { plugin: { name: plugin.name } }); }; diff --git a/packages/agent-bundle/src/config/validate.ts b/packages/agent-bundle/src/config/validate.ts index 76d3731ce..3f423de1e 100644 --- a/packages/agent-bundle/src/config/validate.ts +++ b/packages/agent-bundle/src/config/validate.ts @@ -2165,11 +2165,10 @@ export interface ValidateSourceOptions { } /** - * AB4008-AB4011: the package-identity axes derived from `package.json` - * (issue #94). The package version is authoritative release identity, so a - * conflicting `plugin.version` and any invalid derived value surface as - * warnings — never errors: a missing package.json (or missing name/version - * fields) stays a normal, silent development state with a labeled fallback. + * AB4009-AB4011: the package-identity axes derived from `package.json` + * (issue #94). Invalid derived values surface as warnings — never errors: a + * missing package.json (or missing name/version fields) stays a normal, + * silent development state with a labeled fallback. */ const packageIdentityIssueCode = (kind: PackageIdentityIssueKind): string => { switch (kind) { @@ -2200,34 +2199,15 @@ const validatePackageIdentity = (loaded: LoadedConfig, release: boolean): Diagno 'Correct the package.json field so the derived package identity is valid, then validate again.', )); } - const plugin = loaded.config.plugin as unknown; - const pluginVersion = - typeof plugin === 'object' && plugin !== null && !Array.isArray(plugin) - ? (plugin as Record).version - : undefined; - if ( - identity.packageVersion !== undefined && - typeof pluginVersion === 'string' && - pluginVersion.trim().length > 0 && - pluginVersion !== identity.packageVersion - ) { - diagnostics.push(warningDiagnostic( - 'AB4008', - `Config plugin.version ${JSON.stringify(pluginVersion)} differs from package.json version ${JSON.stringify(identity.packageVersion)}; the package version is authoritative for release identity.`, - loaded.configPath, - 'Align plugin.version with the package.json version, or update package.json.', - )); - } - const declared = typeof pluginVersion === 'string' && pluginVersion.trim().length > 0; - if (release && !declared && identity.packageVersion === undefined) { + if (release && identity.packageVersion === undefined) { // A development-only fallback may exist, but it can never produce a - // release artifact (issue #94): with no authored plugin.version and no - // valid package.json version, this project has no release identity to - // stamp into manifests, host projections, or compiled surfaces. + // release artifact (issue #94): without a valid package.json version this + // project has no release identity to stamp into manifests, host + // projections, or compiled surfaces. diagnostics.push({ code: 'AB4013', - message: 'This project has no release version: plugin.version is omitted and package.json declares no valid semantic version, so the build would package the development fallback.', - recovery: `Add a valid semantic "version" to package.json, or declare plugin.version in the config. Development commands keep the labeled ${developmentFallbackVersion} fallback.`, + message: 'This project has no release version: package.json declares no valid semantic version, so the build would package the development fallback.', + recovery: `Add a valid semantic "version" to package.json. Development commands keep the labeled ${developmentFallbackVersion} fallback.`, severity: 'error', sourcePath: packageJsonPath, }); @@ -2430,25 +2410,12 @@ export const validateSource = ( ? (plugin as Record) : undefined; const pluginName = pluginRecord?.name; - const pluginVersion = pluginRecord?.version; if (typeof pluginName !== 'string' || pluginName.trim().length === 0) { diagnostics.push( sourceDiagnostic('AB4000', 'Plugin metadata must define a nonempty name.', loaded.configPath), ); } - // `plugin.version` is optional since #94 stage 3: omitting it derives the - // version from package.json. Declaring it as anything but a nonempty - // string is still a mistake with no defensible reading. - if (pluginVersion !== undefined && (typeof pluginVersion !== 'string' || pluginVersion.trim().length === 0)) { - diagnostics.push( - sourceDiagnostic( - 'AB4001', - 'Plugin metadata version must be a nonempty string when it is declared; omit it to derive the version from package.json.', - loaded.configPath, - ), - ); - } diagnostics.push(...validatePluginLogo(loaded, pluginRecord)); const skillNames = new Map(); diff --git a/packages/agent-bundle/src/core/project-context.ts b/packages/agent-bundle/src/core/project-context.ts index 4a5359c25..bb4d7862d 100644 --- a/packages/agent-bundle/src/core/project-context.ts +++ b/packages/agent-bundle/src/core/project-context.ts @@ -200,10 +200,9 @@ export const snapshotPackageDescriptiveMetadata = (root: string): PackageDescrip /** * The version a project carries while it has no release identity at all: - * neither an authored `plugin.version` nor a valid `package.json` version. - * It is a development-only value — `agent-bundle build` refuses to package - * a project resting on it (AB4013), so it can never reach a release - * artifact. + * no valid `package.json` version. It is a development-only value — + * `agent-bundle build` refuses to package a project resting on it (AB4013), + * so it can never reach a release artifact. */ export const developmentFallbackVersion = '0.0.0-dev'; diff --git a/packages/agent-bundle/src/core/types.ts b/packages/agent-bundle/src/core/types.ts index 64cc92967..25d5ce0f0 100644 --- a/packages/agent-bundle/src/core/types.ts +++ b/packages/agent-bundle/src/core/types.ts @@ -44,18 +44,6 @@ export interface AgentBundlePluginConfig { */ metadata?: AgentBundleSharedMetadata; name: string; - /** - * The host-facing declared version. Omit it to derive the version from the - * project's `package.json` (issue #94 stage 3): package.json is - * authoritative for release identity, and a declared value that disagrees - * with it reports the AB4008 warning. - * - * @deprecated Declare the release version only in `package.json`. This - * compatibility field will be removed through the normal breaking-change - * policy. - */ - version?: string; - [key: string]: unknown; } export const canonicalHookEvents = Object.freeze([ diff --git a/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts b/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts index 4477b41f7..978c182d3 100644 --- a/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts +++ b/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts @@ -6,9 +6,6 @@ export { MCP_APP_PROTOCOL_VERSION }; export type McpAppProfileId = 'chatgpt' | 'claude' | 'portable'; -/** @deprecated Use McpAppProfileId. */ -export type McpAppHostProfile = McpAppProfileId; - export interface McpAppProfileDescriptor { readonly claimsRealHostParity: false; readonly evidence: 'simulated'; diff --git a/packages/agent-bundle/src/dev/mcp-apps/mcp-app-host-profiles.ts b/packages/agent-bundle/src/dev/mcp-apps/mcp-app-host-profiles.ts index 43c931f59..4eeffa130 100644 --- a/packages/agent-bundle/src/dev/mcp-apps/mcp-app-host-profiles.ts +++ b/packages/agent-bundle/src/dev/mcp-apps/mcp-app-host-profiles.ts @@ -6,8 +6,8 @@ import { isPlainRecord } from '../../core/strict-json.ts'; import { MCP_APP_PROFILE_DESCRIPTORS, - type McpAppHostProfile, type McpAppProfileDescriptor, + type McpAppProfileId, } from '../mcp-app-profile-descriptors.ts'; import { cloneMcpAppFiniteJson, @@ -21,7 +21,7 @@ import type { } from '../../core/types.ts'; export { MCP_APP_PROFILE_DESCRIPTORS } from '../mcp-app-profile-descriptors.ts'; -export type { McpAppHostProfile, McpAppProfileDescriptor, McpAppProfileId } from '../mcp-app-profile-descriptors.ts'; +export type { McpAppProfileDescriptor, McpAppProfileId } from '../mcp-app-profile-descriptors.ts'; export type McpAppCapability = 'camera' | 'clipboardWrite' | 'geolocation' | 'microphone'; @@ -101,7 +101,7 @@ export interface ResolveMcpAppHostProfileOptions { readonly consentedCapabilities?: readonly string[]; readonly declaredCapabilities?: readonly string[]; readonly host: McpAppHostContextInput; - readonly profile: McpAppHostProfile; + readonly profile: McpAppProfileId; readonly resource?: McpAppResourceCandidate; /** Preserved as metadata evidence; it never selects a profile feature. */ readonly toolMetadata?: unknown; @@ -576,7 +576,7 @@ const resolvePermissions = ( }; const resourceFallback = ( - profile: McpAppHostProfile, + profile: McpAppProfileId, configExtensions: McpAppConfigExtensionInspection, reason: McpAppFallbackHostProfile['reason'], warnings: readonly string[] = [], diff --git a/packages/agent-bundle/src/dev/playground/playground-store.ts b/packages/agent-bundle/src/dev/playground/playground-store.ts index f981c1ce9..c47faead9 100644 --- a/packages/agent-bundle/src/dev/playground/playground-store.ts +++ b/packages/agent-bundle/src/dev/playground/playground-store.ts @@ -1995,4 +1995,3 @@ export class PlaygroundService { } /** Extracted-module name; the durable implementation retains its public service identity. */ -export { PlaygroundService as PlaygroundStore }; diff --git a/packages/agent-bundle/src/dev/workbench-server.ts b/packages/agent-bundle/src/dev/workbench-server.ts index 6b9f423ea..5f2f269b0 100644 --- a/packages/agent-bundle/src/dev/workbench-server.ts +++ b/packages/agent-bundle/src/dev/workbench-server.ts @@ -52,7 +52,7 @@ import { import { McpSessionService } from './mcp-session/mcp-session-service.ts'; import { NativePlaygroundService } from './playground/native-playground-service.ts'; import { PlaygroundOrchestrationService } from './playground/playground-orchestration-service.ts'; -import { PlaygroundStore as PlaygroundService } from './playground/playground-store.ts'; +import { PlaygroundService } from './playground/playground-store.ts'; import { createDevPlatformRuntime } from './platform-run.ts'; import type { DevPlatformRuntime } from './platform-runtime.ts'; import { ProjectService, type PreparedProject } from './project-service.ts'; diff --git a/packages/agent-bundle/src/install/commands.ts b/packages/agent-bundle/src/install/commands.ts index b2f9d284b..58d423743 100644 --- a/packages/agent-bundle/src/install/commands.ts +++ b/packages/agent-bundle/src/install/commands.ts @@ -31,7 +31,6 @@ export interface LifecycleCommandOptions { } interface InstallCommandOptions { - readonly force?: boolean; readonly from?: string; readonly json?: boolean; readonly replace?: boolean; @@ -107,7 +106,6 @@ export const registerLifecycleCommands = (program: Command, options: LifecycleCo 'Replace an existing agent-bundle install of this plugin even when its version differs; ' + 'same-version content drift is replaced automatically and foreign installs are always refused', ) - .option('--force', 'Alias for --replace') .option('--mode ', 'Cursor delivery mode: local (default) or marketplace', installMode) .option('--json', 'Write one machine-readable JSON document'); installCommand.action(async (host: InstallHost, commandOptions: InstallCommandOptions) => { @@ -115,7 +113,7 @@ export const registerLifecycleCommands = (program: Command, options: LifecycleCo const result = await install({ from: pinned ?? commandOptions.from ?? process.cwd(), host, - replace: commandOptions.replace === true || commandOptions.force === true, + replace: commandOptions.replace === true, ...(commandOptions.mode === undefined ? {} : { mode: commandOptions.mode }), scope: installScope(commandOptions.scope), }); diff --git a/packages/agent-bundle/src/install/surface.ts b/packages/agent-bundle/src/install/surface.ts index 88d610faa..3c223f669 100644 --- a/packages/agent-bundle/src/install/surface.ts +++ b/packages/agent-bundle/src/install/surface.ts @@ -38,7 +38,7 @@ const header = (model: NormalizedPlugin): string[] => [ const optionalCliReinstall = (host: 'claude' | 'codex'): string[] => [ `With the optional \`agent-bundle\` CLI, \`agent-bundle install ${host} --from ./\` runs this sequence`, 'automatically when the installed copy has the same version but a different content hash; `--replace`', - '(alias `--force`) forces it.', + 'forces it.', ]; /** @@ -187,8 +187,8 @@ const cursorInstructions = (model: NormalizedPlugin): string[] => [ 'node ./install.mjs --replace # also replace a different installed version, or adopt a pre-receipt copy', '```', '', - '`--force` is an alias for `--replace`. A directory that is not an agent-bundle install of this', - 'plugin is always refused with an installed-versus-artifact content-hash comparison; remove it', + 'A directory that is not an agent-bundle install of this plugin is always refused with an', + 'installed-versus-artifact content-hash comparison; remove it', 'manually. The optional `agent-bundle` CLI applies the same policy through', '`agent-bundle install cursor --from ./ [--replace]`.', '', @@ -414,7 +414,7 @@ const portableInstructions = (planned: readonly string[]): string[] => [ '', `The installer records an install receipt (\`${installReceiptFile}\`) and replaces its owned files in`, 'place when the same version was rebuilt with different content; runtime state (`state/`) is never', - 'touched. Pass `--replace` (alias `--force`) to replace a different installed version or to adopt a', + 'touched. Pass `--replace` to replace a different installed version or to adopt a', 'copy installed before receipts existed. Foreign directories are refused with a content-hash', 'comparison. For a client that manages its own copy, remove and re-add the plugin through that client', 'when only content changed at the same version.', @@ -956,7 +956,7 @@ const cursorInstallerSource = (model: NormalizedPlugin): string => { "const pluginData = join(cursorRoot, 'agent-bundle', 'plugin-data', pluginName);", "const receiptsRoot = join(cursorRoot, 'agent-bundle', 'receipts');", 'const marketplaceReceipt = join(receiptsRoot, `${pluginName}.marketplace.json`);', - "const usage = 'Usage: node install.mjs [--mode local|marketplace] [--replace|--force] [--help]\\n" + + "const usage = 'Usage: node install.mjs [--mode local|marketplace] [--replace] [--help]\\n" + " node install.mjs --uninstall [--mode local|marketplace] [--keep-data | --purge-data --confirm-purge] [--force] [--plan]';", '', 'let replace = false;', @@ -970,7 +970,8 @@ const cursorInstallerSource = (model: NormalizedPlugin): string => { 'const argv = process.argv.slice(2);', 'for (let index = 0; index < argv.length; index += 1) {', ' const argument = argv[index];', - " if (argument === '--replace' || argument === '--force') { replace = true; force = argument === '--force'; continue; }", + " if (argument === '--replace') { replace = true; continue; }", + " if (argument === '--force') { force = true; continue; }", " if (argument === '--uninstall') { uninstall = true; continue; }", " if (argument === '--plan') { plan = true; continue; }", " if (argument === '--keep-data') { keepData = true; continue; }", @@ -986,6 +987,8 @@ const cursorInstallerSource = (model: NormalizedPlugin): string => { ' console.error(`Unknown installer argument ${JSON.stringify(argument)}.\\n${usage}`);', ' process.exit(2);', '}', + 'if (!uninstall && force) { console.error(`--force applies to --uninstall only. Use --replace to replace an install.\\n${usage}`); process.exit(2); }', + 'if (uninstall && replace) { console.error(`--replace applies to installation only. Use --force to override uninstall ownership checks.\\n${usage}`); process.exit(2); }', 'if (!uninstall && (plan || keepData || purgeData || confirmPurge)) {', " console.error(`--plan, --keep-data, --purge-data, and --confirm-purge apply to --uninstall only.\\n${usage}`);", ' process.exit(2);', diff --git a/packages/agent-bundle/src/meta.ts b/packages/agent-bundle/src/meta.ts index 28e8b8bfd..bb1eda713 100644 --- a/packages/agent-bundle/src/meta.ts +++ b/packages/agent-bundle/src/meta.ts @@ -19,10 +19,9 @@ export interface AgentBundleMeta { /** The validated semantic release version, absent for unpackaged development projects. */ readonly packageVersion: string | undefined; /** - * The resolved plugin version: the authored `plugin.version` when declared, - * otherwise the package.json version. A release build refuses to package a - * project that has neither (AB4013), so a compiled artifact never carries - * the development fallback. + * The package.json version. A release build refuses to package a project + * without one (AB4013), so a compiled artifact never carries the + * development fallback. */ readonly version: string; } diff --git a/packages/agent-bundle/src/routes/app-template.ts b/packages/agent-bundle/src/routes/app-template.ts index 1bdd1c624..72a2e0bb9 100644 --- a/packages/agent-bundle/src/routes/app-template.ts +++ b/packages/agent-bundle/src/routes/app-template.ts @@ -1,29 +1,16 @@ import { existsSync, statSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; -/** - * How one App route's `config.template` path resolved. `route-relative` is - * the documented form (the path resolves from the route module like its - * imports do); `project-relative` is the legacy form, accepted only while it - * is the sole interpretation that names an existing file. - */ +/** How one App route's `config.template` path resolved from its route module. */ export type AppRouteTemplateResolution = | { - readonly form: 'project-relative' | 'route-relative'; readonly kind: 'resolved'; /** Absolute template path. */ readonly path: string; } | { - /** Both interpretations name different existing files. */ - readonly kind: 'ambiguous'; - readonly projectRelative: string; - readonly routeRelative: string; - } - | { - /** Neither interpretation names an existing file. */ + /** The route-relative interpretation names no existing file. */ readonly kind: 'missing'; - readonly projectRelative: string; readonly routeRelative: string; }; @@ -35,31 +22,15 @@ const isFile = (path: string): boolean => { } }; -/** - * Resolves an App route's `config.template` against both candidate bases. - * An absolute template, or a relative one whose two interpretations coincide, - * has one candidate, which still has to exist; otherwise exactly one existing - * candidate wins, and the ambiguous and missing outcomes are reported for the - * compiler to diagnose (AB4827). - */ +/** Resolves an App route's `config.template` relative to the route module. */ export const resolveAppRouteTemplate = ( - projectRoot: string, routeSource: string, template: string, ): AppRouteTemplateResolution => { const routeRelative = resolve(dirname(routeSource), template); - const projectRelative = resolve(projectRoot, template); - const routeExists = isFile(routeRelative); - if (routeRelative === projectRelative) { - return routeExists - ? { form: 'route-relative', kind: 'resolved', path: routeRelative } - : { kind: 'missing', projectRelative, routeRelative }; - } - const projectExists = isFile(projectRelative); - if (routeExists && projectExists) return { kind: 'ambiguous', projectRelative, routeRelative }; - if (routeExists) return { form: 'route-relative', kind: 'resolved', path: routeRelative }; - if (projectExists) return { form: 'project-relative', kind: 'resolved', path: projectRelative }; - return { kind: 'missing', projectRelative, routeRelative }; + return isFile(routeRelative) + ? { kind: 'resolved', path: routeRelative } + : { kind: 'missing', routeRelative }; }; /** @@ -71,7 +42,6 @@ export const appRouteTemplatePath = (resolution: AppRouteTemplateResolution): st switch (resolution.kind) { case 'resolved': return resolution.path; - case 'ambiguous': case 'missing': return resolution.routeRelative; default: { diff --git a/packages/agent-bundle/src/routes/contract.ts b/packages/agent-bundle/src/routes/contract.ts index b428b2339..6310794e6 100644 --- a/packages/agent-bundle/src/routes/contract.ts +++ b/packages/agent-bundle/src/routes/contract.ts @@ -2,6 +2,7 @@ import ts from 'typescript-5'; import type { Diagnostic } from '../core/diagnostics.ts'; import { readRouteDefinition, type RouteDefinition } from './definition-syntax.ts'; +import type { CompiledRouteKind } from './types.ts'; const modifier = (node: ts.Node, kind: ts.SyntaxKind): boolean => ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((item) => item.kind === kind) ?? false); @@ -59,13 +60,22 @@ export const validateRouteModuleContract = ( moduleText: string, relativePath: string, sourcePath: string, + kind: CompiledRouteKind, ): readonly Diagnostic[] => { const exports = scanRouteModuleExports(moduleText, relativePath); const { named, splitExport } = exports; const hasDefault = exports.named.has('default'); const missing = ['inputSchema', 'resultSchema'].filter((name) => !named.has(name)); const diagnostics: Diagnostic[] = []; - if (missing.length > 0 || !hasDefault) { + if (kind === 'tool' && exports.definition === undefined) { + diagnostics.push(diagnostic( + 'AB4810', + `Tool route module ${relativePath} does not use defineTool.`, + sourcePath, + 'Default-export one direct defineTool({ inputSchema, resultSchema, ... }, handler) call.', + )); + } + if ((kind !== 'tool' || exports.definition !== undefined) && (missing.length > 0 || !hasDefault)) { const details = [ ...(missing.length === 0 ? [] : [`missing named ${missing.join(' and ')}`]), ...(hasDefault ? [] : ['missing default export']), diff --git a/packages/agent-bundle/src/routes/graph.ts b/packages/agent-bundle/src/routes/graph.ts index 478bc849a..26eb73264 100644 --- a/packages/agent-bundle/src/routes/graph.ts +++ b/packages/agent-bundle/src/routes/graph.ts @@ -1010,26 +1010,14 @@ export const compileRouteGraph = async ( } const template = route.config['template']; if (typeof template === 'string') { - const resolution = resolveAppRouteTemplate(projectRoot, route.source, template); + const resolution = resolveAppRouteTemplate(route.source, template); switch (resolution.kind) { case 'resolved': break; - case 'ambiguous': - diagnostics.push(routeError( - 'AB4827', - `MCP App route ${route.provenance.relativePath} declares config.template ${JSON.stringify(template)}, which names two different existing files: ${resolution.routeRelative} (route-relative) and ${resolution.projectRelative} (project-root-relative).`, - 'Templates resolve relative to the route module; rewrite the path so it names the route-relative file only (or remove the project-root-relative duplicate), then inspect again.', - route.source, - )); - break; case 'missing': { - // An absolute template has one candidate; name it once. - const candidates = resolution.routeRelative === resolution.projectRelative - ? `${resolution.routeRelative} does not exist` - : `neither ${resolution.routeRelative} (route-relative) nor ${resolution.projectRelative} (project-root-relative) exists`; diagnostics.push(routeError( 'AB4827', - `MCP App route ${route.provenance.relativePath} declares config.template ${JSON.stringify(template)}, but ${candidates}.`, + `MCP App route ${route.provenance.relativePath} declares config.template ${JSON.stringify(template)}, but ${resolution.routeRelative} does not exist.`, 'Templates resolve relative to the route module; point config.template at an existing HTML file beside the route (for example \'./dashboard.html\'), then inspect again.', route.source, )); @@ -1049,6 +1037,7 @@ export const compileRouteGraph = async ( moduleText, route.provenance.relativePath, route.source, + route.kind, )); } // The generated server inlines the route, so a compiler-carrying diff --git a/packages/agent-bundle/src/routes/public.ts b/packages/agent-bundle/src/routes/public.ts index 76bbc5138..08d3c4a11 100644 --- a/packages/agent-bundle/src/routes/public.ts +++ b/packages/agent-bundle/src/routes/public.ts @@ -247,7 +247,7 @@ export interface AgentProviderNoticeWithholding { /** * One notice as a provider reads it from `inbox()` or `published()`: every * field of the runtime's `AgentNotice`, spelled structurally. `content` is the - * persisted Agent Document snapshot (the runtime's `AgentDocumentSnapshot`); + * persisted Agent Document (the runtime's `AgentDocument`); * it is `unknown` here because the Agent Document types ship with the runtime, * so a provider that needs the authored text narrows it with the runtime's * types — a route reads the same notice through `(await agent()).notices`. @@ -499,10 +499,8 @@ export interface AppRouteConfig { readonly targets?: readonly string[]; /** * Optional HTML shell for the compiled App. The path resolves relative to - * the route module, the way its imports do (`'./dashboard.html'`); the - * older project-root-relative form is still accepted while only one of the - * two interpretations names an existing file. When both exist and differ, - * or neither exists, the build fails with `AB4827` naming both candidates. + * the route module, the way its imports do (`'./dashboard.html'`). A missing + * file fails the build with `AB4827`. */ readonly template?: string; } diff --git a/packages/agent-bundle/src/test/manifest.ts b/packages/agent-bundle/src/test/manifest.ts index f7e6f37ce..6123bdf43 100644 --- a/packages/agent-bundle/src/test/manifest.ts +++ b/packages/agent-bundle/src/test/manifest.ts @@ -166,8 +166,8 @@ export const FALLBACK_PLUGIN_IDENTITY: TestManifestPluginIdentity = Object.freez * True when a manifest's identity is the model-less sentinel. The check is * by reference only: `deepFreeze` freezes in place, so the manifest the * compiler pass hands the presets still carries the sentinel object itself, - * while a real project that happens to declare `plugin.name: 'unknown'` and - * `plugin.version: '0.0.0'` is a distinct model-backed object and keeps its + * while a real project whose resolved model identity is + * `{ name: 'unknown', version: '0.0.0' }` is a distinct object and keeps its * identity. A manifest that crossed a JSON boundary has already been handed * to a worker; only the preset, in the runner process, asks this question. */ diff --git a/packages/agent-bundle/tests/adapter-contract.test.ts b/packages/agent-bundle/tests/adapter-contract.test.ts index dad182af9..2d19074e2 100644 --- a/packages/agent-bundle/tests/adapter-contract.test.ts +++ b/packages/agent-bundle/tests/adapter-contract.test.ts @@ -60,7 +60,7 @@ it('delegates selected native hook sources through registered adapters', async ( config: { example: { nativeHooks: './example-hooks.json' }, hooks: { stop: './hooks/stop.ts' }, - plugin: { name: 'example-fixture', version: '1.0.0' }, + plugin: { name: 'example-fixture' }, }, configPath: join(root, 'agent-bundle.config.ts'), context: { @@ -158,7 +158,7 @@ it('normalizes malformed native hook source values into diagnostics without skip try { const loaded: LoadedConfig = { config: { - plugin: { name: 'invalid-source-fixture', version: '1.0.0' }, + plugin: { name: 'invalid-source-fixture' }, targets: ['invalid', 'valid'], }, configPath: join(root, 'agent-bundle.config.ts'), @@ -198,7 +198,7 @@ it('normalizes thrown native hook sources into diagnostics', async () => { }); const loaded: LoadedConfig = { config: { - plugin: { name: 'throwing-source-fixture', version: '1.0.0' }, + plugin: { name: 'throwing-source-fixture' }, targets: ['throws'], }, configPath: join(root, 'agent-bundle.config.ts'), diff --git a/packages/agent-bundle/tests/amp-adapter.test.ts b/packages/agent-bundle/tests/amp-adapter.test.ts index 9ae76e0b4..537626403 100644 --- a/packages/agent-bundle/tests/amp-adapter.test.ts +++ b/packages/agent-bundle/tests/amp-adapter.test.ts @@ -792,7 +792,7 @@ it('runs a relocated standalone event route with its worker inside the Amp plugi try { const registry = createDefaultRegistry(); const routeGraph = await compileRouteGraph(projectRoot, { - plugin: { name: 'amp-review', version: '1.0.0' }, + plugin: { name: 'amp-review' }, }); const built = await build({ model, diff --git a/packages/agent-bundle/tests/api.test.ts b/packages/agent-bundle/tests/api.test.ts index 07d78c87b..27f15d7a0 100644 --- a/packages/agent-bundle/tests/api.test.ts +++ b/packages/agent-bundle/tests/api.test.ts @@ -37,7 +37,7 @@ const createProject = async (): Promise => { join(root, 'agent-bundle.config.ts'), [ 'export default ({ command, mode, projectRoot, selectedTargets }) => ({', - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", ' targets: selectedTargets.length === 0 ? [\'codex\', \'claude\'] : selectedTargets,', " hooks: { sessionStart: { handler: './src/hook.ts' } },", ' fixtureContext: { command, mode, projectRoot, selectedTargets },', @@ -49,6 +49,7 @@ const createProject = async (): Promise => { join(root, 'src', 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', ), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'hook.ts'), 'export default () => undefined;\n'), ]); return root; @@ -147,7 +148,7 @@ it('prepares and inspects a target owned only by the supplied advanced registry' try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'synthetic-api-fixture', version: '1.0.0' },", + " plugin: { name: 'synthetic-api-fixture' },", " synthetic: { enabled: true },", " targets: ['synthetic'],", '};', @@ -202,7 +203,7 @@ it('projects only the capability contract fields of adapter-owned rows into insp try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'synthetic-api-fixture', version: '1.0.0' },", + " plugin: { name: 'synthetic-api-fixture' },", " hooks: { sessionStart: { handler: './src/hook.ts' } },", " synthetic: { enabled: true },", " targets: ['synthetic'],", @@ -232,7 +233,7 @@ it('accepts claude.userConfig through the public inspection and build APIs', asy try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'user-config-api', version: '1.0.0' },", + " plugin: { name: 'user-config-api' },", " targets: ['claude'],", ' claude: {', ' userConfig: {', @@ -295,7 +296,7 @@ it('returns a frozen invalid inspection for opaque source failures', async () => it('attaches a specific recovery to every invalid inspection diagnostic', async () => { const root = await createProject(); try { - await writeFile(join(root, 'agent-bundle.config.ts'), "export default { plugin: { version: '1.0.0' } };\n"); + await writeFile(join(root, 'agent-bundle.config.ts'), 'export default { plugin: {} };\n'); const result = await inspect({ root }); @@ -320,7 +321,7 @@ it('accepts the public claude.dependencies config surface and plans its manifest try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['claude'],", ' claude: {', " dependencies: [{ name: 'audit-logger', marketplace: 'acme-shared' }, { name: 'policy-kit', version: '^2.0', marketplace: 'acme-shared' }],", @@ -350,7 +351,7 @@ it('reports one modern-MCP source diagnostic for a legacy SSE declaration', asyn try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'codex-sse', version: '1.0.0' },", + " plugin: { name: 'codex-sse' },", " targets: ['codex'],", " mcp: { servers: { events: { transport: 'sse', url: 'https://mcp.example.test/events' } } },", '};', @@ -379,7 +380,7 @@ it('resolves artifact output with CLI, config, and default precedence', async () await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " output: { distPath: 'artifact-out' },", - " plugin: { name: 'output-path-fixture', version: '1.0.0' },", + " plugin: { name: 'output-path-fixture' },", " targets: ['portable'],", '};', '', @@ -398,7 +399,7 @@ it('resolves artifact output with CLI, config, and default precedence', async () await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'output-path-fixture', version: '1.0.0' },", + " plugin: { name: 'output-path-fixture' },", " targets: ['portable'],", '};', '', @@ -512,7 +513,7 @@ it('build reports one informational AB6019 skip for all Claude-validated targets try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['claude', 'codex'],", '};', '', @@ -600,7 +601,7 @@ it('deduplicates identical adapter diagnostics without collapsing distinct stabl try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'adapter-diagnostic-identity', version: '1.0.0' },", + " plugin: { name: 'adapter-diagnostic-identity' },", " synthetic: { enabled: true },", " targets: ['synthetic'],", '};', @@ -627,7 +628,7 @@ it('contains hostile source getters as reusable preparation diagnostics', async await writeFile(join(root, 'agent-bundle.config.ts'), [ "const hostile = { toString() { throw new Error('hostile source getter was stringified'); } };", 'hostile.self = hostile;', - 'const config = { plugin: { name: \'hostile-config\', version: \'1.0.0\' }, targets: [\'codex\'] };', + 'const config = { plugin: { name: \'hostile-config\' }, targets: [\'codex\'] };', "Object.defineProperty(config, 'hooks', { enumerable: true, get() { throw hostile; } });", 'export default config;', '', @@ -655,7 +656,7 @@ it('contains hostile source getters as reusable preparation diagnostics', async await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'recovered-config', version: '1.0.0' },", + " plugin: { name: 'recovered-config' },", " targets: ['codex'],", '};', '', @@ -676,7 +677,7 @@ it('fails closed when routes getter throws during inspection', async () => { writeFile(join(root, 'agent-bundle.config.ts'), [ "const hostile = { toString() { throw new Error('hostile routes getter was stringified'); } };", 'hostile.self = hostile;', - 'const config = { plugin: { name: \'hostile-routes\', version: \'1.0.0\' }, targets: [\'codex\'] };', + 'const config = { plugin: { name: \'hostile-routes\' }, targets: [\'codex\'] };', "Object.defineProperty(config, 'routes', { enumerable: true, get() { throw hostile; } });", 'export default config;', '', @@ -731,7 +732,7 @@ it('contains a throwing adapter plan as a reusable preparation diagnostic', asyn try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'hostile-adapter', version: '1.0.0' },", + " plugin: { name: 'hostile-adapter' },", " synthetic: { enabled: true },", " targets: ['synthetic'],", '};', @@ -779,7 +780,7 @@ it('contains an adapter planner that fails after preparation during inspect', as try { await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'inspection-planner', version: '1.0.0' },", + " plugin: { name: 'inspection-planner' },", " synthetic: { enabled: true },", " targets: ['synthetic'],", '};', @@ -863,7 +864,7 @@ it('reports skipped target/component pairs against each target emission surface' writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " hooks: { sessionStart: { handler: './src/hook.ts' } },", - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " scripts: { report: { entry: './src/report.ts', targets: ['codex'] } },", " targets: ['portable', 'codex', 'claude', 'cursor'],", '};', @@ -963,7 +964,7 @@ it('accounts lsp servers and event routes as distinct canonical kinds with a per ' },', ' },', " hooks: { sessionStart: { handler: './src/hook.ts' } },", - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['portable', 'codex', 'claude', 'cursor'],", '};', '', @@ -1107,7 +1108,7 @@ it('accounts an admitted degraded event route as selected, matching the validati ].join('\n')), writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", ` targets: ['${syntheticTarget}'],`, '};', '', @@ -1162,7 +1163,7 @@ it('judges event-route admission and lsp emission by the component-emission over writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " synthetic: { lspServers: { rust: { command: 'rust-analyzer' } } },", - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", ` targets: ['${syntheticTarget}'],`, '};', '', @@ -1197,7 +1198,7 @@ it('never reports an lsp component as selected when the declaring planner reject " second: { command: 'second-ls', extensionToLanguage: { '.ts': 'typescript' } },", ' },', ' },', - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['claude'],", '};', '', @@ -1227,7 +1228,7 @@ it('reports omitted component features per target from the host feature rows (#1 writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " hooks: { beforeTool: { handler: './src/hook.ts', timeout: 3, tools: ['shell'] } },", - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['claude', 'cursor'],", '};', '', @@ -1303,7 +1304,7 @@ it('never counts an opaque third-party lspServers declaration as emitted by a ho 'export default {', " synthetic: { lspServers: { rust: { command: 'rust-analyzer' } } },", " hooks: { sessionStart: { handler: './src/hook.ts' } },", - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", ` targets: [${JSON.stringify(target)}],`, '};', '', @@ -1337,7 +1338,7 @@ it('never counts an opaque third-party lspServers declaration as emitted by a ho 'export default {', " codex: { lspServers: { rust: { command: 'rust-analyzer' } } },", " hooks: { sessionStart: { handler: './src/hook.ts' } },", - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['codex', 'claude'],", '};', '', @@ -1365,7 +1366,7 @@ it('reports target exclusion before unsupported capability when both omit a comp await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " hooks: { sessionStart: { handler: './src/hook.ts', targets: ['codex'] } },", - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['portable', 'codex'],", '};', '', @@ -1391,7 +1392,7 @@ it('surfaces the computed native matcher on inspected hook entries', async () => " beforeTool: { handler: './src/hook.ts', tools: ['shell', 'file.write'] },", " sessionStart: { handler: './src/hook.ts' },", ' },', - " plugin: { name: 'api-fixture', version: '1.0.0' },", + " plugin: { name: 'api-fixture' },", " targets: ['claude'],", '};', '', @@ -1440,7 +1441,7 @@ it('keeps one supplied registry through advanced artifact, hook, and MCP operati 'export default {', " hooks: { sessionStart: { handler: './src/hook.ts' } },", " mcp: { servers: { synthetic: { entry: './src/mcp-server.ts' } } },", - " plugin: { name: 'synthetic-api-fixture', version: '1.0.0' },", + " plugin: { name: 'synthetic-api-fixture' },", " synthetic: { enabled: true },", " targets: ['synthetic'],", '};', @@ -1572,7 +1573,7 @@ it('keeps rule and command model digests root-independent and sensitive to conte const [leftRoot, rightRoot] = await Promise.all([createProject(), createProject()]); const config = [ 'export default {', - " plugin: { name: 'rule-digest-fixture', version: '1.0.0' },", + " plugin: { name: 'rule-digest-fixture' },", " targets: ['cursor'],", '};', '', @@ -1660,7 +1661,7 @@ it('rejects an output beneath an escaping symlink before loading source or writi "import { writeFileSync } from 'node:fs';", `writeFileSync(${JSON.stringify(marker)}, 'evaluated\\n');`, 'export default {', - " plugin: { name: 'escaping-output', version: '1.0.0' },", + " plugin: { name: 'escaping-output' },", " targets: ['portable'],", '};', '', @@ -1685,7 +1686,7 @@ it('rejects a dangling output symlink before loading source', async () => { "import { writeFileSync } from 'node:fs';", `writeFileSync(${JSON.stringify(marker)}, 'evaluated\\n');`, 'export default {', - " plugin: { name: 'dangling-output', version: '1.0.0' },", + " plugin: { name: 'dangling-output' },", " targets: ['portable'],", '};', '', @@ -1709,7 +1710,7 @@ it('rejects an output symlink to the project root before loading source', async "import { writeFileSync } from 'node:fs';", `writeFileSync(${JSON.stringify(marker)}, 'evaluated\\n');`, 'export default {', - " plugin: { name: 'root-output', version: '1.0.0' },", + " plugin: { name: 'root-output' },", " targets: ['portable'],", '};', '', @@ -1782,7 +1783,7 @@ it('normalizes named top-level scripts with stable IDs, modes, and sorted target join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'script-fixture', version: '1.0.0' },", + " plugin: { name: 'script-fixture' },", " targets: ['codex', 'claude'],", ' scripts: {', " bundle: { entry: './src/bundle.ts', targets: ['codex', 'claude'] },", @@ -1843,7 +1844,7 @@ it('builds conventional src/scripts modules beside explicit entries', async () = join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'conventional-scripts-fixture', version: '1.0.0' },", + " plugin: { name: 'conventional-scripts-fixture' },", " targets: ['portable'],", " scripts: { claimed: './src/scripts/claimed.ts' },", '};', @@ -1899,7 +1900,7 @@ it('refuses unshippable conventional script routes with actionable diagnostics', join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'unshippable-scripts-fixture', version: '1.0.0' },", + " plugin: { name: 'unshippable-scripts-fixture' },", " targets: ['portable'],", " scripts: { audit: './src/tasks/audit.ts' },", '};', @@ -1943,7 +1944,7 @@ it('copies every supported top-level script output suffix byte-for-byte with sou join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'copy-script-fixture', version: '1.0.0' },", + " plugin: { name: 'copy-script-fixture' },", " targets: ['portable', 'codex', 'claude'],", ' scripts: {', " bash: './src/run.BASH',", @@ -2040,7 +2041,7 @@ it('canonicalizes copied script extensions in emitted artifact paths', async () await Promise.all([ writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'uppercase-script-fixture', version: '1.0.0' },", + " plugin: { name: 'uppercase-script-fixture' },", " targets: ['portable'],", " scripts: { upper: './src/run.SH' },", '};', @@ -2092,7 +2093,7 @@ it('documents a versioned MCP App resource URI accepted by source validation', a join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'readme-uri-fixture', version: '1.0.0' },", + " plugin: { name: 'readme-uri-fixture' },", " targets: ['portable'],", ' mcp: { servers: { local: {', " entry: './src/server.ts',", @@ -2124,7 +2125,7 @@ it('rejects unsafe, unsupported, missing, non-file, and unknown-target named scr join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'invalid-script-fixture', version: '1.0.0' },", + " plugin: { name: 'invalid-script-fixture' },", " targets: ['portable'],", ' scripts: {', " '../unsafe': './src/run.sh',", diff --git a/packages/agent-bundle/tests/artifact-cli-bin.test.ts b/packages/agent-bundle/tests/artifact-cli-bin.test.ts index 9c3c705b5..8ecfd8332 100644 --- a/packages/agent-bundle/tests/artifact-cli-bin.test.ts +++ b/packages/agent-bundle/tests/artifact-cli-bin.test.ts @@ -79,7 +79,7 @@ const createFixture = async (options: { writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - ` plugin: { description: 'Artifact routed CLI fixture.', name: ${JSON.stringify(pluginName)}, version: '3.8.7' },`, + ` plugin: { description: 'Artifact routed CLI fixture.', name: ${JSON.stringify(pluginName)} },`, ` targets: ${JSON.stringify(options.targets)},`, ...(options.web === true ? [" web: { apps: ['curator/dashboard'] },"] : []), '});', @@ -215,7 +215,7 @@ const createWebOnlyFixture = async (): Promise => { " entry: './src/mcp/status.ts',", " targets: ['claude', 'portable'],", ' } } },', - ` plugin: { description: 'Web-only artifact fixture.', name: ${JSON.stringify(webOnlyPluginName)}, version: '1.0.0' },`, + ` plugin: { description: 'Web-only artifact fixture.', name: ${JSON.stringify(webOnlyPluginName)} },`, " targets: ['claude', 'portable'],", " web: { apps: ['status/claude-only', 'status/status'] },", '});', @@ -490,7 +490,7 @@ it('refuses a host-emitted file that collides with the routed CLI bin (AB4766)', "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', " claude: { bin: './host-bin' },", - ` plugin: { description: 'Artifact routed CLI fixture.', name: ${JSON.stringify(pluginName)}, version: '3.8.7' },`, + ` plugin: { description: 'Artifact routed CLI fixture.', name: ${JSON.stringify(pluginName)} },`, " targets: ['claude'],", '});', '', diff --git a/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts b/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts index 04b0e4be5..8c4256ad1 100644 --- a/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts +++ b/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts @@ -230,7 +230,7 @@ it('bridges a browser-bound session to a generated stdio artifact without exposi const model = await normalizeProject( loadedProject(root, { mcp: { servers: { fixture: { entry: './src/server.ts' } } }, - plugin: { name: 'browser-bridge-fixture', version: '1.0.0' }, + plugin: { name: 'browser-bridge-fixture' }, targets: [fixture.binding.target], }), { skills: [] }, diff --git a/packages/agent-bundle/tests/build-compose.test.ts b/packages/agent-bundle/tests/build-compose.test.ts index c634a1908..3e2107945 100644 --- a/packages/agent-bundle/tests/build-compose.test.ts +++ b/packages/agent-bundle/tests/build-compose.test.ts @@ -73,7 +73,7 @@ const writeProject = async (root: string, options: FixtureOptions = {}): Promise " codex: { nativeHooks: './native/codex.json' },", " hooks: { sessionStart: './src/hooks/session-start.ts' },", " mcp: { servers: { fixture: { entry: './src/mcp/fixture.ts' } } },", - " plugin: { description: 'Composite root fixture.', name: 'composite-fixture', version: '1.0.0' },", + " plugin: { description: 'Composite root fixture.', name: 'composite-fixture' },", " scripts: { hello: './src/tools/hello.ts' },", targets.trimEnd(), '};', @@ -554,7 +554,7 @@ describe('composite plugin root (#555)', () => { writeProjectFile(root, 'package.json', '{"name":"event-projections","type":"module","version":"1.0.0"}\n'), writeProjectFile(root, 'agent-bundle.config.ts', [ 'export default {', - " plugin: { name: 'event-projections', version: '1.0.0' },", + " plugin: { name: 'event-projections' },", " targets: ['claude', 'codex', 'cursor', 'portable'],", '};', '', diff --git a/packages/agent-bundle/tests/build-reproducibility.test.ts b/packages/agent-bundle/tests/build-reproducibility.test.ts index c3ed6529a..c7f17bc2c 100644 --- a/packages/agent-bundle/tests/build-reproducibility.test.ts +++ b/packages/agent-bundle/tests/build-reproducibility.test.ts @@ -44,22 +44,25 @@ const writeProject = async (root: string): Promise => { writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - " plugin: { description: 'Reproducible build fixture.', name: 'reproducible-fixture', version: '1.0.0' },", + " plugin: { description: 'Reproducible build fixture.', name: 'reproducible-fixture' },", " targets: ['claude', 'codex', 'cursor', 'portable'],", '});', '', ].join('\n')), writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', [ - "import { Agent } from '@agent-bundle/runtime';", - "import { z } from 'zod';", - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.' };", - 'export const inputSchema = z.object({ message: z.string().default("ready") }).strict();', - "export const resultSchema = z.object({ message: z.string() }).strict();", - 'export default async function Lookup({ input }) {', - ' return {`Lookup: ${input.message}`};', - '}', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "import { Agent } from '@agent-bundle/runtime';", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "export const inputSchema = z.object({ message: z.string().default(\"ready\") }).strict();", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "export const resultSchema = z.object({ message: z.string() }).strict();", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "export default defineTool({", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.',", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', " inputSchema,", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', " resultSchema,", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "}, async (input) => {", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', " return {`Lookup: ${input.message}`};", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "});", + writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', ].join('\n')), writeProjectFile(root, 'src/events/session/start.tsx', [ "import { Agent } from '@agent-bundle/runtime';", "export const config = { targets: ['claude', 'codex', 'cursor'] };", diff --git a/packages/agent-bundle/tests/build-source-publication.test.ts b/packages/agent-bundle/tests/build-source-publication.test.ts index c242887b9..ea910fa58 100644 --- a/packages/agent-bundle/tests/build-source-publication.test.ts +++ b/packages/agent-bundle/tests/build-source-publication.test.ts @@ -57,7 +57,7 @@ const publicationConfig = (includeLibrary: boolean): string => `import { writeFi import { join } from 'node:path'; export default ({ projectRoot }) => ({ - plugin: { name: 'ab7101-publication-fixture', version: '1.0.0' }, + plugin: { name: 'ab7101-publication-fixture' }, targets: ['portable'], mcp: { servers: { echoer: {} } }, ${includeLibrary ? "lib: { dts: false, entry: './src/library.ts' }," : ''} diff --git a/packages/agent-bundle/tests/claude-hooks-schema.test.ts b/packages/agent-bundle/tests/claude-hooks-schema.test.ts index 2384a0ed1..de38d9f81 100644 --- a/packages/agent-bundle/tests/claude-hooks-schema.test.ts +++ b/packages/agent-bundle/tests/claude-hooks-schema.test.ts @@ -238,7 +238,7 @@ it('plans a Claude native hooks document that uses every documented handler type const loaded: LoadedConfig = { config: { claude: { nativeHooks: './claude-hooks.json' }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['claude'], }, configPath: join(root, 'agent-bundle.config.ts'), diff --git a/packages/agent-bundle/tests/cli-projection.test.ts b/packages/agent-bundle/tests/cli-projection.test.ts index a40516ca8..38c1f1a55 100644 --- a/packages/agent-bundle/tests/cli-projection.test.ts +++ b/packages/agent-bundle/tests/cli-projection.test.ts @@ -34,7 +34,7 @@ const writeTree = async (root: string, files: Readonly>): }; const fixtureConfig = (extra: Readonly> = {}): AgentBundleConfig => ({ - plugin: { name: 'projection-fixture', version: '1.0.0' }, + plugin: { name: 'projection-fixture' }, ...extra, }); @@ -45,13 +45,22 @@ const toolModule = (options: { readonly config?: string; readonly schema?: string; readonly inputJsonSchema?: RouteInputSchema | null; -} = {}): string => [ - `export const config = ${options.inputJsonSchema === null ? options.config ?? '{}' : (options.config ?? "{ description: 'Submit work.' }").replace('{', '{ inputJsonSchema: ' + JSON.stringify(options.inputJsonSchema ?? { type: 'object', additionalProperties: false, properties: { laneKey: { type: 'string' } }, required: ['laneKey'] }) + ',')};`, - `export const inputSchema = ${options.schema ?? 'z.object({ laneKey: z.string() }).strict()'};`, - 'export const resultSchema = z.object({ ok: z.boolean() });', - 'export default async function Tool() { return undefined; }', - '', -].join('\n'); +} = {}): string => { + const config = options.inputJsonSchema === null + ? options.config ?? '{}' + : (options.config ?? "{ description: 'Submit work.' }").replace( + '{', + `{ inputJsonSchema: ${JSON.stringify(options.inputJsonSchema ?? { type: 'object', additionalProperties: false, properties: { laneKey: { type: 'string' } }, required: ['laneKey'] })},`, + ); + const fields = config.slice(1, -1).trim(); + return [ + "import { defineTool } from 'agent-bundle/routes';", + `export const inputSchema = ${options.schema ?? 'z.object({ laneKey: z.string() }).strict()'};`, + 'export const resultSchema = z.object({ ok: z.boolean() });', + `export default defineTool({ ${fields}${fields.length === 0 ? '' : ', '}inputSchema, resultSchema }, async function Tool() { return undefined; });`, + '', + ].join('\n'); +}; const cliModule = (config: string, mapInput?: string): string => [ `export const config = ${config};`, diff --git a/packages/agent-bundle/tests/cli-routes-build.test.ts b/packages/agent-bundle/tests/cli-routes-build.test.ts index 76b914be5..9942d43b9 100644 --- a/packages/agent-bundle/tests/cli-routes-build.test.ts +++ b/packages/agent-bundle/tests/cli-routes-build.test.ts @@ -67,7 +67,7 @@ it('builds and runs the generated routed-CLI executable', { retry: 2, timeout: 1 writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - " plugin: { description: 'Routed CLI fixture.', name: 'cli-bin-fixture', version: '1.0.0' },", + " plugin: { description: 'Routed CLI fixture.', name: 'cli-bin-fixture' },", ' routes: { mcpCommands: true },', " targets: ['portable'],", '});', @@ -203,29 +203,29 @@ it('builds and runs the generated routed-CLI executable', { retry: 2, timeout: 1 ].join('\n')), writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', [ "import { Agent, agent } from '@agent-bundle/runtime';", + "import { defineTool } from 'agent-bundle/routes';", "import { z } from 'zod';", - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.' };", 'export const inputSchema = z.object({ message: z.string().default("ready") }).strict();', "export const resultSchema = z.object({ invocation: z.enum(['cli', 'tool']), message: z.string(), operationId: z.string(), tooling: z.string(), view: z.unknown() }).strict();", - 'export default async function Lookup({ input }) {', + "export default defineTool({ inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.', inputSchema, resultSchema }, async (input) => {", ' const context = await agent();', " await context.progress.report({ completed: 1, message: 'lookup', total: 1 });", ' const result = { invocation: context.invocation.kind, message: input.message, operationId: context.invocation.operationId, tooling: `${(await context.provider("libraryTooling")).kind}:${(await context.provider("libraryTooling")).tool}`, view: (await context.provider("libraryTooling")).view };', ' return {`Lookup: ${input.message}`};', - '}', + '});', '', ].join('\n')), writeProjectFile(root, 'src/mcp/harness/tools/apply.tsx', [ "import { Agent, agent } from '@agent-bundle/runtime';", + "import { defineTool } from 'agent-bundle/routes';", "import { z } from 'zod';", - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"type\":\"object\"}, description: 'Applies one value.' };", 'export const inputSchema = z.object({ value: z.string() }).strict();', "export const resultSchema = z.object({ invocation: z.enum(['cli', 'tool']), operationId: z.string(), value: z.string() }).strict();", - 'export default async function Apply({ input }) {', + "export default defineTool({ inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"type\":\"object\"}, description: 'Applies one value.', inputSchema, resultSchema }, async (input) => {", ' const context = await agent();', ' const result = { invocation: context.invocation.kind, operationId: context.invocation.operationId, value: input.value };', ' return {`Applied ${input.value}.`};', - '}', + '});', '', ].join('\n')), writeProjectFile(root, 'src/scripts/checksum.ts', [ @@ -464,7 +464,7 @@ it('refuses a routed command that imports agent-bundle/api with AB4837 during de writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - " plugin: { description: 'Routed CLI fixture.', name: 'cli-bin-framework-import-fixture', version: '1.0.0' },", + " plugin: { description: 'Routed CLI fixture.', name: 'cli-bin-framework-import-fixture' },", " targets: ['portable'],", '});', '', @@ -541,7 +541,7 @@ describe('the CLI surface projection in the generated routed-CLI executable', () writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - " plugin: { description: 'CLI projection fixture.', name: 'cli-projection-fixture', version: '1.0.0' },", + " plugin: { description: 'CLI projection fixture.', name: 'cli-projection-fixture' },", ' routes: { mcpCommands: true },', " targets: ['portable'],", '});', @@ -549,25 +549,25 @@ describe('the CLI surface projection in the generated routed-CLI executable', () ].join('\n')), writeProjectFile(root, 'src/mcp/demo/tools/ping.tsx', [ "import { Agent } from '@agent-bundle/runtime';", + "import { defineTool } from 'agent-bundle/routes';", "import { z } from 'zod';", - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Answers a ping.' };", 'export const inputSchema = z.object({}).strict();', "export const resultSchema = z.object({ pong: z.literal(true) }).strict();", - 'export default async function Ping() {', + "export default defineTool({ inputJsonSchema: {\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Answers a ping.', inputSchema, resultSchema }, async () => {", ' return pong;', - '}', + '});', '', ].join('\n')), writeProjectFile(root, 'src/mcp/demo/tools/purge.tsx', [ "import { Agent } from '@agent-bundle/runtime';", + "import { defineTool } from 'agent-bundle/routes';", "import { z } from 'zod';", - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"target\":{\"type\":\"string\"}},\"required\":[\"target\"],\"type\":\"object\"}, annotations: { readOnlyHint: false }, description: 'Purges one cache target.' };", 'export const inputSchema = z.object({ target: z.string().min(1) }).strict();', "export const resultSchema = z.object({ operation: z.literal('purge'), target: z.string() }).strict();", - 'export default async function Purge({ input }) {', + "export default defineTool({ inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"target\":{\"type\":\"string\"}},\"required\":[\"target\"],\"type\":\"object\"}, annotations: { readOnlyHint: false }, description: 'Purges one cache target.', inputSchema, resultSchema }, async (input) => {", " const value = { operation: 'purge', target: input.target };", ' return {`purged: ${input.target}`};', - '}', + '});', '', ].join('\n')), writeProjectFile(root, 'src/mcp/demo/tools/purge.cli.ts', [ @@ -580,8 +580,8 @@ describe('the CLI surface projection in the generated routed-CLI executable', () ].join('\n')), writeProjectFile(root, 'src/mcp/demo/tools/submit.tsx', [ "import { Agent, agent } from '@agent-bundle/runtime';", + "import { defineTool } from 'agent-bundle/routes';", "import { z } from 'zod';", - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"argv\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"cwd\":{\"type\":\"string\",\"default\":\".\"},\"laneKey\":{\"type\":\"string\"},\"tags\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"yes\":{\"type\":\"boolean\"}},\"required\":[\"argv\"],\"type\":\"object\"}, annotations: { readOnlyHint: false }, description: 'Submits one command line as lane work.' };", // The application-owned optional `yes` key (#616): the projection // declares confirm: false, so the shell strips nothing and the value // must reach the tool through the canonical schema. @@ -600,7 +600,7 @@ describe('the CLI surface projection in the generated routed-CLI executable', () ' tags: z.array(z.string()).optional(),', ' yes: z.boolean().optional(),', '});', - 'export default async function Submit({ input }) {', + "export default defineTool({ inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"argv\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"cwd\":{\"type\":\"string\",\"default\":\".\"},\"laneKey\":{\"type\":\"string\"},\"tags\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"yes\":{\"type\":\"boolean\"}},\"required\":[\"argv\"],\"type\":\"object\"}, annotations: { readOnlyHint: false }, description: 'Submits one command line as lane work.', inputSchema, resultSchema }, async (input) => {", ' const { invocation } = await agent();', " const value = { ...input, operation: 'submit' };", ' return (', @@ -609,7 +609,7 @@ describe('the CLI surface projection in the generated routed-CLI executable', () ' {`invocation: ${invocation.kind} ${invocation.operationId} ${invocation.surface}`}', ' ', ' );', - '}', + '});', '', ].join('\n')), writeProjectFile(root, projectionModule, [ diff --git a/packages/agent-bundle/tests/cli-routes.test.ts b/packages/agent-bundle/tests/cli-routes.test.ts index f27011c0a..249ee86a6 100644 --- a/packages/agent-bundle/tests/cli-routes.test.ts +++ b/packages/agent-bundle/tests/cli-routes.test.ts @@ -46,13 +46,20 @@ const writeTree = async (root: string, files: Readonly>): }; const fixtureConfig = (extra: Readonly> = {}): AgentBundleConfig => ({ - plugin: { name: 'cli-fixture', version: '1.0.0' }, + plugin: { name: 'cli-fixture' }, ...extra, }); const codesOf = (diagnostics: readonly { readonly code: string }[]): string[] => diagnostics.map((diagnostic) => diagnostic.code); +const nonInteractiveTerminal = (hostSurface: 'cli' | 'script'): AgentTerminal => ({ + hostSurface, + sharesTarget: false, + stderr: { color: 'none', kind: 'none' }, + stdout: { color: 'none', kind: 'none' }, +}); + const extract = (schema: RouteInputSchema) => projectInputSchemaOptions(schema, 'src/cli/example.ts', '/project/src/cli/example.ts'); it('projects explicit input metadata into scalar, enum, and repeated flags', () => { @@ -98,13 +105,16 @@ const plainCommandModule = (options: { '', ].join('\n'); -const toolModule = (config?: string): string => [ - ...(config === undefined ? [] : [`export const config = ${config};`]), - 'export const inputSchema = operation.inputSchema;', - 'export const resultSchema = operation.resultSchema;', - 'export default async function Tool() { return undefined; }', - '', -].join('\n'); +const toolModule = (config = '{}'): string => { + const fields = config.slice(1, -1).trim(); + return [ + "import { defineTool } from 'agent-bundle/routes';", + 'export const inputSchema = operation.inputSchema;', + 'export const resultSchema = operation.resultSchema;', + `export default defineTool({ ${fields}${fields.length === 0 ? '' : ', '}inputSchema, resultSchema }, async function Tool() { return undefined; });`, + '', + ].join('\n'); +}; describe('compiled command graph', () => { it('compiles nesting, aliases, positionals, and the exit-code policy into graph.cli.commands', async () => { @@ -271,7 +281,7 @@ describe('compiled command graph', () => { const root = await createRoot(); await writeTree(root, { 'agent-bundle.config.ts': [ - "export default { plugin: { name: 'cli-fixture', version: '1.0.0' }, targets: ['portable'] };", + "export default { plugin: { name: 'cli-fixture' }, targets: ['portable'] };", '', ].join('\n'), 'package.json': '{"type":"module"}\n', @@ -298,7 +308,7 @@ describe('compiled command graph', () => { 'agent-bundle.config.ts': [ 'export default {', " bin: { 'cli-fixture': './src/tool.ts' },", - " plugin: { name: 'cli-fixture', version: '1.0.0' },", + " plugin: { name: 'cli-fixture' },", " targets: ['portable'],", '};', '', @@ -794,7 +804,6 @@ describe('generated CLI shell', () => { execute: async () => { throw new Error('plain execute must not run for an MCP command'); }, - isTty: () => false, name: 'curator', render: (command, input) => { calls.push({ command, input, json: argv.includes('--json') }); @@ -808,6 +817,7 @@ describe('generated CLI shell', () => { validate: (value) => value, }; }, + terminal: nonInteractiveTerminal('cli'), version: '1.2.3', writeErr: (text) => void stderr.push(text), writeOut: (text) => void stdout.push(text), @@ -1253,7 +1263,7 @@ describe('rendered command projection (#102 stage 3)', () => { options: { readonly error?: Error; readonly events?: readonly CliRenderedEvent[]; - readonly isTty?: boolean; + readonly terminal?: AgentTerminal; readonly validate?: (value: unknown) => unknown; } = {}, ): Promise => { @@ -1275,9 +1285,9 @@ describe('rendered command projection (#102 stage 3)', () => { execute: async () => { throw new Error('plain execute must not run for a rendered command'); }, - isTty: () => options.isTty ?? false, name: 'curator', render: () => session, + terminal: options.terminal ?? nonInteractiveTerminal('cli'), version: '1.2.3', writeErr: (text) => void stderr.push(text), writeOut: (text) => void stdout.push(text), @@ -1294,7 +1304,14 @@ describe('rendered command projection (#102 stage 3)', () => { }); it('updates progress in place on a TTY before the final document', async () => { - const tty = await runRendered(['report', '/library'], { isTty: true }); + const tty = await runRendered(['report', '/library'], { + terminal: { + hostSurface: 'cli', + sharesTarget: true, + stderr: { color: 'none', kind: 'tty' }, + stdout: { color: 'none', kind: 'tty' }, + }, + }); expect(tty.code).toBe(0); expect(tty.stdout).toBe('\r\u001B[2Kauditing (1/2)\r\u001B[2KFound **2** books.\n'); }); @@ -1347,24 +1364,6 @@ describe('rendered command projection (#102 stage 3)', () => { }); expect(seen).toEqual([terminal]); - // The legacy `isTty` knob still shapes a consistent capability for stdout. - const legacy: AgentTerminal[] = []; - await runGeneratedCliEntry({ - argv: ['report', '/library'], - commands: [renderedCommand], - execute: async () => undefined, - isTty: () => false, - name: 'curator', - render: (_command, _input, context) => { - legacy.push(context.terminal); - return { close: async () => undefined, events: () => eventStream(events), validate: (value) => value }; - }, - version: '1.2.3', - writeErr: () => undefined, - writeOut: () => undefined, - }); - expect(legacy[0]?.hostSurface).toBe('cli'); - expect(legacy[0]?.stdout.kind).not.toBe('tty'); }); it('emits the canonical validated final value under --json', async () => { @@ -1451,8 +1450,8 @@ describe('rendered script projection (#102 stage 3)', () => { validate: (value) => value, }; }, - isTty: () => false, name: 'summarize', + terminal: nonInteractiveTerminal('script'), writeErr: () => undefined, writeOut: (text) => void stdout.push(text), }); @@ -1469,8 +1468,8 @@ describe('rendered script projection (#102 stage 3)', () => { events: () => eventStream([{ document: completeDocument('failed', undefined), sequence: 0, type: 'complete' }]), validate: (value) => value, }), - isTty: () => false, name: 'summarize', + terminal: nonInteractiveTerminal('script'), writeErr: () => undefined, writeOut: () => undefined, }); diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index 3f9e4694f..db3c4522e 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -85,7 +85,7 @@ const createCliProject = async ( join(root, 'agent-bundle.config.ts'), [ 'export default ({ command, mode, projectRoot, selectedTargets }) => ({', - " plugin: { name: 'cli-fixture', version: '1.0.0' },", + " plugin: { name: 'cli-fixture' },", " targets: selectedTargets.length === 0 ? ['portable', 'codex'] : selectedTargets,", ' fixtureContext: { command, mode, projectRoot, selectedTargets },', ...(mcpApp ? [ @@ -123,7 +123,7 @@ const createServiceProject = async (): Promise => { join(root, 'agent-bundle.config.ts'), [ 'export default ({ selectedTargets }) => ({', - " plugin: { name: 'service-fixture', version: '1.0.0' },", + " plugin: { name: 'service-fixture' },", " targets: selectedTargets.length === 0 ? ['codex', 'claude'] : selectedTargets,", " hooks: { sessionStart: { handler: './src/hook.ts' } },", " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", @@ -933,7 +933,7 @@ it('dumps the lowered Rspack configuration of every output with inspect --bundle join(project.root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'cli-fixture', version: '1.0.0' },", + " plugin: { name: 'cli-fixture' },", " targets: ['portable'],", " scripts: { tool: './src/tool.ts' },", " tools: { rspack: { resolve: { alias: { '@fixture': './src' } } } },", @@ -1007,7 +1007,7 @@ it('reports source validation diagnostics on stderr before staging an artifact', await writeFile(join(output, 'sentinel.txt'), 'keep\n'); await writeFile( join(project.root, 'agent-bundle.config.ts'), - "export default { plugin: { version: '1.0.0' } };\n", + 'export default { plugin: {} };\n', ); const result = await runCli(project.root, [ diff --git a/packages/agent-bundle/tests/command-config.test.ts b/packages/agent-bundle/tests/command-config.test.ts index 228af8edf..3b3051c48 100644 --- a/packages/agent-bundle/tests/command-config.test.ts +++ b/packages/agent-bundle/tests/command-config.test.ts @@ -21,7 +21,7 @@ const loadedProject = ( targets: readonly string[], ): LoadedConfig => ({ config: { - plugin: { name: 'command-fixture', version: '1.0.0' }, + plugin: { name: 'command-fixture' }, targets: [...targets], }, configPath: join(root, 'agent-bundle.config.ts'), @@ -40,7 +40,7 @@ const withProject = async ( try { await writeFile( join(root, 'agent-bundle.config.ts'), - "export default { plugin: { name: 'command-fixture', version: '1.0.0' } };\n", + "export default { plugin: { name: 'command-fixture' } };\n", ); await run(root); } finally { @@ -163,7 +163,7 @@ it('discovers flat non-ignored commands deterministically and omits the collecti writeFile(join(root, 'src', 'commands', 'ignored.md'), '# Ignored\n'), ]); const config: AgentBundleConfig = { - plugin: { name: 'command-fixture', version: '1.0.0' }, + plugin: { name: 'command-fixture' }, }; const discovered = await discoverProject(root, config); diff --git a/packages/agent-bundle/tests/compiler-evidence.test.ts b/packages/agent-bundle/tests/compiler-evidence.test.ts index c4f74bfa7..9433ccc8d 100644 --- a/packages/agent-bundle/tests/compiler-evidence.test.ts +++ b/packages/agent-bundle/tests/compiler-evidence.test.ts @@ -270,7 +270,7 @@ describe('compiler evidence on host-pack builds', () => { const root = await fixtureRoot({ 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'compiler-evidence-fixture', version: '1.0.0' },", + " plugin: { name: 'compiler-evidence-fixture' },", " targets: ['portable'],", " tools: { rsbuild: { output: { externals: ['left-pad'] } } },", '};', diff --git a/packages/agent-bundle/tests/composite-rules.test.ts b/packages/agent-bundle/tests/composite-rules.test.ts index 2a71c1258..7f5f77821 100644 --- a/packages/agent-bundle/tests/composite-rules.test.ts +++ b/packages/agent-bundle/tests/composite-rules.test.ts @@ -66,7 +66,7 @@ const writeProject = async (root: string, options: FixtureOptions): Promise { await mkdir(join(fixture.root, 'configs'), { recursive: true }); await writeFile( configPath, - "export default { plugin: { name: 'fresh', version: '1.0.0' } };\n", + "export default { plugin: { name: 'fresh' } };\n", ); - expect((await loadConfig(options)).config.plugin).toEqual({ - name: 'fresh', - version: '1.0.0', - }); + expect((await loadConfig(options)).config.plugin).toEqual({ name: 'fresh' }); await writeFile( configPath, - "export default { plugin: { name: 'fresh', version: '2.0.0' } };\n", + "export default { plugin: { name: 'fresh' } };\n", ); await expect(loadConfig(options)).resolves.toMatchObject({ - config: { plugin: { name: 'fresh', version: '2.0.0' } }, + config: { plugin: { name: 'fresh' } }, configPath, }); } finally { @@ -289,30 +286,30 @@ it('reloads an overwritten static TypeScript default config', async () => { try { await expect(loadConfig(options)).resolves.toMatchObject({ - config: { plugin: { name: 'review', version: '1.0.0' } }, + config: { plugin: { name: 'review' } }, }); await writeFile( fixture.configPath, [ - "enum Version { Current = '1.0.0' }", - "export default { plugin: { name: 'typescript-fresh', version: Version.Current } };", + "enum Name { Current = 'typescript-fresh' }", + 'export default { plugin: { name: Name.Current } };', '', ].join('\n'), ); await expect(loadConfig(options)).resolves.toMatchObject({ - config: { plugin: { name: 'typescript-fresh', version: '1.0.0' } }, + config: { plugin: { name: 'typescript-fresh' } }, }); await writeFile( fixture.configPath, [ - "enum Version { Current = '2.0.0' }", - "export default { plugin: { name: 'typescript-fresh', version: Version.Current } };", + "enum Name { Current = 'typescript-fresher' }", + 'export default { plugin: { name: Name.Current } };', '', ].join('\n'), ); await expect(loadConfig(options)).resolves.toMatchObject({ - config: { plugin: { name: 'typescript-fresh', version: '2.0.0' } }, + config: { plugin: { name: 'typescript-fresher' } }, }); } finally { await removeProjectFixture(fixture.root); @@ -331,7 +328,7 @@ it('discovers an explicit non-conventional skill path relative to the project ro ); const discovered = await discoverProject(fixture.root, { - plugin: { name: 'review', version: '1.0.0' }, + plugin: { name: 'review' }, skills: ['custom/selected'], }); @@ -359,7 +356,7 @@ it('expands glob patterns in explicit skills entries and deduplicates overlappin ); const discovered = await discoverProject(fixture.root, { - plugin: { name: 'review', version: '1.0.0' }, + plugin: { name: 'review' }, skills: ['src/skills/*', 'custom/*/SKILL.md', 'custom/selected'], }); @@ -380,7 +377,7 @@ it('discovers conventional root assets and strips the assets/ prefix from destin await writeFile(join(fixture.root, 'assets/fonts/mono.woff'), 'font'); const discovered = await discoverProject(fixture.root, { - plugin: { name: 'review', version: '1.0.0' }, + plugin: { name: 'review' }, }); expect(discovered.assets).toEqual([ @@ -406,7 +403,7 @@ it('expands explicit asset entries as files, directories, and globs while droppi const discovered = await discoverProject(fixture.root, { assets: ['assets/logo.svg', 'branding', 'docs/*.md', 'missing/logo.svg'], - plugin: { name: 'review', version: '1.0.0' }, + plugin: { name: 'review' }, }); expect(discovered.assets).toEqual([ @@ -428,7 +425,7 @@ it('honors an explicit empty assets list instead of conventional discovery', asy const discovered = await discoverProject(fixture.root, { assets: [], - plugin: { name: 'review', version: '1.0.0' }, + plugin: { name: 'review' }, }); expect(discovered.assets).toEqual([]); diff --git a/packages/agent-bundle/tests/dev-artifact-service.test.ts b/packages/agent-bundle/tests/dev-artifact-service.test.ts index 1bd85e6b4..4b66dff19 100644 --- a/packages/agent-bundle/tests/dev-artifact-service.test.ts +++ b/packages/agent-bundle/tests/dev-artifact-service.test.ts @@ -27,7 +27,7 @@ const createProject = async (): Promise => { join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'artifact-service-fixture', version: '1.0.0' },", + " plugin: { name: 'artifact-service-fixture' },", " targets: ['portable'],", '};', '', @@ -44,6 +44,7 @@ const createProject = async (): Promise => { '', ].join('\n'), ), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), ]); return root; }; @@ -460,7 +461,7 @@ it('changes the canonical model digest when a registered extension changes', asy await Promise.all([ writeFile(join(leftRoot, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'extension-identity', version: '1.0.0' },", + " plugin: { name: 'extension-identity' },", " targets: ['portable'],", " portable: { compatibility: 'v1' },", '};', @@ -468,7 +469,7 @@ it('changes the canonical model digest when a registered extension changes', asy ].join('\n')), writeFile(join(rightRoot, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'extension-identity', version: '1.0.0' },", + " plugin: { name: 'extension-identity' },", " targets: ['portable'],", " portable: { compatibility: 'v2' },", '};', diff --git a/packages/agent-bundle/tests/dev-contract-config.test.ts b/packages/agent-bundle/tests/dev-contract-config.test.ts index 9a5622ab5..89eb64a64 100644 --- a/packages/agent-bundle/tests/dev-contract-config.test.ts +++ b/packages/agent-bundle/tests/dev-contract-config.test.ts @@ -6,7 +6,7 @@ import { createProjectFixture, removeProjectFixture } from './helpers/project-fi const configSource = [ 'export default {', " dev: { contracts: { fixtures: './contract-fixtures.ts', server: 'fixture' } },", - " plugin: { name: 'dev-contract-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-contract-fixture' },", '};', '', ].join('\n'); diff --git a/packages/agent-bundle/tests/dev-coordinator.test.ts b/packages/agent-bundle/tests/dev-coordinator.test.ts index 8535270bf..785bd05f5 100644 --- a/packages/agent-bundle/tests/dev-coordinator.test.ts +++ b/packages/agent-bundle/tests/dev-coordinator.test.ts @@ -24,7 +24,7 @@ import { createProjectFixture } from './helpers/project-fixture.ts'; const createProject = async (): Promise => (await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'dev-coordinator-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-coordinator-fixture' },", " targets: ['portable'],", '};', '', @@ -632,7 +632,7 @@ it('forwards prepared artifact and eval output roots to its watcher', async () = await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " output: { distPath: 'artifact-out' },", - " plugin: { name: 'dev-coordinator-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-coordinator-fixture' },", " targets: ['portable'],", '};', '', @@ -699,7 +699,7 @@ it('adds recovered artifact and eval roots to the live watcher before generated 'export default {', " evals: { runsDir: 'recorded-evals' },", " output: { distPath: 'artifact-out' },", - " plugin: { name: 'dev-coordinator-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-coordinator-fixture' },", " targets: ['portable'],", '};', '', diff --git a/packages/agent-bundle/tests/dev-package-build.test.ts b/packages/agent-bundle/tests/dev-package-build.test.ts index 6c710a41d..751e38027 100644 --- a/packages/agent-bundle/tests/dev-package-build.test.ts +++ b/packages/agent-bundle/tests/dev-package-build.test.ts @@ -17,7 +17,7 @@ it('rebuilds the package build inside the dev loop when its entries change', asy const fixture = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'dev-package-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-package-fixture' },", " targets: ['portable'],", '};', '', diff --git a/packages/agent-bundle/tests/dev-services.test.ts b/packages/agent-bundle/tests/dev-services.test.ts index 6eaaeb619..9ae8cad32 100644 --- a/packages/agent-bundle/tests/dev-services.test.ts +++ b/packages/agent-bundle/tests/dev-services.test.ts @@ -41,12 +41,13 @@ const createProject = async (skillMarkdown: string): Promise => { join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'dev-service-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-service-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), ), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'skills', 'review', 'SKILL.md'), skillMarkdown), ]); return root; @@ -95,12 +96,13 @@ const createRuntimeProject = async (options: Readonly<{ 'export default {', " dev: { runtime: { provider: " + JSON.stringify(options.provider ?? './src/dev/provider.ts') + ' } },', " mcp: { servers: { timeline: { apps: { dashboard: " + appDeclaration + " }, entry: './src/server.ts', targets: ['portable'] } } },", - " plugin: { name: 'dev-runtime-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-runtime-fixture' },", " targets: ['portable'],", ...(options.configExtension === undefined ? [] : [" portable: " + options.configExtension + ',']), '};', '', ].join('\n')), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), ]); return { metadataSentinel, root, sentinel }; }; @@ -180,6 +182,7 @@ it('surfaces a non-finite registered config extension as the closed AB4500 proje severity: 'error', sourcePath: join(project.root, 'agent-bundle.config.ts'), }], + packageVersion: '1.0.0', revision: expect.any(String), state: 'invalid', }); @@ -248,7 +251,7 @@ it('keeps lookalike proxy failures and control-character extension keys redacted try { await writeFile(keyedProject + '/agent-bundle.config.ts', [ 'const config = {', - " plugin: { name: 'bounded-extension-fixture', version: '1.0.0' },", + " plugin: { name: 'bounded-extension-fixture' },", " targets: ['bounded-extension'],", '};', `config[${JSON.stringify(longKey)}] = Number.NaN;`, @@ -293,7 +296,7 @@ it('keeps hostile config-extension accessors redacted behind AB7001', async () = try { await writeFile(project.root + '/agent-bundle.config.ts', [ 'const config = {', - " plugin: { name: 'hostile-extension-fixture', version: '1.0.0' },", + " plugin: { name: 'hostile-extension-fixture' },", " targets: ['portable'],", '};', "Object.defineProperty(config, 'portable', { enumerable: true, get: () => { throw new Error('hostile-extension-secret'); } });", @@ -463,6 +466,7 @@ it('creates an exact deeply frozen root-independent project context', async () = 'configDigest', 'configPath', 'modelDigest', + 'packageVersion', 'revision', 'sourceInputs', ]); @@ -471,6 +475,7 @@ it('creates an exact deeply frozen root-independent project context', async () = expect(left.projectContext?.sourceInputs.map((input) => input.path)).toEqual([ 'a-first.txt', 'agent-bundle.config.ts', + 'package.json', 'src/skills/review/SKILL.md', 'z-last.txt', ]); @@ -746,7 +751,7 @@ it('resolves, excludes, and falls back from configured artifact output paths', a writeFile(join(configuredRoot, 'agent-bundle.config.ts'), [ 'export default {', " output: { distPath: 'build/artifact' },", - " plugin: { name: 'configured-output', version: '1.0.0' },", + " plugin: { name: 'configured-output' },", " targets: ['portable'],", '};', '', @@ -754,7 +759,7 @@ it('resolves, excludes, and falls back from configured artifact output paths', a writeFile(join(malformedRoot, 'agent-bundle.config.ts'), [ 'export default {', ' output: { distPath: 7 },', - " plugin: { name: 'malformed-output', version: '1.0.0' },", + " plugin: { name: 'malformed-output' },", " targets: ['portable'],", '};', '', @@ -802,7 +807,7 @@ it('treats a configured eval run directory as generated output, not project sour await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " evals: { runsDir: 'recorded-evals' },", - " plugin: { name: 'review', version: '1.0.0' },", + " plugin: { name: 'review' },", " targets: ['portable'],", '};', '', @@ -836,7 +841,7 @@ it('reports external configuration symlinks without exposing the underlying path try { await writeFile(externalConfig, [ 'export default {', - " plugin: { name: 'external-config', version: '1.0.0' },", + " plugin: { name: 'external-config' },", " targets: ['portable'],", '};', '', @@ -877,7 +882,7 @@ it('reports snapshot failures as frozen preparation diagnostics', async () => { 'export default ({ projectRoot }) => {', ` symlinkSync(${JSON.stringify(externalOutput)}, \`${'${projectRoot}'}/snapshot-output\`);`, ' return {', - " plugin: { name: 'snapshot-failure', version: '1.0.0' },", + " plugin: { name: 'snapshot-failure' },", " targets: ['portable'],", ' };', '};', @@ -1380,7 +1385,7 @@ it.each([ await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', ` claude: { ${field}: './payload' },`, - " plugin: { name: 'escaped-payload', version: '1.0.0' },", + " plugin: { name: 'escaped-payload' },", " targets: ['claude'],", '};', '', @@ -1474,7 +1479,7 @@ it('changes a payload source revision when an executable bit is lost', async () await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " claude: { bin: './bin' },", - " plugin: { name: 'executable-payload', version: '1.0.0' },", + " plugin: { name: 'executable-payload' },", " targets: ['claude'],", '};', '', diff --git a/packages/agent-bundle/tests/dev-web-host-routes.test.ts b/packages/agent-bundle/tests/dev-web-host-routes.test.ts index 9a917ef1f..3ffcb9449 100644 --- a/packages/agent-bundle/tests/dev-web-host-routes.test.ts +++ b/packages/agent-bundle/tests/dev-web-host-routes.test.ts @@ -49,7 +49,7 @@ it('serves an exposed App and registers its opening call for page binding', asyn " apps: { status: { entry: './views/status.ts', resourceUri: 'ui://fixture/status.html', template: './views/status.html' } },", " entry: './src/server.ts',", ' } } },', - " plugin: { name: 'dev-web-host-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-web-host-fixture' },", " targets: ['portable'],", " web: { apps: [{ app: 'status/status', input: { service: 'compiler' }, tool: 'show-status' }] },", '});', diff --git a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts index f5e41f7e6..9d9aa497c 100644 --- a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts +++ b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts @@ -85,7 +85,7 @@ it('serves prebuilt workbench assets from an installed tarball without the repos await mkdir(join(project, 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile(join(project, 'package.json'), '{"type":"module"}\n'), - writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-workbench', version: '1.0.0' }, targets: ['portable'] };\n"), + writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-workbench' }, targets: ['portable'] };\n"), writeFile(join(project, 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n'), ]); @@ -136,7 +136,7 @@ it('packages both react-server render children and renders a route invocation fr await mkdir(join(project, 'src', 'mcp', 'status', 'tools'), { recursive: true }); await Promise.all([ writeFile(join(project, 'package.json'), '{"type":"module"}\n'), - writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-route-invocation', version: '1.0.0' }, targets: ['claude'] };\n"), + writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-route-invocation' }, targets: ['claude'] };\n"), writeFile(join(project, 'src', 'mcp', 'status', 'tools', 'report.tsx'), [ "import { Agent } from '@agent-bundle/runtime';", "import { createElement } from 'react';", @@ -200,7 +200,7 @@ it('runs the Agent API from an omit-dev installed tarball with its runtime MCP d await mkdir(join(project, 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile(join(project, 'package.json'), '{"type":"module"}\n'), - writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-agent-api', version: '1.0.0' }, targets: ['portable'] };\n"), + writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-agent-api' }, targets: ['portable'] };\n"), writeFile(join(project, 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n'), ]); const port = await availablePort(); diff --git a/packages/agent-bundle/tests/dev-workbench.test.ts b/packages/agent-bundle/tests/dev-workbench.test.ts index 62e67c025..c1323b715 100644 --- a/packages/agent-bundle/tests/dev-workbench.test.ts +++ b/packages/agent-bundle/tests/dev-workbench.test.ts @@ -111,7 +111,7 @@ const writeMcpProject = async (root: string): Promise => { ), ]); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'server.ts'), [ "import { McpServer } from '@modelcontextprotocol/server';", "import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';", @@ -132,7 +132,7 @@ const writeMcpProject = async (root: string): Promise => { '', 'export default defineConfig({', " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", - " plugin: { name: 'workbench-mcp-fixture', version: '1.0.0' },", + " plugin: { name: 'workbench-mcp-fixture' },", " skills: ['src/skills/review'],", " targets: ['portable'],", '});', @@ -144,7 +144,7 @@ const writeMcpProject = async (root: string): Promise => { const writeHookProject = async (root: string): Promise => { await mkdir(join(root, 'src', 'hooks'), { recursive: true }); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'hooks', 'session-start.ts'), [ 'export default (event: { source?: string }) => ({', " additionalContext: `workbench:${event.source}`,", @@ -157,7 +157,7 @@ const writeHookProject = async (root: string): Promise => { '', 'export default defineConfig({', " hooks: { sessionStart: './src/hooks/session-start.ts' },", - " plugin: { name: 'workbench-hook-fixture', version: '1.0.0' },", + " plugin: { name: 'workbench-hook-fixture' },", " skills: ['src/skills/review'],", " targets: ['claude'],", '});', @@ -238,7 +238,7 @@ const writeCompilingRuntimeAppProject = async (root: string, stateKey: string): '', 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", - " plugin: { name: 'compiling-runtime-apps', version: '1.0.0' },", + " plugin: { name: 'compiling-runtime-apps' },", " skills: ['src/skills/review'],", " targets: ['portable'],", '});', @@ -455,7 +455,7 @@ it('enables the optional agent API from dev.agentApi when no CLI override is sup '', 'export default defineConfig({', ' dev: { agentApi: true },', - " plugin: { name: 'agent-api-config-fixture', version: '1.0.0' },", + " plugin: { name: 'agent-api-config-fixture' },", " skills: ['src/skills/review'],", " targets: ['portable'],", '});', @@ -509,7 +509,7 @@ it('publishes routes created and deleted below source roots absent at startup', "import { defineConfig } from 'agent-bundle/config';", '', 'export default defineConfig({', - " plugin: { name: 'watcher-route-roots', version: '1.0.0' },", + " plugin: { name: 'watcher-route-roots' },", " targets: ['claude', 'codex'],", '});', '', @@ -643,7 +643,7 @@ it('latches a runtime declaration added to an ordinary Workbench session as rest '', 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", - " plugin: { name: 'review', version: '1.0.0' },", + " plugin: { name: 'review' },", " skills: ['src/skills/review'],", '});', '', @@ -686,7 +686,7 @@ it('keeps the ordinary foreground and artifact lane available when provider star '', 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", - " plugin: { name: 'review', version: '1.0.0' },", + " plugin: { name: 'review' },", " skills: ['src/skills/review'],", '});', '', @@ -736,7 +736,7 @@ it('retains Runtime App routes through invalid config updates and reconciles onl 'export default defineConfig({', ...(includeRuntime ? [" dev: { runtime: { provider: './src/dev/provider.ts' } },"] : []), ` fixtureMarker: ${JSON.stringify(marker)},`, - " plugin: { name: 'late-runtime-apps', version: '1.0.0' },", + " plugin: { name: 'late-runtime-apps' },", " skills: ['src/skills/review'],", ` targets: ${JSON.stringify(targets)},`, ...(extension === undefined ? [] : [` portable: ${extension},`]), @@ -933,7 +933,7 @@ it('fences a closing foreground before a held valid runtime reconcile can attach '', 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", - " plugin: { name: 'late-runtime-close', version: '1.0.0' },", + " plugin: { name: 'late-runtime-close' },", " skills: ['src/skills/review'],", ` targets: ${JSON.stringify(targets)},`, '});', @@ -1033,7 +1033,7 @@ it('does not reconcile a valid preparation released after foreground close begin const declaration = [ " dev: { runtime: { provider: './src/dev/provider.ts' } },", ` fixtureMarker: ${JSON.stringify(marker)},`, - " plugin: { name: 'prepared-runtime-close', version: '1.0.0' },", + " plugin: { name: 'prepared-runtime-close' },", " skills: ['src/skills/review'],", " targets: ['portable'],", ]; @@ -1186,7 +1186,7 @@ it('does not publish a prepared runtime topology after foreground close begins', ' await state.preparationReleased;', ' return defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", - " plugin: { name: 'prepared-topology-close', version: '1.0.0' },", + " plugin: { name: 'prepared-topology-close' },", " skills: ['src/skills/review'],", " targets: ['portable'],", ' });', @@ -1360,7 +1360,7 @@ it('prepares the optional runtime once with the development config context befor ` await appendFile(${JSON.stringify(configCallsPath)}, JSON.stringify({ command, mode }) + '\\n');`, ' return {', " dev: { runtime: { provider: './src/dev/provider.ts' } },", - " plugin: { name: 'runtime-fixture', version: '1.0.0' },", + " plugin: { name: 'runtime-fixture' },", " skills: ['src/skills/review'],", ' };', '});', @@ -1460,7 +1460,7 @@ it('builds and serves a target owned only by the workbench registry', async () = writeFile(join(assetsRoot, 'index.html'), 'Agent Bundle workbench'), writeFile(join(project.root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'workbench-custom-registry', version: '1.0.0' },", + " plugin: { name: 'workbench-custom-registry' },", " targets: ['workbench-synthetic'],", ' workbenchSynthetic: { enabled: true },', '};', diff --git a/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts b/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts index ffe566ac3..ee54dab77 100644 --- a/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts +++ b/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts @@ -41,7 +41,7 @@ const projectFiles: Readonly> = { 'agent-bundle.config.ts': [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - " plugin: { description: 'Handler graph fixture.', name: 'handler-graph-fixture', version: '1.0.0' },", + " plugin: { description: 'Handler graph fixture.', name: 'handler-graph-fixture' },", " targets: ['claude'],", '});', '', @@ -222,7 +222,7 @@ describe('handler artifact graph (#595)', () => { }); it('attaches the handler leaf to the event route node, and the leaf\'s source graph is cheap by construction', async () => { - const graph = await compileRouteGraph(root, { plugin: { name: 'handler-graph-fixture', version: '1.0.0' } }); + const graph = await compileRouteGraph(root, { plugin: { name: 'handler-graph-fixture' } }); expect(graph.diagnostics).toEqual([]); expect(graph.events.map((route) => route.id)).toEqual(['event:tool/before']); expect(graph.events[0]!.handler).toEqual({ diff --git a/packages/agent-bundle/tests/examples-contract.test.ts b/packages/agent-bundle/tests/examples-contract.test.ts index 273a26da7..23366c69b 100644 --- a/packages/agent-bundle/tests/examples-contract.test.ts +++ b/packages/agent-bundle/tests/examples-contract.test.ts @@ -300,8 +300,7 @@ it('derives the Audiobook Curator release identity from package.json as the one const inspection = await inspect({ root }); expect(inspection.state).toBe('ready'); if (inspection.state !== 'ready') throw new Error('unreachable'); - // package.json declares 1.0.0 once and the config declares no - // plugin.version, so the model version is inferred and cannot mismatch. + // package.json declares 1.0.0 once, so the model version cannot mismatch. expect(inspection.projectContext.packageName).toBe('@agent-bundle-example/audiobook-curator'); expect(inspection.projectContext.packageVersion).toBe('1.0.0'); expect(inspection.model.metadata).toMatchObject({ @@ -311,7 +310,6 @@ it('derives the Audiobook Curator release identity from package.json as the one version: '1.0.0', }); expect(projectVersionLabel(inspection.projectContext)).toBe('1.0.0'); - expect(inspection.diagnostics.filter((diagnostic) => diagnostic.code === 'AB4008')).toEqual([]); }); diff --git a/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts index 78339bb02..0937ced0d 100644 --- a/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts @@ -10,7 +10,6 @@ export default { plugin: { description: 'Proves portable installer and filesystem/schema conformance.', name: 'host-install-portable-proof', - version: '1.0.0', }, // The §5.4 descriptive metadata is declared once in package.json and shared // with every host; only the §5.6 client extension is portable-specific. diff --git a/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts index 48dae91f3..a756d4c31 100644 --- a/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts @@ -5,7 +5,6 @@ export default { plugin: { description: 'Proves canonical Skill tokens resolving in a real Claude session.', name: 'host-install-token-proof', - version: '1.0.0', }, skills: ['src/skills/token-probe'], targets: ['claude'], diff --git a/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts index 7a5c38b87..eeccbeff4 100644 --- a/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts @@ -12,7 +12,6 @@ export default { description: 'Proves real host installation of Skills, Hooks, and MCP metadata.', logo: './docs/media/logo.svg', name: 'host-install-proof', - version: '1.0.0', }, routes: { mcpCommands: true, diff --git a/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts index b61405b13..2425d0f6f 100644 --- a/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts @@ -3,7 +3,6 @@ export default { plugin: { description: 'Exercises native Eval hosts from an installed Agent Bundle tarball.', name: 'packed-native-smoke', - version: '1.0.0', }, skills: ['src/skills/review'], targets: ['claude', 'codex'], diff --git a/packages/agent-bundle/tests/fixtures/packed-native-smoke/package.json b/packages/agent-bundle/tests/fixtures/packed-native-smoke/package.json index c49133364..05e9cf090 100644 --- a/packages/agent-bundle/tests/fixtures/packed-native-smoke/package.json +++ b/packages/agent-bundle/tests/fixtures/packed-native-smoke/package.json @@ -1,5 +1,6 @@ { "name": "agent-bundle-packed-native-smoke", "private": true, - "type": "module" + "type": "module", + "version": "1.0.0" } diff --git a/packages/agent-bundle/tests/function-authoring-build.test.ts b/packages/agent-bundle/tests/function-authoring-build.test.ts index b79050ecc..b88f87cd0 100644 --- a/packages/agent-bundle/tests/function-authoring-build.test.ts +++ b/packages/agent-bundle/tests/function-authoring-build.test.ts @@ -13,7 +13,7 @@ it('runs compiled function events, explicit JSX views, and CLI metadata and JSON await symlink(join(process.cwd(), 'examples/audiobook-curator/node_modules'), join(root, 'node_modules'), 'dir'); const files = { 'package.json': JSON.stringify({ name: 'function-authoring', type: 'module', version: '1.0.0', dependencies: { '@agent-bundle/runtime': 'workspace:*', react: '19.2.8', zod: '4.5.4' } }), - 'agent-bundle.config.ts': `import { defineConfig } from 'agent-bundle/config'; export default defineConfig({ plugin: { name: 'function-authoring', version: '1.0.0' }, targets: ['claude'] });`, + 'agent-bundle.config.ts': `import { defineConfig } from 'agent-bundle/config'; export default defineConfig({ plugin: { name: 'function-authoring' }, targets: ['claude'] });`, 'src/providers/unused.ts': `throw new Error('unused provider evaluated'); export default () => 'unused';`, 'src/providers/policy.ts': `export default () => 'Writes disabled';`, 'src/events/tool/before.ts': `import { events } from 'agent-bundle/routes'; export default events.tool.before({}, async ({canonical, provider}) => { if (canonical.payload.toolName?.value === 'Write') return { outcome: 'deny', reason: await provider('policy') }; });`, @@ -95,7 +95,7 @@ it('shares one deadline across a lightweight handler and its deferred view', asy await symlink(join(process.cwd(), 'examples/audiobook-curator/node_modules'), join(root, 'node_modules'), 'dir'); const files = { 'package.json': JSON.stringify({ name: 'event-deadline', type: 'module', version: '1.0.0', dependencies: { '@agent-bundle/runtime': 'workspace:*', react: '19.2.8' } }), - 'agent-bundle.config.ts': `import { defineConfig } from 'agent-bundle/config'; export default defineConfig({ plugin: { name: 'event-deadline', version: '1.0.0' }, targets: ['claude'] });`, + 'agent-bundle.config.ts': `import { defineConfig } from 'agent-bundle/config'; export default defineConfig({ plugin: { name: 'event-deadline' }, targets: ['claude'] });`, 'src/events/tool/after.ts': `export const config = { timeoutMs: 1000 }; export default async ({ render }) => { await new Promise((resolve) => setTimeout(resolve, 350)); @@ -157,7 +157,7 @@ it('mounts identities, state, notices, and provider observations in a compiled l await symlink(join(process.cwd(), 'examples/audiobook-curator/node_modules'), join(root, 'node_modules'), 'dir'); const files = { 'package.json': JSON.stringify({ name: 'event-context', type: 'module', version: '1.0.0', dependencies: { '@agent-bundle/runtime': 'workspace:*', zod: '4.5.4' } }), - 'agent-bundle.config.ts': `import { defineConfig } from 'agent-bundle/config'; export default defineConfig({ plugin: { name: 'event-context', version: '1.0.0' }, targets: ['claude'] });`, + 'agent-bundle.config.ts': `import { defineConfig } from 'agent-bundle/config'; export default defineConfig({ plugin: { name: 'event-context' }, targets: ['claude'] });`, 'src/state.ts': `import { defineState } from '@agent-bundle/runtime/state'; import { z } from 'zod'; export default defineState({ id: 'event-context/state', lifetime: 'workspace-durable', initial: { count: 7 }, schema: z.object({ count: z.number() }), events: { tick: z.object({}) }, reduce: (state) => state });`, 'src/providers/context.ts': `export default async (ctx) => JSON.stringify({ session: ctx.session, workspace: ctx.workspace, lineage: ctx.lineage, plugin: ctx.plugin, snapshot: await ctx.state.read(), notices: ctx.notices !== undefined });`, diff --git a/packages/agent-bundle/tests/function-authoring.test.ts b/packages/agent-bundle/tests/function-authoring.test.ts index 8f428379f..bafd19039 100644 --- a/packages/agent-bundle/tests/function-authoring.test.ts +++ b/packages/agent-bundle/tests/function-authoring.test.ts @@ -22,7 +22,7 @@ export default tool({ description: 'Status', inputSchema: z.object({ verbose: z. it('extracts a bounded definition without inspecting or evaluating its handler', () => { expect(extractRouteConfig(source, path, path)).toMatchObject({ config: { description: 'Status' }, diagnostics: [] }); - expect(validateRouteModuleContract(source, path, path)).toEqual([]); + expect(validateRouteModuleContract(source, path, path, 'tool')).toEqual([]); }); it('extracts shorthand schemas in their original module scope', () => { @@ -31,7 +31,7 @@ import { z } from 'zod'; const inputSchema = z.object({ verbose: z.boolean().default(false) }); export default defineTool({ inputSchema, resultSchema: z.string() }, async () => 'ok');`; expect(readRouteDefinition(parseModule(path, shorthand))?.inputSchema).toBeDefined(); - expect(validateRouteModuleContract(shorthand, path, path)).toEqual([]); + expect(validateRouteModuleContract(shorthand, path, path, 'tool')).toEqual([]); }); it('normalizes the same inferred handler for generated and source execution', async () => { diff --git a/packages/agent-bundle/tests/generated-route-server.test.ts b/packages/agent-bundle/tests/generated-route-server.test.ts index d89704ff4..fda89b2b8 100644 --- a/packages/agent-bundle/tests/generated-route-server.test.ts +++ b/packages/agent-bundle/tests/generated-route-server.test.ts @@ -75,7 +75,7 @@ it('lists and calls a generated filesystem tool through final-only Flight', { re })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'generated-routes-fixture', version: '1.0.0' }, targets: ['portable'] });", + "export default defineConfig({ plugin: { name: 'generated-routes-fixture' }, targets: ['portable'] });", '', ].join('\n')), writeProjectFile(root, 'src/mcp/curator/resources/catalog.tsx', [ @@ -113,23 +113,26 @@ it('lists and calls a generated filesystem tool through final-only Flight', { re // contract reads: the build has to select the automatic JSX runtime, or // the emitted module calls a `React` factory that is not in scope. writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Inspect one source.' };", - "export const inputSchema = z.object({ source: z.string() }).strict();", - "export const resultSchema = z.object({ actor: z.unknown(), host: z.unknown(), invocationKind: z.literal('tool'), lineage: z.unknown(), session: z.unknown(), source: z.string(), workspace: z.unknown() }).strict();", - 'export default async function Inspect({ input, signal }) {', - " if (signal.aborted) throw new DOMException('aborted', 'AbortError');", - ' const context = await agent();', - ' const result = { actor: context.actor, host: context.host, invocationKind: context.invocation.kind, lineage: context.lineage, session: context.session, source: input.source, workspace: context.workspace };', - ' return (', - ' ', - ' {`Inspected **${input.source}**.`}', - ' ', - ' );', - '}', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "import { Agent, agent } from '@agent-bundle/runtime';", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "export const inputSchema = z.object({ source: z.string() }).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "export const resultSchema = z.object({ actor: z.unknown(), host: z.unknown(), invocationKind: z.literal('tool'), lineage: z.unknown(), session: z.unknown(), source: z.string(), workspace: z.unknown() }).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "export default defineTool({", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "annotations: { readOnlyHint: true }, description: 'Inspect one source.',", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " inputSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " resultSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "}, async (input, { signal }) => {", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " if (signal.aborted) throw new DOMException('aborted', 'AbortError');", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " const context = await agent();", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " const result = { actor: context.actor, host: context.host, invocationKind: context.invocation.kind, lineage: context.lineage, session: context.session, source: input.source, workspace: context.workspace };", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " return (", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " ", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " {`Inspected **${input.source}**.`}", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " ", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " );", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "});", + writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', ].join('\n')), ]); const output = join(root, 'artifact'); @@ -233,7 +236,7 @@ const writeGeneratedProject = async ( })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - `export default defineConfig({ plugin: { name: 'generated-routes-fixture', version: '1.0.0' }, targets: ['${target}'] });`, + `export default defineConfig({ plugin: { name: 'generated-routes-fixture' }, targets: ['${target}'] });`, '', ].join('\n')), ...Object.entries(files).map(([path, contents]) => writeProjectFile(root, path, contents)), @@ -397,7 +400,7 @@ it('augments a generated server from config and projects result _meta and text-o " args: ['--strict'],", " env: { CURATOR_MODE: 'strict' },", ' } } },', - " plugin: { name: 'generated-augment-fixture', version: '1.0.0' },", + " plugin: { name: 'generated-augment-fixture' },", " targets: ['portable'],", '});', '', @@ -405,31 +408,37 @@ it('augments a generated server from config and projects result _meta and text-o writeProjectFile(root, 'views/panel.ts', "document.body.textContent = 'Curator panel';\n"), // #383: `Agent.Result metadata` is the result-level `_meta`. writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { _meta: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, description: 'Annotated result.' };", - 'export const inputSchema = z.object({}).strict();', - "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - 'export default async function Annotated() {', - " return createElement(Agent.Result, { metadata: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", - '}', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { Agent } from '@agent-bundle/runtime';", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { createElement } from 'react';", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "export const inputSchema = z.object({}).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "export default defineTool({", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "_meta: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, description: 'Annotated result.',", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', " inputSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', " resultSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "}, async () => {", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', " return createElement(Agent.Result, { metadata: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "});", + writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', ].join('\n')), // A text-only tool declares no object result, so it advertises no // outputSchema and returns no structuredContent. writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { description: 'Text only.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.undefined();', - 'export default async function Plain() {', - " return createElement(Agent.Result, null, createElement(Agent.Text, null, 'plain text'));", - '}', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { Agent } from '@agent-bundle/runtime';", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { createElement } from 'react';", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "export const inputSchema = z.object({}).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "export const resultSchema = z.undefined();", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "export default defineTool({", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "description: 'Text only.',", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', " inputSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', " resultSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "}, async () => {", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', " return createElement(Agent.Result, null, createElement(Agent.Text, null, 'plain text'));", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "});", + writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', ].join('\n')), ]); const output = join(root, 'artifact'); @@ -505,18 +514,21 @@ it('compiles appResourceUri() and local literal references to the App route reso // The tool references the App through the compile-time helper (run-time // import from the light routes subpath, never the compiler root). 'src/mcp/curator/tools/open.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { appResourceUri } from 'agent-bundle/routes';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { _meta: { ui: { resourceUri: appResourceUri('dashboard') } }, description: 'Open the dashboard.' };", - 'export const inputSchema = z.object({}).strict();', - "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - 'export default async function Open() {', - " return createElement(Agent.Result, { value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/open.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/open.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/open.tsx': "import { appResourceUri } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/open.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/open.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/open.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/open.tsx': "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + 'src/mcp/curator/tools/open.tsx': "export default defineTool({", + 'src/mcp/curator/tools/open.tsx': "_meta: { ui: { resourceUri: appResourceUri('dashboard') } }, description: 'Open the dashboard.',", + 'src/mcp/curator/tools/open.tsx': " inputSchema,", + 'src/mcp/curator/tools/open.tsx': " resultSchema,", + 'src/mcp/curator/tools/open.tsx': "}, async () => {", + 'src/mcp/curator/tools/open.tsx': " return createElement(Agent.Result, { value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", + 'src/mcp/curator/tools/open.tsx': "});", + 'src/mcp/curator/tools/open.tsx': ].join('\n'), // The resource references the same constant module the App reads. 'src/mcp/curator/resources/catalog.tsx': [ "import { Agent } from '@agent-bundle/runtime';", @@ -682,23 +694,26 @@ it('observes one process-lifetime provider across consecutive generated tool cal '', ].join('\n'), 'src/mcp/curator/tools/warmth.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Observe process lifetime.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();', - 'export default async function Warmth() {', - ' const context = await agent();', - ' const processLifetime = context.process;', - " if (processLifetime === undefined || typeof processLifetime !== 'object' || processLifetime === null) {", - " throw new Error('process-lifetime provider was not installed');", - ' }', - ' const value = processLifetime as { hits: number; instanceId: string; pid: number };', - " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/warmth.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/warmth.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/warmth.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/warmth.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/warmth.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/warmth.tsx': "export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();", + 'src/mcp/curator/tools/warmth.tsx': "export default defineTool({", + 'src/mcp/curator/tools/warmth.tsx': "annotations: { readOnlyHint: true }, description: 'Observe process lifetime.',", + 'src/mcp/curator/tools/warmth.tsx': " inputSchema,", + 'src/mcp/curator/tools/warmth.tsx': " resultSchema,", + 'src/mcp/curator/tools/warmth.tsx': "}, async () => {", + 'src/mcp/curator/tools/warmth.tsx': " const context = await agent();", + 'src/mcp/curator/tools/warmth.tsx': " const processLifetime = context.process;", + 'src/mcp/curator/tools/warmth.tsx': " if (processLifetime === undefined || typeof processLifetime !== 'object' || processLifetime === null) {", + 'src/mcp/curator/tools/warmth.tsx': " throw new Error('process-lifetime provider was not installed');", + 'src/mcp/curator/tools/warmth.tsx': " }", + 'src/mcp/curator/tools/warmth.tsx': " const value = processLifetime as { hits: number; instanceId: string; pid: number };", + 'src/mcp/curator/tools/warmth.tsx': " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", + 'src/mcp/curator/tools/warmth.tsx': "});", + 'src/mcp/curator/tools/warmth.tsx': ].join('\n'), }); const session = await connectGeneratedServer(root); try { @@ -749,25 +764,28 @@ it('emits notifications/resources/updated for the durable notice inbox to the su '', ].join('\n'), 'src/mcp/curator/tools/notify.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { description: 'Publish a notice to a host-scoped recipient.' };", - 'export const inputSchema = z.object({ host: z.string(), message: z.string() }).strict();', - "export const resultSchema = z.object({ noticeId: z.string(), state: z.literal('pending') }).strict();", - 'export default async function Notify({ input }) {', - ' const context = await agent();', - " if (context.notices === undefined) throw new TypeError('notices unavailable');", - ' const published = await context.notices.publish({', - " content: { root: { kind: 'text', text: input.message }, status: 'success', version: 1 },", - " priority: 'normal',", - ' recipient: { host: { name: input.host } },', - ' }, { idempotencyKey: `notice:${input.host}:${input.message}` });', - ' const value = { noticeId: published.notice.id, state: published.notice.state };', - " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `published ${value.noticeId}`));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/notify.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/notify.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/notify.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/notify.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/notify.tsx': "export const inputSchema = z.object({ host: z.string(), message: z.string() }).strict();", + 'src/mcp/curator/tools/notify.tsx': "export const resultSchema = z.object({ noticeId: z.string(), state: z.literal('pending') }).strict();", + 'src/mcp/curator/tools/notify.tsx': "export default defineTool({", + 'src/mcp/curator/tools/notify.tsx': "description: 'Publish a notice to a host-scoped recipient.',", + 'src/mcp/curator/tools/notify.tsx': " inputSchema,", + 'src/mcp/curator/tools/notify.tsx': " resultSchema,", + 'src/mcp/curator/tools/notify.tsx': "}, async (input) => {", + 'src/mcp/curator/tools/notify.tsx': " const context = await agent();", + 'src/mcp/curator/tools/notify.tsx': " if (context.notices === undefined) throw new TypeError('notices unavailable');", + 'src/mcp/curator/tools/notify.tsx': " const published = await context.notices.publish({", + 'src/mcp/curator/tools/notify.tsx': " content: { root: { kind: 'text', text: input.message }, status: 'success', version: 1 },", + 'src/mcp/curator/tools/notify.tsx': " priority: 'normal',", + 'src/mcp/curator/tools/notify.tsx': " recipient: { host: { name: input.host } },", + 'src/mcp/curator/tools/notify.tsx': " }, { idempotencyKey: `notice:${input.host}:${input.message}` });", + 'src/mcp/curator/tools/notify.tsx': " const value = { noticeId: published.notice.id, state: published.notice.state };", + 'src/mcp/curator/tools/notify.tsx': " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `published ${value.noticeId}`));", + 'src/mcp/curator/tools/notify.tsx': "});", + 'src/mcp/curator/tools/notify.tsx': ].join('\n'), }); const inboxUri = 'agent-bundle://notices/inbox'; const session = await connectGeneratedServer(root); @@ -835,20 +853,23 @@ it('emits MCP progress notifications only when a progress token is supplied', { roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/progress.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Report progress.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - 'export default async function Progress() {', - ' const context = await agent();', - " await context.progress.report({ completed: 1, message: 'halfway', total: 2 });", - " await context.progress.report({ completed: 2, message: 'done', total: 2 });", - " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'finished'));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/progress.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/progress.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/progress.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/progress.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/progress.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/progress.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + 'src/mcp/curator/tools/progress.tsx': "export default defineTool({", + 'src/mcp/curator/tools/progress.tsx': "annotations: { readOnlyHint: true }, description: 'Report progress.',", + 'src/mcp/curator/tools/progress.tsx': " inputSchema,", + 'src/mcp/curator/tools/progress.tsx': " resultSchema,", + 'src/mcp/curator/tools/progress.tsx': "}, async () => {", + 'src/mcp/curator/tools/progress.tsx': " const context = await agent();", + 'src/mcp/curator/tools/progress.tsx': " await context.progress.report({ completed: 1, message: 'halfway', total: 2 });", + 'src/mcp/curator/tools/progress.tsx': " await context.progress.report({ completed: 2, message: 'done', total: 2 });", + 'src/mcp/curator/tools/progress.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'finished'));", + 'src/mcp/curator/tools/progress.tsx': "});", + 'src/mcp/curator/tools/progress.tsx': ].join('\n'), }); const session = await connectGeneratedServer(root); const notifications: Array<{ readonly message?: string; readonly progress: number; readonly progressToken: string | number; readonly total?: number }> = []; @@ -882,25 +903,28 @@ it('streams an authored Suspense fallback to the stdio client before the render roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/gated.tsx': [ - "import { existsSync } from 'node:fs';", - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement, Suspense } from 'react';", - "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Wait for a gate.' };", - 'export const inputSchema = z.object({ gate: z.string() }).strict();', - 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - 'const Gated = async ({ gate, signal }) => {', - ' while (!existsSync(gate)) {', - " if (signal.aborted) throw new DOMException('gate abandoned', 'AbortError');", - ' await new Promise((resolve) => setTimeout(resolve, 25));', - ' }', - " return createElement(Agent.Text, null, 'released');", - '};', - 'export default async function GatedTool({ input, signal }) {', - " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'waiting', total: 1 }) }, createElement(Gated, { gate: input.gate, signal })));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/gated.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/gated.tsx': "import { existsSync } from 'node:fs';", + 'src/mcp/curator/tools/gated.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/gated.tsx': "import { createElement, Suspense } from 'react';", + 'src/mcp/curator/tools/gated.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/gated.tsx': "export const inputSchema = z.object({ gate: z.string() }).strict();", + 'src/mcp/curator/tools/gated.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + 'src/mcp/curator/tools/gated.tsx': "const Gated = async ({ gate, signal }) => {", + 'src/mcp/curator/tools/gated.tsx': " while (!existsSync(gate)) {", + 'src/mcp/curator/tools/gated.tsx': " if (signal.aborted) throw new DOMException('gate abandoned', 'AbortError');", + 'src/mcp/curator/tools/gated.tsx': " await new Promise((resolve) => setTimeout(resolve, 25));", + 'src/mcp/curator/tools/gated.tsx': " }", + 'src/mcp/curator/tools/gated.tsx': " return createElement(Agent.Text, null, 'released');", + 'src/mcp/curator/tools/gated.tsx': "};", + 'src/mcp/curator/tools/gated.tsx': "export default defineTool({", + 'src/mcp/curator/tools/gated.tsx': "annotations: { readOnlyHint: true }, description: 'Wait for a gate.',", + 'src/mcp/curator/tools/gated.tsx': " inputSchema,", + 'src/mcp/curator/tools/gated.tsx': " resultSchema,", + 'src/mcp/curator/tools/gated.tsx': "}, async (input, { signal }) => {", + 'src/mcp/curator/tools/gated.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'waiting', total: 1 }) }, createElement(Gated, { gate: input.gate, signal })));", + 'src/mcp/curator/tools/gated.tsx': "});", + 'src/mcp/curator/tools/gated.tsx': ].join('\n'), }); const session = await connectGeneratedServer(root); const notifications: Array<{ readonly message?: string; readonly progress: number }> = []; @@ -958,15 +982,18 @@ it('projects thrown route errors as the SDK tool error or a JSON-RPC error, neve 'src/mcp/curator/resources/broken.ts': throwing('resource'), 'src/mcp/curator/tools/broken.ts': throwing('tool'), 'src/mcp/curator/tools/fine.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { description: 'Renders.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - "export default async function Fine() { return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'fine')); }", - '', - ].join('\n'), + 'src/mcp/curator/tools/fine.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/fine.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/fine.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/fine.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/fine.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/fine.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + 'src/mcp/curator/tools/fine.tsx': "export default defineTool({", + 'src/mcp/curator/tools/fine.tsx': "description: 'Renders.',", + 'src/mcp/curator/tools/fine.tsx': " inputSchema,", + 'src/mcp/curator/tools/fine.tsx': " resultSchema,", + 'src/mcp/curator/tools/fine.tsx': "}, async () => { return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'fine')); });", + 'src/mcp/curator/tools/fine.tsx': ].join('\n'), }); const session = await connectGeneratedServer(root); try { @@ -1006,22 +1033,25 @@ it('maps notifications/cancelled into the renderer AbortSignal', { retry: 2, tim roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/hang.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Hang until cancelled.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - 'export default async function Hang({ signal }) {', - ' await new Promise((_, reject) => {', - " const fail = () => reject(new DOMException('aborted', 'AbortError'));", - ' if (signal.aborted) { fail(); return; }', - " signal.addEventListener('abort', fail, { once: true });", - ' });', - " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'should not complete'));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/hang.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/hang.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/hang.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/hang.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/hang.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/hang.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + 'src/mcp/curator/tools/hang.tsx': "export default defineTool({", + 'src/mcp/curator/tools/hang.tsx': "annotations: { readOnlyHint: true }, description: 'Hang until cancelled.',", + 'src/mcp/curator/tools/hang.tsx': " inputSchema,", + 'src/mcp/curator/tools/hang.tsx': " resultSchema,", + 'src/mcp/curator/tools/hang.tsx': "}, async (_input, { signal }) => {", + 'src/mcp/curator/tools/hang.tsx': " await new Promise((_, reject) => {", + 'src/mcp/curator/tools/hang.tsx': " const fail = () => reject(new DOMException('aborted', 'AbortError'));", + 'src/mcp/curator/tools/hang.tsx': " if (signal.aborted) { fail(); return; }", + 'src/mcp/curator/tools/hang.tsx': " signal.addEventListener('abort', fail, { once: true });", + 'src/mcp/curator/tools/hang.tsx': " });", + 'src/mcp/curator/tools/hang.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'should not complete'));", + 'src/mcp/curator/tools/hang.tsx': "});", + 'src/mcp/curator/tools/hang.tsx': ].join('\n'), }); const session = await connectGeneratedServer(root); try { @@ -1040,32 +1070,38 @@ it('fails closed when the generated runtime worker restarts', { retry: 2, timeou roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/warmth.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Observe process lifetime.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();', - 'export default async function Warmth() {', - ' const context = await agent();', - ' const value = context.process as { hits: number; instanceId: string; pid: number };', - " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/warmth.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/warmth.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/warmth.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/warmth.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/warmth.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/warmth.tsx': "export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();", + 'src/mcp/curator/tools/warmth.tsx': "export default defineTool({", + 'src/mcp/curator/tools/warmth.tsx': "annotations: { readOnlyHint: true }, description: 'Observe process lifetime.',", + 'src/mcp/curator/tools/warmth.tsx': " inputSchema,", + 'src/mcp/curator/tools/warmth.tsx': " resultSchema,", + 'src/mcp/curator/tools/warmth.tsx': "}, async () => {", + 'src/mcp/curator/tools/warmth.tsx': " const context = await agent();", + 'src/mcp/curator/tools/warmth.tsx': " const value = context.process as { hits: number; instanceId: string; pid: number };", + 'src/mcp/curator/tools/warmth.tsx': " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", + 'src/mcp/curator/tools/warmth.tsx': "});", + 'src/mcp/curator/tools/warmth.tsx': ].join('\n'), 'src/mcp/curator/tools/halt.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { description: 'Halt the Flight worker.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - 'export default async function Halt() {', - ' process.exit(1);', - " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'halted'));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/halt.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/halt.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/halt.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/halt.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/halt.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/halt.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + 'src/mcp/curator/tools/halt.tsx': "export default defineTool({", + 'src/mcp/curator/tools/halt.tsx': "description: 'Halt the Flight worker.',", + 'src/mcp/curator/tools/halt.tsx': " inputSchema,", + 'src/mcp/curator/tools/halt.tsx': " resultSchema,", + 'src/mcp/curator/tools/halt.tsx': "}, async () => {", + 'src/mcp/curator/tools/halt.tsx': " process.exit(1);", + 'src/mcp/curator/tools/halt.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'halted'));", + 'src/mcp/curator/tools/halt.tsx': "});", + 'src/mcp/curator/tools/halt.tsx': ].join('\n'), 'src/events/session/start.tsx': [ "import { Agent } from '@agent-bundle/runtime';", "import { createElement } from 'react';", @@ -1101,17 +1137,20 @@ it('keeps a second generated server from the same install alive while the first roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/ping.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Answer from this process.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ pid: z.number() }).strict();', - 'export default async function Ping() {', - " return createElement(Agent.Result, { value: { pid: process.pid } }, createElement(Agent.Text, null, 'pong'));", - '}', - '', - ].join('\n'), + 'src/mcp/curator/tools/ping.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/ping.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/curator/tools/ping.tsx': "import { createElement } from 'react';", + 'src/mcp/curator/tools/ping.tsx': "import { z } from 'zod';", + 'src/mcp/curator/tools/ping.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/curator/tools/ping.tsx': "export const resultSchema = z.object({ pid: z.number() }).strict();", + 'src/mcp/curator/tools/ping.tsx': "export default defineTool({", + 'src/mcp/curator/tools/ping.tsx': "annotations: { readOnlyHint: true }, description: 'Answer from this process.',", + 'src/mcp/curator/tools/ping.tsx': " inputSchema,", + 'src/mcp/curator/tools/ping.tsx': " resultSchema,", + 'src/mcp/curator/tools/ping.tsx': "}, async () => {", + 'src/mcp/curator/tools/ping.tsx': " return createElement(Agent.Result, { value: { pid: process.pid } }, createElement(Agent.Text, null, 'pong'));", + 'src/mcp/curator/tools/ping.tsx': "});", + 'src/mcp/curator/tools/ping.tsx': ].join('\n'), 'src/events/session/start.tsx': [ "import { Agent } from '@agent-bundle/runtime';", "import { createElement } from 'react';", @@ -1182,35 +1221,41 @@ it('renders one tool/after event route through two native thin clients', { retry })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'generated-events-fixture', version: '1.0.0' }, targets: ['claude', 'cursor'] });", + "export default defineConfig({ plugin: { name: 'generated-events-fixture' }, targets: ['claude', 'cursor'] });", '', ].join('\n')), writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - 'export const inputSchema = z.object({}).strict();', - "export const resultSchema = z.object({ providerKind: z.literal('tool'), contextFrozen: z.literal(true) }).strict();", - 'export default async function Status() {', - ' const context = await agent();', - ' const requestValue = (await context.provider("requestValue")) as { kind: string };', - ' const value = { providerKind: requestValue.kind, contextFrozen: Object.isFrozen(context) };', - " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `provider:${requestValue.kind}`));", - '}', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { Agent, agent } from '@agent-bundle/runtime';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { createElement } from 'react';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const inputSchema = z.object({}).strict();", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const resultSchema = z.object({ providerKind: z.literal('tool'), contextFrozen: z.literal(true) }).strict();", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export default defineTool({", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " inputSchema,", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " resultSchema,", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "}, async () => {", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " const context = await agent();", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " const requestValue = (await context.provider(\"requestValue\")) as { kind: string };", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " const value = { providerKind: requestValue.kind, contextFrozen: Object.isFrozen(context) };", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `provider:${requestValue.kind}`));", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "});", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', ].join('\n')), writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - 'export default async function Explode() {', - " await (await agent()).provider('throwing');", - " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'unreachable'));", - '}', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { Agent, agent } from '@agent-bundle/runtime';", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { createElement } from 'react';", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "export const inputSchema = z.object({}).strict();", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "export default defineTool({", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " inputSchema,", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " resultSchema,", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "}, async () => {", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " await (await agent()).provider('throwing');", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'unreachable'));", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "});", + writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', ].join('\n')), writeProjectFile(root, 'src/providers/request-value.ts', [ "import type { AgentProviderFactory } from 'agent-bundle';", 'const provideRequest: AgentProviderFactory = ({ invocation }) => Object.freeze({ kind: invocation.kind });', @@ -1368,21 +1413,24 @@ it('renders composite root events through each selected host in one warm runtime })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'generated-plugin-events-fixture', version: '1.0.0' }, targets: ['claude', 'codex', 'cursor'] });", + "export default defineConfig({ plugin: { name: 'generated-plugin-events-fixture' }, targets: ['claude', 'codex', 'cursor'] });", '', ].join('\n')), writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "export const config = { description: 'Keep the shared event runtime warm.' };", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - 'export default async function Status() {', - " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'ready'));", - '}', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { Agent } from '@agent-bundle/runtime';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { createElement } from 'react';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const inputSchema = z.object({}).strict();", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export default defineTool({", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "description: 'Keep the shared event runtime warm.',", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " inputSchema,", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " resultSchema,", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "}, async () => {", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'ready'));", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "});", + writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', ].join('\n')), writeProjectFile(root, 'src/events/tool/after.tsx', [ "import { Agent, agent } from '@agent-bundle/runtime';", "import { createElement } from 'react';", @@ -1521,7 +1569,7 @@ it('runs an explicitly standalone event route without a shared runtime', { timeo })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'standalone-event-fixture', version: '1.0.0' }, targets: ['cursor'] });", + "export default defineConfig({ plugin: { name: 'standalone-event-fixture' }, targets: ['cursor'] });", '', ].join('\n')), writeProjectFile(root, 'src/events/tool/after.tsx', [ @@ -1575,7 +1623,7 @@ it('exits 1 with the message on stderr and no stdout when a standalone event rou })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'thrown-event-fixture', version: '1.0.0' }, targets: ['cursor'] });", + "export default defineConfig({ plugin: { name: 'thrown-event-fixture' }, targets: ['cursor'] });", '', ].join('\n')), writeProjectFile(root, 'src/events/tool/before.tsx', [ @@ -1628,7 +1676,7 @@ it('replays Claude and Codex subagent fixtures through standalone event-route wr })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'subagent-events-fixture', version: '1.0.0' }, targets: ['claude', 'codex'] });", + "export default defineConfig({ plugin: { name: 'subagent-events-fixture' }, targets: ['claude', 'codex'] });", '', ].join('\n')), writeProjectFile(root, 'src/events/agent/start.tsx', [ @@ -1697,7 +1745,7 @@ it('dispatches shared event routes through the invoking host contract', { timeou })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'plugin-subagent-events-fixture', version: '1.0.0' }, targets: ['claude', 'codex'] });", + "export default defineConfig({ plugin: { name: 'plugin-subagent-events-fixture' }, targets: ['claude', 'codex'] });", '', ].join('\n')), writeProjectFile(root, 'src/events/agent/start.tsx', [ diff --git a/packages/agent-bundle/tests/helpers/project-fixture.ts b/packages/agent-bundle/tests/helpers/project-fixture.ts index 9ab4330cc..1d4b39c1a 100644 --- a/packages/agent-bundle/tests/helpers/project-fixture.ts +++ b/packages/agent-bundle/tests/helpers/project-fixture.ts @@ -41,7 +41,22 @@ export const createProjectFixture = async ( const configPath = join(root, 'agent-bundle.config.ts'); if (options.config !== undefined || options.files !== undefined) { - const files = options.files ?? {}; + const authoredPackage = options.files?.['package.json']; + let packageJson = authoredPackage ?? '{"type":"module"}\n'; + if (typeof packageJson === 'string') { + try { + const document = JSON.parse(packageJson) as unknown; + if (document !== null && typeof document === 'object' && !Array.isArray(document) && !('version' in document)) { + packageJson = `${JSON.stringify({ ...document, version: '1.0.0' })}\n`; + } + } catch { + // Malformed package documents belong to the test that authored them. + } + } + const files: Readonly> = { + ...options.files, + 'package.json': packageJson, + }; await Promise.all([configPath, ...Object.keys(files).map((relativePath) => join(root, relativePath))] .map((path) => mkdir(dirname(path), { recursive: true }))); await Promise.all([ @@ -79,6 +94,7 @@ export const createProjectFixture = async ( await Promise.all([ writeFile(join(root, '.gitignore'), '*.log\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile( join(root, 'node_modules/agent-bundle/package.json'), JSON.stringify({ @@ -102,7 +118,7 @@ export const createProjectFixture = async ( ' projectRoot,', ' selectedTargets,', '}) => ({', - " plugin: { name: 'review', version: '1.0.0' },", + " plugin: { name: 'review' },", ` skills: ${skills},`, ' fixtureContext: { command, mode, projectRoot, selectedTargets },', '}));', diff --git a/packages/agent-bundle/tests/hook-playground-service.test.ts b/packages/agent-bundle/tests/hook-playground-service.test.ts index 59312058e..107568a2a 100644 --- a/packages/agent-bundle/tests/hook-playground-service.test.ts +++ b/packages/agent-bundle/tests/hook-playground-service.test.ts @@ -163,7 +163,7 @@ const publishHookEpoch = async ( sessionStart: './src/hooks/session-start.ts', stop: './src/hooks/stop.ts', }, - plugin: { name: 'hook-playground-fixture', version: '1.0.0' }, + plugin: { name: 'hook-playground-fixture' }, targets: ['codex', 'claude'], }), { skills: [] }, diff --git a/packages/agent-bundle/tests/hook-receipt-pipe.test.ts b/packages/agent-bundle/tests/hook-receipt-pipe.test.ts index 0614b85f5..070ddd570 100644 --- a/packages/agent-bundle/tests/hook-receipt-pipe.test.ts +++ b/packages/agent-bundle/tests/hook-receipt-pipe.test.ts @@ -97,7 +97,7 @@ it('posts a host-invoked hook execution to the dev server as hook.received / hoo })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'hook-receipt-fixture', version: '1.0.0' }, targets: ['claude'] });", + "export default defineConfig({ plugin: { name: 'hook-receipt-fixture' }, targets: ['claude'] });", '', ].join('\n')), writeProjectFile(root, 'src/events/tool/before.ts', [ diff --git a/packages/agent-bundle/tests/hooks.test.ts b/packages/agent-bundle/tests/hooks.test.ts index 4b2726afc..6a26bfe36 100644 --- a/packages/agent-bundle/tests/hooks.test.ts +++ b/packages/agent-bundle/tests/hooks.test.ts @@ -626,7 +626,7 @@ it('normalizes a shorthand session-start hook into a frozen stable record', asyn const loaded: LoadedConfig = { config: { hooks: { sessionStart: './src/hooks/session-start.ts' }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, configPath, context: { @@ -666,7 +666,7 @@ it('filters inherited hook targets through adapter hook capabilities', async () const loaded: LoadedConfig = { config: { hooks: { sessionStart: './src/hooks/session-start.ts' }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['portable', 'codex', 'claude'], }, configPath, @@ -697,7 +697,7 @@ it('loads and deterministically merges target-native hook documents after genera claude: { nativeHooks: './claude-hooks.json' }, codex: { nativeHooks: './codex-hooks.json' }, hooks: { sessionStart: './src/hooks/session-start.ts' }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['codex', 'claude'], }, configPath, @@ -771,7 +771,7 @@ it('reports stable target-native hook file diagnostics before merge', async () = config: { claude: { nativeHooks: './claude-broken.json' }, codex: { nativeHooks: './missing-codex.json' }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['codex', 'claude'], }, configPath: join(root, 'agent-bundle.config.ts'), @@ -1939,7 +1939,7 @@ it('normalizes a mixed hook fixture and reports malformed hook declarations', as sessionStart: './src/hooks/session-start.ts', stop: './src/hooks/stop.ts', }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, configPath, context: { command: 'build', mode: 'production', projectRoot: root, selectedTargets: [] }, diff --git a/packages/agent-bundle/tests/host-discovery-dev-server.test.ts b/packages/agent-bundle/tests/host-discovery-dev-server.test.ts index 38a373b83..996b22fef 100644 --- a/packages/agent-bundle/tests/host-discovery-dev-server.test.ts +++ b/packages/agent-bundle/tests/host-discovery-dev-server.test.ts @@ -41,7 +41,7 @@ it.each([ config: [ 'export default {', configOutput.trimEnd(), - " plugin: { name: 'host-discovery-dev-server', version: '1.0.0' },", + " plugin: { name: 'host-discovery-dev-server' },", " targets: ['claude'],", '};', '', diff --git a/packages/agent-bundle/tests/host-mcp-proxy.test.ts b/packages/agent-bundle/tests/host-mcp-proxy.test.ts index 108d3e3c3..74150231a 100644 --- a/packages/agent-bundle/tests/host-mcp-proxy.test.ts +++ b/packages/agent-bundle/tests/host-mcp-proxy.test.ts @@ -71,7 +71,7 @@ const writeProxyProject = async (root: string): Promise => { '', 'export default defineConfig({', " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", - " plugin: { name: 'host-proxy-fixture', version: '1.0.0' },", + " plugin: { name: 'host-proxy-fixture' },", " targets: ['portable'],", '});', '', diff --git a/packages/agent-bundle/tests/inspect-bundler.test.ts b/packages/agent-bundle/tests/inspect-bundler.test.ts index 8617f82ac..5582a3431 100644 --- a/packages/agent-bundle/tests/inspect-bundler.test.ts +++ b/packages/agent-bundle/tests/inspect-bundler.test.ts @@ -41,7 +41,7 @@ const createProject = async (): Promise => { join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'bundler-fixture', version: '1.0.0' },", + " plugin: { name: 'bundler-fixture' },", " targets: ['portable'],", " scripts: { tool: './src/tool.ts' },", ' mcp: {', @@ -252,7 +252,7 @@ it('wires output.sourceMap into the lowered generated-executable config', async join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'bundler-fixture', version: '1.0.0' },", + " plugin: { name: 'bundler-fixture' },", " targets: ['portable'],", " scripts: { tool: './src/tool.ts' },", ' output: { sourceMap: true },', @@ -297,7 +297,7 @@ it('reports a tools hatch the lowering refuses as an invalid inspection naming t join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'bundler-fixture', version: '1.0.0' },", + " plugin: { name: 'bundler-fixture' },", " targets: ['portable'],", " scripts: { tool: './src/tool.ts' },", // Aliasing a reserved specifier is refused only once the invariant hook diff --git a/packages/agent-bundle/tests/inspect-state.test.ts b/packages/agent-bundle/tests/inspect-state.test.ts index 1a41aec4b..d92c34347 100644 --- a/packages/agent-bundle/tests/inspect-state.test.ts +++ b/packages/agent-bundle/tests/inspect-state.test.ts @@ -27,7 +27,7 @@ const createProject = async (): Promise => { writeFile(join(root, 'package.json'), '{"type":"module"}\n'), writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'state-fixture', version: '1.0.0' },", + " plugin: { name: 'state-fixture' },", " targets: ['portable'],", '};', '', @@ -174,7 +174,7 @@ it('reports the declared notice retention policy and rejects a malformed one as await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " notices: { retention: { maxTerminal: 12, terminalTtl: '2d' } },", - " plugin: { name: 'state-fixture', version: '1.0.0' },", + " plugin: { name: 'state-fixture' },", " targets: ['portable'],", '};', '', @@ -190,7 +190,7 @@ it('reports the declared notice retention policy and rejects a malformed one as await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " notices: { retention: { terminalTtl: 'soon' } },", - " plugin: { name: 'state-fixture', version: '1.0.0' },", + " plugin: { name: 'state-fixture' },", " targets: ['portable'],", '};', '', @@ -238,7 +238,7 @@ it('reports stateless inspection and rejects competing state focuses', async () ].join('\n')); await writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'state-fixture', version: '1.0.0' },", + " plugin: { name: 'state-fixture' },", " targets: ['portable'],", ' state: false,', '};', diff --git a/packages/agent-bundle/tests/install-cli.test.ts b/packages/agent-bundle/tests/install-cli.test.ts index 9e8659a7a..4ca600dbc 100644 --- a/packages/agent-bundle/tests/install-cli.test.ts +++ b/packages/agent-bundle/tests/install-cli.test.ts @@ -41,7 +41,7 @@ describe('package-bound lifecycle commands', () => { it('pins the bundle root and forwards install flags to installBundle', async () => { const calls: unknown[] = []; const { code, stdout } = await runPinned( - ['install', 'cursor', '--mode', 'marketplace', '--scope', 'project', '--force', '--json'], + ['install', 'cursor', '--mode', 'marketplace', '--scope', 'project', '--replace', '--json'], { installBundle: async (options) => { calls.push(options); diff --git a/packages/agent-bundle/tests/install-surface.test.ts b/packages/agent-bundle/tests/install-surface.test.ts index d7bb21a0b..8a5a825a8 100644 --- a/packages/agent-bundle/tests/install-surface.test.ts +++ b/packages/agent-bundle/tests/install-surface.test.ts @@ -782,7 +782,7 @@ it('documents the same-version reinstall recipe per host, including Claude\'s ve expect(writesFor('cursor').get('INSTALL.md')).toContain('content-hash comparison'); const installer = writesFor('cursor').get('install.mjs') ?? ''; - expect(installer).toContain("argument === '--replace' || argument === '--force'"); + expect(installer).toContain("argument === '--replace'"); expect(installer).toContain(`const receiptFile = ${JSON.stringify(installReceiptFile)};`); expect(installer).toContain(`const receiptFormat = ${JSON.stringify(installReceiptFormat)};`); expect(installer).toContain(`const legacyReceiptFormat = ${JSON.stringify(legacyInstallReceiptFormat)};`); @@ -843,7 +843,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla const help = await run(installer, ['--help'], home); expect(help).toMatchObject({ code: 0 }); - expect(help.stdout).toContain('[--replace|--force]'); + expect(help.stdout).toContain('[--replace]'); const unknown = await run(installer, ['--bogus'], home); expect(unknown.code).toBe(2); expect(unknown.stderr).toContain('Unknown installer argument "--bogus"'); @@ -1055,7 +1055,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla expect(legacy.stderr).toContain(`content ${legacyHash.slice(0, 12)}`); expect(legacy.stderr).toContain('same version, different content'); expect(legacy.stderr).toContain('--replace'); - const adopted = await run(installer, ['--force'], home); + const adopted = await run(installer, ['--replace'], home); expect(adopted).toMatchObject({ code: 0, stderr: '' }); expect(adopted.stdout).toContain('Replaced install-fixture@1.2.3'); expect(await readFile(join(destination, 'payload.txt'), 'utf8')).toBe('rebuilt\n'); diff --git a/packages/agent-bundle/tests/install.test.ts b/packages/agent-bundle/tests/install.test.ts index 9672f4883..df9a37eec 100644 --- a/packages/agent-bundle/tests/install.test.ts +++ b/packages/agent-bundle/tests/install.test.ts @@ -1876,7 +1876,7 @@ it('dispatches the public CLI install command to the native installer', async () const terminal = captureCliTerminal(); const calls: unknown[] = []; const code = await runCli( - ['install', 'claude', '--from', '/tmp/example bundle', '--scope', 'project', '--force', '--json'], + ['install', 'claude', '--from', '/tmp/example bundle', '--scope', 'project', '--replace', '--json'], terminal.output, { installBundle: async (options: unknown) => { diff --git a/packages/agent-bundle/tests/layout-build.test.ts b/packages/agent-bundle/tests/layout-build.test.ts index c8d859b6f..134b93645 100644 --- a/packages/agent-bundle/tests/layout-build.test.ts +++ b/packages/agent-bundle/tests/layout-build.test.ts @@ -67,7 +67,7 @@ const writeLayoutProject = async (root: string, layouts: Readonly { ' },', ' } },', " payload: { runtime: definePrebuilt({ source: './built/runtime', runtimeDependencies: ['zod'] }) },", - " plugin: { description: 'The authoritative manifest combined proof.', name: 'manifest-combined-proof', version: '1.0.0' },", + " plugin: { description: 'The authoritative manifest combined proof.', name: 'manifest-combined-proof' },", ' routes: { mcpCommands: true },', " targets: ['claude', 'portable'],", " web: { apps: [{ allow: ['call-tool'], app: 'proof/status', tool: 'show-status' }] },", @@ -170,13 +170,13 @@ describe('the authoritative manifest combined proof', () => { ].join('\n'), 'src/mcp/proof/tools/show-status.tsx': [ "import { Agent } from '@agent-bundle/runtime';", + "import { defineTool } from 'agent-bundle/routes';", "import { z } from 'zod';", - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\"}},\"required\":[\"message\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Show combined proof status.', _meta: { ui: { resourceUri: 'ui://manifest-combined-proof/status.html' } } };", 'export const inputSchema = z.object({ message: z.string().min(1) }).strict();', 'export const resultSchema = z.object({ message: z.string() }).strict();', - 'export default async function ShowStatus({ input }) {', + "export default defineTool({ inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\"}},\"required\":[\"message\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Show combined proof status.', _meta: { ui: { resourceUri: 'ui://manifest-combined-proof/status.html' } }, inputSchema, resultSchema }, async (input) => {", ' return {input.message};', - '}', + '});', '', ].join('\n'), 'src/providers/state-probe.ts': [ diff --git a/packages/agent-bundle/tests/mcp-app-host-profiles.test.ts b/packages/agent-bundle/tests/mcp-app-host-profiles.test.ts index c051ebbf9..c31981398 100644 --- a/packages/agent-bundle/tests/mcp-app-host-profiles.test.ts +++ b/packages/agent-bundle/tests/mcp-app-host-profiles.test.ts @@ -70,7 +70,7 @@ const normalizedExtensions = async (extensions: Readonly (await normalizeProject({ config: { ...extensions, - plugin: { name: 'host-profile-normalized-config', version: '1.0.0' }, + plugin: { name: 'host-profile-normalized-config' }, }, configPath: `${projectRoot}/agent-bundle.config.ts`, context: { diff --git a/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts b/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts index 45b047e29..f77bccc64 100644 --- a/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts +++ b/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts @@ -20,7 +20,7 @@ it('runs an authenticated initialize and tools/list probe against a real built s ' },', ' },', " payload: { runtime: './prebuilt/runtime' },", - " plugin: { name: 'mcp-probe-dev-server', version: '1.0.0' },", + " plugin: { name: 'mcp-probe-dev-server' },", " targets: ['claude'],", '};', '', @@ -115,7 +115,7 @@ it('joins detached probe plugin-data cleanup into Workbench shutdown', { timeout ' },', ' },', " payload: { runtime: './prebuilt/runtime' },", - " plugin: { name: 'mcp-probe-dev-server-shutdown', version: '1.0.0' },", + " plugin: { name: 'mcp-probe-dev-server-shutdown' },", " targets: ['claude'],", '};', '', diff --git a/packages/agent-bundle/tests/mcp-session-service.test.ts b/packages/agent-bundle/tests/mcp-session-service.test.ts index 5a3c9856a..c443ad454 100644 --- a/packages/agent-bundle/tests/mcp-session-service.test.ts +++ b/packages/agent-bundle/tests/mcp-session-service.test.ts @@ -160,7 +160,7 @@ const publishFixtureEpoch = async ( }, }, }, - plugin: { name: 'persistent-mcp-fixture', version: '1.0.0' }, + plugin: { name: 'persistent-mcp-fixture' }, targets: [...targets], }), { skills: [] }, @@ -199,7 +199,7 @@ const publishRemoteEpoch = async (root: string, id: string): Promise }, }, }, - plugin: { name: 'persistent-mcp-remote-fixture', version: '1.0.0' }, + plugin: { name: 'persistent-mcp-remote-fixture' }, targets: ['portable'], }), { skills: [] }, diff --git a/packages/agent-bundle/tests/mcp.test.ts b/packages/agent-bundle/tests/mcp.test.ts index de12de430..b3cca647f 100644 --- a/packages/agent-bundle/tests/mcp.test.ts +++ b/packages/agent-bundle/tests/mcp.test.ts @@ -57,7 +57,7 @@ it('rejects legacy MCP SSE source declarations with one AB4317 diagnostic', () = }, }, }, - plugin: { name: 'legacy-sse', version: '1.0.0' }, + plugin: { name: 'legacy-sse' }, } as unknown as AgentBundleConfig; const diagnostics = validateSource(loadedProject('/workspace', config), { skills: [] }, registry); @@ -104,7 +104,7 @@ it('normalizes local, prebuilt, and HTTP MCP server declarations', async () => { const model = await normalizeProject( loadedProject(root, { mcp: { servers }, - plugin: { name: 'mcp-fixture', version: '1.0.0' }, + plugin: { name: 'mcp-fixture' }, targets: ['portable', 'codex', 'claude'], }), { skills: [] }, @@ -181,7 +181,7 @@ it('normalizes deeply frozen local MCP App declarations independently of the pro }, }, }, - plugin: { name: 'mcp-app-fixture', version: '1.0.0' }, + plugin: { name: 'mcp-app-fixture' }, targets: ['portable', 'claude'], } as unknown as AgentBundleConfig; try { @@ -237,7 +237,7 @@ it('keeps local MCP server identities and output aliases independent of the proj } const config: AgentBundleConfig = { mcp: { servers: { 'same server': { entry: './src/server.ts' } } }, - plugin: { name: 'mcp-fixture', version: '1.0.0' }, + plugin: { name: 'mcp-fixture' }, }; const [leftModel, rightModel] = await Promise.all([left, right].map((root) => normalizeProject(loadedProject(root, config), { skills: [] }, registry))); @@ -276,7 +276,7 @@ it('reports source and model diagnostics before an MCP server can be compiled', 'unknown target': { command: 'server', targets: ['unknown'] }, }, }, - plugin: { name: 'mcp-fixture', version: '1.0.0' }, + plugin: { name: 'mcp-fixture' }, } as unknown as AgentBundleConfig; const loaded = loadedProject(root, config); @@ -332,7 +332,7 @@ it('rejects a hostile normalized legacy SSE transport before adapters can plan i }, }, }, - plugin: { name: 'hostile-model', version: '1.0.0' }, + plugin: { name: 'hostile-model' }, }), { skills: [] }, registry, @@ -405,7 +405,7 @@ it('rejects unsafe, duplicate, and nonlocal MCP App declarations before browser }, }, }, - plugin: { name: 'mcp-app-invalid', version: '1.0.0' }, + plugin: { name: 'mcp-app-invalid' }, } as unknown as AgentBundleConfig; expect(validateSource(loadedProject(root, malformed), { skills: [] }, registry).map(({ code }) => code)).toEqual( @@ -439,7 +439,7 @@ it('rejects unsafe, duplicate, and nonlocal MCP App declarations before browser }, }, }, - plugin: { name: 'mcp-app-invalid', version: '1.0.0' }, + plugin: { name: 'mcp-app-invalid' }, }), { skills: [] }, registry, @@ -484,7 +484,7 @@ it('rejects non-JSON MCP App metadata before normalization', async () => { }, }, }, - plugin: { name: 'mcp-app-meta', version: '1.0.0' }, + plugin: { name: 'mcp-app-meta' }, } as unknown as AgentBundleConfig; const diagnostics = validateSource(loadedProject(root, config), { skills: [] }, registry); @@ -527,7 +527,7 @@ it('bundles each local MCP entry once and maps every target manifest to that art }, }, }, - plugin: { name: 'mcp-fixture', version: '1.0.0' }, + plugin: { name: 'mcp-fixture' }, targets: ['portable', 'codex', 'claude'], }), { skills: [] }, @@ -681,7 +681,7 @@ it('inlines agent-bundle/launch-env into a self-connecting entry so it can apply const model = await normalizeProject( loadedProject(root, { mcp: { servers: { probe: { entry: './src/probe.ts' } } }, - plugin: { name: 'mcp-self-connecting-env', version: '1.0.0' }, + plugin: { name: 'mcp-self-connecting-env' }, targets: ['portable'], }), { skills: [] }, @@ -760,7 +760,7 @@ it('lets the operator .env beat a manifest env default the host passed through, }, }, }, - plugin: { name: 'mcp-manifest-env', version: '1.0.0' }, + plugin: { name: 'mcp-manifest-env' }, targets: ['claude'], }), { skills: [] }, @@ -868,7 +868,7 @@ it('redirects stdout written at module scope by the server module to stderr befo const model = await normalizeProject( loadedProject(root, { mcp: { servers: { chatty: { entry: './src/server.ts' } } }, - plugin: { name: 'mcp-module-scope-stdout', version: '1.0.0' }, + plugin: { name: 'mcp-module-scope-stdout' }, targets: ['portable'], }), { skills: [] }, @@ -940,7 +940,7 @@ it('builds one deterministic self-contained MCP App view and injects it through }, }, }, - plugin: { name: 'mcp-app-build', version: '1.0.0' }, + plugin: { name: 'mcp-app-build' }, targets: ['portable', 'codex', 'claude'], }), { skills: [] }, @@ -1027,8 +1027,8 @@ it('injects one release identity into both the Node bundle and the browser MCP A await mkdir(join(root, 'src'), { recursive: true }); await mkdir(join(root, 'views'), { recursive: true }); await writeFile(join(root, 'agent-bundle.config.ts'), 'export default {};\n'); - // package.json is the only version source: the config declares no - // plugin.version, so every compiled surface must agree on 4.5.6. + // package.json is the only version source, so every compiled surface must + // agree on 4.5.6. await writeFile( join(root, 'package.json'), `${JSON.stringify({ name: '@scope/meta-fixture', version: '4.5.6' })}\n`, @@ -1128,7 +1128,7 @@ it('compiles one shared MCP App once and serves it from every identically declar public: { apps: { widget: { ...widget } }, entry: './src/public.ts' }, }, }, - plugin: { name: 'mcp-app-shared', version: '1.0.0' }, + plugin: { name: 'mcp-app-shared' }, targets: ['portable'], }; // Both fixture entries are deliberately self-connecting registry probes, @@ -1196,7 +1196,7 @@ it('rejects conflicting same-name MCP App declarations at compilation planning', }, }, }, - plugin: { name: 'mcp-app-conflict', version: '1.0.0' }, + plugin: { name: 'mcp-app-conflict' }, targets: ['portable'], }), { skills: [] }, @@ -1301,7 +1301,7 @@ it('uses the selected streamable HTTP manifest with propagated cancellation and }, }, }, - plugin: { name: 'mcp-remote-fixture', version: '1.0.0' }, + plugin: { name: 'mcp-remote-fixture' }, targets: ['claude'], }), { skills: [] }, @@ -1373,7 +1373,7 @@ it('rejects a selected projection without its manifest-declared MCP document', a }, }, }, - plugin: { name: 'mcp-projection-document', version: '1.0.0' }, + plugin: { name: 'mcp-projection-document' }, targets: ['claude', 'codex'], }), { skills: [] }, @@ -1439,7 +1439,7 @@ it('creates session state only after setup succeeds and always inherits the stdi inherited: { entry: './src/server.ts' }, }, }, - plugin: { name: 'mcp-stdio-options', version: '1.0.0' }, + plugin: { name: 'mcp-stdio-options' }, targets: ['portable'], }), { skills: [] }, @@ -1571,7 +1571,7 @@ it('serves compiler-bundled MCP App resources from a copied artifact without pro }, }, }, - plugin: { name: 'mcp-app-resource', version: '1.0.0' }, + plugin: { name: 'mcp-app-resource' }, targets: ['portable'], }), { skills: [] }, @@ -1702,7 +1702,7 @@ it('lists tools from a validated copied artifact without reading project source' }, }, }, - plugin: { name: 'mcp-service-fixture', version: '1.0.0' }, + plugin: { name: 'mcp-service-fixture' }, targets: ['portable'], }), { skills: [] }, diff --git a/packages/agent-bundle/tests/normalization.test.ts b/packages/agent-bundle/tests/normalization.test.ts index ddd20f9d3..d09b306ba 100644 --- a/packages/agent-bundle/tests/normalization.test.ts +++ b/packages/agent-bundle/tests/normalization.test.ts @@ -114,7 +114,7 @@ it.each([ ])('normalizes target selection from $label', async (testCase) => { const loaded = loadedProject( { - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: testCase.configTargets, }, { selectedTargets: testCase.selectedTargets }, @@ -133,7 +133,7 @@ it('normalizes registered extensions and validates registered script and hook ta example: { nested: { enabled: true } }, hooks: { sessionStart: { handler: './hooks/start.ts', targets: ['example'] } }, ignored: { mustNotReachTheModel: true }, - plugin: { name: 'extension-fixture', version: '1.0.0' }, + plugin: { name: 'extension-fixture' }, scripts: { run: { entry: './packages/agent-bundle/src/config/normalize.ts', @@ -148,10 +148,7 @@ it('normalizes registered extensions and validates registered script and hook ta targetRegistry: NormalizationTargetRegistry, ) => Diagnostic[]; - // This fixture is rooted at the workspace, whose package.json version - // (0.0.0) differs from the fixture's plugin.version, so the AB4008 - // mismatch warning (#94) is the only expected diagnostic. - expect(sourceValidator(loaded, { skills: [] }, extensionRegistry).map(({ code }) => code)).toEqual(['AB4008']); + expect(sourceValidator(loaded, { skills: [] }, extensionRegistry).map(({ code }) => code)).toEqual([]); const model = await normalizeProject(loaded, { skills: [] }, extensionRegistry); const extensions = (model as unknown as { @@ -188,7 +185,7 @@ it('enumerates lsp components with unambiguous ids for any server name (#100)', '\uD800': { command: 'third-ls', extensionToLanguage: { '.c': 'c' } }, }, }, - plugin: { name: 'claude-lsp-fixture', version: '1.0.0' }, + plugin: { name: 'claude-lsp-fixture' }, targets: ['claude', 'cursor'], }), { skills: [] }, createDefaultRegistry()); @@ -220,7 +217,7 @@ it('normalizes the typed Claude LSP source surface through the strict JSON exten }, }, }, - plugin: { name: 'claude-lsp-fixture', version: '1.0.0' }, + plugin: { name: 'claude-lsp-fixture' }, targets: ['claude'], }), { skills: [] }, createDefaultRegistry()); const extension = model.extensions.claude; @@ -262,7 +259,7 @@ it('enumerates claude.bin relative to the config file into immutable executable const loaded: LoadedConfig = { ...loadedProject({ claude: { bin: '../tools' }, - plugin: { name: 'claude-bin-fixture', version: '1.0.0' }, + plugin: { name: 'claude-bin-fixture' }, targets: ['claude'], }, { root }), configPath: join(configDir, 'agent-bundle.config.ts'), @@ -323,7 +320,7 @@ it('enumerates Claude workflows and output styles relative to the config file in outputStyles: '../styles', workflows: '../workflows', }, - plugin: { name: 'claude-payload-fixture', version: '1.0.0' }, + plugin: { name: 'claude-payload-fixture' }, targets: ['claude'], }, { root }), configPath: join(configDir, 'agent-bundle.config.ts'), @@ -372,7 +369,7 @@ it.each([ if (create) await mkdir(binRoot, { recursive: true }); const loaded = loadedProject({ claude: { bin: './tools' }, - plugin: { name: 'claude-bin-diagnostic', version: '1.0.0' }, + plugin: { name: 'claude-bin-diagnostic' }, targets: ['claude'], }, { root }); @@ -404,7 +401,7 @@ it.each([ if (issue === 'empty') await mkdir(sourceRoot, { recursive: true }); const loaded = loadedProject({ claude: { [field]: './payload' }, - plugin: { name: 'claude-payload-diagnostic', version: '1.0.0' }, + plugin: { name: 'claude-payload-diagnostic' }, targets: ['claude'], }, { root }); @@ -428,7 +425,7 @@ it('rejects a Claude payload directory symlink that resolves outside the project await symlink(outside, linked, 'dir'); const loaded = loadedProject({ claude: { outputStyles: './styles' }, - plugin: { name: 'claude-payload-symlink', version: '1.0.0' }, + plugin: { name: 'claude-payload-symlink' }, targets: ['claude'], }, { root }); @@ -480,7 +477,7 @@ it('rejects non-JSON values in registered config extensions before normalization for (const value of values) { await expect(normalizeProject(loadedProject({ example: value, - plugin: { name: 'extension-json-fixture', version: '1.0.0' }, + plugin: { name: 'extension-json-fixture' }, }), { skills: [] }, extensionRegistry)).rejects.toThrow( 'AB4500: A registered config extension must contain strict finite JSON data.', ); @@ -503,7 +500,7 @@ it('detaches and freezes strict JSON extension values with special own keys and }); const model = await normalizeProject(loadedProject({ example: extension, - plugin: { name: 'extension-json-fixture', version: '1.0.0' }, + plugin: { name: 'extension-json-fixture' }, }), { skills: [] }, extensionRegistry); const value = model.extensions.example!.value as Record; @@ -532,7 +529,7 @@ it('reports unknown hook and script targets through the target registry', () => }; const loaded = loadedProject({ hooks: { stop: { handler: './hooks/stop.ts', targets: ['unknown'] } }, - plugin: { name: 'registry-validation', version: '1.0.0' }, + plugin: { name: 'registry-validation' }, scripts: { run: { entry: './packages/agent-bundle/src/config/normalize.ts', @@ -546,10 +543,7 @@ it('reports unknown hook and script targets through the target registry', () => registry: NormalizationTargetRegistry, ) => Diagnostic[]; - // The workspace root package.json version (0.0.0) differs from the - // fixture's plugin.version, adding the AB4008 mismatch warning (#94). expect(sourceValidator(loaded, { skills: [] }, targetRegistry).map(({ code }) => code)).toEqual([ - 'AB4008', 'AB4203', 'AB4406', ]); @@ -563,7 +557,7 @@ it('validates native hook targets through the target registry', async () => { supports: (name, capability) => name === 'example' && capability === 'hooks', }; const loaded = loadedProject({ - plugin: { name: 'native-hook-targets', version: '1.0.0' }, + plugin: { name: 'native-hook-targets' }, }); const model = await normalizeProject(loaded, { skills: [] }, targetRegistry); const nativeHooks = [ @@ -594,7 +588,7 @@ it('produces root-independent IDs, complete provenance, and deeply immutable out const leftRoot = '/workspace/left'; const rightRoot = '/different/right'; const config: AgentBundleConfig = { - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, skills: ['src/skills/review'], }; const leftSkill = skill(leftRoot, 'review', 'review', { @@ -656,7 +650,7 @@ it('reports stable source diagnostics for malformed and conflicting Skills', () sourcePath: duplicate.source, }); const loaded = loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }); const diagnostics = validateSource(loaded, { skills: [first, duplicate] }, registry); @@ -681,7 +675,7 @@ it('maps pinned Agent Skills schema issues to stable source diagnostics without document.frontmatter.unknown = true; expect(validateSource( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { skills: [document] }, registry, )).toEqual([ @@ -719,7 +713,7 @@ it('validates raw optional portable Skill fields before sanitizing the Skill IR' document.frontmatter.compatibility = false; expect(validateSource( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { skills: [document] }, registry, )).toEqual([ @@ -735,8 +729,6 @@ it('validates raw optional portable Skill fields before sanitizing the Skill IR' it('diagnoses a missing plugin object instead of throwing', () => { const loaded = loadedProject({} as AgentBundleConfig); - // Only the name is required: an omitted version is inferred from - // package.json, so AB4001 fires on a declared-but-invalid value alone. expect(validateSource(loaded, { skills: [] }, registry)).toMatchObject([ { code: 'AB4000', sourcePath: loaded.configPath }, ]); @@ -761,7 +753,7 @@ it('validates reference-style links while ignoring Markdown code examples', () = }); const diagnostics = validateSource( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { skills: [document] }, registry, ); @@ -783,7 +775,7 @@ it('accepts decoded inline Skill resource paths that contain spaces', () => { }); expect(validateSource( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { skills: [document] }, registry, )).toEqual([]); @@ -805,7 +797,7 @@ it('uses the first definition when validating shortcut Markdown references', () resources: ['SKILL.md', 'references/existing.md'], }); const loaded = loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }); expect(validateSource(loaded, { skills: [missing] }, registry)).toMatchObject([ @@ -832,7 +824,7 @@ it('does not treat definitions as uses and reserves an external first definition ].join('\n'), }); const loaded = loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }); expect(validateSource(loaded, { skills: [unusedDefinition] }, registry)).toEqual([]); @@ -849,7 +841,7 @@ it('normalizes explicit host-native hook tool selectors alongside canonical tool tools: ['shell', 'codex:view_image', 'claude:WebSearch', 'claude:WebSearch', 'portable:Nope'], }, }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['claude', 'codex'], }), { skills: [] }, @@ -865,7 +857,7 @@ it('normalizes explicit host-native hook tool selectors alongside canonical tool const withoutSelectors = await normalizeProject( loadedProject({ hooks: { beforeTool: { handler: './hooks/guard.ts', tools: ['shell'] } }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['claude', 'codex'], }), { skills: [] }, @@ -880,7 +872,7 @@ it('validates host-native hook tool selectors against the registry and hook targ validateSource( loadedProject({ hooks: { beforeTool: { handler: './hooks/guard.ts', ...(targets === undefined ? {} : { targets: [...targets] }), tools: [...tools] } }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }), { skills: [] }, registry, @@ -890,7 +882,7 @@ it('validates host-native hook tool selectors against the registry and hook targ expect(validateSource( loadedProject({ hooks: { beforeTool: { handler: './hooks/guard.ts', tools: ['claude:WebSearch'] } }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['codex'], }), { skills: [] }, @@ -905,7 +897,7 @@ it('validates host-native hook tool selectors against the registry and hook targ it('normalizes output.sourceMap as a generated-executable opt-in', async () => { const defaulted = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { skills: [] }, registry, ); @@ -914,7 +906,7 @@ it('normalizes output.sourceMap as a generated-executable opt-in', async () => { const optedIn = await normalizeProject( loadedProject({ output: { sourceMap: true }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }), { skills: [] }, registry, @@ -924,7 +916,7 @@ it('normalizes output.sourceMap as a generated-executable opt-in', async () => { const optedOut = await normalizeProject( loadedProject({ output: { distPath: 'artifact', sourceMap: false }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }), { skills: [] }, registry, @@ -934,7 +926,7 @@ it('normalizes output.sourceMap as a generated-executable opt-in', async () => { it('normalizes the generated-executable runtime floor and validates raises only', async () => { const defaulted = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { skills: [] }, registry, ); @@ -942,7 +934,7 @@ it('normalizes the generated-executable runtime floor and validates raises only' const raised = await normalizeProject( loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, runtime: { node: '24.1' }, }), { skills: [] }, @@ -952,7 +944,7 @@ it('normalizes the generated-executable runtime floor and validates raises only' const diagnosticsFor = (runtime: unknown): readonly string[] => validateSource( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' }, runtime } as AgentBundleConfig), + loadedProject({ plugin: { name: 'review-tools' }, runtime } as AgentBundleConfig), { skills: [] }, registry, ).map(({ code }) => code); @@ -973,7 +965,7 @@ it('normalizes the generated-executable runtime floor and validates raises only' it('reports unknown targets, duplicate IDs, and portable output collisions', async () => { const root = '/workspace/project'; const loaded = loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['portable', 'future-host'], }); const model = await normalizeProject( @@ -1010,7 +1002,7 @@ it('rejects the retired plugin target as unknown (#555 acceptance 3)', async () // The composite root is the only output shape; `plugin` is not a target // that selects anything inside it, so it fails like any other unknown name. const model = await normalizeProject(loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['plugin'], }), { skills: [] }, registry); @@ -1032,7 +1024,7 @@ it('normalizes discovered assets with stable IDs, provenance, and all selected t skills: [], }; const conventional = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' }, targets: ['portable', 'claude'] }), + loadedProject({ plugin: { name: 'review-tools' }, targets: ['portable', 'claude'] }), discovered, registry, ); @@ -1059,14 +1051,14 @@ it('normalizes discovered assets with stable IDs, provenance, and all selected t ]); const explicit = await normalizeProject( - loadedProject({ assets: ['assets/logo.svg'], plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ assets: ['assets/logo.svg'], plugin: { name: 'review-tools' } }), discovered, registry, ); expect(explicit.assets?.every((asset) => asset.provenance.kind === 'explicit')).toBe(true); const withoutAssets = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { skills: [] }, registry, ); @@ -1075,7 +1067,7 @@ it('normalizes discovered assets with stable IDs, provenance, and all selected t it('reports duplicate asset destinations as duplicate IDs and output collisions', async () => { const model = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' }, targets: ['portable'] }), + loadedProject({ plugin: { name: 'review-tools' }, targets: ['portable'] }), { assets: [ { bytes: 6, relativePath: 'logo.svg', source: '/workspace/project/assets/logo.svg' }, @@ -1099,7 +1091,7 @@ it('validates the assets configuration shape, containment, and literal existence const diagnosticsFor = (assets: unknown, root?: string): readonly string[] => validateSource( loadedProject( - { assets, plugin: { name: 'review-tools', version: '1.0.0' } } as AgentBundleConfig, + { assets, plugin: { name: 'review-tools' } } as AgentBundleConfig, root === undefined ? {} : { root }, ), { skills: [] }, @@ -1110,12 +1102,10 @@ it('validates the assets configuration shape, containment, and literal existence expect(diagnosticsFor('assets')).toEqual(['AB4600']); expect(diagnosticsFor([''])).toEqual(['AB4600']); expect(diagnosticsFor([42])).toEqual(['AB4600']); - // Fixtures rooted at the workspace also report the AB4008 - // plugin.version/package version mismatch warning (#94). - expect(diagnosticsFor(['../outside'], process.cwd())).toEqual(['AB4601', 'AB4008']); - expect(diagnosticsFor(['definitely-missing-asset-entry'], process.cwd())).toEqual(['AB4602', 'AB4008']); - expect(diagnosticsFor(['definitely-missing/*.svg'], process.cwd())).toEqual(['AB4008']); - expect(diagnosticsFor(['package.json'], process.cwd())).toEqual(['AB4008']); + expect(diagnosticsFor(['../outside'], process.cwd())).toEqual(['AB4601']); + expect(diagnosticsFor(['definitely-missing-asset-entry'], process.cwd())).toEqual(['AB4602']); + expect(diagnosticsFor(['definitely-missing/*.svg'], process.cwd())).toEqual([]); + expect(diagnosticsFor(['package.json'], process.cwd())).toEqual([]); }); const scriptRouteFixture = (root: string, relativePath: string): CompiledAgentRoute => { @@ -1145,7 +1135,7 @@ const routeGraphWithScripts = (root: string, relativePaths: readonly string[]): it('normalizes shippable conventional script routes through the explicit scripts pipeline', async () => { const root = '/workspace/project'; const loaded = loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, scripts: { 'detect-risk': { entry: './src/tasks/detect-risk.ts', targets: ['claude'] } }, }); const discovered: DiscoveredProject = { @@ -1193,7 +1183,7 @@ it('normalizes shippable conventional script routes through the explicit scripts it('normalizes conventional scripts when config declares none', async () => { const root = '/workspace/project'; const model = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { routeGraph: routeGraphWithScripts(root, ['src/scripts/verify-release.ts']), skills: [] }, registry, ); @@ -1213,7 +1203,7 @@ it('normalizes conventional scripts when config declares none', async () => { it('gates nested and conflicting conventional script routes as AB4808/AB4809 and ships rendered ones', () => { const root = '/workspace/project'; const loaded = loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, scripts: { 'detect-risk': './src/tasks/detect-risk.ts' }, }); const discovered: DiscoveredProject = { @@ -1258,7 +1248,7 @@ it('ships a bin-claimed plain conventional script as both surfaces and refuses a notes: { entry: './src/scripts/render-notes.tsx' }, 'notes-again': './src/scripts/render-notes.tsx', }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }); const discovered: DiscoveredProject = { routeGraph: routeGraphWithScripts(root, ['src/scripts/hauler.ts', 'src/scripts/render-notes.tsx']), @@ -1286,7 +1276,7 @@ it('ships a bin-claimed plain conventional script as both surfaces and refuses a it('normalizes rendered conventional script routes onto the renderer pipeline (#102 stage 3)', async () => { const root = '/workspace/project'; const model = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { routeGraph: routeGraphWithScripts(root, ['src/scripts/render-notes.tsx']), skills: [] }, registry, ); @@ -1307,7 +1297,7 @@ it('normalizes rendered conventional script routes onto the renderer pipeline (# it('keeps shippable conventional script routes free of stage-1 gate diagnostics', () => { const root = '/workspace/project'; const diagnostics = validateSource( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { routeGraph: routeGraphWithScripts(root, ['src/scripts/verify-release.ts']), skills: [] }, registry, ); @@ -1369,7 +1359,7 @@ it('carries event-route handler provenance and provider selection into normalize const model = await normalizeProject( loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, targets: ['claude'], }), { routeGraph, skills: [] }, @@ -1394,7 +1384,7 @@ it('normalizes generated MCP route servers without a handwritten server declarat const root = '/workspace/project'; const graph = routeGraphWithGeneratedServer(root); const model = await normalizeProject( - loadedProject({ plugin: { name: 'review-tools', version: '1.0.0' } }), + loadedProject({ plugin: { name: 'review-tools' } }), { routeGraph: graph, skills: [] }, registry, ); diff --git a/packages/agent-bundle/tests/notice-retention-config.test.ts b/packages/agent-bundle/tests/notice-retention-config.test.ts index e94e36a3b..d7c4ad297 100644 --- a/packages/agent-bundle/tests/notice-retention-config.test.ts +++ b/packages/agent-bundle/tests/notice-retention-config.test.ts @@ -11,7 +11,7 @@ import type { AgentBundleConfig } from '../src/core/types.ts'; const config = (notices: unknown): AgentBundleConfig => ({ notices, - plugin: { name: 'fixture', version: '1.0.0' }, + plugin: { name: 'fixture' }, } as AgentBundleConfig); describe('notices.retention config (AB4833)', () => { @@ -50,7 +50,7 @@ describe('notices.retention config (AB4833)', () => { expect(Object.isFrozen(result.retention)).toBe(true); expect(Object.isFrozen(result.retention?.resolved)).toBe(true); // No config at all, or `notices: {}`, means the runtime defaults and nothing to report. - expect(normalizeNoticeRetention({ plugin: { name: 'f', version: '1.0.0' } }, '/p/c.ts', true)).toEqual({ diagnostics: [] }); + expect(normalizeNoticeRetention({ plugin: { name: 'f' } }, '/p/c.ts', true)).toEqual({ diagnostics: [] }); expect(normalizeNoticeRetention(config({}), '/p/c.ts', false)).toEqual({ diagnostics: [] }); }); diff --git a/packages/agent-bundle/tests/package-build.test.ts b/packages/agent-bundle/tests/package-build.test.ts index 6da897c99..348797a2c 100644 --- a/packages/agent-bundle/tests/package-build.test.ts +++ b/packages/agent-bundle/tests/package-build.test.ts @@ -45,12 +45,12 @@ const conventionFixture = (): Readonly> => ({ 'agent-bundle.config.ts': [ 'export default {', " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), - 'package.json': '{"name":"package-build-fixture","type":"module","private":true}\n', + 'package.json': '{"name":"package-build-fixture","type":"module","private":true,"version":"1.0.0"}\n', 'node_modules/evidence-package/index.js': 'globalThis.__evidencePackageLoaded = true;\n', 'node_modules/evidence-package/package.json': '{"name":"evidence-package","type":"module","version":"1.0.0"}\n', 'tsconfig.json': JSON.stringify({ @@ -130,7 +130,7 @@ describe('framework-owned package build', () => { const root = await fixtureRoot({ ...conventionFixture(), 'AGENTS.md': '# Authored repository instructions\n', - 'package.json': '{"name":"package-build-fixture","type":"module","private":true,"imports":{"#fixture":"./dist/index.js"},"scripts":{"prepack":"agent-bundle prepack","test":"rstest"}}\n', + 'package.json': '{"name":"package-build-fixture","type":"module","private":true,"version":"1.0.0","imports":{"#fixture":"./dist/index.js"},"scripts":{"prepack":"agent-bundle prepack","test":"rstest"}}\n', }); await installTypescriptToolchain(root); const result = await build({ output: 'artifact', packageOutputs: true, root }); @@ -195,6 +195,7 @@ describe('framework-owned package build', () => { private: true, scripts: { postinstall: 'node setup.js' }, type: 'module', + version: '1.0.0', }), }); await installTypescriptToolchain(root); @@ -213,7 +214,7 @@ describe('framework-owned package build', () => { 'agent-bundle.config.ts': [ 'export default {', " bin: { hauler: './src/scripts/hauler.ts' },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', @@ -224,6 +225,7 @@ describe('framework-owned package build', () => { private: true, scripts: { postinstall: 'node ./dist/bin/hauler.js' }, type: 'module', + version: '1.0.0', }), 'src/scripts/hauler.ts': [ 'export const main = async (argv: readonly string[]): Promise => {', @@ -267,7 +269,7 @@ describe('framework-owned package build', () => { ' echoer: {},', " plain: { entry: './src/plain.ts' },", ' } },', - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', @@ -299,7 +301,7 @@ describe('framework-owned package build', () => { 'let sawAsyncChunks;', 'export default {', ' lib: false,', - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", ' tools: {', " rsbuild: { source: { define: { __HATCH_DEFINE__: JSON.stringify('hatched') } } },", @@ -336,7 +338,7 @@ describe('framework-owned package build', () => { 'export default {', " lib: { entry: './src/index.ts', dts: false },", " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", " tools: { rsbuild: { output: { externals: ['left-pad', 'right-pad'] } } },", '};', @@ -377,7 +379,7 @@ describe('framework-owned package build', () => { 'export default {', " lib: { entry: './src/index.ts', dts: false },", " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", rspackExternalsMutator(['left-pad', 'right-pad']), '};', @@ -418,7 +420,7 @@ describe('framework-owned package build', () => { 'export default {', ' lib: false,', " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", ' tools: {', ' rspack: (config) => {', @@ -457,7 +459,7 @@ describe('framework-owned package build', () => { 'export default {', ' lib: false,', " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", rspackExternalsMutator(['left-pad'], [" config.externalsType = 'node-commonjs';"]), '};', @@ -489,7 +491,7 @@ describe('framework-owned package build', () => { 'export default {', ' lib: false,', " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', @@ -534,7 +536,7 @@ describe('framework-owned package build', () => { 'export default {', " lib: { entry: './src/index.ts', dts: false },", " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', @@ -579,7 +581,7 @@ describe('framework-owned package build', () => { 'export default {', ' lib: false,', " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', @@ -614,7 +616,7 @@ describe('framework-owned package build', () => { 'export default {', ' lib: false,', " mcp: { servers: { echoer: {} } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', @@ -755,7 +757,7 @@ describe('mcp run', () => { " SHARED: 'manifest',", " STATE_DIR: 'agent-bundle:path:plugin-root/.runtime',", ' } } } },', - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['codex', 'portable'],", '};', '', @@ -850,12 +852,12 @@ describe('mcp run', () => { 'agent-bundle.config.ts': [ 'export default {', " mcp: { servers: { pinner: { entry: './src/pin.ts' } } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), - 'package.json': '{"name":"package-build-fixture","type":"module","private":true}\n', + 'package.json': '{"name":"package-build-fixture","type":"module","private":true,"version":"1.0.0"}\n', '.env': 'MCP_RUN_TRACKER_COOKIE=secret\n', // A consumer trusting the documented anchor exactly as PR #49 intends. 'src/pin.ts': [ @@ -904,12 +906,12 @@ describe('mcp run', () => { 'agent-bundle.config.ts': [ 'export default {', " mcp: { servers: { exiter: { entry: './src/exit.ts' } } },", - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), - 'package.json': '{"name":"package-build-fixture","type":"module","private":true}\n', + 'package.json': '{"name":"package-build-fixture","type":"module","private":true,"version":"1.0.0"}\n', 'src/exit.ts': 'process.exitCode = 7;\nexport const marker = true;\n', }); await build({ output: 'artifact', root }); @@ -940,12 +942,12 @@ describe('mcp run', () => { " transport: 'streamable-http',", " url: 'https://mcp.example.test/stream',", ' } } },', - " plugin: { name: 'package-build-fixture', version: '1.0.0' },", + " plugin: { name: 'package-build-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), - 'package.json': '{"name":"package-build-fixture","type":"module","private":true}\n', + 'package.json': '{"name":"package-build-fixture","type":"module","private":true,"version":"1.0.0"}\n', }); await build({ output: 'artifact', root }); await expect(runMcp({ diff --git a/packages/agent-bundle/tests/package-conventions.test.ts b/packages/agent-bundle/tests/package-conventions.test.ts index 4fedf1c8a..7129cebd2 100644 --- a/packages/agent-bundle/tests/package-conventions.test.ts +++ b/packages/agent-bundle/tests/package-conventions.test.ts @@ -59,7 +59,7 @@ describe('package build normalization', () => { const model = await normalizeProject(loadedProject({ bin: { 'my-tool': './src/entry.ts', other: { entry: './src/entry.ts' } }, lib: { dts: false, entry: './src/library.ts' }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); expect(model.packageBuild).toEqual({ @@ -91,7 +91,7 @@ describe('package build normalization', () => { it('defaults lib dts to true for the string form', async () => { const root = await projectRoot({ 'src/index.ts': 'export const a = 1;\n' }); const packageBuild = normalizePackageBuild( - { lib: './src/index.ts', plugin: { name: 'p', version: '1.0.0' } }, + { lib: './src/index.ts', plugin: { name: 'p' } }, root, `${root}/agent-bundle.config.ts`, ); @@ -104,7 +104,7 @@ describe('package build normalization', () => { 'src/index.ts': 'export const answer = 42;\n', }); const model = await normalizeProject(loadedProject({ - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); expect(model.packageBuild).toEqual({ @@ -134,7 +134,7 @@ describe('package build normalization', () => { const explicit = await normalizeProject(loadedProject({ bin: { renamed: './src/other.ts' }, lib: false, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); expect(explicit.packageBuild?.bins.map((bin) => bin.name)).toEqual(['renamed']); expect(explicit.packageBuild?.lib).toBeUndefined(); @@ -142,7 +142,7 @@ describe('package build normalization', () => { const disabled = await normalizeProject(loadedProject({ bin: false, lib: false, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); expect(disabled.packageBuild).toBeUndefined(); }); @@ -150,7 +150,7 @@ describe('package build normalization', () => { it('skips the bin convention when the plugin name is not a safe output name', async () => { const root = await projectRoot({ 'src/cli.ts': 'export const main = async () => 0;\n' }); const model = await normalizeProject(loadedProject({ - plugin: { name: '@scope/unsafe name', version: '1.0.0' }, + plugin: { name: '@scope/unsafe name' }, }, root), { skills: [] }, registry); expect(model.packageBuild).toBeUndefined(); }); @@ -158,7 +158,7 @@ describe('package build normalization', () => { it('leaves projects without package entries untouched', async () => { const root = await projectRoot({}); const model = await normalizeProject(loadedProject({ - plugin: { name: 'plain', version: '1.0.0' }, + plugin: { name: 'plain' }, }, root), { skills: [] }, registry); expect(model.packageBuild).toBeUndefined(); }); @@ -171,7 +171,7 @@ describe('conventional MCP entries', () => { }); const loaded = loadedProject({ mcp: { servers: { curator: {} } }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root); expect(validateSource(loaded, { skills: [] }, registry)).toEqual([]); @@ -196,7 +196,7 @@ describe('conventional MCP entries', () => { }); const model = await normalizeProject(loadedProject({ mcp: { servers: { curator: { entry: './src/explicit.ts' } } }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); expect(model.mcpServers[0]).toMatchObject({ provenance: { kind: 'config' }, @@ -208,7 +208,7 @@ describe('conventional MCP entries', () => { const root = await projectRoot({}); const diagnostics = validateSource(loadedProject({ mcp: { servers: { curator: {} } }, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); expect(diagnostics).toEqual([expect.objectContaining({ code: 'AB4304', @@ -225,7 +225,7 @@ describe('bin, lib, and tools validation', () => { const root = await projectRoot(files); return validateSource(loadedProject({ ...config, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); }; @@ -460,7 +460,7 @@ describe('artifact output validation', () => { const root = await projectRoot({}); return validateSource(loadedProject({ output: output as never, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry); }; @@ -525,7 +525,7 @@ describe('migration nudges (AB473x)', () => { return { diagnostics: validateSource(loadedProject({ ...config, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root), { skills: [] }, registry), root, }; @@ -680,7 +680,7 @@ describe('migration nudges (AB473x)', () => { const root = await projectRoot(files); const loaded = loadedProject({ ...config, - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }, root); const discovered = await discoverProject(root, loaded.config); return { diagnostics: validateSource(loaded, discovered, registry), root }; @@ -743,7 +743,7 @@ describe('migration nudges (AB473x)', () => { }); const discovered = await discoverProject(root, { - plugin: { name: 'review-tools', version: '1.0.0' }, + plugin: { name: 'review-tools' }, }); expect(discovered.skills).toMatchObject([{ diff --git a/packages/agent-bundle/tests/package-identity.test.ts b/packages/agent-bundle/tests/package-identity.test.ts index 7ca18451d..bc1e389cd 100644 --- a/packages/agent-bundle/tests/package-identity.test.ts +++ b/packages/agent-bundle/tests/package-identity.test.ts @@ -25,20 +25,12 @@ const registry: NormalizationTargetRegistry = { supports: () => false, }; -/** `undeclared` omits `plugin.version` the way an inference-era config does. */ -const undeclared = Symbol('undeclared plugin.version'); - -const config = (version: string | typeof undeclared = '1.0.0'): AgentBundleConfig => ({ - plugin: version === undeclared - ? { name: 'identity-fixture' } - : { name: 'identity-fixture', version }, -}); +const config = (): AgentBundleConfig => ({ plugin: { name: 'identity-fixture' } }); const loadedProject = ( root: string, - pluginVersion: string | typeof undeclared = '1.0.0', ): LoadedConfig => ({ - config: config(pluginVersion), + config: config(), configPath: join(root, 'agent-bundle.config.ts'), context: { command: 'build', @@ -124,7 +116,7 @@ it('labels the development fallback distinctly from a release version', () => { it('carries the derived axes through the normalized model into the project context', async () => { await withProject(JSON.stringify({ name: '@scope/pkg', version: '2.3.4' }), async (root) => { - const loaded = loadedProject(root, '2.3.4'); + const loaded = loadedProject(root); const model = await normalizeProject(loaded, { skills: [] }, registry); expect(model.metadata).toMatchObject({ packageName: '@scope/pkg', packageVersion: '2.3.4', version: '2.3.4' }); @@ -161,31 +153,6 @@ it('omits both axes from the model and context for unpackaged projects', async ( }); }); -it('warns with AB4008 when plugin.version differs from the package version', async () => { - await withProject(JSON.stringify({ name: '@scope/pkg', version: '2.0.0' }), async (root) => { - const diagnostics = validateSource(loadedProject(root, '1.0.0'), { skills: [] }, registry); - expect(diagnostics).toMatchObject([{ - code: 'AB4008', - severity: 'warning', - sourcePath: join(root, 'agent-bundle.config.ts'), - }]); - expect(diagnostics[0]!.message).toContain('"1.0.0"'); - expect(diagnostics[0]!.message).toContain('"2.0.0"'); - }); -}); - -it('stays silent when plugin.version matches the package version or no package version exists', async () => { - await withProject(JSON.stringify({ name: '@scope/pkg', version: '1.0.0' }), async (root) => { - expect(validateSource(loadedProject(root, '1.0.0'), { skills: [] }, registry)).toEqual([]); - }); - await withProject(JSON.stringify({ name: '@scope/pkg' }), async (root) => { - expect(validateSource(loadedProject(root, '1.0.0'), { skills: [] }, registry)).toEqual([]); - }); - await withProject(undefined, async (root) => { - expect(validateSource(loadedProject(root, '1.0.0'), { skills: [] }, registry)).toEqual([]); - }); -}); - it('warns with AB4009/AB4010/AB4011 for invalid package identity values', async () => { await withProject(JSON.stringify({ name: 'Not Valid!', version: 'one.two' }), async (root) => { expect(validateSource(loadedProject(root), { skills: [] }, registry)).toMatchObject([ @@ -218,7 +185,7 @@ it('ignores a package.json symlinked outside the project root', async () => { it('infers the plugin version from package.json when the config omits it', async () => { await withProject(JSON.stringify({ name: '@scope/pkg', version: '2.3.4' }), async (root) => { - const loaded = loadedProject(root, undeclared); + const loaded = loadedProject(root); expect(validateSource(loaded, { skills: [] }, registry, { release: true })).toEqual([]); const model = await normalizeProject(loaded, { skills: [] }, registry); @@ -230,20 +197,9 @@ it('infers the plugin version from package.json when the config omits it', async }); }); -it('rejects a declared plugin.version that is not a nonempty string', async () => { - await withProject(JSON.stringify({ name: '@scope/pkg', version: '2.3.4' }), async (root) => { - for (const declared of ['', ' ', 3 as unknown as string]) { - const loaded = loadedProject(root, declared); - expect(validateSource(loaded, { skills: [] }, registry)).toMatchObject([ - { code: 'AB4001', severity: 'error', sourcePath: join(root, 'agent-bundle.config.ts') }, - ]); - } - }); -}); - it('keeps the development fallback for an unpackaged project with no declared version', async () => { await withProject(undefined, async (root) => { - const loaded = loadedProject(root, undeclared); + const loaded = loadedProject(root); // Development preparation reports nothing: this is a normal dev state. expect(validateSource(loaded, { skills: [] }, registry)).toEqual([]); @@ -254,7 +210,7 @@ it('keeps the development fallback for an unpackaged project with no declared ve it('fails a release build closed when no source declares a release version', async () => { const releaseDiagnostics = (root: string) => - validateSource(loadedProject(root, undeclared), { skills: [] }, registry, { release: true }); + validateSource(loadedProject(root), { skills: [] }, registry, { release: true }); await withProject(undefined, async (root) => { expect(releaseDiagnostics(root)).toMatchObject([ @@ -269,11 +225,6 @@ it('fails a release build closed when no source declares a release version', asy { code: 'AB4013', severity: 'error' }, ]); }); - // A declared plugin.version is a release version on its own. - await withProject(undefined, async (root) => { - expect(validateSource(loadedProject(root, '1.0.0'), { skills: [] }, registry, { release: true })) - .toEqual([]); - }); }); it('separates the release refusal from the development fallback across commands', async () => { @@ -300,7 +251,7 @@ it('exposes the derived axes on the development source status', async () => { await withProject(JSON.stringify({ name: '@scope/pkg', version: '2.3.4' }), async (root) => { await writeFile( join(root, 'agent-bundle.config.ts'), - "export default { plugin: { name: 'identity-fixture', version: '2.3.4' }, targets: ['portable'] };\n", + "export default { plugin: { name: 'identity-fixture' }, targets: ['portable'] };\n", ); const prepared = await new ProjectService({ root, targets: ['portable'] }).prepare('inspect'); expect(prepared.source).toMatchObject({ @@ -313,7 +264,7 @@ it('exposes the derived axes on the development source status', async () => { await withProject(JSON.stringify({ name: '@scope/pkg' }), async (root) => { await writeFile( join(root, 'agent-bundle.config.ts'), - "export default { plugin: { name: 'identity-fixture', version: '1.0.0' }, targets: ['portable'] };\n", + "export default { plugin: { name: 'identity-fixture' }, targets: ['portable'] };\n", ); const prepared = await new ProjectService({ root, targets: ['portable'] }).prepare('inspect'); expect(prepared.source.packageName).toBe('@scope/pkg'); diff --git a/packages/agent-bundle/tests/packed-consumer.test.ts b/packages/agent-bundle/tests/packed-consumer.test.ts index 32e6556bb..315c1850d 100644 --- a/packages/agent-bundle/tests/packed-consumer.test.ts +++ b/packages/agent-bundle/tests/packed-consumer.test.ts @@ -164,7 +164,7 @@ it('uses only an installed tarball after source deletion', async () => { writeFile(join(scriptProjectRoot, 'package.json'), '{"type":"module"}\n'), writeFile( join(scriptProjectRoot, 'agent-bundle.config.ts'), - "export default { plugin: { name: 'packed-script-run', version: '1.0.0' }, scripts: { shell: './shell.sh' }, targets: ['portable'] };\n", + "export default { plugin: { name: 'packed-script-run' }, scripts: { shell: './shell.sh' }, targets: ['portable'] };\n", ), writeFile(join(scriptProjectRoot, 'shell.sh'), "printf 'packed script stdout\\n'\nprintf 'packed script stderr\\n' >&2\n"), ]); @@ -390,7 +390,7 @@ it('uses only an installed tarball after source deletion', async () => { writeFile(join(frameworkRoot, 'agent-bundle.config.ts'), [ 'export default {', ' mcp: { servers: { greeter: {} } },', - " plugin: { name: 'framework-build-fixture', version: '1.0.0' },", + " plugin: { name: 'framework-build-fixture' },", " targets: ['claude', 'codex', 'portable'],", '};', '', diff --git a/packages/agent-bundle/tests/path-token-resolver.test.ts b/packages/agent-bundle/tests/path-token-resolver.test.ts index f4dbfa8ac..a0f423105 100644 --- a/packages/agent-bundle/tests/path-token-resolver.test.ts +++ b/packages/agent-bundle/tests/path-token-resolver.test.ts @@ -255,7 +255,7 @@ it('resolves Claude path tokens outside command when launching a generated artif }, }, }, - plugin: { name: 'path-token-fixture', version: '1.0.0' }, + plugin: { name: 'path-token-fixture' }, targets: ['claude'], }), { skills: [] }, diff --git a/packages/agent-bundle/tests/playground-orchestration-service.test.ts b/packages/agent-bundle/tests/playground-orchestration-service.test.ts index 2e907c629..86d7b0794 100644 --- a/packages/agent-bundle/tests/playground-orchestration-service.test.ts +++ b/packages/agent-bundle/tests/playground-orchestration-service.test.ts @@ -28,7 +28,7 @@ import type { PlaygroundSubscription, PlaygroundTraceEvent, } from '../src/dev/playground/playground-store.ts'; -import { PlaygroundStore } from '../src/dev/playground/playground-store.ts'; +import { PlaygroundService } from '../src/dev/playground/playground-store.ts'; import type { ProjectStatus } from '../src/dev/types.ts'; import { eventuallyPasses } from './support/eventually.ts'; import { deepFreeze } from '../src/core/freeze.ts'; @@ -553,7 +553,7 @@ it('exports and promotes the real durable response event reference from a native const root = await mkdtemp(join(tmpdir(), 'agent-bundle-native-playground-durable-')); const projectRoot = join(root, 'project'); await mkdir(projectRoot, { recursive: true }); - const trace = new PlaygroundStore({ + const trace = new PlaygroundService({ projectId: 'native-durable-project', projectRoot, storageRoot: join(projectRoot, '.agent-bundle', 'playground'), diff --git a/packages/agent-bundle/tests/playground-service.test.ts b/packages/agent-bundle/tests/playground-service.test.ts index 86971c354..5ed93bbf6 100644 --- a/packages/agent-bundle/tests/playground-service.test.ts +++ b/packages/agent-bundle/tests/playground-service.test.ts @@ -7,7 +7,7 @@ import { expect, it } from '@rstest/core'; import { timeScale } from './support/time-scale.ts'; import { - PlaygroundStore as PlaygroundService, + PlaygroundService, PlaygroundServiceCloseError, PlaygroundSessionCloseError, type PlaygroundEventInput, diff --git a/packages/agent-bundle/tests/plugin-logo.test.ts b/packages/agent-bundle/tests/plugin-logo.test.ts index dbac38888..c1d8e8ccf 100644 --- a/packages/agent-bundle/tests/plugin-logo.test.ts +++ b/packages/agent-bundle/tests/plugin-logo.test.ts @@ -71,7 +71,6 @@ it('rejects a missing or invalid plugin.logo with AB4012', async () => { const missing = await loadedProject({ logo: 'docs/media/missing.svg', name: 'logo-fixture', - version: '1.0.0', }); expect(validateSource(missing, { skills: [] }, registry).filter(({ code }) => code === 'AB4012')).toMatchObject([{ code: 'AB4012', @@ -82,7 +81,6 @@ it('rejects a missing or invalid plugin.logo with AB4012', async () => { const empty = await loadedProject({ logo: ' ', name: 'logo-fixture', - version: '1.0.0', }); expect(validateSource(empty, { skills: [] }, registry).filter(({ code }) => code === 'AB4012')).toMatchObject([{ code: 'AB4012', @@ -93,7 +91,6 @@ it('rejects a missing or invalid plugin.logo with AB4012', async () => { const directory = await loadedProject({ logo: 'docs/media', name: 'logo-fixture', - version: '1.0.0', }, { 'docs/media/.keep': '' }); expect(validateSource(directory, { skills: [] }, registry).filter(({ code }) => code === 'AB4012')).toMatchObject([{ code: 'AB4012', @@ -104,7 +101,6 @@ it('rejects a missing or invalid plugin.logo with AB4012', async () => { const outside = await loadedProject({ logo: '../outside.svg', name: 'logo-fixture', - version: '1.0.0', }); expect(validateSource(outside, { skills: [] }, registry).filter(({ code }) => code === 'AB4012')).toMatchObject([{ code: 'AB4012', @@ -118,7 +114,6 @@ it('accepts an existing in-project plugin.logo and normalizes it onto metadata', description: 'Logo fixture', logo: 'docs/media/logo.svg', name: 'logo-fixture', - version: '1.0.0', }, { 'docs/media/logo.svg': logoSvg }); expect(validateSource(loaded, { skills: [] }, registry).filter(({ code }) => code === 'AB4012')).toEqual([]); @@ -134,7 +129,6 @@ it('accepts an existing in-project plugin.logo and normalizes it onto metadata', it('omits logo from the normalized model when the field is absent', async () => { const loaded = await loadedProject({ name: 'logo-fixture', - version: '1.0.0', }); const model = await normalizeProject(loaded, { skills: [] }, registry); expect(model.metadata).not.toHaveProperty('logo'); diff --git a/packages/agent-bundle/tests/prebuilt-payload.test.ts b/packages/agent-bundle/tests/prebuilt-payload.test.ts index 64787d9a5..997f63971 100644 --- a/packages/agent-bundle/tests/prebuilt-payload.test.ts +++ b/packages/agent-bundle/tests/prebuilt-payload.test.ts @@ -14,7 +14,7 @@ import { createProjectFixture, removeProjectFixture } from './helpers/project-fi const configSource = (options: { readonly payload?: string; readonly hooks?: string; readonly mcp?: string }): string => [ 'export default {', - " plugin: { name: 'prebuilt-fixture', version: '1.0.0', description: 'Prebuilt payload fixture.' },", + " plugin: { name: 'prebuilt-fixture', description: 'Prebuilt payload fixture.' },", " targets: ['claude', 'codex', 'portable'],", ...(options.payload === undefined ? [] : [options.payload]), ...(options.hooks === undefined ? [] : [options.hooks]), diff --git a/packages/agent-bundle/tests/project-context-walk-bound.test.ts b/packages/agent-bundle/tests/project-context-walk-bound.test.ts index c21acccd9..474b15ac6 100644 --- a/packages/agent-bundle/tests/project-context-walk-bound.test.ts +++ b/packages/agent-bundle/tests/project-context-walk-bound.test.ts @@ -15,12 +15,13 @@ const createProject = async (): Promise => { join(root, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'dev-service-fixture', version: '1.0.0' },", + " plugin: { name: 'dev-service-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), ), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'skills', 'review', 'SKILL.md'), [ '---', 'name: review', diff --git a/packages/agent-bundle/tests/projection/script-dispatch.test.ts b/packages/agent-bundle/tests/projection/script-dispatch.test.ts index 725c35ed2..5e453237e 100644 --- a/packages/agent-bundle/tests/projection/script-dispatch.test.ts +++ b/packages/agent-bundle/tests/projection/script-dispatch.test.ts @@ -454,8 +454,8 @@ describe('plain scripts at the script dispatch level', () => { expect(run.exitCode).toBe(0); expect(run.stderr).toBe(''); - // The route-harness fixture declares plugin name/version and has no package.json. - expect(run.stdout).toBe('route-harness@1.0.0 - -\n'); + // The route-harness fixture has a package version but no npm package name. + expect(run.stdout).toBe('route-harness@1.0.0 - 1.0.0\n'); const compiled = testManifest(); const packaged = await runScript('identity', [], { diff --git a/packages/agent-bundle/tests/provider-typegen.test.ts b/packages/agent-bundle/tests/provider-typegen.test.ts index 697e64d4c..6a6d921d5 100644 --- a/packages/agent-bundle/tests/provider-typegen.test.ts +++ b/packages/agent-bundle/tests/provider-typegen.test.ts @@ -55,7 +55,7 @@ it('types await context.provider(key) from the generated provider declarations', writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - " plugin: { name: 'provider-typegen-fixture', version: '1.0.0' },", + " plugin: { name: 'provider-typegen-fixture' },", " targets: ['portable'],", '});', '', @@ -88,12 +88,15 @@ it('types await context.provider(key) from the generated provider declarations', '', ].join('\n')), writeProjectFile(root, 'src/mcp/curator/tools/status.ts', [ - "import { z } from 'zod';", - 'export const inputSchema = z.object({}).strict();', - "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - "export default async function Status() { return { status: 'ready' as const }; }", - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const inputSchema = z.object({}).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export default defineTool({", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " inputSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " resultSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "}, async () => { return { status: 'ready' as const }; });", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', ].join('\n')), writeProjectFile(root, 'assertions.ts', [ "import { agent } from '@agent-bundle/runtime';", "import type { ProviderKey, ProviderValue } from './.agent-bundle/routes.js';", diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts index 90a62ba09..427f27b31 100644 --- a/packages/agent-bundle/tests/public-api-packed.test.ts +++ b/packages/agent-bundle/tests/public-api-packed.test.ts @@ -140,7 +140,7 @@ const createBuildProject = async (root: string): Promise<{ readonly output: stri writeFile(join(project, 'package.json'), '{"type":"module"}\n'), writeFile( join(project, 'agent-bundle.config.ts'), - "export default { plugin: { name: 'manifest-version-fixture', version: '1.0.0' }, targets: ['portable'] };\n", + "export default { plugin: { name: 'manifest-version-fixture' }, targets: ['portable'] };\n", ), writeFile( join(project, 'src', 'skills', 'review', 'SKILL.md'), @@ -331,7 +331,7 @@ it('imports the externalized config entry from a packed npm consumer', async () 'const config: AgentBundleConfig = {', " claude: { nativeHooks: './claude-hooks.json' },", " codex: { nativeHooks: './codex-hooks.json' },", - " plugin: { name: 'packed-config-types', version: '1.0.0' },", + " plugin: { name: 'packed-config-types' },", " portable: { compatibility: 'v1' },", '};', '', diff --git a/packages/agent-bundle/tests/public-api.test.ts b/packages/agent-bundle/tests/public-api.test.ts index c99f77693..b1bc1fe9b 100644 --- a/packages/agent-bundle/tests/public-api.test.ts +++ b/packages/agent-bundle/tests/public-api.test.ts @@ -82,7 +82,7 @@ const createBuildProject = async (root: string): Promise<{ readonly output: stri writeFile(join(project, 'package.json'), '{"type":"module"}\n'), writeFile( join(project, 'agent-bundle.config.ts'), - "export default { plugin: { name: 'manifest-version-fixture', version: '1.0.0' }, targets: ['portable'] };\n", + "export default { plugin: { name: 'manifest-version-fixture' }, targets: ['portable'] };\n", ), writeFile( join(project, 'src', 'skills', 'review', 'SKILL.md'), @@ -405,7 +405,7 @@ it('keeps bundled config extension types in emitted root declarations', async () 'const config: AgentBundleConfig = {', " claude: { nativeHooks: './claude-hooks.json' },", " codex: { nativeHooks: './codex-hooks.json' },", - " plugin: { name: 'packed-root-types', version: '1.0.0' },", + " plugin: { name: 'packed-root-types' },", " portable: { compatibility: 'v1' },", '};', '', diff --git a/packages/agent-bundle/tests/rendered-skills.test.ts b/packages/agent-bundle/tests/rendered-skills.test.ts index c87e6ef76..3a45d5d66 100644 --- a/packages/agent-bundle/tests/rendered-skills.test.ts +++ b/packages/agent-bundle/tests/rendered-skills.test.ts @@ -164,7 +164,7 @@ describe('rendered skill compilation', () => { }); it('discovers rendered-only skill directories by convention and validates them cleanly', async () => { - const loaded = loadedProject({ plugin: { name: 'rendered', version: '1.0.0' } }, fixtureRoot); + const loaded = loadedProject({ plugin: { name: 'rendered' } }, fixtureRoot); const discovered = await discoverProject(fixtureRoot, loaded.config); expect(discovered.skills.map((skill) => skill.frontmatter.name)).toEqual(['deploy-checklist']); expect(validateSource(loaded, discovered, registry)).toEqual([]); @@ -177,7 +177,7 @@ describe('rendered skill compilation', () => { }); it('emits the compiled SKILL.md as a generated write entry in artifact plans', async () => { - const loaded = loadedProject({ plugin: { name: 'rendered', version: '1.0.0' } }, fixtureRoot); + const loaded = loadedProject({ plugin: { name: 'rendered' } }, fixtureRoot); const model = await normalizeProject(loaded, await discoverProject(fixtureRoot, loaded.config), registry); const plan = standardPluginArtifactPlan({ diagnostics: [], @@ -272,10 +272,6 @@ describe('rendered skill compilation', () => { version: '3.4.5', }); - // An authored plugin.version wins over package.json, for the skill too. - const authored = await discoverProject(root, { plugin: { name: 'identity-plugin', version: '9.0.0' } }); - expect(authored.skills[0]?.body).toContain('Version `9.0.0`; package `@acme/identity-plugin@3.4.5`.'); - // Without a caller-supplied identity the reserved specifier is not aliased: // it resolves however the project resolves `agent-bundle` — here, not at all. const direct = await parseSkill(join(root, 'src', 'skills', 'identity'), root); diff --git a/packages/agent-bundle/tests/route-caller-input-types.test.ts b/packages/agent-bundle/tests/route-caller-input-types.test.ts index 131276485..a83faa49a 100644 --- a/packages/agent-bundle/tests/route-caller-input-types.test.ts +++ b/packages/agent-bundle/tests/route-caller-input-types.test.ts @@ -109,7 +109,7 @@ it('types callers by schema input and components by schema output in a clean gen })), writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", - "export default defineConfig({ plugin: { name: 'caller-input-types-fixture', version: '1.0.0' }, targets: ['portable'] });", + "export default defineConfig({ plugin: { name: 'caller-input-types-fixture' }, targets: ['portable'] });", '', ].join('\n')), // A defaulted field: optional to the caller, present for the component. diff --git a/packages/agent-bundle/tests/route-contract-imports.test.ts b/packages/agent-bundle/tests/route-contract-imports.test.ts index 1f27c6b4c..196dfbbf9 100644 --- a/packages/agent-bundle/tests/route-contract-imports.test.ts +++ b/packages/agent-bundle/tests/route-contract-imports.test.ts @@ -155,7 +155,7 @@ const writeFixture = async (variant: FixtureVariant): Promise => { writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - ` plugin: { description: 'Route contract fixture.', name: '${name}', version: '1.0.0' },`, + ` plugin: { description: 'Route contract fixture.', name: '${name}' },`, ' routes: { mcpCommands: true },', " targets: ['portable'],", '});', @@ -181,7 +181,7 @@ const writeFixture = async (variant: FixtureVariant): Promise => { const routeGraph = async (root: string): Promise => { const graph = await compileRouteGraph(root, { - plugin: { name: 'route-contract-fixture', version: '1.0.0' }, + plugin: { name: 'route-contract-fixture' }, routes: { mcpCommands: true }, targets: ['portable'], }); @@ -296,7 +296,7 @@ it('uses explicit metadata with imported runtime schemas and preserves generated expect(importedTypes).toEqual([]); const bareGraph = await compileRouteGraph(bareRoot, { - plugin: { name: 'route-contract-bare-fixture', version: '1.0.0' }, + plugin: { name: 'route-contract-bare-fixture' }, routes: { mcpCommands: true }, targets: ['portable'], }) as RouteContractGraph; diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index 7c03be454..f7675f290 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -29,6 +29,15 @@ const createRoot = async (): Promise => { }; const moduleSource = 'export const inputSchema = {}; export const resultSchema = {}; export default async () => undefined;\n'; +const toolSource = (fields = '', imports: readonly string[] = []): string => [ + ...imports, + "import { defineTool } from 'agent-bundle/routes';", + 'export const inputSchema = {};', + 'export const resultSchema = {};', + `export default defineTool({ ${fields}${fields.length === 0 ? '' : ', '}inputSchema, resultSchema }, async () => undefined);`, + '', +].join('\n'); +const toolModuleSource = toolSource(); const writeTree = async (root: string, files: Readonly>): Promise => { for (const [path, contents] of Object.entries(files)) { @@ -39,7 +48,7 @@ const writeTree = async (root: string, files: Readonly>): }; const fixtureConfig = (extra: Readonly> = {}): AgentBundleConfig => ({ - plugin: { name: 'routes-fixture', version: '1.0.0' }, + plugin: { name: 'routes-fixture' }, ...extra, }); @@ -63,7 +72,7 @@ const conventionalTree: Readonly> = { 'src/mcp/curator/apps/dashboard.tsx': `export const config = { resourceUri: 'ui://curator/dashboard.html' }; ${moduleSource}`, 'src/mcp/curator/prompts/curate.tsx': moduleSource, 'src/mcp/curator/resources/catalog.ts': moduleSource, - 'src/mcp/curator/tools/inspect.tsx': moduleSource, + 'src/mcp/curator/tools/inspect.tsx': toolModuleSource, 'src/providers/git-worktree.ts': moduleSource, 'src/scripts/rebuild-index.ts': moduleSource, }; @@ -94,12 +103,12 @@ const createInspectProject = async (files: Readonly>): Pr await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), - 'package.json': '{"type":"module"}\n', + 'package.json': '{"type":"module","version":"1.0.0"}\n', ...files, }); return root; @@ -139,12 +148,12 @@ it('compiles the conventional tree into one frozen graph with a machine-independ source: join(root, 'src/providers/git-worktree.ts'), }]); - // The IR is immutable and every route carries the shared frozen empty config. + // The IR is immutable and routes without authored metadata carry empty config. expect(Object.isFrozen(graph)).toBe(true); expect(Object.isFrozen(graph.servers)).toBe(true); expect(Object.isFrozen(curator!.routes[0])).toBe(true); expect(Object.isFrozen(graph.cli!.routes)).toBe(true); - expect(curator!.routes.filter((route) => route.kind !== 'app').every((route) => route.config === emptyRouteConfig)).toBe(true); + expect(curator!.routes.filter((route) => route.kind !== 'app').every((route) => Object.keys(route.config).length === 0)).toBe(true); expect(curator!.routes.find((route) => route.kind === 'app')?.config).toEqual({ resourceUri: 'ui://curator/dashboard.html' }); expect(graph.events[0]!.config).toEqual({}); @@ -167,18 +176,26 @@ it('populates bounded input schemas for every route kind and includes them in th 'export default async () => undefined;', '', ].join('\n'); + const boundedTool = [ + "import { defineTool } from 'agent-bundle/routes';", + `export const inputSchema = z.object({ count: z.number().optional(), root: z.string().describe('Project root.') }).strict();`, + 'export const resultSchema = {};', + `export default defineTool({ inputJsonSchema: ${JSON.stringify(metadata)}, inputSchema, resultSchema }, async () => undefined);`, + '', + ].join('\n'); const tree = { 'src/cli/audit.ts': bounded(), 'src/events/stop.ts': bounded(), 'src/mcp/curator/apps/dashboard.tsx': bounded("export const config = { resourceUri: 'ui://curator/dashboard.html' };"), 'src/mcp/curator/prompts/curate.ts': bounded(), 'src/mcp/curator/resources/catalog.ts': bounded(), - 'src/mcp/curator/tools/inspect.ts': bounded(), + 'src/mcp/curator/tools/inspect.ts': boundedTool, 'src/mcp/curator/tools/rich.ts': [ + "import { defineTool } from 'agent-bundle/routes';", 'const sharedSchema = z.object({ nested: z.object({ value: z.string() }) });', 'export const inputSchema = sharedSchema;', 'export const resultSchema = {};', - 'export default async () => undefined;', + 'export default defineTool({ inputSchema, resultSchema }, async () => undefined);', '', ].join('\n'), 'src/scripts/rebuild.ts': bounded(), @@ -208,7 +225,7 @@ it('populates bounded input schemas for every route kind and includes them in th const changedRoot = await createRoot(); await writeTree(changedRoot, tree); const inspectPath = join(changedRoot, 'src/mcp/curator/tools/inspect.ts'); - await writeFile(inspectPath, (await readFile(inspectPath, 'utf8')).replace('Project root.', 'Workspace root.')); + await writeFile(inspectPath, (await readFile(inspectPath, 'utf8')).replaceAll('Project root.', 'Workspace root.')); const changed = await compileRouteGraph(changedRoot, fixtureConfig()); expect(changed.digest).not.toBe(graph.digest); @@ -223,17 +240,20 @@ it('does not infer metadata by following imported runtime schemas', async () => '}).strict();', '', ].join('\n'); - const route = (specifier: string): string => [ + const route = (specifier: string, tool = false): string => [ + ...(tool ? ["import { defineTool } from 'agent-bundle/routes';"] : []), `import { statusInputSchema } from '${specifier}';`, 'export const inputSchema = statusInputSchema;', 'export const resultSchema = {};', - 'export default async () => undefined;', + tool + ? 'export default defineTool({ inputSchema, resultSchema }, async () => undefined);' + : 'export default async () => undefined;', '', ].join('\n'); await writeTree(root, { 'src/cli/status.ts': route('../lib/protocol-schemas.js'), 'src/lib/protocol-schemas.ts': schema, - 'src/mcp/hauler/tools/hauler_status.tsx': route('../../../lib/protocol-schemas.js'), + 'src/mcp/hauler/tools/hauler_status.tsx': route('../../../lib/protocol-schemas.js', true), }); const graph = await compileRouteGraph(root, fixtureConfig()); expect(graph.diagnostics).toEqual([]); @@ -327,8 +347,8 @@ it('skips ignored paths, private segments, and declaration files', async () => { await writeTree(root, { '.gitignore': 'src/scripts/generated.ts\n', 'src/events/.internal/probe.ts': moduleSource, - 'src/mcp/curator/tools/_draft.ts': moduleSource, - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/_draft.ts': toolModuleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, 'src/mcp/curator/tools/types.d.ts': 'export type Probe = string;\n', 'src/scripts/generated.ts': moduleSource, }); @@ -436,7 +456,7 @@ it('gates a bin-claimed rendered script with AB4737 only when it exports no main " tool: './src/scripts/render-tool.tsx',", " typed: './src/scripts/render-typed.tsx',", ' },', - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable'],", '};', '', @@ -524,7 +544,7 @@ it('gates a bin-claimed plain script with AB4738 only when its bin would run a d " hauler: './src/scripts/hauler.ts',", " plain: './src/scripts/plain.ts',", ' },', - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable'],", '};', '', @@ -551,7 +571,7 @@ it('gates a bin-claimed plain script with AB4738 only when its bin would run a d it('errors with AB4800 when a declared entry, command, or url claims a routed server', async () => { const root = await createRoot(); - await writeTree(root, { 'src/mcp/curator/tools/inspect.ts': moduleSource }); + await writeTree(root, { 'src/mcp/curator/tools/inspect.ts': toolModuleSource }); const graph = await compileRouteGraph(root, fixtureConfig({ mcp: { servers: { curator: { url: 'https://example.test/mcp' } } }, })); @@ -564,7 +584,7 @@ it('errors with AB4800 when a declared entry, command, or url claims a routed se it('errors with AB4800 when an entry module and route modules claim one MCP server, and inspect turns invalid', async () => { const files = { 'src/mcp/curator.ts': moduleSource, - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }; const root = await createRoot(); await writeTree(root, files); @@ -584,7 +604,7 @@ it('keeps routes and silences AB4800 under an explicit generated mode', async () const root = await createRoot(); await writeTree(root, { 'src/mcp/curator.ts': moduleSource, - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const graph = await compileRouteGraph(root, fixtureConfig({ routes: { servers: { curator: 'generated' } }, @@ -606,12 +626,12 @@ it('accepts a config declaration that augments a route-generated server with env " targets: ['portable'],", " transport: 'stdio',", ' } } },', - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable', 'claude'],", '};', '', ].join('\n'), - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, 'views/panel.ts': "document.body.textContent = 'panel';\n", }); @@ -641,14 +661,14 @@ it('errors with AB4340 when a declaration for a route-generated server redeclare 'agent-bundle.config.ts': [ 'export default {', " mcp: { servers: { curator: { entry: './src/mcp/curator.ts', env: { CURATOR_MODE: 'strict' } } } },", - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " routes: { servers: { curator: 'generated' } },", " targets: ['portable'],", '};', '', ].join('\n'), 'src/mcp/curator.ts': moduleSource, - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const validation = await validate({ root: project }); @@ -664,14 +684,14 @@ it('checks an augmenting declaration\'s Apps against the route-declared Apps of const configWithApps = (apps: string): string => [ 'export default {', ` mcp: { servers: { curator: { apps: { ${apps} } } } },`, - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'); const routes = { 'src/mcp/curator/apps/dashboard.tsx': `export const config = { resourceUri: 'ui://curator/dashboard.html' }; ${moduleSource}`, - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, 'views/panel.ts': "document.body.textContent = 'panel';\n", }; @@ -707,12 +727,12 @@ it('applies the local-entry field rules to an augmenting declaration', async () 'agent-bundle.config.ts': [ 'export default {', " mcp: { servers: { curator: { cwd: './elsewhere', headers: { a: 'b' }, transport: 'streamable-http' } } },", - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable'],", '};', '', ].join('\n'), - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const validation = await validate({ root: project }); @@ -727,7 +747,7 @@ it('omits a server\'s routes and silences AB4800 under an explicit custom mode', const root = await createRoot(); await writeTree(root, { 'src/mcp/curator.ts': moduleSource, - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const graph = await compileRouteGraph(root, fixtureConfig({ mcp: { servers: { curator: { entry: './src/mcp/curator.ts' } } }, @@ -750,7 +770,7 @@ it('skips a server layout entirely when routes.servers pins that server to a non 'src/mcp/curator.ts': moduleSource, 'src/mcp/curator/layout.ts': 'export default { children: undefined };\n', 'src/mcp/curator/layout.tsx': 'export default { children: undefined };\n', - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, 'src/mcp/relay/layout.tsx': 'export default ({ children }) => children;\n', }); const graph = await compileRouteGraph(root, fixtureConfig({ @@ -793,8 +813,8 @@ it('errors with AB4801 when the conventional CLI entry and command routes both e it('errors with AB4802 when two route modules derive one id', async () => { const root = await createRoot(); await writeTree(root, { - 'src/mcp/curator/tools/inspect.ts': moduleSource, - 'src/mcp/curator/tools/inspect.tsx': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, + 'src/mcp/curator/tools/inspect.tsx': toolModuleSource, }); const graph = await compileRouteGraph(root, fixtureConfig()); @@ -808,7 +828,7 @@ it('errors with AB4803 on unsafe identity segments', async () => { const root = await createRoot(); await writeTree(root, { 'src/cli/-doctor.ts': moduleSource, - 'src/mcp/bad name/tools/inspect.ts': moduleSource, + 'src/mcp/bad name/tools/inspect.ts': toolModuleSource, }); const graph = await compileRouteGraph(root, fixtureConfig()); @@ -819,7 +839,7 @@ it('errors with AB4803 on unsafe identity segments', async () => { it('errors with AB4804 on invalid routes mode overrides', async () => { const root = await createRoot(); - await writeTree(root, { 'src/mcp/curator/tools/inspect.ts': moduleSource }); + await writeTree(root, { 'src/mcp/curator/tools/inspect.ts': toolModuleSource }); const graph = await compileRouteGraph(root, fixtureConfig({ routes: { cli: 42, servers: { curator: 'bogus' } }, })); @@ -859,7 +879,7 @@ it('serves the shared empty graph under the routes focus of a route-free project it('selects the compiled graph under the routes inspect focus', async () => { const root = await createInspectProject({ - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, 'src/scripts/rebuild-index.ts': moduleSource, }); const result = await inspect({ focus: 'routes', root }); @@ -877,18 +897,20 @@ it('shows projected MCP command provenance and safety in the routes inspect focu await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", ' routes: { mcpCommands: true },', " targets: ['portable'],", '};', '', ].join('\n'), - 'package.json': '{"type":"module"}\n', + 'package.json': '{"type":"module","version":"1.0.0"}\n', 'src/mcp/curator/tools/read_item.tsx': [ - "export const config = { annotations: { readOnlyHint: true }, description: 'Read one item.' };", - moduleSource, + "import { defineTool } from 'agent-bundle/routes';", + 'export const inputSchema = {};', + 'export const resultSchema = {};', + "export default defineTool({ annotations: { readOnlyHint: true }, description: 'Read one item.', inputSchema, resultSchema }, async () => undefined);", ].join('\n'), - 'src/mcp/curator/tools/write_item.tsx': moduleSource, + 'src/mcp/curator/tools/write_item.tsx': toolModuleSource, }); const result = await inspect({ focus: 'routes', root }); @@ -916,7 +938,7 @@ it('shows projected MCP command provenance and safety in the routes inspect focu it('dumps the graph through the CLI --routes focus and rejects ambiguous focuses', async () => { const root = await createInspectProject({ - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const terminal = captureCliTerminal(); const code = await runCli(['inspect', '--root', root, '--routes', '--json'], terminal.output); @@ -942,15 +964,15 @@ it('evaluates a stateful config factory once when inspecting the routes focus', " const path = join(ctx.projectRoot, 'config-load-count.txt');", " writeFileSync(path, `${Number(readFileSync(path, 'utf8')) + 1}\\n`);", ' return {', - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable'],", ' };', '};', '', ].join('\n'), 'config-load-count.txt': '0\n', - 'package.json': '{"type":"module"}\n', - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'package.json': '{"type":"module","version":"1.0.0"}\n', + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const result = await inspect({ focus: 'routes', root }); @@ -971,8 +993,10 @@ it('extracts each route module static config export into the graph', async () => const root = await createRoot(); await writeTree(root, { 'src/mcp/curator/tools/search.ts': [ - "export const config = { annotations: { readOnlyHint: true }, title: 'Search' } as const;", - moduleSource, + "import { defineTool } from 'agent-bundle/routes';", + 'export const inputSchema = {};', + 'export const resultSchema = {};', + "export default defineTool({ annotations: { readOnlyHint: true }, inputSchema, resultSchema, title: 'Search' }, async () => undefined);", ].join('\n'), 'src/scripts/rebuild.ts': moduleSource, }); @@ -989,9 +1013,11 @@ it('compiles dynamic-config routes with an empty config beside the named error', const root = await createRoot(); await writeTree(root, { 'src/mcp/curator/tools/search.ts': [ + "import { defineTool } from 'agent-bundle/routes';", 'const title = process.env.TITLE;', - 'export const config = { title };', - moduleSource, + 'export const inputSchema = {};', + 'export const resultSchema = {};', + 'export default defineTool({ inputSchema, resultSchema, title }, async () => undefined);', ].join('\n'), }); @@ -1021,11 +1047,10 @@ it('resolves appResourceUri() references and local const strings to the App rout "export const config = { _meta: { ui: { resourceUri: URI } }, uri: 'catalog://books' };", moduleSource, ].join('\n'), - 'src/mcp/curator/tools/inspect.ts': [ - "import { appResourceUri as app } from 'agent-bundle/routes';", - "export const config = { _meta: { ui: { resourceUri: app('dashboard') } }, related: [app('../apps/dashboard'), app('curator/dashboard')] };", - moduleSource, - ].join('\n'), + 'src/mcp/curator/tools/inspect.ts': toolSource( + "_meta: { ui: { resourceUri: app('dashboard') } }, related: [app('../apps/dashboard'), app('curator/dashboard')]", + ["import { appResourceUri as app } from 'agent-bundle/routes';"], + ), }); const graph = await compileRouteGraph(root, fixtureConfig()); @@ -1047,11 +1072,10 @@ it('resolves appResourceUri() references and local const strings to the App rout "export const config = { resourceUri: 'ui://curator/panel.html' };", moduleSource, ].join('\n'), - 'src/mcp/curator/tools/inspect.ts': [ - "import { appResourceUri } from 'agent-bundle/routes';", - "export const config = { _meta: { ui: { resourceUri: appResourceUri('dashboard') } } };", - moduleSource, - ].join('\n'), + 'src/mcp/curator/tools/inspect.ts': toolSource( + "_meta: { ui: { resourceUri: appResourceUri('dashboard') } }", + ["import { appResourceUri } from 'agent-bundle/routes';"], + ), }); const renamedGraph = await compileRouteGraph(renamed, fixtureConfig()); expect(renamedGraph.diagnostics).toEqual([]); @@ -1066,16 +1090,14 @@ it('diagnoses an appResourceUri() reference to an unknown App with AB4826 and ke "export const config = { resourceUri: 'ui://curator/dashboard.html' };", moduleSource, ].join('\n'), - 'src/mcp/curator/tools/inspect.ts': [ - "import { appResourceUri } from 'agent-bundle/routes';", - "export const config = { _meta: { ui: { resourceUri: appResourceUri('panel') } } };", - moduleSource, - ].join('\n'), - 'src/mcp/curator/tools/search.ts': [ - "import { APP_RESOURCE_URI } from '../constants.ts';", - 'export const config = { _meta: { ui: { resourceUri: APP_RESOURCE_URI } } };', - moduleSource, - ].join('\n'), + 'src/mcp/curator/tools/inspect.ts': toolSource( + "_meta: { ui: { resourceUri: appResourceUri('panel') } }", + ["import { appResourceUri } from 'agent-bundle/routes';"], + ), + 'src/mcp/curator/tools/search.ts': toolSource( + '_meta: { ui: { resourceUri: APP_RESOURCE_URI } }', + ["import { APP_RESOURCE_URI } from '../constants.ts';"], + ), 'src/mcp/curator/constants.ts': "export const APP_RESOURCE_URI = process.env.APP_URI ?? 'ui://curator/dashboard.html';\n", }); @@ -1097,11 +1119,10 @@ it('diagnoses an appResourceUri() reference to an unknown App with AB4826 and ke it('resolves appResourceUri() references only against Apps of the same generated server', async () => { const app = "export const config = { resourceUri: 'ui://curator/dashboard.html' };\n" + moduleSource; - const referencing = (reference: string): string => [ - "import { appResourceUri } from 'agent-bundle/routes';", - `export const config = { _meta: { ui: { resourceUri: appResourceUri('${reference}') } } };`, - moduleSource, - ].join('\n'); + const referencing = (reference: string): string => toolSource( + `_meta: { ui: { resourceUri: appResourceUri('${reference}') } }`, + ["import { appResourceUri } from 'agent-bundle/routes';"], + ); // A generated server registers only its own Apps, so a route on another // server can never serve this URI: the qualified form is rejected too. @@ -1164,7 +1185,7 @@ it('resolves appResourceUri() references only against Apps of the same generated .toEqual({ _meta: { ui: { resourceUri: 'ui://curator/dashboard.html' } } }); }); -it('resolves an App route template relative to the route module, accepting the legacy root-relative form only when unambiguous', async () => { +it('resolves an App route template relative to the route module', async () => { const app = (template: string): string => [ `export const config = { resourceUri: 'ui://curator/dashboard.html', template: '${template}' };`, moduleSource, @@ -1182,37 +1203,12 @@ it('resolves an App route template relative to the route module, accepting the l expect(routeRelativeGraph.diagnostics).toEqual([]); expect(appOf(routeRelativeGraph).config).toEqual({ resourceUri: 'ui://curator/dashboard.html', template: './dashboard.html' }); - // Legacy project-root-relative: still accepted while it is the only match, without a diagnostic. - const rootRelative = await createRoot(); - await writeTree(rootRelative, { - 'src/mcp/curator/apps/dashboard.tsx': app('./views/dashboard.html'), - 'views/dashboard.html': html, - }); - expect((await compileRouteGraph(rootRelative, fixtureConfig())).diagnostics).toEqual([]); - - // Both interpretations name different existing files: AB4827 names both. - const ambiguous = await createRoot(); - await writeTree(ambiguous, { - 'src/mcp/curator/apps/dashboard.tsx': app('./views/dashboard.html'), - 'src/mcp/curator/apps/views/dashboard.html': html, - 'views/dashboard.html': html, - }); - const ambiguousGraph = await compileRouteGraph(ambiguous, fixtureConfig()); - expect(codesOf(ambiguousGraph.diagnostics)).toEqual(['AB4827']); - expect(ambiguousGraph.diagnostics[0]).toMatchObject({ severity: 'error', sourcePath: join(ambiguous, 'src/mcp/curator/apps/dashboard.tsx') }); - expect(ambiguousGraph.diagnostics[0]!.message).toContain('names two different existing files'); - expect(ambiguousGraph.diagnostics[0]!.message).toContain(`${join(ambiguous, 'src/mcp/curator/apps/views/dashboard.html')} (route-relative)`); - expect(ambiguousGraph.diagnostics[0]!.message).toContain(`${join(ambiguous, 'views/dashboard.html')} (project-root-relative)`); - expect(ambiguousGraph.diagnostics[0]!.recovery).toContain('relative to the route module'); - - // Neither exists: AB4827 names both candidates and the fix. + // A missing route-relative template reports the resolved candidate and fix. const missing = await createRoot(); await writeTree(missing, { 'src/mcp/curator/apps/dashboard.tsx': app('./dashboard.html') }); const missingGraph = await compileRouteGraph(missing, fixtureConfig()); expect(codesOf(missingGraph.diagnostics)).toEqual(['AB4827']); - expect(missingGraph.diagnostics[0]!.message).toContain('but neither'); - expect(missingGraph.diagnostics[0]!.message).toContain(`${join(missing, 'src/mcp/curator/apps/dashboard.html')} (route-relative)`); - expect(missingGraph.diagnostics[0]!.message).toContain(`${join(missing, 'dashboard.html')} (project-root-relative)`); + expect(missingGraph.diagnostics[0]!.message).toContain(`but ${join(missing, 'src/mcp/curator/apps/dashboard.html')} does not exist`); // An absolute template has a single candidate, which still has to exist. const absolute = await createRoot(); @@ -1238,15 +1234,13 @@ it('resolves an App route template relative to the route module, accepting the l }); it('rejects a route that advertises an App the server does not build for every target with AB4828', async () => { - const tool = (resourceUri: string): string => [ - `export const config = { _meta: { ui: { resourceUri: ${resourceUri} } } };`, - moduleSource, - ].join('\n'); + const tool = (resourceUri: string, imports: readonly string[] = []): string => + toolSource(`_meta: { ui: { resourceUri: ${resourceUri} } }`, imports); const restrictedApp = "export const config = { resourceUri: 'ui://curator/dashboard.html', targets: ['codex'] };\ndocument.body.textContent = 'dashboard';\n"; const configWith = (lines: readonly string[]): string => [ 'export default {', ...lines, - " plugin: { name: 'routes-fixture', version: '1.0.0' },", + " plugin: { name: 'routes-fixture' },", " targets: ['portable', 'codex'],", '};', '', @@ -1256,7 +1250,7 @@ it('rejects a route that advertises an App the server does not build for every t const referenced = await createInspectProject({ 'agent-bundle.config.ts': configWith([]), 'src/mcp/curator/apps/dashboard.tsx': restrictedApp, - 'src/mcp/curator/tools/open.ts': tool("appResourceUri('dashboard')").replace('export const config', "import { appResourceUri } from 'agent-bundle/routes';\nexport const config"), + 'src/mcp/curator/tools/open.ts': tool("appResourceUri('dashboard')", ["import { appResourceUri } from 'agent-bundle/routes';"]), }); const referencedErrors = (await validate({ root: referenced })).diagnostics.filter((diagnostic) => diagnostic.severity === 'error'); expect(codesOf(referencedErrors)).toEqual(['AB4828']); @@ -1276,7 +1270,7 @@ it('rejects a route that advertises an App the server does not build for every t const restrictedServer = await createInspectProject({ 'agent-bundle.config.ts': configWith([" mcp: { servers: { curator: { targets: ['codex'] } } },"]), 'src/mcp/curator/apps/dashboard.tsx': restrictedApp, - 'src/mcp/curator/tools/open.ts': tool("appResourceUri('dashboard')").replace('export const config', "import { appResourceUri } from 'agent-bundle/routes';\nexport const config"), + 'src/mcp/curator/tools/open.ts': tool("appResourceUri('dashboard')", ["import { appResourceUri } from 'agent-bundle/routes';"]), }); expect((await validate({ root: restrictedServer })).diagnostics.filter((diagnostic) => diagnostic.severity === 'error')).toEqual([]); @@ -1302,7 +1296,7 @@ it('rejects two App routes of one server that declare the same resourceUri with await writeTree(sameServer, { 'src/mcp/curator/apps/dashboard.tsx': app('ui://curator/dashboard.html'), 'src/mcp/curator/apps/panel.tsx': app('ui://curator/dashboard.html'), - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const sameServerGraph = await compileRouteGraph(sameServer, fixtureConfig()); expect(codesOf(sameServerGraph.diagnostics)).toEqual(['AB4829']); @@ -1338,9 +1332,9 @@ it('rejects two App routes of one server that declare the same resourceUri with const acrossServers = await createRoot(); await writeTree(acrossServers, { 'src/mcp/archive/apps/dashboard.tsx': app('ui://shared/dashboard.html'), - 'src/mcp/archive/tools/list.ts': moduleSource, + 'src/mcp/archive/tools/list.ts': toolModuleSource, 'src/mcp/curator/apps/dashboard.tsx': app('ui://shared/dashboard.html'), - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const acrossServersGraph = await compileRouteGraph(acrossServers, fixtureConfig()); expect(acrossServersGraph.diagnostics).toEqual([]); @@ -1358,7 +1352,7 @@ it('rejects two App routes of one server that declare the same resourceUri with const project = await createInspectProject({ 'src/mcp/curator/apps/dashboard.tsx': app('ui://curator/dashboard.html'), 'src/mcp/curator/apps/panel.tsx': app('ui://curator/dashboard.html'), - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const validation = await validate({ root: project }); expect(codesOf(validation.diagnostics.filter((diagnostic) => diagnostic.severity === 'error'))).toEqual(['AB4829']); @@ -1370,30 +1364,12 @@ it('normalizes the App route template to its resolved path for the build', async const routeRelative = await createInspectProject({ 'src/mcp/curator/apps/dashboard.html': html, 'src/mcp/curator/apps/dashboard.tsx': "export const config = { resourceUri: 'ui://curator/dashboard.html', template: './dashboard.html' };\ndocument.body.textContent = 'dashboard';\n", - 'src/mcp/curator/tools/inspect.ts': moduleSource, + 'src/mcp/curator/tools/inspect.ts': toolModuleSource, }); const ready = (await inspect({ root: routeRelative })) as ReadyInspectResult; expect(ready.state).toBe('ready'); expect(ready.model.mcpApps?.map((app) => app.template)).toEqual([join(routeRelative, 'src/mcp/curator/apps/dashboard.html')]); - const legacy = await createInspectProject({ - 'src/mcp/curator/apps/dashboard.tsx': "export const config = { resourceUri: 'ui://curator/dashboard.html', template: './views/dashboard.html' };\ndocument.body.textContent = 'dashboard';\n", - 'src/mcp/curator/tools/inspect.ts': moduleSource, - 'views/dashboard.html': html, - }); - const legacyReady = (await inspect({ root: legacy })) as ReadyInspectResult; - expect(legacyReady.state).toBe('ready'); - expect(legacyReady.model.mcpApps?.map((app) => app.template)).toEqual([join(legacy, 'views/dashboard.html')]); - - const ambiguous = await createInspectProject({ - 'src/mcp/curator/apps/dashboard.tsx': "export const config = { resourceUri: 'ui://curator/dashboard.html', template: './views/dashboard.html' };\ndocument.body.textContent = 'dashboard';\n", - 'src/mcp/curator/apps/views/dashboard.html': html, - 'src/mcp/curator/tools/inspect.ts': moduleSource, - 'views/dashboard.html': html, - }); - const validation = await validate({ root: ambiguous }); - expect(codesOf(validation.diagnostics.filter((diagnostic) => diagnostic.severity === 'error'))).toEqual(['AB4827']); - expect((await inspect({ root: ambiguous })).state).toBe('invalid'); }); it('covers the route config in the graph digest', async () => { @@ -1655,13 +1631,14 @@ it('resolves generated helper types for schema and event route contracts and the '', ].join('\n'), 'src/mcp/curator/tools/inspect.ts': [ + "import { defineTool } from 'agent-bundle/routes';", '// What a caller sends (`_input`: the limit is optional) versus what the component receives (`_output`).', 'export interface InspectInput { readonly source: string; readonly limit?: number; }', 'export interface InspectParsedInput { readonly source: string; readonly limit: number; }', 'export interface InspectResult { readonly accepted: boolean; }', 'export const inputSchema = {} as { readonly _input: InspectInput; readonly _output: InspectParsedInput };', 'export const resultSchema = {} as { readonly _output: InspectResult };', - 'export default async function Inspect() { return undefined; }', + 'export default defineTool({ inputSchema, resultSchema }, async function Inspect() { return undefined; });', '', ].join('\n'), }); @@ -1685,8 +1662,9 @@ it('resolves generated helper types for schema and event route contracts and the 'tool-free/routes.d.ts': routesModule.generateRouteTypes(toolFree), // Stand-ins for the runtime's empty `Register` and `agent-bundle/app`'s empty `AppRegister`, so each // augmentation has a declaration to merge into. - 'runtime-stub.d.ts': 'export interface Register {}\n', + 'runtime-stub.d.ts': 'export interface AgentRequestContext {}\nexport interface Register {}\n', 'app-stub.d.ts': 'export interface AppRegister {}\n', + 'routes-stub.d.ts': 'export declare const defineTool: (config: unknown, handler: (...args: never[]) => unknown) => unknown;\n', 'assertions.ts': [ "import type { Register } from '@agent-bundle/runtime';", "import type { AppRegister } from 'agent-bundle/app';", @@ -1752,6 +1730,7 @@ it('resolves generated helper types for schema and event route contracts and the paths: { '@agent-bundle/runtime': [join(root, 'runtime-stub.d.ts')], 'agent-bundle/app': [join(root, 'app-stub.d.ts')], + 'agent-bundle/routes': [join(root, 'routes-stub.d.ts')], }, skipLibCheck: false, strict: true, @@ -1772,18 +1751,22 @@ it('validates the single async route-module authoring contract statically', asyn const root = await createRoot(); await writeTree(root, { 'src/mcp/curator/tools/valid.tsx': [ + "import { defineTool } from 'agent-bundle/routes';", 'export const inputSchema = {};', 'export const resultSchema = {};', - 'export default async function Valid() { return undefined; }', + 'export default defineTool({ inputSchema, resultSchema }, async function Valid() { return undefined; });', '', ].join('\n'), 'src/mcp/curator/tools/split.tsx': [ - 'export const resultSchema = {};', - 'export const execute = async () => ({});', - 'export const render = () => undefined;', - 'export default function Split() { return undefined; }', - '', - ].join('\n'), + 'src/mcp/curator/tools/split.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/curator/tools/split.tsx': "export const resultSchema = {};", + 'src/mcp/curator/tools/split.tsx': "export const execute = async () => ({});", + 'src/mcp/curator/tools/split.tsx': "export const render = () => undefined;", + 'src/mcp/curator/tools/split.tsx': "export default defineTool({", + 'src/mcp/curator/tools/split.tsx': " inputSchema,", + 'src/mcp/curator/tools/split.tsx': " resultSchema,", + 'src/mcp/curator/tools/split.tsx': "}, async () => { return undefined; });", + 'src/mcp/curator/tools/split.tsx': ].join('\n'), }); const graph = await compileRouteGraph(root, fixtureConfig()); @@ -1794,92 +1777,6 @@ it('validates the single async route-module authoring contract statically', asyn ]); }); -it('accepts explicit default re-exports when one tool is placed on two servers (#446)', async () => { - const root = await createRoot(); - await writeTree(root, { - // The primary placement: a full route module. - 'src/mcp/public/tools/search.tsx': [ - "export const config = { description: 'Search.' };", - 'export const inputSchema = {};', - 'export const resultSchema = {};', - 'export default async function Search() { return undefined; }', - '', - ].join('\n'), - // The second placement carries its own config and re-exports the rest. - 'src/mcp/library/tools/search.tsx': [ - "export const config = { description: 'Search from the widget server.' };", - "export { default, inputSchema, resultSchema } from '../../public/tools/search.tsx';", - '', - ].join('\n'), - // A named component aliased to default, through a shared page module. - 'src/pages/download.tsx': 'export async function DownloadPage() { return undefined; }\n', - 'src/mcp/library/tools/download.tsx': [ - "export const config = { description: 'Download.' };", - "export { DownloadPage as default } from '../../../pages/download.tsx';", - "export { inputSchema, resultSchema } from '../../public/tools/search.tsx';", - '', - ].join('\n'), - // A chain: the shared module itself re-exports its default, and a - // `.js` specifier names the emitted extension of a `.tsx` source. - 'src/pages/_delete-impl.tsx': 'export default async () => undefined;\n', - 'src/pages/delete.tsx': "export { default } from './_delete-impl.js';\n", - 'src/mcp/library/tools/delete.tsx': [ - "export const config = { description: 'Delete.' };", - "export { default } from '../../../pages/delete.tsx';", - "export { inputSchema, resultSchema } from '../../public/tools/search.tsx';", - '', - ].join('\n'), - // A default from a package the scan cannot read is verified at run time. - 'src/mcp/library/tools/external.tsx': [ - "export const config = { description: 'External.' };", - 'export const inputSchema = {};', - 'export const resultSchema = {};', - "export { default } from '@shared/routes/external';", - '', - ].join('\n'), - // A synchronous component satisfies the same contract as an async component. - 'src/pages/sync.tsx': 'export default function SyncPage() { return undefined; }\n', - 'src/mcp/library/tools/sync.tsx': [ - "export const config = { description: 'Sync.' };", - "export { default } from '../../../pages/sync.tsx';", - "export { inputSchema, resultSchema } from '../../public/tools/search.tsx';", - '', - ].join('\n'), - // A type-only default re-export emits no binding and never satisfies the contract. - 'src/mcp/library/tools/typed.tsx': [ - "export const config = { description: 'Typed.' };", - "export { type default } from '../../public/tools/search.tsx';", - "export { inputSchema, resultSchema } from '../../public/tools/search.tsx';", - '', - ].join('\n'), - }); - - const graph = await compileRouteGraph(root, fixtureConfig()); - - expect(graph.diagnostics.map(({ code, message, sourcePath }) => ({ - code, - message, - source: sourcePath?.slice(root.length + 1).replaceAll('\\', '/'), - }))).toEqual([ - { - code: 'AB4810', - message: 'Route module src/mcp/library/tools/typed.tsx does not satisfy the public route contract: missing default export.', - source: 'src/mcp/library/tools/typed.tsx', - }, - ]); - expect(graph.servers.map((server) => [server.name, server.routes.map((route) => route.id)])).toEqual([ - ['library', [ - 'tool:library/delete', - 'tool:library/download', - 'tool:library/external', - 'tool:library/search', - 'tool:library/sync', - 'tool:library/typed', - ]], - ['public', ['tool:public/search']], - ]); -}); - it('reads declared runtime exports without following re-exports or requiring async', () => { const scan = routesModule.scanRouteModuleExports; expect([...scan("export { default, value } from './missing.js';", 'route.ts').named]).toEqual(['default', 'value']); @@ -2100,7 +1997,7 @@ it('fails unavailable event routes before packaging while admitting supported ta const eventSource = 'export default async function WorkspaceOpen() { return undefined; }\n'; const configSource = [ 'export default {', - " plugin: { name: 'event-capability-fixture', version: '1.0.0' },", + " plugin: { name: 'event-capability-fixture' },", " targets: ['claude', 'cursor'],", '};', '', @@ -2143,7 +2040,7 @@ it('selects event-route projections by required capability rows', async () => { await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'event-requires-fixture', version: '1.0.0' },", + " plugin: { name: 'event-requires-fixture' },", " targets: ['claude', 'codex', 'cursor', 'portable'],", '};', '', @@ -2169,7 +2066,7 @@ it('rejects event routes that declare both targets and requires', async () => { await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'event-selection-fixture', version: '1.0.0' },", + " plugin: { name: 'event-selection-fixture' },", " targets: ['cursor'],", '};', '', @@ -2196,7 +2093,7 @@ it('names every selected host and capability row when no projection satisfies a await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'event-requirement-fixture', version: '1.0.0' },", + " plugin: { name: 'event-requirement-fixture' },", " targets: ['cursor', 'portable'],", '};', '', @@ -2224,7 +2121,7 @@ it('rejects malformed event route targets with AB4825', async () => { await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'event-targets-fixture', version: '1.0.0' },", + " plugin: { name: 'event-targets-fixture' },", " targets: ['cursor'],", '};', '', @@ -2250,7 +2147,7 @@ it('preserves sub-second event route timeout precision in the normalized model', await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'event-timeout-fixture', version: '1.0.0' },", + " plugin: { name: 'event-timeout-fixture' },", " targets: ['cursor'],", '};', '', @@ -2276,7 +2173,7 @@ it('rejects explicit shared mode when no generated runtime can host an event rou await writeTree(root, { 'agent-bundle.config.ts': [ 'export default {', - " plugin: { name: 'event-runtime-fixture', version: '1.0.0' },", + " plugin: { name: 'event-runtime-fixture' },", " targets: ['cursor'],", '};', '', diff --git a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts index fc41aea6d..3104c483e 100644 --- a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts +++ b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts @@ -114,7 +114,7 @@ it('invokes compiled tool and event routes through the foreground server', { tim "import { join } from 'node:path';", '', 'export default {', - " plugin: { name: 'route-invocation-dev-server', version: '1.0.0' },", + " plugin: { name: 'route-invocation-dev-server' },", " targets: ['claude'],", ' tools: {', " rsbuild: { source: { define: { __ROUTE_INVOCATION_DEFINE__: JSON.stringify('defined') } } },", @@ -251,137 +251,164 @@ it('invokes compiled tool and event routes through the foreground server', { tim '', ].join('\n'), 'src/mcp/alpha/tools/fail.tsx': [ - "import { appendFileSync } from 'node:fs';", - "import { z } from 'zod';", - '', - "appendFileSync('.agent-bundle/alpha-worker.marker', 'load\\n');", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ failed: z.boolean() }).strict();', - '', - 'export default async function Fail() {', - " appendFileSync('.agent-bundle/alpha-handler.marker', 'run\\n');", - " throw new Error('Generated route must default-export an async Server Component.');", - '}', - '', - ].join('\n'), + 'src/mcp/alpha/tools/fail.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/alpha/tools/fail.tsx': "import { appendFileSync } from 'node:fs';", + 'src/mcp/alpha/tools/fail.tsx': "import { z } from 'zod';", + 'src/mcp/alpha/tools/fail.tsx': "", + 'src/mcp/alpha/tools/fail.tsx': "appendFileSync('.agent-bundle/alpha-worker.marker', 'load\\n');", + 'src/mcp/alpha/tools/fail.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/alpha/tools/fail.tsx': "export const resultSchema = z.object({ failed: z.boolean() }).strict();", + 'src/mcp/alpha/tools/fail.tsx': "", + 'src/mcp/alpha/tools/fail.tsx': "export default defineTool({", + 'src/mcp/alpha/tools/fail.tsx': " inputSchema,", + 'src/mcp/alpha/tools/fail.tsx': " resultSchema,", + 'src/mcp/alpha/tools/fail.tsx': "}, async () => {", + 'src/mcp/alpha/tools/fail.tsx': " appendFileSync('.agent-bundle/alpha-handler.marker', 'run\\n');", + 'src/mcp/alpha/tools/fail.tsx': " throw new Error('Generated route must default-export an async Server Component.');", + 'src/mcp/alpha/tools/fail.tsx': "});", + 'src/mcp/alpha/tools/fail.tsx': ].join('\n'), 'src/mcp/omega/tools/pass.tsx': [ - "import { appendFileSync } from 'node:fs';", - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - "appendFileSync('.agent-bundle/omega-worker.marker', 'load\\n');", - 'export const inputSchema = z.object({}).strict();', - "export const resultSchema = z.object({ selected: z.literal('omega') }).strict();", - '', - 'export default async function Pass() {', - " return createElement(Agent.Result, { value: { selected: 'omega' } });", - '}', - '', - ].join('\n'), + 'src/mcp/omega/tools/pass.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/omega/tools/pass.tsx': "import { appendFileSync } from 'node:fs';", + 'src/mcp/omega/tools/pass.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/omega/tools/pass.tsx': "import { createElement } from 'react';", + 'src/mcp/omega/tools/pass.tsx': "import { z } from 'zod';", + 'src/mcp/omega/tools/pass.tsx': "", + 'src/mcp/omega/tools/pass.tsx': "appendFileSync('.agent-bundle/omega-worker.marker', 'load\\n');", + 'src/mcp/omega/tools/pass.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/omega/tools/pass.tsx': "export const resultSchema = z.object({ selected: z.literal('omega') }).strict();", + 'src/mcp/omega/tools/pass.tsx': "", + 'src/mcp/omega/tools/pass.tsx': "export default defineTool({", + 'src/mcp/omega/tools/pass.tsx': " inputSchema,", + 'src/mcp/omega/tools/pass.tsx': " resultSchema,", + 'src/mcp/omega/tools/pass.tsx': "}, async () => {", + 'src/mcp/omega/tools/pass.tsx': " return createElement(Agent.Result, { value: { selected: 'omega' } });", + 'src/mcp/omega/tools/pass.tsx': "});", + 'src/mcp/omega/tools/pass.tsx': ].join('\n'), 'src/mcp/importer/tools/fail.tsx': [ - "import { appendFileSync } from 'node:fs';", - "import { z } from 'zod';", - '', - "appendFileSync('.agent-bundle/importer-worker.marker', 'load\\n');", - "throw new Error('Generated route must default-export from import.');", - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ failed: z.boolean() }).strict();', - 'export default async function Fail() { return undefined; }', - '', - ].join('\n'), + 'src/mcp/importer/tools/fail.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/importer/tools/fail.tsx': "import { appendFileSync } from 'node:fs';", + 'src/mcp/importer/tools/fail.tsx': "import { z } from 'zod';", + 'src/mcp/importer/tools/fail.tsx': "", + 'src/mcp/importer/tools/fail.tsx': "appendFileSync('.agent-bundle/importer-worker.marker', 'load\\n');", + 'src/mcp/importer/tools/fail.tsx': "throw new Error('Generated route must default-export from import.');", + 'src/mcp/importer/tools/fail.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/importer/tools/fail.tsx': "export const resultSchema = z.object({ failed: z.boolean() }).strict();", + 'src/mcp/importer/tools/fail.tsx': "export default defineTool({", + 'src/mcp/importer/tools/fail.tsx': " inputSchema,", + 'src/mcp/importer/tools/fail.tsx': " resultSchema,", + 'src/mcp/importer/tools/fail.tsx': "}, async () => { return undefined; });", + 'src/mcp/importer/tools/fail.tsx': ].join('\n'), 'src/mcp/status/tools/counter.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - 'export const inputSchema = z.object({ key: z.string() }).strict();', - 'export const resultSchema = z.object({ count: z.number() }).strict();', - 'export default async function Counter({ input }) {', - ' const context = await agent();', - " if (context.state === undefined) throw new Error('state unavailable');", - " const committed = await context.state.dispatch('incremented', { by: 1 }, { idempotencyKey: `${input.key}:${crypto.randomUUID()}` });", - ' return createElement(Agent.Result, { value: { count: committed.state.count } });', - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/counter.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/counter.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/counter.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/counter.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/counter.tsx': "", + 'src/mcp/status/tools/counter.tsx': "export const inputSchema = z.object({ key: z.string() }).strict();", + 'src/mcp/status/tools/counter.tsx': "export const resultSchema = z.object({ count: z.number() }).strict();", + 'src/mcp/status/tools/counter.tsx': "export default defineTool({", + 'src/mcp/status/tools/counter.tsx': " inputSchema,", + 'src/mcp/status/tools/counter.tsx': " resultSchema,", + 'src/mcp/status/tools/counter.tsx': "}, async (input) => {", + 'src/mcp/status/tools/counter.tsx': " const context = await agent();", + 'src/mcp/status/tools/counter.tsx': " if (context.state === undefined) throw new Error('state unavailable');", + 'src/mcp/status/tools/counter.tsx': " const committed = await context.state.dispatch('incremented', { by: 1 }, { idempotencyKey: `${input.key}:${crypto.randomUUID()}` });", + 'src/mcp/status/tools/counter.tsx': " return createElement(Agent.Result, { value: { count: committed.state.count } });", + 'src/mcp/status/tools/counter.tsx': "});", + 'src/mcp/status/tools/counter.tsx': ].join('\n'), 'src/mcp/status/tools/refuse.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - 'export const inputSchema = z.object({ reason: z.string() }).strict();', - 'export const resultSchema = z.object({ refused: z.boolean() }).strict();', - 'export default async function Refuse({ input }) {', - " return createElement(Agent.Result, { value: { refused: true } }, createElement(Agent.Error, { code: 'refused' }, `Refused: ${input.reason}`));", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/refuse.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/refuse.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/refuse.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/refuse.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/refuse.tsx': "", + 'src/mcp/status/tools/refuse.tsx': "export const inputSchema = z.object({ reason: z.string() }).strict();", + 'src/mcp/status/tools/refuse.tsx': "export const resultSchema = z.object({ refused: z.boolean() }).strict();", + 'src/mcp/status/tools/refuse.tsx': "export default defineTool({", + 'src/mcp/status/tools/refuse.tsx': " inputSchema,", + 'src/mcp/status/tools/refuse.tsx': " resultSchema,", + 'src/mcp/status/tools/refuse.tsx': "}, async (input) => {", + 'src/mcp/status/tools/refuse.tsx': " return createElement(Agent.Result, { value: { refused: true } }, createElement(Agent.Error, { code: 'refused' }, `Refused: ${input.reason}`));", + 'src/mcp/status/tools/refuse.tsx': "});", + 'src/mcp/status/tools/refuse.tsx': ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { ALIAS_VALUE } from '@fixture/value';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "import { Panel } from './panel.js';", - 'declare const __ROUTE_INVOCATION_DEFINE__: string;', - '', - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"}},\"required\":[\"service\",\"source\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.' };", - "export const inputSchema = z.object({ service: z.string().min(1), source: z.string() }).strict();", - 'export const resultSchema = z.object({ alias: z.string(), define: z.string(), pluginRoot: z.string(), service: z.string(), source: z.string(), stateRoot: z.string() }).strict();', - '', - 'export default async function Report({ input }) {', - ' const context = await agent();', - ' await context.provider("clock");', - " if (context.plugin.state !== 'available') throw new Error('plugin unavailable');", - ' const value = { alias: ALIAS_VALUE, define: __ROUTE_INVOCATION_DEFINE__, pluginRoot: context.plugin.value.root, service: input.service, source: input.source, stateRoot: context.plugin.value.stateRoot };', - " return createElement(Agent.Result, { value }, createElement(Panel), createElement(Agent.Text, null, './panel.js'), createElement(Agent.Text, null, `Service ${input.service}`));", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/report.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/report.tsx': "import { ALIAS_VALUE } from '@fixture/value';", + 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/report.tsx': "import { Panel } from './panel.js';", + 'src/mcp/status/tools/report.tsx': "declare const __ROUTE_INVOCATION_DEFINE__: string;", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({ service: z.string().min(1), source: z.string() }).strict();", + 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ alias: z.string(), define: z.string(), pluginRoot: z.string(), service: z.string(), source: z.string(), stateRoot: z.string() }).strict();", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export default defineTool({", + 'src/mcp/status/tools/report.tsx': "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"}},\"required\":[\"service\",\"source\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.',", + 'src/mcp/status/tools/report.tsx': " inputSchema,", + 'src/mcp/status/tools/report.tsx': " resultSchema,", + 'src/mcp/status/tools/report.tsx': "}, async (input) => {", + 'src/mcp/status/tools/report.tsx': " const context = await agent();", + 'src/mcp/status/tools/report.tsx': " await context.provider(\"clock\");", + 'src/mcp/status/tools/report.tsx': " if (context.plugin.state !== 'available') throw new Error('plugin unavailable');", + 'src/mcp/status/tools/report.tsx': " const value = { alias: ALIAS_VALUE, define: __ROUTE_INVOCATION_DEFINE__, pluginRoot: context.plugin.value.root, service: input.service, source: input.source, stateRoot: context.plugin.value.stateRoot };", + 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value }, createElement(Panel), createElement(Agent.Text, null, './panel.js'), createElement(Agent.Text, null, `Service ${input.service}`));", + 'src/mcp/status/tools/report.tsx': "});", + 'src/mcp/status/tools/report.tsx': ].join('\n'), 'src/mcp/status/tools/panel.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - "export const Panel = () => createElement(Agent.Text, null, 'panel rendered');", - "export const inputSchema = z.object({}).strict();", - "export const resultSchema = z.object({ panel: z.literal(true) }).strict();", - 'export default async function PanelRoute() {', - " return createElement(Agent.Result, { value: { panel: true } }, createElement(Panel));", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/panel.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/panel.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/panel.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/panel.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/panel.tsx': "", + 'src/mcp/status/tools/panel.tsx': "export const Panel = () => createElement(Agent.Text, null, 'panel rendered');", + 'src/mcp/status/tools/panel.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/status/tools/panel.tsx': "export const resultSchema = z.object({ panel: z.literal(true) }).strict();", + 'src/mcp/status/tools/panel.tsx': "export default defineTool({", + 'src/mcp/status/tools/panel.tsx': " inputSchema,", + 'src/mcp/status/tools/panel.tsx': " resultSchema,", + 'src/mcp/status/tools/panel.tsx': "}, async () => {", + 'src/mcp/status/tools/panel.tsx': " return createElement(Agent.Result, { value: { panel: true } }, createElement(Panel));", + 'src/mcp/status/tools/panel.tsx': "});", + 'src/mcp/status/tools/panel.tsx': ].join('\n'), 'src/mcp/status/tools/crash.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement, Suspense } from 'react';", - "import { z } from 'zod';", - '', - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ok: z.boolean() }).strict();', - "const Boom = async () => { throw new Error('render exploded'); };", - '', - 'export default async function Crash() {', - " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'crashing', total: 1 }) }, createElement(Boom)));", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/crash.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/crash.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/crash.tsx': "import { createElement, Suspense } from 'react';", + 'src/mcp/status/tools/crash.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/crash.tsx': "", + 'src/mcp/status/tools/crash.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/status/tools/crash.tsx': "export const resultSchema = z.object({ ok: z.boolean() }).strict();", + 'src/mcp/status/tools/crash.tsx': "const Boom = async () => { throw new Error('render exploded'); };", + 'src/mcp/status/tools/crash.tsx': "", + 'src/mcp/status/tools/crash.tsx': "export default defineTool({", + 'src/mcp/status/tools/crash.tsx': " inputSchema,", + 'src/mcp/status/tools/crash.tsx': " resultSchema,", + 'src/mcp/status/tools/crash.tsx': "}, async () => {", + 'src/mcp/status/tools/crash.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'crashing', total: 1 }) }, createElement(Boom)));", + 'src/mcp/status/tools/crash.tsx': "});", + 'src/mcp/status/tools/crash.tsx': ].join('\n'), 'src/mcp/status/tools/burst.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - 'export const inputSchema = z.object({ messageBytes: z.number().int().min(1).max(65_536), ticks: z.number().int().min(1).max(900) }).strict();', - 'export const resultSchema = z.object({ ticks: z.number() }).strict();', - '', - 'export default async function Burst({ input }) {', - ' const { progress } = await agent();', - ' for (let step = 1; step <= input.ticks; step += 1) {', - " await progress.report({ completed: step, message: `${step}:${'p'.repeat(input.messageBytes)}`, total: input.ticks });", - ' }', - " return createElement(Agent.Result, { value: { ticks: input.ticks } }, createElement(Agent.Text, null, `Reported ${input.ticks} ticks.`));", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/burst.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/burst.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/burst.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/burst.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/burst.tsx': "", + 'src/mcp/status/tools/burst.tsx': "export const inputSchema = z.object({ messageBytes: z.number().int().min(1).max(65_536), ticks: z.number().int().min(1).max(900) }).strict();", + 'src/mcp/status/tools/burst.tsx': "export const resultSchema = z.object({ ticks: z.number() }).strict();", + 'src/mcp/status/tools/burst.tsx': "", + 'src/mcp/status/tools/burst.tsx': "export default defineTool({", + 'src/mcp/status/tools/burst.tsx': " inputSchema,", + 'src/mcp/status/tools/burst.tsx': " resultSchema,", + 'src/mcp/status/tools/burst.tsx': "}, async (input) => {", + 'src/mcp/status/tools/burst.tsx': " const { progress } = await agent();", + 'src/mcp/status/tools/burst.tsx': " for (let step = 1; step <= input.ticks; step += 1) {", + 'src/mcp/status/tools/burst.tsx': " await progress.report({ completed: step, message: `${step}:${'p'.repeat(input.messageBytes)}`, total: input.ticks });", + 'src/mcp/status/tools/burst.tsx': " }", + 'src/mcp/status/tools/burst.tsx': " return createElement(Agent.Result, { value: { ticks: input.ticks } }, createElement(Agent.Text, null, `Reported ${input.ticks} ticks.`));", + 'src/mcp/status/tools/burst.tsx': "});", + 'src/mcp/status/tools/burst.tsx': ].join('\n'), 'src/mcp/status/tools/report.cli.ts': [ "export const config = { command: ['report'], confirm: true, flags: { service: { name: 'name' }, source: { required: false } } };", "export const mapInput = (input) => ({ ...input, source: input.source ?? 'cli-projection' });", @@ -389,15 +416,18 @@ it('invokes compiled tool and event routes through the foreground server', { tim ].join('\n'), 'src/mcp/status/tools/plain.cli.ts': "export const config = { command: ['plain-tool'] };\n", 'src/mcp/status/tools/plain.ts': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - 'export const inputSchema = z.object({}).strict();', - "export const resultSchema = z.object({ selected: z.literal('plain-tool') }).strict();", - "export default async function PlainTool() { return createElement(Agent.Result, { value: { selected: 'plain-tool' } }); }", - '', - ].join('\n'), + 'src/mcp/status/tools/plain.ts': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/plain.ts': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/plain.ts': "import { createElement } from 'react';", + 'src/mcp/status/tools/plain.ts': "import { z } from 'zod';", + 'src/mcp/status/tools/plain.ts': "", + 'src/mcp/status/tools/plain.ts': "export const inputSchema = z.object({}).strict();", + 'src/mcp/status/tools/plain.ts': "export const resultSchema = z.object({ selected: z.literal('plain-tool') }).strict();", + 'src/mcp/status/tools/plain.ts': "export default defineTool({", + 'src/mcp/status/tools/plain.ts': " inputSchema,", + 'src/mcp/status/tools/plain.ts': " resultSchema,", + 'src/mcp/status/tools/plain.ts': "}, async () => { return createElement(Agent.Result, { value: { selected: 'plain-tool' } }); });", + 'src/mcp/status/tools/plain.ts': ].join('\n'), 'src/providers/clock.ts': [ 'export default () => ({ now: 0 });', '', @@ -1458,7 +1488,7 @@ it('invokes compiled tool and event routes through the foreground server', { tim it('fails closed when a valid host is ineligible for the compiled event route', { timeout: 180_000 }, async () => { const project = await createProjectFixture({ - config: "export default { plugin: { name: 'route-invocation-host-binding', version: '1.0.0' }, targets: ['claude', 'codex'] };\n", + config: "export default { plugin: { name: 'route-invocation-host-binding' }, targets: ['claude', 'codex'] };\n", files: { 'package.json': '{"dependencies":{"@agent-bundle/runtime":"workspace:*"},"type":"module"}\n', 'src/events/tool/before.ts': "export const config = { runtime: 'standalone', targets: ['claude'] }; export default () => ({ outcome: 'deny', reason: 'blocked' });\n", @@ -1560,7 +1590,7 @@ it('fails closed when a valid host is ineligible for the compiled event route', it('enforces compiled handler, MCP schemas, and operator env across production surfaces', { timeout: 180_000 }, async () => { const project = await createProjectFixture({ - config: "export default { plugin: { name: 'route-parity', version: '1.0.0' }, targets: ['claude'] };\n", + config: "export default { plugin: { name: 'route-parity' }, targets: ['claude'] };\n", files: { 'package.json': '{"dependencies":{"@agent-bundle/runtime":"workspace:*","react":"19.2.8","zod":"4.5.4"},"type":"module"}\n', 'src/events/tool/before.ts': "export default () => ({ outcome: 'deny', reason: 'blocked' });\n", @@ -1573,21 +1603,24 @@ it('enforces compiled handler, MCP schemas, and operator env across production s '', ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - "import { writeFileSync } from 'node:fs';", - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - "export const config = { inputJsonSchema: { type: 'object', additionalProperties: false, properties: { service: { type: 'string' } }, required: ['service'] } };", - "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", - "export const resultSchema = z.object({ operator: z.string(), service: z.string() }).strict();", - '', - 'export default async function Report({ input }) {', - " writeFileSync('.agent-bundle/handler-ran', 'yes');", - " return createElement(Agent.Result, { value: { operator: process.env.OPERATOR_VALUE ?? 'missing', service: input.service } });", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/report.tsx': "import { writeFileSync } from 'node:fs';", + 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", + 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ operator: z.string(), service: z.string() }).strict();", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export default defineTool({", + 'src/mcp/status/tools/report.tsx': "inputJsonSchema: { type: 'object', additionalProperties: false, properties: { service: { type: 'string' } }, required: ['service'] },", + 'src/mcp/status/tools/report.tsx': " inputSchema,", + 'src/mcp/status/tools/report.tsx': " resultSchema,", + 'src/mcp/status/tools/report.tsx': "}, async (input) => {", + 'src/mcp/status/tools/report.tsx': " writeFileSync('.agent-bundle/handler-ran', 'yes');", + 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value: { operator: process.env.OPERATOR_VALUE ?? 'missing', service: input.service } });", + 'src/mcp/status/tools/report.tsx': "});", + 'src/mcp/status/tools/report.tsx': ].join('\n'), }, prefix: 'agent-bundle-route-parity-', }); @@ -1696,7 +1729,7 @@ it('publishes invocation routes only after a successful initial or recovered bui const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'route-invocation-publication-gate', version: '1.0.0' },", + " plugin: { name: 'route-invocation-publication-gate' },", " targets: ['claude'],", '};', '', @@ -1704,19 +1737,22 @@ it('publishes invocation routes only after a successful initial or recovered bui files: { 'package.json': '{"dependencies":{"@agent-bundle/runtime":"workspace:*","react":"19.2.8","zod":"4.5.4"},"type":"module"}\n', 'src/mcp/status/tools/report.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - "import './missing.js';", - '', - "export const inputSchema = z.object({}).strict();", - 'export const resultSchema = z.object({ version: z.string() }).strict();', - '', - 'export default async function Report() {', - " return createElement(Agent.Result, { value: { version: 'published' } }, createElement(Agent.Text, null, 'Published route.'));", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/report.tsx': "import './missing.js';", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ version: z.string() }).strict();", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export default defineTool({", + 'src/mcp/status/tools/report.tsx': " inputSchema,", + 'src/mcp/status/tools/report.tsx': " resultSchema,", + 'src/mcp/status/tools/report.tsx': "}, async () => {", + 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value: { version: 'published' } }, createElement(Agent.Text, null, 'Published route.'));", + 'src/mcp/status/tools/report.tsx': "});", + 'src/mcp/status/tools/report.tsx': ].join('\n'), }, prefix: 'agent-bundle-route-invocation-publication-gate-', }); @@ -1859,7 +1895,7 @@ it('bounds the render history a compiled child produces by count and bytes acros const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'route-invocation-retention', version: '1.0.0' },", + " plugin: { name: 'route-invocation-retention' },", " targets: ['claude'],", '};', '', diff --git a/packages/agent-bundle/tests/route-invocation-service.test.ts b/packages/agent-bundle/tests/route-invocation-service.test.ts index 0c2a57eb5..d404fc21a 100644 --- a/packages/agent-bundle/tests/route-invocation-service.test.ts +++ b/packages/agent-bundle/tests/route-invocation-service.test.ts @@ -1310,16 +1310,19 @@ const tsxSiblingProject = async (): Promise => routeProject( '', ].join('\n'), 'src/mcp/fixture/tools/report.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - '', - "import { Panel } from './panel.js';", - '', - 'export default async function Report() {', - " return createElement(Agent.Result, null, createElement(Panel), createElement(Agent.Text, null, './panel.js'));", - '}', - '', - ].join('\n'), + 'src/mcp/fixture/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/fixture/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/fixture/tools/report.tsx': "import { createElement } from 'react';", + 'src/mcp/fixture/tools/report.tsx': "", + 'src/mcp/fixture/tools/report.tsx': "import { Panel } from './panel.js';", + 'src/mcp/fixture/tools/report.tsx': "", + 'src/mcp/fixture/tools/report.tsx': "export default defineTool({", + 'src/mcp/fixture/tools/report.tsx': " inputSchema,", + 'src/mcp/fixture/tools/report.tsx': " resultSchema,", + 'src/mcp/fixture/tools/report.tsx': "}, async () => {", + 'src/mcp/fixture/tools/report.tsx': " return createElement(Agent.Result, null, createElement(Panel), createElement(Agent.Text, null, './panel.js'));", + 'src/mcp/fixture/tools/report.tsx': "});", + 'src/mcp/fixture/tools/report.tsx': ].join('\n'), }, ); @@ -1349,22 +1352,25 @@ const burstRouteProject = async (): Promise => routeProject( 'burst', { 'src/mcp/fixture/tools/burst.tsx': [ - "import { Agent, agent } from '@agent-bundle/runtime';", - "import { createElement, Suspense } from 'react';", - '', - 'const Chunk = async ({ index }) => {', - ' await new Promise((resolve) => setTimeout(resolve, 20 * (index + 1)));', - " return createElement(Agent.Text, null, `${index}:${'x'.repeat(150 * 1024)}`);", - '};', - '', - 'export default async function Burst() {', - ' const { progress } = await agent();', - ' for (let step = 1; step <= 400; step += 1) await progress.report({ completed: step, total: 400 });', - ' return createElement(Agent.Result, null, ...Array.from({ length: 5 }, (_, index) =>', - ' createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: index, total: 5 }), key: index }, createElement(Chunk, { index }))));', - '}', - '', - ].join('\n'), + 'src/mcp/fixture/tools/burst.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/fixture/tools/burst.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", + 'src/mcp/fixture/tools/burst.tsx': "import { createElement, Suspense } from 'react';", + 'src/mcp/fixture/tools/burst.tsx': "", + 'src/mcp/fixture/tools/burst.tsx': "const Chunk = async ({ index }) => {", + 'src/mcp/fixture/tools/burst.tsx': " await new Promise((resolve) => setTimeout(resolve, 20 * (index + 1)));", + 'src/mcp/fixture/tools/burst.tsx': " return createElement(Agent.Text, null, `${index}:${'x'.repeat(150 * 1024)}`);", + 'src/mcp/fixture/tools/burst.tsx': "};", + 'src/mcp/fixture/tools/burst.tsx': "", + 'src/mcp/fixture/tools/burst.tsx': "export default defineTool({", + 'src/mcp/fixture/tools/burst.tsx': " inputSchema,", + 'src/mcp/fixture/tools/burst.tsx': " resultSchema,", + 'src/mcp/fixture/tools/burst.tsx': "}, async () => {", + 'src/mcp/fixture/tools/burst.tsx': " const { progress } = await agent();", + 'src/mcp/fixture/tools/burst.tsx': " for (let step = 1; step <= 400; step += 1) await progress.report({ completed: step, total: 400 });", + 'src/mcp/fixture/tools/burst.tsx': " return createElement(Agent.Result, null, ...Array.from({ length: 5 }, (_, index) =>", + 'src/mcp/fixture/tools/burst.tsx': " createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: index, total: 5 }), key: index }, createElement(Chunk, { index }))));", + 'src/mcp/fixture/tools/burst.tsx': "});", + 'src/mcp/fixture/tools/burst.tsx': ].join('\n'), }, ); diff --git a/packages/agent-bundle/tests/route-register-typegen.test.ts b/packages/agent-bundle/tests/route-register-typegen.test.ts index 841ce590f..7e0e86ea7 100644 --- a/packages/agent-bundle/tests/route-register-typegen.test.ts +++ b/packages/agent-bundle/tests/route-register-typegen.test.ts @@ -86,33 +86,42 @@ it('types every route-aware public surface from the generated route registration writeProjectFile(root, 'agent-bundle.config.ts', [ "import { defineConfig } from 'agent-bundle/config';", 'export default defineConfig({', - " plugin: { name: 'route-register-fixture', version: '1.0.0' },", + " plugin: { name: 'route-register-fixture' },", " targets: ['claude'],", '});', '', ].join('\n')), writeProjectFile(root, 'src/mcp/curator/tools/status.ts', [ - "import { z } from 'zod';", - 'export const inputSchema = z.object({}).strict();', - "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - "export default async function Status() { return { status: 'ready' as const }; }", - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const inputSchema = z.object({}).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export default defineTool({", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " inputSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " resultSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "}, async () => { return { status: 'ready' as const }; });", + writeProjectFile(root, 'src/mcp/curator/tools/status.ts', ].join('\n')), writeProjectFile(root, 'src/mcp/curator/tools/find.ts', [ - "import { z } from 'zod';", - 'export const inputSchema = z.object({ query: z.string() }).strict();', - 'export const resultSchema = z.object({ hits: z.number() }).strict();', - 'export default async function Find() { return { hits: 1 }; }', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "export const inputSchema = z.object({ query: z.string() }).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "export const resultSchema = z.object({ hits: z.number() }).strict();", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "export default defineTool({", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', " inputSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', " resultSchema,", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "}, async () => { return { hits: 1 }; });", + writeProjectFile(root, 'src/mcp/curator/tools/find.ts', ].join('\n')), // A second server registering the same tool name: the wire helpers see the union of both inputs. writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', [ - "import { z } from 'zod';", - 'export const inputSchema = z.object({ isbn: z.string() }).strict();', - 'export const resultSchema = z.object({ shelved: z.boolean() }).strict();', - 'export default async function Find() { return { shelved: true }; }', - '', - ].join('\n')), + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "import { defineTool } from 'agent-bundle/routes';", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "import { z } from 'zod';", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "export const inputSchema = z.object({ isbn: z.string() }).strict();", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "export const resultSchema = z.object({ shelved: z.boolean() }).strict();", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "export default defineTool({", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', " inputSchema,", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', " resultSchema,", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "}, async () => { return { shelved: true }; });", + writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', ].join('\n')), writeProjectFile(root, 'src/mcp/curator/prompts/brief.ts', [ "import { z } from 'zod';", 'export const inputSchema = z.object({ topic: z.string() }).strict();', diff --git a/packages/agent-bundle/tests/route-task-support.test.ts b/packages/agent-bundle/tests/route-task-support.test.ts index cc461c9b8..41deebc3c 100644 --- a/packages/agent-bundle/tests/route-task-support.test.ts +++ b/packages/agent-bundle/tests/route-task-support.test.ts @@ -35,16 +35,19 @@ const writeTree = async (root: string, files: Readonly>): } }; -const config: AgentBundleConfig = { plugin: { name: 'task-support-fixture', version: '1.0.0' } }; - -const toolModule = (routeConfig?: string): string => [ - "import { z } from 'zod';", - ...(routeConfig === undefined ? [] : [`export const config = ${routeConfig};`]), - 'export const inputSchema = z.object({});', - 'export const resultSchema = z.object({ ok: z.boolean() });', - 'export default async function Tool() { return undefined; }', - '', -].join('\n'); +const config: AgentBundleConfig = { plugin: { name: 'task-support-fixture' } }; + +const toolModule = (routeConfig = '{}'): string => { + const fields = routeConfig.slice(1, -1).trim(); + return [ + "import { defineTool } from 'agent-bundle/routes';", + "import { z } from 'zod';", + 'export const inputSchema = z.object({});', + 'export const resultSchema = z.object({ ok: z.boolean() });', + `export default defineTool({ ${fields}${fields.length === 0 ? '' : ', '}inputSchema, resultSchema }, async function Tool() { return undefined; });`, + '', + ].join('\n'); +}; const resourceModule = (routeConfig: string): string => [ "import { z } from 'zod';", diff --git a/packages/agent-bundle/tests/route-unit/load-route-module.test.ts b/packages/agent-bundle/tests/route-unit/load-route-module.test.ts index d9c73acc2..b383289f3 100644 --- a/packages/agent-bundle/tests/route-unit/load-route-module.test.ts +++ b/packages/agent-bundle/tests/route-unit/load-route-module.test.ts @@ -17,6 +17,12 @@ const rejection = async (load: Promise): Promise => { throw new Error('The load resolved, so no harness diagnostic was produced.'); }; +const configOf = ( + module: Readonly>, + config: Readonly>, +): Readonly> => + Object.fromEntries(Object.keys(config).map((key) => [key, module[key]])); + /** * `loadRouteModule` (#493) is the supported replacement for a hand-maintained * list of static route imports in a schema-identity suite: the evaluated @@ -35,7 +41,8 @@ describe('loadRouteModule', () => { expect(module.resultSchema).toBeInstanceOf(z.ZodObject); expect(typeof module.default).toBe('function'); // The static config the compiler extracted into the manifest is the module's. - expect(module.config).toEqual(testManifest().routes['tool:harness/catalog']!.config); + const config = testManifest().routes['tool:harness/catalog']!.config; + expect(configOf(module, config)).toEqual(config); }); it('loads every renderable route id the manifest reports, including CLI commands and scripts', async () => { @@ -51,8 +58,8 @@ describe('loadRouteModule', () => { for (const route of loadable) { const module = await loadRouteModule(route.id); expect(typeof module.default, route.id).toBe('function'); - // The compiler records `{}` for a module that exports no static config. - expect(module.config ?? {}, route.id).toEqual(route.config); + expect(route.kind === 'tool' ? configOf(module, route.config) : module.config ?? {}, route.id) + .toEqual(route.config); } const report = await loadRouteModule('cli:report'); @@ -76,7 +83,9 @@ describe('loadRouteModule', () => { loadRouteModule('tool:harness/echo'), loadRouteModule('tool:harness/echo'), ]); - expect(first).toBe(second); + expect(first.default).toBe(second.default); + expect(first.inputSchema).toBe(second.inputSchema); + expect(first.resultSchema).toBe(second.resultSchema); // The rendered document's value parses through the very schema the loaded // module exports, so the harness and the consumer agree on one contract. diff --git a/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts b/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts index f932219c6..58a391647 100644 --- a/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts +++ b/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts @@ -38,13 +38,13 @@ it('inspects the Workbench surface of a project with a rendered skill under the 'src/mcp/demo/tools/ping.tsx': [ "import React from 'react';", "import { Agent } from '@agent-bundle/runtime';", + "import { defineTool } from 'agent-bundle/routes';", "import { z } from 'zod';", - "export const config = { annotations: { readOnlyHint: true }, description: 'Ping.' };", 'export const inputSchema = z.object({}).strict();', 'export const resultSchema = z.object({ ok: z.literal(true) }).strict();', - 'export default async function Ping() {', + "export default defineTool({ annotations: { readOnlyHint: true }, description: 'Ping.', inputSchema, resultSchema }, async () => {", ' return pong;', - '}', + '});', '', ].join('\n'), 'src/skills/demo/SKILL.tsx': [ diff --git a/packages/agent-bundle/tests/rule-config.test.ts b/packages/agent-bundle/tests/rule-config.test.ts index f0e735b0a..85b8b0ebe 100644 --- a/packages/agent-bundle/tests/rule-config.test.ts +++ b/packages/agent-bundle/tests/rule-config.test.ts @@ -21,7 +21,7 @@ const loadedProject = ( targets: readonly string[], ): LoadedConfig => ({ config: { - plugin: { name: 'rule-fixture', version: '1.0.0' }, + plugin: { name: 'rule-fixture' }, targets: [...targets], }, configPath: join(root, 'agent-bundle.config.ts'), @@ -40,7 +40,7 @@ const withProject = async ( try { await writeFile( join(root, 'agent-bundle.config.ts'), - "export default { plugin: { name: 'rule-fixture', version: '1.0.0' } };\n", + "export default { plugin: { name: 'rule-fixture' } };\n", ); await run(root); } finally { @@ -184,7 +184,7 @@ it('discovers flat non-ignored rules deterministically and omits the collection writeFile(join(root, 'src', 'rules', 'ignored.mdc'), '# Ignored\n'), ]); const config: AgentBundleConfig = { - plugin: { name: 'rule-fixture', version: '1.0.0' }, + plugin: { name: 'rule-fixture' }, }; const discovered = await discoverProject(root, config); diff --git a/packages/agent-bundle/tests/skill-document-service.test.ts b/packages/agent-bundle/tests/skill-document-service.test.ts index 01c4280b4..6f57cb204 100644 --- a/packages/agent-bundle/tests/skill-document-service.test.ts +++ b/packages/agent-bundle/tests/skill-document-service.test.ts @@ -29,7 +29,7 @@ class TrackingEpochStore extends EpochStore { const createProject = async (): Promise => (await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'skill-document-fixture', version: '1.0.0' },", + " plugin: { name: 'skill-document-fixture' },", " targets: ['portable'],", '};', '', diff --git a/packages/agent-bundle/tests/skill-ir.test.ts b/packages/agent-bundle/tests/skill-ir.test.ts index 0e23e5171..fd25a1db5 100644 --- a/packages/agent-bundle/tests/skill-ir.test.ts +++ b/packages/agent-bundle/tests/skill-ir.test.ts @@ -77,7 +77,7 @@ const loadedProject = (config: AgentBundleConfig, root: string): LoadedConfig => }); const pluginConfig = (targets: readonly string[]): AgentBundleConfig => ({ - plugin: { name: 'skill-ir', version: '0.0.0' }, + plugin: { name: 'skill-ir' }, targets: [...targets], }); diff --git a/packages/agent-bundle/tests/support/dev-contract-project.ts b/packages/agent-bundle/tests/support/dev-contract-project.ts index 632006954..9a2e979c3 100644 --- a/packages/agent-bundle/tests/support/dev-contract-project.ts +++ b/packages/agent-bundle/tests/support/dev-contract-project.ts @@ -68,7 +68,7 @@ export const writeDevContractProject = async ( ...(options.contracts ? [` dev: { contracts: { fixtures: './contract-fixtures.ts', server: ${JSON.stringify(DEV_CONTRACT_SERVER)} } },`] : []), - " plugin: { name: 'dev-contract-gate', version: '1.0.0' },", + " plugin: { name: 'dev-contract-gate' },", ' routes: { mcpCommands: true },', " targets: ['portable'],", '};', diff --git a/packages/agent-bundle/tests/support/eval-project.ts b/packages/agent-bundle/tests/support/eval-project.ts index f465aae07..79365483e 100644 --- a/packages/agent-bundle/tests/support/eval-project.ts +++ b/packages/agent-bundle/tests/support/eval-project.ts @@ -117,7 +117,7 @@ export const seedEvalProject = async ( ? [" evals: { semanticGrader: { harness: 'claude', model: 'claude-sonnet-4-5' } },"] : []), ...(options.marketplace === true ? [' marketplace: true,'] : []), - " plugin: { name: 'review', version: '1.0.0' },", + " plugin: { name: 'review' },", " skills: ['src/skills/review'],", ` targets: ${JSON.stringify(options.targets ?? ['portable'])},`, '});', diff --git a/packages/agent-bundle/tests/support/mcp-conformance.ts b/packages/agent-bundle/tests/support/mcp-conformance.ts index f7a1b4ce4..573f176ac 100644 --- a/packages/agent-bundle/tests/support/mcp-conformance.ts +++ b/packages/agent-bundle/tests/support/mcp-conformance.ts @@ -333,7 +333,7 @@ export const runMcpConformance = async (): Promise => { rm(join(project, 'src/state.ts'), { force: true }), writeFile(join(project, 'agent-bundle.config.ts'), [ 'export default {', - " plugin: { name: 'route-harness', version: '1.0.0' },", + " plugin: { name: 'route-harness' },", ' routes: { mcpCommands: true },', " targets: ['claude'],", '};', diff --git a/packages/agent-bundle/tests/target-hook-contract.test.ts b/packages/agent-bundle/tests/target-hook-contract.test.ts index f3f349b73..b86b78623 100644 --- a/packages/agent-bundle/tests/target-hook-contract.test.ts +++ b/packages/agent-bundle/tests/target-hook-contract.test.ts @@ -571,7 +571,7 @@ it('keeps a mismatched native selector visible so normalized planning fails clos }; const config = { hooks: { beforeTool: { handler: './hooks/guard.ts', tools: ['claude:WebSearch'] } }, - plugin: { name: 'native-selector-fixture', version: '1.0.0' }, + plugin: { name: 'native-selector-fixture' }, targets: ['codex'], } satisfies AgentBundleConfig; const model = await normalizeProject({ diff --git a/packages/agent-bundle/tests/trace-dev-server.test.ts b/packages/agent-bundle/tests/trace-dev-server.test.ts index cd3aaf35f..63d232bb2 100644 --- a/packages/agent-bundle/tests/trace-dev-server.test.ts +++ b/packages/agent-bundle/tests/trace-dev-server.test.ts @@ -32,7 +32,7 @@ it('serves replay and live trace entries and lowers build failures', { timeout: const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'trace-dev-server', version: '1.0.0' },", + " plugin: { name: 'trace-dev-server' },", " targets: ['claude'],", '};', '', @@ -51,17 +51,20 @@ it('serves replay and live trace entries and lowers build failures', { timeout: '', ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - 'export const inputSchema = z.object({}).strict();', - 'export const resultSchema = z.object({ ready: z.boolean() }).strict();', - 'export default async function Report() {', - " return createElement(Agent.Text, null, 'Ready.');", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({}).strict();", + 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ ready: z.boolean() }).strict();", + 'src/mcp/status/tools/report.tsx': "export default defineTool({", + 'src/mcp/status/tools/report.tsx': " inputSchema,", + 'src/mcp/status/tools/report.tsx': " resultSchema,", + 'src/mcp/status/tools/report.tsx': "}, async () => {", + 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Text, null, 'Ready.');", + 'src/mcp/status/tools/report.tsx': "});", + 'src/mcp/status/tools/report.tsx': ].join('\n'), }, prefix: 'agent-bundle-trace-dev-server-', }); diff --git a/packages/agent-bundle/tests/web-config.test.ts b/packages/agent-bundle/tests/web-config.test.ts index 7116258c2..f2e966a93 100644 --- a/packages/agent-bundle/tests/web-config.test.ts +++ b/packages/agent-bundle/tests/web-config.test.ts @@ -114,7 +114,7 @@ const baseConfig = (web: AgentBundleConfig['web']): AgentBundleConfig => ({ }, }, }, - plugin: { name: 'catalog-tools', version: '1.0.0' }, + plugin: { name: 'catalog-tools' }, web, }); @@ -227,7 +227,7 @@ it('skips static tool validation for hand-written MCP server factories', async ( }, }, }, - plugin: { name: 'catalog-tools', version: '1.0.0' }, + plugin: { name: 'catalog-tools' }, web: { apps: [{ app: 'catalog/details', tool: 'runtime-tool' }] }, }; const { diagnostics } = await webDiagnostics(config, discovered({ generated: false })); diff --git a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts index 1485fc07e..e0850a0f9 100644 --- a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts +++ b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts @@ -21,7 +21,7 @@ it('matches the route manifest and lifecycle inventory a real dev server serves' const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'workbench-surface-dev-server', version: '1.0.0' },", + " plugin: { name: 'workbench-surface-dev-server' },", " targets: ['claude'],", '};', '', @@ -52,19 +52,22 @@ it('matches the route manifest and lifecycle inventory a real dev server serves' '', ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - "import { Agent } from '@agent-bundle/runtime';", - "import { createElement } from 'react';", - "import { z } from 'zod';", - '', - "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.' };", - "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", - 'export const resultSchema = z.object({ service: z.string() }).strict();', - '', - 'export default async function Report({ input }) {', - " return createElement(Agent.Result, { value: { service: input.service } }, createElement(Agent.Text, null, input.service));", - '}', - '', - ].join('\n'), + 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", + 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", + 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", + 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", + 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ service: z.string() }).strict();", + 'src/mcp/status/tools/report.tsx': "", + 'src/mcp/status/tools/report.tsx': "export default defineTool({", + 'src/mcp/status/tools/report.tsx': "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.',", + 'src/mcp/status/tools/report.tsx': " inputSchema,", + 'src/mcp/status/tools/report.tsx': " resultSchema,", + 'src/mcp/status/tools/report.tsx': "}, async (input) => {", + 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value: { service: input.service } }, createElement(Agent.Text, null, input.service));", + 'src/mcp/status/tools/report.tsx': "});", + 'src/mcp/status/tools/report.tsx': ].join('\n'), 'src/providers/clock.ts': [ 'export default () => ({ now: 0 });', '', diff --git a/packages/agent-bundle/tests/workbench-surface.test.ts b/packages/agent-bundle/tests/workbench-surface.test.ts index 9b2b5c5fb..599e7de36 100644 --- a/packages/agent-bundle/tests/workbench-surface.test.ts +++ b/packages/agent-bundle/tests/workbench-surface.test.ts @@ -255,7 +255,7 @@ describe('capability counts', () => { const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'workbench-surface-counts', version: '1.0.0' },", + " plugin: { name: 'workbench-surface-counts' },", " targets: ['claude', 'codex'],", " hooks: { PostToolUse: [{ handler: 'src/hooks/audit.ts', targets: [] }] },", ' scripts: {', @@ -294,7 +294,7 @@ describe('capability counts', () => { const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'workbench-surface-prebuilt-hooks', version: '1.0.0' },", + " plugin: { name: 'workbench-surface-prebuilt-hooks' },", " targets: ['claude', 'codex'],", " payload: { runtime: './built/runtime' },", ' hooks: { afterTool: [', @@ -321,7 +321,7 @@ describe('capability counts', () => { const prebuiltOnly = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'workbench-surface-prebuilt-only', version: '1.0.0' },", + " plugin: { name: 'workbench-surface-prebuilt-only' },", " targets: ['claude'],", " payload: { runtime: './built/runtime' },", " hooks: { afterTool: [{ args: ['--host', 'claude'], handler: { prebuilt: './built/runtime/hook.js' }, targets: ['claude'], tools: ['file.write'] }] },", @@ -351,7 +351,7 @@ describe('capability counts', () => { const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'workbench-surface-nothing-emitted', version: '1.0.0' },", + " plugin: { name: 'workbench-surface-nothing-emitted' },", " targets: ['claude'],", " hooks: { PostToolUse: [{ handler: 'src/hooks/audit.ts', targets: [] }] },", " scripts: { nowhere: { entry: 'src/tools/nowhere.ts', targets: [] } },", @@ -388,7 +388,7 @@ describe('preparation parity with the Workbench server', () => { const suiteModule = resolve(import.meta.dirname, '../src/eval/suite.ts'); const configFactory = (name: string, evalsDir: string): string => [ 'export default (context) => ({', - ` plugin: { name: ${JSON.stringify(name)}, version: '1.0.0' },`, + ` plugin: { name: ${JSON.stringify(name)} },`, // Production selects claude alone; the Workbench (development) sees both. " targets: context.mode === 'development' ? ['claude', 'codex'] : ['claude'],", ` evals: { include: [${JSON.stringify(`${evalsDir}/**/*.eval.ts`)}] },`, @@ -457,7 +457,7 @@ describe('an unusable project', () => { const project = await createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'workbench-surface-invalid', version: '1.0.0' },", + " plugin: { name: 'workbench-surface-invalid' },", " targets: ['claude', 'no-such-host'],", '};', '', diff --git a/packages/rsc-markdown-stream/src/resolve.js b/packages/rsc-markdown-stream/src/resolve.js index 8b21eae1e..b7b44de9b 100644 --- a/packages/rsc-markdown-stream/src/resolve.js +++ b/packages/rsc-markdown-stream/src/resolve.js @@ -5,10 +5,7 @@ import * as React from 'react'; -const ELEMENT_TYPES = new Set([ - Symbol.for('react.transitional.element'), // React 19 - Symbol.for('react.element'), // React <= 18 -]); +const ELEMENT_TYPE = Symbol.for('react.transitional.element'); const FRAGMENT = Symbol.for('react.fragment'); const STRICT_MODE = Symbol.for('react.strict_mode'); @@ -19,7 +16,6 @@ const LAZY = Symbol.for('react.lazy'); const MEMO = Symbol.for('react.memo'); const FORWARD_REF = Symbol.for('react.forward_ref'); const CONTEXT = Symbol.for('react.context'); -const PROVIDER = Symbol.for('react.provider'); // React <= 18 const CONSUMER = Symbol.for('react.consumer'); // React 19 const ACTIVITY = Symbol.for('react.activity'); // React 19.2 const VIEW_TRANSITION = Symbol.for('react.view_transition'); @@ -30,16 +26,15 @@ const DROPPED_TAGS = new Set([ 'script', 'style', 'template', 'noscript', 'head', 'title', 'meta', 'link', 'base', ]); -// Which internals key exists depends on the react build: client (React 19), -// react-server (React 19, e.g. inside an RSC server module graph), or legacy -// (React <= 18). Located with computed access because a static namespace +// Which internals key exists depends on the React 19 build: client or +// react-server (inside an RSC server module graph). Located with computed +// access because a static namespace // property access on a missing export is an ESM linking error under strict // bundlers (rspack/webpack) when the react-server condition selects a build // without the client export. const internalsKey = [ '__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE', '__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE', - '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED', ].find((key) => React[key] !== undefined && React[key] !== null); const ReactSharedInternals = internalsKey === undefined ? null : React[internalsKey]; @@ -224,7 +219,7 @@ export async function* resolveStream(node, ctx = null, config = null) { return; } if (typeof node === 'object') { - if (ELEMENT_TYPES.has(node.$$typeof)) { + if (node.$$typeof === ELEMENT_TYPE) { yield* resolveElement(node.type, node.props, ctx, config); return; } @@ -315,19 +310,7 @@ async function* resolveElement(type, props, ctx, config) { config, ); return; - case PROVIDER: - yield* resolveStream( - props.children, - { context: type._context, value: props.value, parent: ctx }, - config, - ); - return; case CONTEXT: - if (typeof props.children === 'function') { - // React <= 18 renders the context object directly. - yield* resolveStream(props.children(readContextValue(type, ctx)), ctx, config); - return; - } // React 19: the context object itself is the provider. yield* resolveStream( props.children, diff --git a/packages/rsc-runtime/README.md b/packages/rsc-runtime/README.md index a6190ca3b..04746edc8 100644 --- a/packages/rsc-runtime/README.md +++ b/packages/rsc-runtime/README.md @@ -24,8 +24,7 @@ the other — shell/replace content stays internal, and the request resolves to one final `CallToolResult`. Image, audio, and resource blocks are capability-gated — unsupported rich content uses a declared fallback or a typed `McpProjectionError`, never a silent drop. The existing -`lowerMcpResult` / `lowerHookResult` helpers remain synchronous compatibility -APIs for the operations-model path. +`documentToCallToolResult` helper projects an already-complete Agent Document. Task-augmented tool calls (`CreateTaskResult`, `tasks/get`, `tasks/result`, `tasks/cancel`, `tasks/list` — the MCP 2025-11-25 Tasks utility) are served by @@ -35,27 +34,16 @@ what feeds a task's `tasks/get` status as well as `notifications/progress`. See [MCP conformance evidence](https://github.com/ScriptedAlchemy/agent-bundle/blob/main/docs/mcp-conformance.md#task-augmented-requests-served-2026-09-04). -```tsx -import { Mcp, lowerMcpResult } from '@agent-bundle/runtime'; - -const result = lowerMcpResult( - - Ready. - , -); -``` - The package also exports the protocol-oriented `Agent.*` vocabulary and the versioned `AgentDocument`/`AgentRenderEvent` contracts. `createAgentDocument` detaches and freezes a v1 document, enforcing finite depth, node-count, and byte limits. `createAgentRenderEventSequence` assigns monotonic sequence numbers to `shell | progress | replace | error | complete`, applies event bounds, and rejects post-completion writes with a typed `handoff-required` -error. These contracts land beside the existing `Hook`/`Mcp` lowerers; those -synchronous compatibility APIs remain operative. +error. -The package exports `Hook`, `Mcp`, `Agent`, both lowerers, the request-store -APIs, the Agent Document contracts, `createAgentRenderDispatcher`, +The package exports `Agent`, the request-store APIs, the Agent Document +contracts, `createAgentRenderDispatcher`, `projectMcpRenderStream`, `createWarmFlightHost`, `decodeAgentFlightStream`, and the `@agent-bundle/runtime/flight/server` render entry. Rich Markdown authoring rides `rsc-markdown-stream`, the Markdown renderer published from @@ -183,7 +171,7 @@ integration. ## Notices (optional) `@agent-bundle/runtime/notices` is the narrow recipient-scoped notice core. -It stores detached, finite `AgentDocumentSnapshot` content in one ordinary +It stores detached, finite `AgentDocument` content in one ordinary state-kernel definition; host wiring opens that definition with the workspace-durable SQLite driver and passes the resulting ledger as `runAgentRequest({ noticeLedger })`. Stateless projects import neither @@ -304,7 +292,7 @@ unavailable outcome when none is supported; it never fabricates a channel. ### Redaction (#99 acceptance item 7) -A notice's free text lives only in its detached `AgentDocumentSnapshot` — +A notice's free text lives only in its detached `AgentDocument` — `text`, `markdown`, `context`, `progress.message`, `error.message`, `resource.name`/`uri`, and every string inside `json.value`, `result.metadata`, and the document `value`. Recipient, priority, dedupe key, timestamps, and diff --git a/packages/rsc-runtime/src/agent-document.ts b/packages/rsc-runtime/src/agent-document.ts index 4ad5bef43..a837a4c56 100644 --- a/packages/rsc-runtime/src/agent-document.ts +++ b/packages/rsc-runtime/src/agent-document.ts @@ -88,8 +88,6 @@ export interface AgentDocument { readonly version: typeof AGENT_DOCUMENT_VERSION; } -export type AgentDocumentSnapshot = AgentDocument; - export interface AgentRenderError { readonly code: string; readonly data?: JsonValue; @@ -97,7 +95,7 @@ export interface AgentRenderError { } export type AgentRenderEvent = - | { readonly document: AgentDocumentSnapshot; readonly sequence: number; readonly type: 'shell' } + | { readonly document: AgentDocument; readonly sequence: number; readonly type: 'shell' } | { readonly completed: number; readonly message?: string; @@ -107,7 +105,7 @@ export type AgentRenderEvent = } | { readonly boundaryId: string; - readonly document: AgentDocumentSnapshot; + readonly document: AgentDocument; readonly sequence: number; readonly type: 'replace'; } @@ -117,7 +115,7 @@ export type AgentRenderEvent = readonly sequence: number; readonly type: 'error'; } - | { readonly document: AgentDocumentSnapshot; readonly sequence: number; readonly type: 'complete' }; + | { readonly document: AgentDocument; readonly sequence: number; readonly type: 'complete' }; export type AgentRenderEventInput = | { readonly document: AgentDocument; readonly type: 'shell' } diff --git a/packages/rsc-runtime/src/elements.ts b/packages/rsc-runtime/src/elements.ts index b05583b90..73ee5c296 100644 --- a/packages/rsc-runtime/src/elements.ts +++ b/packages/rsc-runtime/src/elements.ts @@ -117,59 +117,3 @@ export const Agent = Object.freeze({ Result: AgentResult, Text: AgentText, }); - -const Result = ({ children }: PropsWithChildren): ReactElement => - createElement('agent-hook-result', null, children); - -const AdditionalContext = ({ children }: PropsWithChildren): ReactElement => - createElement('agent-hook-additional-context', null, children); - -export const Hook = { AdditionalContext, Result }; - -export interface McpResultProps extends PropsWithChildren { - _meta?: unknown; - structuredContent?: unknown; - isError?: boolean; -} - -export interface McpDataProps { - data: string; - mimeType: string; -} - -export interface McpResourceLinkProps { - uri: string; - name: string; - mimeType?: string; -} - -export interface McpEmbeddedResourceProps extends PropsWithChildren { - uri: string; - mimeType?: string; - text?: string; - blob?: string; -} - -const McpResult = ({ _meta, children, isError, structuredContent }: McpResultProps): ReactElement => - createElement('mcp-result', { _meta, isError, structuredContent }, children); - -const McpText = ({ children }: PropsWithChildren): ReactElement => createElement('mcp-text', null, children); - -const McpImage = ({ data, mimeType }: McpDataProps): ReactElement => createElement('mcp-image', { data, mimeType }); - -const McpAudio = ({ data, mimeType }: McpDataProps): ReactElement => createElement('mcp-audio', { data, mimeType }); - -const McpResourceLink = ({ mimeType, name, uri }: McpResourceLinkProps): ReactElement => - createElement('mcp-resource-link', { mimeType, name, uri }); - -const McpEmbeddedResource = ({ blob, children, mimeType, text, uri }: McpEmbeddedResourceProps): ReactElement => - createElement('mcp-embedded-resource', { blob, mimeType, text, uri }, children); - -export const Mcp = { - Audio: McpAudio, - EmbeddedResource: McpEmbeddedResource, - Image: McpImage, - ResourceLink: McpResourceLink, - Result: McpResult, - Text: McpText, -}; diff --git a/packages/rsc-runtime/src/index.ts b/packages/rsc-runtime/src/index.ts index ad7caa984..13ebee3e2 100644 --- a/packages/rsc-runtime/src/index.ts +++ b/packages/rsc-runtime/src/index.ts @@ -1,4 +1,4 @@ -export { Agent, Hook, Mcp } from './elements.js'; +export { Agent } from './elements.js'; export type { AgentErrorProps, AgentJsonProps, @@ -10,10 +10,6 @@ export type { AgentResourceProps, AgentResultProps, AgentTextProps, - McpDataProps, - McpEmbeddedResourceProps, - McpResourceLinkProps, - McpResultProps, } from './elements.js'; export { AGENT_DOCUMENT_VERSION, @@ -28,7 +24,6 @@ export type { AgentContextNode, AgentDocument, AgentDocumentNode, - AgentDocumentSnapshot, AgentDocumentStatus, AgentErrorNode, AgentImageNode, @@ -91,11 +86,7 @@ export type { MarkdownSerializer, MarkdownSerializerHelpers, } from './markdown-content.js'; -export { lowerHookResult } from './lower-hook.js'; -export type { NativePostToolUseOutput } from './lower-hook.js'; -export { lowerMcpResult } from './lower-mcp.js'; export type { JsonObject, JsonValue, McpCallToolResult, McpContentBlock } from './lower-mcp.js'; -export { createRscRequestContext } from './request-context.js'; export type { AgentRenderInvocation } from './agent-request.js'; // Registry-free lineage helpers: what a payload proves on its own. The // registry itself ships behind the './lineage' subpath with the state kernel. @@ -113,5 +104,4 @@ export type { AgentNoticesHandle, AgentRecipient, } from './notices/contract.js'; -export type { RscRequestContext } from './request-context.js'; export * from './plugin.js'; diff --git a/packages/rsc-runtime/src/lower-hook.ts b/packages/rsc-runtime/src/lower-hook.ts deleted file mode 100644 index 28143bcf6..000000000 --- a/packages/rsc-runtime/src/lower-hook.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Children, isValidElement, type ReactElement, type ReactNode } from 'react'; - -import { Hook } from './elements.js'; - -export interface NativePostToolUseOutput { - hookSpecificOutput: { - additionalContext: string; - hookEventName: 'PostToolUse'; - }; -} - -type AgentElementProps = { children?: ReactNode }; - -const hookComponents = new Set(Object.values(Hook)); - -const isHookComponent = (value: unknown): value is ((props: AgentElementProps) => ReactElement) => - hookComponents.has(value); - -const resolveHookElement = (node: ReactNode): ReactNode => { - let element = node; - while (isValidElement(element) && isHookComponent(element.type)) { - element = element.type(element.props); - } - return element; -}; - -const isAgentElement = (node: ReactNode, name: string): node is React.ReactElement => { - const resolved = resolveHookElement(node); - return isValidElement(resolved) && resolved.type === name; -}; - -const flattenText = (node: ReactNode): string => { - if (typeof node === 'string' || typeof node === 'number') { - return String(node); - } - - if (Array.isArray(node)) { - return node.map(flattenText).join(''); - } - - if (isAgentElement(node, 'agent-hook-result')) { - throw new Error('Hook result contains duplicate roots'); - } - - throw new Error('Hook additional context may contain only string or number children'); -}; - -export const lowerHookResult = (node: ReactNode): NativePostToolUseOutput => { - const roots = Children.toArray(node).map(resolveHookElement); - if (roots.length !== 1 || !isAgentElement(roots[0] as ReactNode, 'agent-hook-result')) { - throw new Error('Expected exactly one agent-hook-result root'); - } - - const result = roots[0] as React.ReactElement; - const contexts = Children.toArray(result.props.children).map(resolveHookElement).map((child) => { - if (isAgentElement(child, 'agent-hook-result')) { - throw new Error('Hook result contains duplicate roots'); - } - - if (!isAgentElement(child, 'agent-hook-additional-context')) { - throw new Error('Hook result may contain only agent-hook-additional-context elements'); - } - - return flattenText(child.props.children); - }); - - if (contexts.length === 0) { - throw new Error('Hook result requires additional context'); - } - - return { - hookSpecificOutput: { - additionalContext: contexts.join(''), - hookEventName: 'PostToolUse', - }, - }; -}; diff --git a/packages/rsc-runtime/src/lower-mcp.ts b/packages/rsc-runtime/src/lower-mcp.ts index 25f0741a1..21ef8fd8e 100644 --- a/packages/rsc-runtime/src/lower-mcp.ts +++ b/packages/rsc-runtime/src/lower-mcp.ts @@ -1,45 +1,6 @@ import { Buffer } from 'node:buffer'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { Children, isValidElement, type ReactElement, type ReactNode } from 'react'; - -type McpElement = { - type: string; - props: Record; -}; - -// Every Mcp.* export is itself a function component, so a single function -// check unwraps both framework and user server components. -const isServerComponent = (value: unknown): value is ((props: Record) => ReactNode) => - typeof value === 'function'; - -const asMcpElement = (node: ReactNode): McpElement => { - let element = node; - while (isValidElement(element) && isServerComponent(element.type)) { - element = element.type(element.props as Record); - } - - if (!isValidElement(element) || typeof element.type !== 'string' || !element.type.startsWith('mcp-')) { - throw new Error('Expected an MCP result element'); - } - - return { props: element.props as Record, type: element.type }; -}; - -const requiredString = (value: unknown, message: string): string => { - if (typeof value !== 'string' || value.trim() === '') { - throw new Error(message); - } - return value; -}; - -const textChild = (children: unknown, message: string): string => { - const values = Children.toArray(children as ReactNode); - if (values.length !== 1 || typeof values[0] !== 'string') { - throw new Error(message); - } - return values[0]; -}; export interface JsonObject { readonly [key: string]: JsonValue; @@ -53,7 +14,7 @@ export type McpContentBlock = CallToolResult['content'][number]; /** * The MCP `CallToolResult` this package emits. The content blocks are the * SDK's own; `_meta` and `structuredContent` are the finite JSON objects the - * lowerers copy through the wire boundary, stated as such rather than as the + * projectors copy through the wire boundary, stated as such rather than as the * SDK's `unknown`. That keeps the value assignable to the `CallToolResult` of * `@modelcontextprotocol/server` 2.x and of the SDK's 1.x line (which types * `structuredContent` as a record) alike. A type alias, not an interface: only @@ -66,7 +27,6 @@ export type McpCallToolResult = { readonly isError?: boolean; readonly structuredContent?: JsonObject; }; - /** Incremental depth / node / byte checks while cloning JSON (Agent Document bounds). */ export interface JsonSnapshotBudget { readonly addBytes: (n: number) => void; @@ -162,18 +122,6 @@ const cloneJsonValue = ( } }; -const jsonRecord = (value: unknown, message: string): JsonObject => { - try { - if (value === null || typeof value !== 'object' || Array.isArray(value)) { - throw new Error('not a plain object'); - } - // cloneJsonValue either throws or returns a clone of the guarded plain object. - return cloneJsonValue(value, new Set(), '') as JsonObject; - } catch (error) { - throw new Error(`${message} (${error instanceof Error ? error.message : String(error)})`, { cause: error }); - } -}; - const deepFreezeJson = (value: JsonValue): JsonValue => { if (typeof value === 'object' && value !== null) { for (const child of Object.values(value)) deepFreezeJson(child); @@ -200,93 +148,3 @@ export const snapshotJsonValue = ( throw new Error(`${message} (${error instanceof Error ? error.message : String(error)})`, { cause: error }); } }; - -/** - * Copies declaration-level metadata through the same JSON wire boundary as - * MCP results and deep-freezes the copy, so frozen definitions cannot be - * mutated through their metadata after the fact. - */ -export const frozenJsonRecord = (value: unknown, message: string): Readonly> => { - const clone = jsonRecord(value, message); - deepFreezeJson(clone); - return clone; -}; - -const lowerContent = (node: ReactNode): McpContentBlock => { - const element = asMcpElement(node); - const { props } = element; - switch (element.type) { - case 'mcp-text': - return { text: textChild(props.children, 'mcp-text requires one text child'), type: 'text' }; - case 'mcp-image': - return { - data: requiredString(props.data, 'mcp-image requires non-empty data and mimeType'), - mimeType: requiredString(props.mimeType, 'mcp-image requires non-empty data and mimeType'), - type: 'image', - }; - case 'mcp-audio': - return { - data: requiredString(props.data, 'mcp-audio requires non-empty data and mimeType'), - mimeType: requiredString(props.mimeType, 'mcp-audio requires non-empty data and mimeType'), - type: 'audio', - }; - case 'mcp-resource-link': { - const mimeType = props.mimeType; - if (mimeType !== undefined && typeof mimeType !== 'string') { - throw new Error('mcp-resource-link mimeType must be a string'); - } - return { - ...(mimeType === undefined ? {} : { mimeType }), - name: requiredString(props.name, 'mcp-resource-link requires non-empty uri and name'), - type: 'resource_link', - uri: requiredString(props.uri, 'mcp-resource-link requires non-empty uri and name'), - }; - } - case 'mcp-embedded-resource': { - const hasText = props.text !== undefined; - const hasTextChild = props.children !== undefined; - const hasBlob = props.blob !== undefined; - if (Number(hasText) + Number(hasTextChild) + Number(hasBlob) !== 1) { - throw new Error('mcp-embedded-resource accepts exactly one text or blob value'); - } - const mimeType = props.mimeType; - if (mimeType !== undefined && typeof mimeType !== 'string') { - throw new Error('mcp-embedded-resource mimeType must be a string'); - } - const resource = { - ...(mimeType === undefined ? {} : { mimeType }), - uri: requiredString(props.uri, 'mcp-embedded-resource requires a non-empty uri'), - ...(hasBlob - ? { blob: requiredString(props.blob, 'mcp-embedded-resource blob must be non-empty') } - : { - text: hasTextChild - ? textChild(props.children, 'mcp-embedded-resource requires one text child') - : requiredString(props.text, 'mcp-embedded-resource text must be non-empty'), - }), - }; - return { resource, type: 'resource' }; - } - case 'mcp-result': - throw new Error('mcp-result may not be nested'); - default: - throw new Error(`Unsupported MCP result element: ${element.type}`); - } -}; - -export const lowerMcpResult = (node: ReactNode): McpCallToolResult => { - const root = asMcpElement(node); - if (root.type !== 'mcp-result') throw new Error('Expected mcp-result as the root element'); - if (root.props.isError !== undefined && typeof root.props.isError !== 'boolean') { - throw new Error('mcp-result isError must be a boolean'); - } - const structuredContent = root.props.structuredContent; - const metadata = root.props._meta; - return { - content: Children.toArray(root.props.children as ReactNode).map(lowerContent), - ...(metadata === undefined ? {} : { _meta: jsonRecord(metadata, 'mcp-result _meta must be JSON-serializable') }), - ...(structuredContent === undefined - ? {} - : { structuredContent: jsonRecord(structuredContent, 'mcp-result structuredContent must be JSON-serializable') }), - ...(root.props.isError === undefined ? {} : { isError: root.props.isError }), - }; -}; diff --git a/packages/rsc-runtime/src/notices/contract.ts b/packages/rsc-runtime/src/notices/contract.ts index 5515f8597..746590c05 100644 --- a/packages/rsc-runtime/src/notices/contract.ts +++ b/packages/rsc-runtime/src/notices/contract.ts @@ -1,4 +1,4 @@ -import type { AgentDocumentSnapshot } from '../agent-document.js'; +import type { AgentDocument } from '../agent-document.js'; import type { AgentActorIdentity, AgentHostIdentity, @@ -181,7 +181,7 @@ export interface AgentNotice { readonly availability?: AgentNoticeAvailability; readonly availabilityReservation?: AgentNoticeAvailabilityReservation; /** The persisted snapshot as authored; routes disclose it per {@link AgentNotice.sensitivity}. */ - readonly content: AgentDocumentSnapshot; + readonly content: AgentDocument; readonly createdAt: string; readonly dedupeKey?: string; readonly expiredAt?: string; @@ -245,7 +245,7 @@ export interface AgentNoticeLedgerSnapshot { } export interface AgentNoticePublishInput { - readonly content: AgentDocumentSnapshot; + readonly content: AgentDocument; readonly dedupeKey?: string; readonly expiresAt?: string; readonly nextAttemptAt?: string; diff --git a/packages/rsc-runtime/src/notices/redaction.ts b/packages/rsc-runtime/src/notices/redaction.ts index a5be4a83a..bbed71509 100644 --- a/packages/rsc-runtime/src/notices/redaction.ts +++ b/packages/rsc-runtime/src/notices/redaction.ts @@ -1,13 +1,13 @@ import { RedactionLimitError, compilePolicy } from 'flare-redact'; import { DEFAULT_AGENT_RENDER_LIMITS } from '../agent-document.js'; -import type { AgentDocumentNode, AgentDocumentSnapshot } from '../agent-document.js'; +import type { AgentDocumentNode, AgentDocument } from '../agent-document.js'; import type { JsonValue } from '../lower-mcp.js'; /** * Notice content redaction (#99 acceptance item 7). * - * A notice's free text lives only in its detached `AgentDocumentSnapshot` + * A notice's free text lives only in its detached `AgentDocument` * (`text`, `markdown`, `context`, `progress.message`, `error.message`, * `resource.name`/`uri`, and every string inside `json.value`, * `result.metadata`, and the document `value`). Recipient, priority, dedupe @@ -158,13 +158,13 @@ const redactNode = (node: AgentDocumentNode): AgentDocumentNode => { * The document a route hands out in place of content it may not disclose: * one text node carrying the mark, with the original status and version. */ -export const noticeRedactionPlaceholder = (snapshot: AgentDocumentSnapshot): AgentDocumentSnapshot => Object.freeze({ +export const noticeRedactionPlaceholder = (snapshot: AgentDocument): AgentDocument => Object.freeze({ root: Object.freeze({ kind: 'text' as const, text: NOTICE_REDACTION_MARK }), status: snapshot.status, version: snapshot.version, }); -const documentBytes = (document: AgentDocumentSnapshot): number => +const documentBytes = (document: AgentDocument): number => new TextEncoder().encode(JSON.stringify(document)).byteLength; /** @@ -177,8 +177,8 @@ const documentBytes = (document: AgentDocumentSnapshot): number => * out oversized: the bound is a promise to hosts, made at publish and kept on * egress. */ -export const redactNoticeDocument = (snapshot: AgentDocumentSnapshot): AgentDocumentSnapshot => { - const redacted: AgentDocumentSnapshot = Object.freeze({ +export const redactNoticeDocument = (snapshot: AgentDocument): AgentDocument => { + const redacted: AgentDocument = Object.freeze({ ...snapshot, root: redactNode(snapshot.root), ...(snapshot.value === undefined ? {} : { value: redactJson(snapshot.value) }), @@ -226,7 +226,7 @@ export const NOTICE_TITLE_MAX_LENGTH = 120; * {@link NOTICE_TITLE_MAX_LENGTH}. A document without prose has no title and * yields an empty string, which title-only routes treat as nothing to show. */ -export const noticeTitle = (snapshot: AgentDocumentSnapshot): string => { +export const noticeTitle = (snapshot: AgentDocument): string => { const prose = firstProse(snapshot.root) ?? ''; const line = prose.split(/\r?\n/u).map((part) => part.trim()).find((part) => part.length > 0) ?? ''; return line.length <= NOTICE_TITLE_MAX_LENGTH ? line : `${line.slice(0, NOTICE_TITLE_MAX_LENGTH - 1)}…`; @@ -255,9 +255,9 @@ export type AgentNoticeDisclosure = * `signal` route carries no content and gets `undefined`. */ export const disclosedNoticeContent = ( - content: AgentDocumentSnapshot, + content: AgentDocument, disclosure: Extract, -): AgentDocumentSnapshot | undefined => { +): AgentDocument | undefined => { switch (disclosure.shape) { case 'body': return disclosure.redacted ? redactNoticeDocument(content) : content; diff --git a/packages/rsc-runtime/src/notices/state.ts b/packages/rsc-runtime/src/notices/state.ts index 4e27d3719..6d4a7b369 100644 --- a/packages/rsc-runtime/src/notices/state.ts +++ b/packages/rsc-runtime/src/notices/state.ts @@ -3,7 +3,6 @@ import { z } from 'zod'; import { createAgentDocument, type AgentDocument, - type AgentDocumentSnapshot, } from '../agent-document.js'; import type { AgentStateDefinition, @@ -90,7 +89,7 @@ const principalSchema = z.object({ workspace: observed(z.object({ root: z.string().min(1) }).strict()), }).strict(); -const documentSchema = z.custom((value) => { +const documentSchema = z.custom((value) => { try { createAgentDocument(value as AgentDocument); return true; diff --git a/packages/rsc-runtime/src/request-context.ts b/packages/rsc-runtime/src/request-context.ts deleted file mode 100644 index bd6981761..000000000 --- a/packages/rsc-runtime/src/request-context.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; - -export interface RscRequestContext { - run(value: T, operation: () => TResult): TResult; - use(): T; -} - -export const createRscRequestContext = (label: string): Readonly> => { - const storage = new AsyncLocalStorage(); - return Object.freeze({ - run(value: T, operation: () => TResult): TResult { - return storage.run(value, operation); - }, - use(): T { - const value = storage.getStore(); - if (value === undefined) { - throw new Error(`${label} used outside a render request`); - } - return value; - }, - }); -}; diff --git a/packages/rsc-runtime/tests/docs-contract.test.ts b/packages/rsc-runtime/tests/docs-contract.test.ts deleted file mode 100644 index e7a789490..000000000 --- a/packages/rsc-runtime/tests/docs-contract.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expect, it } from '@rstest/core'; -import { createElement } from 'react'; -import { Mcp, lowerMcpResult } from '../src/index.js'; - -it('rejects the multi-child `Mcp.Text` the documented template literal avoids', () => { - expect(() => lowerMcpResult(createElement( - Mcp.Result, - null, - createElement(Mcp.Text, null, 'Runtime is ', 'ready', '.'), - ))).toThrow('mcp-text requires one text child'); - }); diff --git a/packages/rsc-runtime/tests/mcp-projector.test.ts b/packages/rsc-runtime/tests/mcp-projector.test.ts index 88a9864b2..96b317fcd 100644 --- a/packages/rsc-runtime/tests/mcp-projector.test.ts +++ b/packages/rsc-runtime/tests/mcp-projector.test.ts @@ -9,6 +9,7 @@ import { type AgentRenderEvent, type McpProgressNotificationParams, } from '../src/index.js'; +import { snapshotJsonValue } from '../src/lower-mcp.js'; const document = (overrides: Partial = {}): AgentDocument => createAgentDocument({ @@ -30,6 +31,19 @@ const eventsOf = (events: readonly AgentRenderEvent[]): ReadableStream { + expect(snapshotJsonValue( + { items: [1, undefined], omitted: undefined }, + 'invalid JSON', + )).toEqual({ items: [1, null] }); + + const cyclic: Record = {}; + cyclic.self = cyclic; + expect(() => snapshotJsonValue(cyclic, 'invalid JSON')).toThrow('cyclic value at self'); + expect(() => snapshotJsonValue({ value: Number.POSITIVE_INFINITY }, 'invalid JSON')) + .toThrow('non-finite number at value'); +}); + describe('projectMcpRenderStream', () => { it('emits notifications/progress only when the caller supplied a progress token', async () => { const sent: McpProgressNotificationParams[] = []; diff --git a/packages/rsc-runtime/tests/notices-redaction.test.ts b/packages/rsc-runtime/tests/notices-redaction.test.ts index 3862a3060..391278264 100644 --- a/packages/rsc-runtime/tests/notices-redaction.test.ts +++ b/packages/rsc-runtime/tests/notices-redaction.test.ts @@ -26,7 +26,7 @@ import { type AgentNoticeSensitivity, type AgentNoticePrincipal, } from '../src/notices/index.js'; -import type { AgentDocumentSnapshot } from '../src/index.js'; +import type { AgentDocument } from '../src/index.js'; import { DEFAULT_AGENT_RENDER_LIMITS, agent, @@ -37,7 +37,7 @@ import { } from '../src/index.js'; import { createMemoryStateDriver } from '../src/state/index.js'; -const document = (text: string): AgentDocumentSnapshot => ({ +const document = (text: string): AgentDocument => ({ root: { kind: 'text' as const, text }, status: 'success' as const, version: 1 as const, @@ -151,7 +151,7 @@ describe('secret pass (flare-redact)', () => { }); it('redacts every prose field of a document and nothing else', () => { - const snapshot: AgentDocumentSnapshot = { + const snapshot: AgentDocument = { root: { children: [ { kind: 'markdown', text: 'password: p4ss' }, diff --git a/packages/rsc-runtime/tests/runtime.test.ts b/packages/rsc-runtime/tests/runtime.test.ts deleted file mode 100644 index b60fc6b40..000000000 --- a/packages/rsc-runtime/tests/runtime.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, expect, it } from '@rstest/core'; -import { createElement } from 'react'; - -import { - Hook, - Mcp, - createRscRequestContext, - lowerHookResult, - lowerMcpResult, -} from '../src/index.js'; - -describe('@agent-bundle/runtime', () => { - it('lowers one Hook result', () => { - expect(lowerHookResult( - createElement(Hook.Result, null, createElement(Hook.AdditionalContext, null, 'ready')), - )).toEqual({ - hookSpecificOutput: { - additionalContext: 'ready', - hookEventName: 'PostToolUse', - }, - }); - }); - - it('lowers one MCP result without sharing mutable structured content', () => { - const structuredContent = { status: 'ready' }; - const result = lowerMcpResult( - createElement(Mcp.Result, { structuredContent }, createElement(Mcp.Text, null, 'ready')), - ); - - structuredContent.status = 'changed'; - expect(result).toEqual({ - content: [{ text: 'ready', type: 'text' }], - structuredContent: { status: 'ready' }, - }); - }); - - it('follows SDK wire semantics for undefined inside structured content and _meta', () => { - // Repro from issue #44: handlers written against SDK serialization leave - // optional fields undefined; JSON.stringify drops them from objects and - // lowers them to null inside arrays. - const structuredContent = { - count: 3, - items: [1, undefined, 'x', { keep: true, nested: undefined }], - note: undefined, - report: { deep: { drop: undefined }, keep: 'y' }, - }; - const result = lowerMcpResult(createElement( - Mcp.Result, - { - _meta: { note: undefined, ui: { resourceUri: 'ui://demo/widget.html', subtitle: undefined } }, - structuredContent, - }, - createElement(Mcp.Text, null, 'ok'), - )); - - expect(result.structuredContent).toEqual(JSON.parse(JSON.stringify(structuredContent))); - expect(JSON.stringify(result.structuredContent)).toBe( - '{"count":3,"items":[1,null,"x",{"keep":true}],"report":{"deep":{},"keep":"y"}}', - ); - expect(Object.hasOwn(result.structuredContent!, 'note')).toBe(false); - expect(result._meta).toEqual({ ui: { resourceUri: 'ui://demo/widget.html' } }); - expect(Object.hasOwn(result._meta!, 'note')).toBe(false); - }); - - it('keeps an all-undefined structured content object as an empty record', () => { - const result = lowerMcpResult(createElement( - Mcp.Result, - { structuredContent: { note: undefined } }, - createElement(Mcp.Text, null, 'ok'), - )); - expect(JSON.stringify(result.structuredContent)).toBe('{}'); - }); - - it('still rejects non-wire JSON shapes and points at the offending key path', () => { - const lower = (structuredContent: unknown) => () => lowerMcpResult( - createElement(Mcp.Result, { structuredContent }, createElement(Mcp.Text, null, 'ok')), - ); - - const cyclic: Record = { name: 'loop' }; - cyclic.self = { inner: cyclic }; - expect(lower(cyclic)).toThrow( - 'mcp-result structuredContent must be JSON-serializable (cyclic value at self.inner)', - ); - expect(lower({ report: { get bad() { return 1; } } })).toThrow( - 'mcp-result structuredContent must be JSON-serializable (non-enumerable or accessor property at report.bad)', - ); - expect(lower({ items: Object.assign([], { 0: 1, 2: 2, length: 3 }) })).toThrow( - 'mcp-result structuredContent must be JSON-serializable (sparse or decorated array at items)', - ); - expect(lower({ callback: () => undefined })).toThrow( - 'mcp-result structuredContent must be JSON-serializable (non-JSON value at callback)', - ); - expect(lower({ ratio: Number.POSITIVE_INFINITY })).toThrow( - 'mcp-result structuredContent must be JSON-serializable (non-finite number at ratio)', - ); - expect(lower({ wrapped: new Map() })).toThrow( - 'mcp-result structuredContent must be JSON-serializable (non-plain object at wrapped)', - ); - }); - - it('resolves synchronous server components around MCP protocol elements', () => { - const Status = ({ value }: { readonly value: string }) => createElement( - Mcp.Result, - { structuredContent: { value } }, - createElement(Mcp.Text, null, value), - ); - - expect(lowerMcpResult(createElement(Status, { value: 'ready' }))).toEqual({ - content: [{ text: 'ready', type: 'text' }], - structuredContent: { value: 'ready' }, - }); - }); - - it('isolates request context across concurrent work', async () => { - const context = createRscRequestContext('test request'); - const barrier = Promise.withResolvers(); - - const first = context.run('a', async () => { - await barrier.promise; - return context.use(); - }); - const second = context.run('b', async () => { - barrier.resolve(); - return context.use(); - }); - - await expect(Promise.all([first, second])).resolves.toEqual(['a', 'b']); - }); - - it('rejects request context access outside a request', () => { - const context = createRscRequestContext('test request'); - expect(() => context.use()).toThrow('test request used outside a render request'); - }); -}); diff --git a/packages/workbench/src/main.tsx b/packages/workbench/src/main.tsx index fe8e44466..25ffa9c7b 100644 --- a/packages/workbench/src/main.tsx +++ b/packages/workbench/src/main.tsx @@ -306,6 +306,7 @@ const Workbench = () => { scheduleRuntimeBootstrap(); }); }; + const unsubscribeEvents = next.subscribeEvents((event) => { runtimeEvents.receive(event); }); void next.connect( (nextStatus) => { if (!mounted) return; @@ -321,7 +322,6 @@ const Workbench = () => { (reason) => { if (mounted) setConnectionError(connectionFailure(reason)); }, - (event) => { runtimeEvents.receive(event); }, ).catch((reason: unknown) => { if (mounted) setConnectionError(connectionFailure(reason)); }); @@ -330,6 +330,7 @@ const Workbench = () => { if (resetRuntimeInstance.current === resetRuntimeBootstrap) resetRuntimeInstance.current = () => undefined; if (runtimeRetry !== undefined) clearTimeout(runtimeRetry); runtimeEvents.close(); + unsubscribeEvents(); unsubscribeConnection(); void (async () => { try { @@ -402,9 +403,9 @@ const Workbench = () => { client: clients.invocationClient, events: { subscribe: (listener) => projectClient.subscribeEvents(listener) }, }), [clients, projectClient]); - const runtimeBackend = useMemo(() => capabilities?.features.runtime === true && runtimeControllerState !== undefined + const runtimeBackend = useMemo(() => capabilities?.runtime === true && runtimeControllerState !== undefined ? createRuntimeBackend({ controller: runtimeControllerState, runtimeClient: clients.runtimeClient }) - : undefined, [capabilities?.features.runtime, clients, runtimeControllerState]); + : undefined, [capabilities?.runtime, clients, runtimeControllerState]); const backends = useMemo(() => Object.freeze( runtimeBackend === undefined ? [devServerBackend] : [runtimeBackend, devServerBackend], ), [devServerBackend, runtimeBackend]); diff --git a/packages/workbench/src/mcp/mcp-page.tsx b/packages/workbench/src/mcp/mcp-page.tsx index f461e34eb..fbedd507d 100644 --- a/packages/workbench/src/mcp/mcp-page.tsx +++ b/packages/workbench/src/mcp/mcp-page.tsx @@ -75,7 +75,7 @@ interface McpPageCommonProps { readonly initialToolPrefill?: McpToolPrefill; /** Absent when the host has no Inspector launcher; the section then offers only the config download. */ readonly inspectorLaunch?: McpPageInspectorLaunch; - readonly onDownloadConfig?: (download: McpConfigDownload) => void; + readonly onDownloadConfig?: (download: McpDownload) => void; readonly onDownloadTrace?: (download: McpDownload) => void; /** The shell router; a frame's lifted `correlationId` links to the unified Trace through it. */ readonly onNavigate?: (location: WorkbenchLocation) => void; @@ -158,11 +158,9 @@ export interface McpPageRuntimeProps extends McpPageCommonProps { readonly source: Extract; } -/** Legacy artifact props remain source-compatible; runtime callers supply only the discriminated source. */ +/** Artifact inspection and runtime sessions are separate Workbench product modes. */ export type McpPageProps = McpPageArtifactProps | McpPageRuntimeProps; -export type McpConfigDownload = McpDownload; - export interface McpProtocolEvidenceProps { readonly ariaLabel: string; readonly onNavigate?: (location: WorkbenchLocation) => void; @@ -988,7 +986,7 @@ const connectionSummary = (connection: McpBrowserSessionModel['connection']): st .join(' · ') || 'Connection negotiated.'; }; -export const mcpConfigDownload = (config: McpSessionInspectorConfig, sessionId: string): McpConfigDownload => ({ +export const mcpConfigDownload = (config: McpSessionInspectorConfig, sessionId: string): McpDownload => ({ blob: new Blob([`${JSON.stringify(config, null, 2)}\n`], { type: 'application/json' }), filename: `mcp-${sessionId}-inspector.json`, }); diff --git a/packages/workbench/src/project-client.ts b/packages/workbench/src/project-client.ts index 9ccf1268f..eb01f6ab8 100644 --- a/packages/workbench/src/project-client.ts +++ b/packages/workbench/src/project-client.ts @@ -360,7 +360,6 @@ export class ProjectClient { #activity = activityFor([]); readonly #activityListeners = new Set(); #eventDrainPromise: Promise | undefined; - #eventListener: ProjectEventListener | undefined; readonly #eventSubscribers = new Set(); #eventQueue: QueuedProjectEvent[] = []; #eventSource: EventSourceLike | undefined; @@ -415,12 +414,10 @@ export class ProjectClient { async connect( listener: (status: ProjectStatus) => void, onError?: ProjectClientErrorListener, - onEvent?: ProjectEventListener, ): Promise { if (this.#closed) throw new ProjectClientError('Workbench client is closed.'); this.#listener = listener; this.#errorListener = onError; - this.#eventListener = onEvent; let snapshot; try { snapshot = await this.#foreground.sessionSnapshot(); @@ -521,7 +518,6 @@ export class ProjectClient { this.#recoveryVersion += 1; this.#statusGeneration += 1; this.#errorListener = undefined; - this.#eventListener = undefined; this.#eventSubscribers.clear(); this.#listener = undefined; } @@ -765,11 +761,7 @@ export class ProjectClient { } #publishEvent(event: ProjectEventMessage): void { - const listeners = [ - ...(this.#eventListener === undefined ? [] : [this.#eventListener]), - ...this.#eventSubscribers, - ]; - for (const listener of listeners) { + for (const listener of this.#eventSubscribers) { try { listener(event); } catch (error) { diff --git a/packages/workbench/src/workbench-capabilities.ts b/packages/workbench/src/workbench-capabilities.ts index f3128c05c..3a3654ed9 100644 --- a/packages/workbench/src/workbench-capabilities.ts +++ b/packages/workbench/src/workbench-capabilities.ts @@ -22,22 +22,6 @@ export interface WorkbenchRouteCatalog { readonly state: RouteCatalogState; } -/** - * Which optional surfaces this build declares. Navigation no longer derives - * from these — the application tree does — but the shell still gates what it - * wires: the runtime backend exists only for a project with a `devRuntime` - * provider, and Advanced sections read the flags for their empty states. - */ -export interface WorkbenchFeatures { - readonly evals: boolean; - readonly hooks: boolean; - readonly mcp: boolean; - /** The foreground owns a development Runtime controller (`ProjectStatus.runtime`). */ - readonly runtime: boolean; - readonly scripts: boolean; - readonly skills: boolean; -} - export interface WorkbenchCapabilities { readonly buildId: string; readonly counts: Readonly<{ @@ -48,9 +32,10 @@ export interface WorkbenchCapabilities { readonly skills: number; readonly targets: number; }>; - readonly features: WorkbenchFeatures; readonly inspection: ArtifactInspection; readonly routes: WorkbenchRouteCatalog; + /** The foreground owns a development Runtime controller (`ProjectStatus.runtime`). */ + readonly runtime: boolean; readonly skillTree: SkillDocumentTree; } @@ -92,26 +77,6 @@ const routeCatalog = async ( } }; -const manifestHas = (manifest: RouteManifest | undefined, select: (manifest: RouteManifest) => number): boolean => - manifest !== undefined && select(manifest) > 0; - -/** - * Feature detection unions the compiled route graph with the artifact catalog: - * a project may declare a surface through either, and neither may hide the other. - */ -const featuresFor = ( - counts: WorkbenchCapabilities['counts'], - routes: WorkbenchRouteCatalog, - runtime: boolean, -): WorkbenchFeatures => Object.freeze({ - evals: counts.evalSuites > 0, - hooks: counts.hooks > 0 || manifestHas(routes.manifest, (manifest) => manifest.events.length), - mcp: counts.mcpServers > 0 || manifestHas(routes.manifest, (manifest) => manifest.servers.length), - runtime, - scripts: counts.scripts > 0 || manifestHas(routes.manifest, (manifest) => manifest.scripts.length), - skills: counts.skills > 0, -}); - /** Composes existing strict route catalogs into one build-scoped Workbench view. */ export const loadWorkbenchCapabilities = async ({ artifactClient, @@ -145,9 +110,9 @@ export const loadWorkbenchCapabilities = async ({ return Object.freeze({ buildId, counts, - features: featuresFor(counts, routes, runtime), inspection, routes, + runtime, skillTree, }); }; diff --git a/packages/workbench/tests/build-status-model.test.ts b/packages/workbench/tests/build-status-model.test.ts index ee3e4fd4d..4da52db60 100644 --- a/packages/workbench/tests/build-status-model.test.ts +++ b/packages/workbench/tests/build-status-model.test.ts @@ -197,7 +197,7 @@ it('lists source, build, contract-gate, catalog, host, and runtime problems, err state: 'stale', }, runtimeDiagnostic: 'AB8200 — provider failed to load', - status: { ...gatedStatus, source: { ...gatedStatus.source, diagnostics: [{ code: 'AB4001', message: 'Description is missing.', severity: 'info', sourcePath: 'agent-bundle.config.ts' }] } }, + status: { ...gatedStatus, source: { ...gatedStatus.source, diagnostics: [{ code: 'AB4002', message: 'Description is missing.', severity: 'info', sourcePath: 'agent-bundle.config.ts' }] } }, tree: treeWith('src/scripts/sync.ts'), }); @@ -208,7 +208,7 @@ it('lists source, build, contract-gate, catalog, host, and runtime problems, err ['error', 'runtime', undefined], ['warning', 'route-catalog', 'AB5101'], ['warning', 'route-catalog', undefined], - ['info', 'source', 'AB4001'], + ['info', 'source', 'AB4002'], ]); const contractFailure = problems[1]!; expect(contractFailure.node).toEqual({ kind: 'tool', name: 'version', server: 'fixture' }); diff --git a/packages/workbench/tests/project-client.test.ts b/packages/workbench/tests/project-client.test.ts index a59a86083..33add0c51 100644 --- a/packages/workbench/tests/project-client.test.ts +++ b/packages/workbench/tests/project-client.test.ts @@ -485,15 +485,12 @@ it('delivers synchronous runtime events once in FIFO order and refreshes after a }), }); - await client.connect( - () => undefined, - (reason) => errors.push(reason), - (event) => { - if (event.type !== 'runtime.event') return; - received.push(event.sequence); - frozen.push(Object.isFrozen(event) && Object.isFrozen(event.payload)); - }, - ); + client.subscribeEvents((event) => { + if (event.type !== 'runtime.event') return; + received.push(event.sequence); + frozen.push(Object.isFrozen(event) && Object.isFrozen(event.payload)); + }); + await client.connect(() => undefined, (reason) => errors.push(reason)); stream.emit('runtime.event', runtimeEvent(8)); stream.emit('runtime.event', runtimeEvent(9)); stream.emit('runtime.event', runtimeEvent(10)); @@ -529,7 +526,7 @@ it('delivers route.invocation events to subscribers without refreshing project s return Response.json({ status: status() }); }), }); - await client.connect(() => undefined, undefined, (event) => received.push(`legacy:${event.type}`)); + await client.connect(() => undefined); client.subscribeEvents((event) => { if (event.type !== 'route.invocation') return; received.push(`${event.type}:${String(event.sequence)}:${event.payload.invocation.id}:${String(Object.isFrozen(event.payload.invocation))}`); @@ -566,8 +563,8 @@ it('delivers route.invocation events to subscribers without refreshing project s await flushEvents(); expect(received).toEqual([ - 'legacy:route.invocation', 'route.invocation:1:inv_1:true', - 'legacy:route.invocation', 'route.invocation:2:inv_2:true', + 'route.invocation:1:inv_1:true', + 'route.invocation:2:inv_2:true', ]); expect(client.lastEventId).toBe(2); expect(requests).toEqual(['/api/project/status']); @@ -586,11 +583,8 @@ it('preserves a synchronous runtime event after replay gap delivery', async () = }), }); - await client.connect( - () => undefined, - undefined, - (event) => received.push(event.type === 'replay.gap' ? event.type : `${event.type}:${event.sequence}`), - ); + client.subscribeEvents((event) => received.push(event.type === 'replay.gap' ? event.type : `${event.type}:${event.sequence}`)); + await client.connect(() => undefined); stream.emit('replay.gap', { data: JSON.stringify({ earliestAvailableSequence: 14, @@ -617,15 +611,12 @@ it('reports a runtime listener exception and still delivers later queued runtime fetch: withForegroundSession(async () => Response.json({ status: status() })), }); - await client.connect( - () => undefined, - (reason) => errors.push(reason), - (event) => { - if (event.type !== 'runtime.event') return; - received.push(event.sequence); - if (event.sequence === 8) throw new Error('listener failure'); - }, - ); + client.subscribeEvents((event) => { + if (event.type !== 'runtime.event') return; + received.push(event.sequence); + if (event.sequence === 8) throw new Error('listener failure'); + }); + await client.connect(() => undefined, (reason) => errors.push(reason)); stream.emit('runtime.event', runtimeEvent(8)); stream.emit('runtime.event', runtimeEvent(9)); await flushEvents(); @@ -644,13 +635,10 @@ it('clears queued runtime delivery and ignores late callbacks after close', asyn fetch: withForegroundSession(async () => Response.json({ status: status() })), }); - await client.connect( - () => undefined, - undefined, - (event) => { - if (event.type === 'runtime.event') received.push(event.sequence); - }, - ); + client.subscribeEvents((event) => { + if (event.type === 'runtime.event') received.push(event.sequence); + }); + await client.connect(() => undefined); stream.emit('runtime.event', runtimeEvent(8)); client.close(); stream.emit('runtime.event', runtimeEvent(9)); @@ -1087,7 +1075,6 @@ it('delivers subscribed replay and live events FIFO, exposing gaps before refres await client.connect( () => statuses.push('status'), (error) => errors.push(error), - (event) => received.push(`legacy:${eventName(event)}`), ); received.length = 0; statuses.length = 0; @@ -1098,9 +1085,9 @@ it('delivers subscribed replay and live events FIFO, exposing gaps before refres await flushEvents(); expect(received).toEqual([ - 'legacy:runtime.event:1', 'first:runtime.event:1', 'third:runtime.event:1', - 'legacy:gap:none', 'first:gap:none', 'third:gap:none', - 'legacy:runtime.event:3', 'first:runtime.event:3', 'third:runtime.event:3', + 'first:runtime.event:1', 'third:runtime.event:1', + 'first:gap:none', 'third:gap:none', + 'first:runtime.event:3', 'third:runtime.event:3', ]); expect(errors).toHaveLength(1); expect(errors[0]).toMatchObject({ message: 'subscriber failure' }); diff --git a/packages/workbench/tests/support/workbench-e2e.ts b/packages/workbench/tests/support/workbench-e2e.ts index 27a5013f0..c4b36e290 100644 --- a/packages/workbench/tests/support/workbench-e2e.ts +++ b/packages/workbench/tests/support/workbench-e2e.ts @@ -35,42 +35,9 @@ export const e2e = test.extend({ } satisfies PlaywrightOptions, }); -/** - * Deleted hash-page names → PR 1 destinations. Callers that still pass - * `'logs'` / `'evals'` / `'mcp'` keep compiling; new suites should pass a - * pathname (`/`, `/advanced/evals`, a `workbenchLeafPath` result). - */ -const legacyPagePath = Object.freeze({ - artifacts: '/advanced/artifact', - comparisons: '/advanced/evals', - discovery: '/advanced/hosts', - evals: '/advanced/evals', - hooks: '/', - hosts: '/advanced/hosts', - lifecycles: '/trace', - logs: '/advanced/logs', - mcp: '/advanced/protocol', - overview: '/', - playground: '/', - routes: '/', - runtime: '/', - skills: '/', -} as const); - -export type WorkbenchLegacyPage = keyof typeof legacyPagePath; - -/** Pathname the shell should show for a primary area or a leftover hash-page name. */ -export const workbenchPathname = (pageOrPath = '/'): string => { - if (pageOrPath.startsWith('/')) return pageOrPath; - return legacyPagePath[pageOrPath as WorkbenchLegacyPage] ?? `/${pageOrPath}`; -}; - -/** - * Canonical Workbench URL. Pathnames are the URL model (`/routes/…`, - * `/advanced/evals`). Hash-only `#page` routing is gone. - */ -export const workbenchUrl = (origin: string, pageOrPath = '/'): string => - new URL(workbenchPathname(pageOrPath), origin.endsWith('/') ? origin : `${origin}/`).href; +/** Canonical Workbench URL using the pathname route model. */ +export const workbenchUrl = (origin: string, path = '/'): string => + new URL(path, origin.endsWith('/') ? origin : `${origin}/`).href; const idleTimeout = 15_000 * timeScale; const buildSettleTimeout = 60_000 * timeScale; diff --git a/packages/workbench/tests/workbench-capabilities.test.ts b/packages/workbench/tests/workbench-capabilities.test.ts index f43010288..803c6750a 100644 --- a/packages/workbench/tests/workbench-capabilities.test.ts +++ b/packages/workbench/tests/workbench-capabilities.test.ts @@ -183,38 +183,27 @@ const clientsFor = ({ }, }); -it('derives the Skills Starter features from its validated catalogs', async () => { +it('derives the Skills Starter counts from its validated catalogs', async () => { const capabilities = await loadWorkbenchCapabilities({ buildId: 'build-a', ...clientsFor({ evalSuites: 1, skills: 1, targets: 3 }), }); - expect(capabilities.features).toEqual({ evals: true, hooks: false, mcp: false, runtime: false, scripts: false, skills: true }); expect(capabilities.counts).toEqual({ evalSuites: 1, hooks: 0, mcpServers: 0, scripts: 0, skills: 1, targets: 3 }); expect(capabilities.routes.state).toBe('current'); expect(capabilities.routes.manifest?.servers).toEqual([]); expect(Object.isFrozen(capabilities)).toBe(true); expect(Object.isFrozen(capabilities.counts)).toBe(true); - expect(Object.isFrozen(capabilities.features)).toBe(true); }); -it('detects hooks and scripts from the artifact catalog without advertising unrelated features', async () => { - const capabilities = await loadWorkbenchCapabilities({ - buildId: 'build-a', - ...clientsFor({ hooks: 1, scripts: 2, targets: 3 }), - }); - - expect(capabilities.features).toEqual({ evals: false, hooks: true, mcp: false, runtime: false, scripts: true, skills: false }); -}); - -it('detects the complete feature set for a full bundle and carries the runtime topology flag', async () => { +it('carries the runtime topology flag', async () => { const capabilities = await loadWorkbenchCapabilities({ buildId: 'build-a', runtime: true, ...clientsFor({ evalSuites: 1, hooks: 1, mcpServers: 1, scripts: 1, skills: 1, targets: 3 }), }); - expect(capabilities.features).toEqual({ evals: true, hooks: true, mcp: true, runtime: true, scripts: true, skills: true }); + expect(capabilities.runtime).toBe(true); expect(capabilities.inspection.epochId).toBe('build-a'); }); @@ -231,7 +220,6 @@ it('detects hooks, MCP, and scripts from the compiled route graph alone', async ...clientsFor({ cliRoutes: 1, events: 1, routeScripts: 2, routeServers: 2 }), }); - expect(capabilities.features).toEqual({ evals: false, hooks: true, mcp: true, runtime: false, scripts: true, skills: false }); expect(capabilities.counts.hooks).toBe(0); expect(capabilities.counts.mcpServers).toBe(0); expect(capabilities.routes.manifest?.servers.map((server) => server.name)).toEqual(['server-0', 'server-1']); @@ -248,7 +236,6 @@ it('reports a manifest compiled from newer source than the published build as st expect(capabilities.routes.state).toBe('stale'); expect(capabilities.routes.manifest).toBeDefined(); - expect(capabilities.features.hooks).toBe(true); }); it('keeps every artifact-derived feature when the manifest route is unavailable', async () => { @@ -258,7 +245,6 @@ it('keeps every artifact-derived feature when the manifest route is unavailable' routeManifestClient: { manifest: async () => { throw new Error('Route manifest is not available.'); } }, }); - expect(capabilities.features).toEqual({ evals: true, hooks: true, mcp: true, runtime: false, scripts: true, skills: true }); expect(capabilities.routes).toEqual({ message: 'Route manifest is not available.', state: 'unavailable' }); expect(applicationTreeSourcesFor(capabilities)).toEqual({ inspection: capabilities.inspection, diff --git a/website/docs/en/examples/audiobook-curator.mdx b/website/docs/en/examples/audiobook-curator.mdx index decbcaa08..dc1dab5bc 100644 --- a/website/docs/en/examples/audiobook-curator.mdx +++ b/website/docs/en/examples/audiobook-curator.mdx @@ -65,8 +65,8 @@ those features, not the build. This example has no hooks. projected commands compile into `dist/bin/audiobook-curator.mjs` for `package.json` `bin`, and `dist/index.js` plus declarations serve `exports`. See [CLI and library package entries](../guide/authoring/package-entries.mdx). -- **`package.json` is the single version source.** The config declares no `plugin.version`; the - resolved version flows into project context, artifact manifests, `inspect` output, dev status, +- **`package.json` is the single version source.** The resolved version flows into project + context, artifact manifests, `inspect` output, dev status, and the `agent-bundle/meta` constant this plugin imports. ## Every command renders its tool's document diff --git a/website/docs/en/guide/authoring/index.mdx b/website/docs/en/guide/authoring/index.mdx index 85317e6f2..a363fe3a0 100644 --- a/website/docs/en/guide/authoring/index.mdx +++ b/website/docs/en/guide/authoring/index.mdx @@ -29,11 +29,9 @@ export default defineConfig({ | `name` | The host-native plugin slug. Never the npm package name. | | `description` | One sentence shown by hosts and written into generated manifests. | | `logo` | Project-relative path to an image copied into host artifacts that support it. | -| `version` | **Deprecated.** The release version belongs in `package.json`. | -`package.json` is authoritative for release identity. A declared `plugin.version` that disagrees -with it reports the `AB4008` warning, and a release build with no version anywhere is refused -outright (`AB4013`) rather than packaging the development fallback. Plugin code reads its own +`package.json` is authoritative for release identity. A release build with no valid package +version is refused outright (`AB4013`) rather than packaging the development fallback. Plugin code reads its own identity from `agent-bundle/meta` instead of a hand-maintained version module. ## Targets diff --git a/website/docs/en/guide/authoring/mcp.mdx b/website/docs/en/guide/authoring/mcp.mdx index ea76b20af..5bbe20fb2 100644 --- a/website/docs/en/guide/authoring/mcp.mdx +++ b/website/docs/en/guide/authoring/mcp.mdx @@ -59,41 +59,7 @@ src/mcp/curator/ The path supplies identity: `src/mcp/curator/tools/status.tsx` is the `status` tool of the `curator` server. No declaration is needed for the server to exist. -### Compatibility: split tool exports - -Existing tools can still export `config`, schemas, and a default Server Component -separately. This is a compatibility format; use `defineTool` for new tools. Resources, prompts, -and apps retain their existing contracts. A legacy tool looks like this: - -```tsx -// src/mcp/curator/tools/status.tsx -import React from 'react'; -import type { ToolConfig, ToolRouteProps } from 'agent-bundle'; -import { Agent, agent } from '@agent-bundle/runtime'; -import { z } from 'zod'; - -export const config = { - annotations: { readOnlyHint: true }, - description: 'Read runtime status.', -} satisfies ToolConfig; -export const inputSchema = z.object({ verbose: z.boolean().optional() }).strict(); -export const resultSchema = z.object({ status: z.literal('ready') }).strict(); - -export default async function Status( - { input, signal }: ToolRouteProps, -) { - if (signal.aborted) throw new DOMException('aborted', 'AbortError'); - if (input.verbose) await agent(); - const result = { status: 'ready' as const }; - return ( - - Runtime is ready. - - ); -} -``` - -The compiler statically reads `config`, imports schemas and implementations only into generated +The compiler statically reads the `defineTool` options, imports schemas and implementations only into generated entries, installs `runAgentRequest`, and derives the real MCP server from the route graph. Each call renders through a warm internal Flight dispatcher and lowers the final Agent Document to legal MCP output. Flight is an implementation transport inside the generated runtime — never a @@ -132,27 +98,6 @@ the generated CLI executable, whichever projection mechanism produced the comman generated MCP server still passes `kind: 'tool'`. See [Project one tool as an idiomatic command](./package-entries.mdx#project-one-tool-as-an-idiomatic-command). -A route may re-export its component and schemas from another module. This is how one tool is -placed on two generated servers when only `config` differs between the placements — an MCP App -`tools/call`, for example, reaches the server that served the widget: - -```tsx -// src/mcp/widgets/tools/status.tsx -import type { ToolConfig } from 'agent-bundle'; - -export const config = { - _meta: { ui: { resourceUri: 'ui://widgets/status.html' } }, - annotations: { readOnlyHint: true }, - description: 'Read runtime status.', -} satisfies ToolConfig; - -export { default, inputSchema, resultSchema } from '../../curator/tools/status.tsx'; -``` - -The route contract check (`AB4810`) checks that the required exports are declared. -The bundler resolves re-exports, and the runtime checks the loaded component is callable. -Both sync and async components are accepted. - `ToolConfig` and `ToolRouteProps` are public types: ```ts twoslash diff --git a/website/docs/en/guide/concepts/architecture.mdx b/website/docs/en/guide/concepts/architecture.mdx index 7f942a737..3730b8cd9 100644 --- a/website/docs/en/guide/concepts/architecture.mdx +++ b/website/docs/en/guide/concepts/architecture.mdx @@ -435,7 +435,7 @@ keeps only the outline. ### Sections the writer emits The document has two parts with two version numbers. The **public -contract** (`manifestVersion: 5`) is what consumers read. The +contract** (`manifestVersion: 6`) is what consumers read. The **compiler record** (`compiler`, `recordVersion: 1`) is the operational account of the run — producer, project digests, provenance, adapter revisions, pinned schemas, validation — and changes without bumping @@ -589,9 +589,8 @@ issue: | [#564](https://github.com/ScriptedAlchemy/agent-bundle/issues/564) | Production `web` surface on that same bridge | Step 3 landed the authoritative `agent-bundle.manifest.json` as version 2; -distribution install pointers advanced it to version 3, and Amp executable -entry pointers advanced it to 4, and the JSON-input CLI projection mode -(`routes.cli.commands[].projection.input`) advances the current contract to `manifestVersion` 5. The field contract is +subsequent executable, distribution, and route-contract additions advanced +the current contract to `manifestVersion` 6. The field contract is [Artifact manifest](../../reference/artifact-manifest.mdx). #592 stays open until the remaining acceptance criteria on that issue land. diff --git a/website/docs/en/guide/distribution/installation.mdx b/website/docs/en/guide/distribution/installation.mdx index 0c4b2d8a1..eaa2a5fb5 100644 --- a/website/docs/en/guide/distribution/installation.mdx +++ b/website/docs/en/guide/distribution/installation.mdx @@ -153,7 +153,7 @@ Every emitted installer — `agent-bundle install ` and the standalone `in shares one replace policy. An identical copy is an `already-installed` no-op. A copy of the **same version whose content hash differs** is replaced automatically, so rebuilding without a version bump no longer needs an uninstall and `rm -rf`. A different version is -refused with `AB7005` unless you pass `--replace` (alias `--force`), and a foreign directory — one +refused with `AB7005` unless you pass `--replace`, and a foreign directory — one this plugin's installer did not place — is refused either way. Cursor and Amp copies carry an install receipt (`.agent-bundle-install.json`: plugin, version, host, content hash, owned files); replacement is in place and touches owned files only, never unowned entries such as legacy or in-place `state/`, and diff --git a/website/docs/en/guide/distribution/validation.mdx b/website/docs/en/guide/distribution/validation.mdx index a1c10a8ab..2e4728f20 100644 --- a/website/docs/en/guide/distribution/validation.mdx +++ b/website/docs/en/guide/distribution/validation.mdx @@ -316,9 +316,9 @@ names or runs, or a prebuilt payload declares in `runtimeDependencies` (`defineP | `AB7014` | A `dependencies`, `optionalDependencies`, or `peerDependencies` field names packages a consumer never needs installed. Use requires one of the three evidence sources above; compiled bundles that inline a package do not count. The diagnostic names any `dist` bundles that inlined an otherwise unused package. It is an error for `dependencies` and `optionalDependencies`, and a warning for `peerDependencies`; optional peers are skipped, and an `optionalDependencies` entry supersedes the same name under `dependencies`. | | `AB7015` | A `dependencies`, `optionalDependencies`, or `peerDependencies` entry a consumer's npm cannot resolve through a registry. Name and specifier are read with `npm-package-arg`, npm's own parser, and come out as one of three kinds: registry (a version, range, dist-tag, or `npm:` alias of one), fetched (a git, GitHub-shorthand, remote-tarball, or path source — npm 12 refuses git and remote fetches by default (`allow-git`, `allow-remote`), and a path never exists on the consumer's disk), or unparseable (a name npm rejects, a scheme it lacks such as `link:`, `portal:`, or a typo, a selector that is neither a range nor a URL-safe dist-tag, an alias of a non-registry target, or an invalid URL — the manifest read itself fails, so this is reported even on an optional peer). A fetched `optionalDependencies` entry warns because npm can continue without it, but escalates to an error when an install script needs the package: its `bin` command appears in command position, a `node_modules//` file is run, or a bare package is preloaded with `-r`/`--require`/`--import`/`--loader` (including through `NODE_OPTIONS`). A peer also present in `dependencies` or `optionalDependencies` is judged by that concrete entry. The canonical root is validated for `npm publish ./dist`, so `workspace:` and `catalog:` are always rejected; packed `bundleDependencies` and installable in-package `file:` sources are not reported. Depend on a published version, or bundle the package and declare it under `devDependencies`. | -A release build also refuses a project with **no** release version at all (`AB4013`), so a -published artifact never carries the `0.0.0-dev.` development fallback. A -declared `plugin.version` that disagrees with `package.json` reports the `AB4008` warning. +A release build also refuses a project whose `package.json` has **no valid release version** +(`AB4013`), so a published artifact never carries the +`0.0.0-dev.` development fallback. ## Next diff --git a/website/docs/en/reference/cli.mdx b/website/docs/en/reference/cli.mdx index 2d492e70f..8bf00d31a 100644 --- a/website/docs/en/reference/cli.mdx +++ b/website/docs/en/reference/cli.mdx @@ -190,7 +190,7 @@ agent-bundle install [--from ] [--scope ] [--mode ] \ | `--from ` | `process.cwd()` | The composite root holding `agent-bundle.manifest.json`. Identity comes from `application` and `projections[host]`; other hosts use `documents.plugin`, while Amp copies only the directory containing `documents.entry`. `AB7001` covers a missing/invalid manifest, projection, required host file, unsafe local name, or missing Claude/Codex marketplace identity. | | `--scope ` | `user` | `user`, `project`, or `local`. Claude accepts all three; Amp accepts `project` and `user` (its system root); Codex and Cursor are user-scoped. Run Amp project-scope installation from the destination project root. | | `--mode ` | `local` | Cursor only. `local` copies the bundle into `~/.cursor/plugins/local/`; `marketplace` stages a committed local marketplace repository under `~/.cursor/agent-bundle/marketplaces/` and prints the Customize → Plugins → "Add Plugins from Local Repository" step that makes Cursor manage the plugin as a marketplace install. | -| `--replace` (alias `--force`) | off | Replace an existing agent-bundle install of this plugin even when its version differs. Without it, an identical copy is an `already-installed` no-op, a same-version copy whose content hash differs is replaced automatically, and a different version is `AB7005`. Foreign directories are always refused (`AB7005`). | +| `--replace` | off | Replace an existing agent-bundle install of this plugin even when its version differs. Without it, an identical copy is an `already-installed` no-op, a same-version copy whose content hash differs is replaced automatically, and a different version is `AB7005`. Foreign directories are always refused (`AB7005`). | The emitted standalone `install.mjs` accepts the same `--replace`. Cursor copies carry an install receipt (`.agent-bundle-install.json`), replacement diff --git a/website/docs/en/reference/configuration.mdx b/website/docs/en/reference/configuration.mdx index 1acb69aac..24b434b15 100644 --- a/website/docs/en/reference/configuration.mdx +++ b/website/docs/en/reference/configuration.mdx @@ -1,5 +1,5 @@ --- -description: 'Field reference for agent-bundle.config.ts: every top-level key, its accepted shapes, defaults, and validation rules, including the web exposure key (AB4341) and the deprecated plugin.version.' +description: 'Field reference for agent-bundle.config.ts: every top-level key, its accepted shapes, defaults, and validation rules, including the web exposure key (AB4341).' --- # Configuration @@ -84,7 +84,6 @@ also compiled for that operation. See | `description` | One sentence written into generated manifests. | | `logo` | Project-relative image path. Missing, non-file, or outside the project reports `AB4012`; missing from the built artifact or escaping the deploy tree reports `AB6025`. | | `metadata` | The shared descriptive layer: `author`, `homepage`, `keywords`, `license`, `repository`. Each field defaults to the same field in `package.json`; a value here overrides it, and `null` shares nothing. A malformed value reports `AB4014`. | -| `version` | **Deprecated.** | ### Shared descriptive metadata @@ -124,11 +123,9 @@ Claude Code's `.claude-plugin/plugin.json` carries no descriptive fields — its admits none — and its `author.name` stays the generated plugin name. The shared values reach Claude Code through the marketplace entry. -`package.json` is authoritative for release identity. Declare the release version only there: -a `plugin.version` that disagrees with `package.json` reports the `AB4008` warning, and a release -build with no version anywhere is refused with `AB4013` rather than shipping the -`0.0.0-dev.` development fallback. The field remains only for compatibility and -will be removed under the normal breaking-change policy. +`package.json` is authoritative for release identity. Declare the release version there. A +release build with no valid `package.json` version is refused with `AB4013` rather than shipping +the `0.0.0-dev.` development fallback. ## targets diff --git a/website/docs/en/reference/targets-artifacts.mdx b/website/docs/en/reference/targets-artifacts.mdx index d289b09e7..7fa619f56 100644 --- a/website/docs/en/reference/targets-artifacts.mdx +++ b/website/docs/en/reference/targets-artifacts.mdx @@ -141,7 +141,7 @@ proof. npm installation does not automatically register a plugin with a host. ## agent-bundle.manifest.json -The current closed artifact contract is **`manifestVersion: 5`**. It is the index of the built +The current closed artifact contract is **`manifestVersion: 6`**. It is the index of the built application, not another file the plugin author maintains. | Section | Responsibility | diff --git a/website/docs/zh/examples/audiobook-curator.mdx b/website/docs/zh/examples/audiobook-curator.mdx index 75f21202d..4ecb67ea6 100644 --- a/website/docs/zh/examples/audiobook-curator.mdx +++ b/website/docs/zh/examples/audiobook-curator.mdx @@ -54,7 +54,7 @@ description: '有声书策展器示例:一个由路由模块、请求上下文 - **包构建归框架所有。** 配置没有声明 `bin`,也没有声明 `scripts`。投影出的命令编译为 `dist/bin/audiobook-curator.mjs` 以服务 `package.json` 的 `bin`,而 `dist/index.js` 加声明文件服务 `exports`。见 [CLI 与库包入口](../guide/authoring/package-entries.mdx)。 -- **`package.json` 是唯一的版本来源。** 配置没有声明 `plugin.version`;解析出的版本会流入项目上下文、 +- **`package.json` 是唯一的版本来源。** 解析出的版本会流入项目上下文、 产物清单、`inspect` 输出、dev 状态,以及这个插件导入的 `agent-bundle/meta` 常量。 ## 每条命令都渲染其工具的文档 diff --git a/website/docs/zh/guide/authoring/index.mdx b/website/docs/zh/guide/authoring/index.mdx index fe866febb..7e04a6e92 100644 --- a/website/docs/zh/guide/authoring/index.mdx +++ b/website/docs/zh/guide/authoring/index.mdx @@ -28,10 +28,9 @@ export default defineConfig({ | `name` | 宿主原生的插件 slug。绝不是 npm 包名。 | | `description` | 一句话说明,宿主会展示它,它也会写入生成的清单。 | | `logo` | 相对项目根目录的图片路径,会复制到支持该字段的宿主产物中。 | -| `version` | **已废弃。** 发布版本号属于 `package.json`。 | -`package.json` 对发布标识具有权威性。声明的 `plugin.version` 与之不一致时会报告 `AB4008` 警告; -而任何地方都没有版本号的发布构建会被直接拒绝(`AB4013`),而不是打包开发期回退值。插件代码通过 +`package.json` 对发布标识具有权威性。没有有效包版本号的发布构建会被直接拒绝(`AB4013`), +而不是打包开发期回退值。插件代码通过 `agent-bundle/meta` 读取自身标识,而不是手工维护一个版本模块。 ## Targets diff --git a/website/docs/zh/guide/authoring/mcp.mdx b/website/docs/zh/guide/authoring/mcp.mdx index eeb7323d0..55b02f094 100644 --- a/website/docs/zh/guide/authoring/mcp.mdx +++ b/website/docs/zh/guide/authoring/mcp.mdx @@ -54,40 +54,7 @@ src/mcp/curator/ 路径提供身份:`src/mcp/curator/tools/status.tsx` 就是 `curator` 服务器的 `status` 工具。服务器的存在 不需要任何声明。 -### 兼容格式:拆分工具导出 - -旧工具仍可分别导出 `config`、schema 和 默认 Server Component。这是兼容格式; -新工具请使用 `defineTool`。资源、提示和应用继续使用各自现有的契约。旧格式示例: - -```tsx -// src/mcp/curator/tools/status.tsx -import React from 'react'; -import type { ToolConfig, ToolRouteProps } from 'agent-bundle'; -import { Agent, agent } from '@agent-bundle/runtime'; -import { z } from 'zod'; - -export const config = { - annotations: { readOnlyHint: true }, - description: 'Read runtime status.', -} satisfies ToolConfig; -export const inputSchema = z.object({ verbose: z.boolean().optional() }).strict(); -export const resultSchema = z.object({ status: z.literal('ready') }).strict(); - -export default async function Status( - { input, signal }: ToolRouteProps, -) { - if (signal.aborted) throw new DOMException('aborted', 'AbortError'); - if (input.verbose) await agent(); - const result = { status: 'ready' as const }; - return ( - - Runtime is ready. - - ); -} -``` - -编译器静态读取 `config`,只把 schema 与实现导入生成的入口,装配 `runAgentRequest`,并从路由图推导出 +编译器静态读取 `defineTool` 选项,只把 schema 与实现导入生成的入口,装配 `runAgentRequest`,并从路由图推导出 真实的 MCP 服务器。每次调用都经由一个常驻的内部 Flight 分发器渲染,并把最终的 Agent Document 降级为 合法的 MCP 输出。Flight 是生成运行时内部的实现传输层——绝不是面向宿主的公开线上协议,原始 Flight 字节也永远不会跨越 MCP 线路。 @@ -115,25 +82,6 @@ draft-07 校验器都接受每一个合法取值。zod 元组(`z.tuple([...])` CLI flag 元数据;`tools/list` 和运行时校验仍使用真实 schema。参见 [输入元数据](./package-entries.mdx#在-mcp-与-cli-之间共享同一份-schema)。 -路由可以从另一个模块重新导出自己的组件与 schema。当同一个工具需要放在两个生成的服务器上、而两处 -只有 `config` 不同时,就用这种写法——例如 MCP App 的 `tools/call` 会到达提供该 widget 的那台服务器: - -```tsx -// src/mcp/widgets/tools/status.tsx -import type { ToolConfig } from 'agent-bundle'; - -export const config = { - _meta: { ui: { resourceUri: 'ui://widgets/status.html' } }, - annotations: { readOnlyHint: true }, - description: 'Read runtime status.', -} satisfies ToolConfig; - -export { default, inputSchema, resultSchema } from '../../curator/tools/status.tsx'; -``` - -路由契约检查(`AB4810`)确认必要的导出已声明。打包器负责解析重新导出, -运行时检查加载的组件是否可调用。同步和异步组件都受支持。 - `ToolConfig` 与 `ToolRouteProps` 都是公开类型: ```ts twoslash diff --git a/website/docs/zh/guide/concepts/architecture.mdx b/website/docs/zh/guide/concepts/architecture.mdx index eb1fc62ca..411980560 100644 --- a/website/docs/zh/guide/concepts/architecture.mdx +++ b/website/docs/zh/guide/concepts/architecture.mdx @@ -379,7 +379,7 @@ ENOENT)或 `invalid`。 ### 写入器发出的分节 -文档分为两部分,各有自己的版本号。**公开契约**(`manifestVersion: 5`) +文档分为两部分,各有自己的版本号。**公开契约**(`manifestVersion: 6`) 是消费者读取的部分。**编译器记录**(`compiler`,`recordVersion: 1`)是 这次运行的操作性账目——生产者、项目摘要、来源、适配器修订、固定的 schema、验证结果——它的变化不会推动 `manifestVersion`;消费者读取方 @@ -516,8 +516,8 @@ flag 元数据来自字面量 `config.inputJsonSchema`;编译后的可执行 | [#594](https://github.com/ScriptedAlchemy/agent-bundle/issues/594) | 生成的类型化 App 客户端 + 共享桥接 | | [#564](https://github.com/ScriptedAlchemy/agent-bundle/issues/564) | 基于同一桥接的生产 `web` 表面 | -第 3 步以版本 2 落地了权威的 `agent-bundle.manifest.json`;分发安装指针将其推进到版本 3, -Amp 可执行入口指针把它推进到 4,JSON 输入的 CLI 投影模式(`routes.cli.commands[].projection.input`)再把当前契约推进到 `manifestVersion` 5。 +第 3 步以版本 2 落地了权威的 `agent-bundle.manifest.json`;后续的可执行、分发与路由契约字段 +把当前契约推进到 `manifestVersion` 6。 字段契约见 [产物清单](../../reference/artifact-manifest.mdx)。#592 会一直保持打开,直到该 issue 上剩余的验收标准落地。 diff --git a/website/docs/zh/guide/distribution/installation.mdx b/website/docs/zh/guide/distribution/installation.mdx index f7b73286e..7ee5720c3 100644 --- a/website/docs/zh/guide/distribution/installation.mdx +++ b/website/docs/zh/guide/distribution/installation.mdx @@ -125,7 +125,7 @@ Amp 不属于开发期安装宿主;请使用其有归属回执的 `agent-bundl 每个输出的安装器——`agent-bundle install ` 与独立的 `install.mjs`——共用同一套替换策略。 内容完全相同的副本是 `already-installed` 空操作。**版本相同但内容哈希不同**的副本会被自动替换,因此不升版本 -地重建不再需要卸载加 `rm -rf`。版本不同则以 `AB7005` 拒绝,除非传入 `--replace`(别名 `--force`);外来目录 +地重建不再需要卸载加 `rm -rf`。版本不同则以 `AB7005` 拒绝,除非传入 `--replace`;外来目录 ——不是本插件安装器放置的——无论如何都会被拒绝。Cursor 与 Amp 副本携带安装回执(`.agent-bundle-install.json`: 插件、版本、宿主、内容哈希、归属文件);替换就地进行,只触碰归属文件,绝不动旧版或就地的 `state/` 之类的非归属条目, `--replace` 会接管回执出现之前的副本。本发行版构建的产物把框架状态放在 diff --git a/website/docs/zh/guide/distribution/validation.mdx b/website/docs/zh/guide/distribution/validation.mdx index 655fdf0ee..00a4bfa69 100644 --- a/website/docs/zh/guide/distribution/validation.mdx +++ b/website/docs/zh/guide/distribution/validation.mdx @@ -272,9 +272,8 @@ CLI、MCP 入口、钩子包装层以及包构建的 JavaScript bundle——都 | `AB7014` | `dependencies`、`optionalDependencies` 或 `peerDependencies` 字段点名了消费者无需安装的包。必须有上列三类证据之一才算使用;已编译 bundle 内联该包不算。诊断会点名内联了这个未使用包的所有 `dist` bundle。对 `dependencies` 与 `optionalDependencies` 是 error,对 `peerDependencies` 是 warning;可选 peer 会跳过,`optionalDependencies` 中的同名项优先于 `dependencies`。 | | `AB7015` | `dependencies`、`optionalDependencies` 或 `peerDependencies` 中的某一项无法被消费者的 npm 经由注册表解析。包名与说明符一并交给 npm 自己的解析器 `npm-package-arg` 读取,结果分为三类:注册表类(版本、范围、dist-tag,或指向它们的 `npm:` 别名)、抓取类(git、GitHub 简写、远程 tarball 或路径来源——npm 12 默认拒绝 git 与远程抓取(`allow-git`、`allow-remote`),而路径在消费者磁盘上并不存在),以及不可解析类(npm 不接受的包名、它不支持的协议如 `link:`、`portal:` 或拼写错误、既非范围也非 URL 安全 dist-tag 的选择器、指向非注册表目标的别名,或非法 URL——清单本身就读不下去,因此即使出现在可选 peer 上也会被报告)。抓取类的 `optionalDependencies` 项只发出警告,因为 npm 可以在没有它时继续;但消费者安装脚本需要该包时会升级为错误:其 `bin` 命令位于命令位置、运行了 `node_modules//` 文件,或通过 `-r`/`--require`/`--import`/`--loader`(包括 `NODE_OPTIONS`)预加载了裸包。若 `dependencies` 或 `optionalDependencies` 也声明了同名 peer,则只按该具体条目判断。规范根目录按 `npm publish ./dist` 校验,因此 `workspace:` 与 `catalog:` 一律拒绝;已打包的 `bundleDependencies` 与包内可安装的 `file:` 来源不会被报告。请依赖已发布的版本,或把该包打包进产物并声明在 `devDependencies` 下。 | -发布构建同样会拒绝**完全没有**发布版本的项目(`AB4013`),因此已发布的产物绝不会携带 -`0.0.0-dev.` 这个开发期回退值。声明的 `plugin.version` 与 `package.json` 不一致时会 -报告 `AB4008` 警告。 +发布构建同样会拒绝 `package.json` 中**没有有效发布版本**的项目(`AB4013`),因此已发布的产物绝不会携带 +`0.0.0-dev.` 这个开发期回退值。 ## 下一步 diff --git a/website/docs/zh/reference/cli.mdx b/website/docs/zh/reference/cli.mdx index b6327b821..9b3cf49d4 100644 --- a/website/docs/zh/reference/cli.mdx +++ b/website/docs/zh/reference/cli.mdx @@ -180,7 +180,7 @@ agent-bundle install [--from ] [--scope ] [--mode ] \ | `--from ` | `process.cwd()` | 存放 `agent-bundle.manifest.json` 的组合根。身份来自 `application` 与 `projections[host]`;其他宿主使用 `documents.plugin`,Amp 只复制 `documents.entry` 所在目录。缺少清单、投影、所需宿主文件、不安全本地名称或 Claude/Codex 市场身份时为 `AB7001`。 | | `--scope ` | `user` | `user`、`project` 或 `local`。Claude 接受全部三种;Amp 接受 `project` 与 `user`(系统根;项目安装应从目标项目根运行);Codex 与 Cursor 是 user 作用域。 | | `--mode ` | `local` | 仅限 Cursor。`local` 把捆绑包复制到 `~/.cursor/plugins/local/`;`marketplace` 在 `~/.cursor/agent-bundle/marketplaces/` 下暂存一个已提交的本地市场仓库,并打印 Customize → Plugins →“Add Plugins from Local Repository”这一步,让 Cursor 把该插件当作市场安装来管理。 | -| `--replace`(别名 `--force`) | 关闭 | 即使版本不同,也替换该插件已有的 agent-bundle 安装。不带它时,内容完全相同的副本是 `already-installed` 空操作,版本相同但内容哈希不同的副本会被自动替换,版本不同则为 `AB7005`。外来目录总是被拒绝(`AB7005`)。 | +| `--replace` | 关闭 | 即使版本不同,也替换该插件已有的 agent-bundle 安装。不带它时,内容完全相同的副本是 `already-installed` 空操作,版本相同但内容哈希不同的副本会被自动替换,版本不同则为 `AB7005`。外来目录总是被拒绝(`AB7005`)。 | 输出的独立 `install.mjs` 接受同样的 `--replace`。Cursor 副本携带安装回执 (`.agent-bundle-install.json`),替换只触碰归属文件,`--replace` 会接管回执出现之前的副本;Claude 的替换 diff --git a/website/docs/zh/reference/configuration.mdx b/website/docs/zh/reference/configuration.mdx index 1c73f0ba4..d3112e2e8 100644 --- a/website/docs/zh/reference/configuration.mdx +++ b/website/docs/zh/reference/configuration.mdx @@ -1,5 +1,5 @@ --- -description: 'agent-bundle.config.ts 字段参考:每个顶层键、可接受的形态、默认值与校验规则,包括 web 暴露键(AB4341)与已废弃的 plugin.version 字段。' +description: 'agent-bundle.config.ts 字段参考:每个顶层键、可接受的形态、默认值与校验规则,包括 web 暴露键(AB4341)。' --- # 配置 @@ -78,7 +78,6 @@ export default defineConfig({ | `description` | 一句话,会写进生成的清单中。 | | `logo` | 项目相对的图片路径。缺失、不是文件或位于项目之外报告 `AB4012`;在已构建产物中缺失或逃逸出部署树报告 `AB6025`。 | | `metadata` | 共享描述层:`author`、`homepage`、`keywords`、`license`、`repository`。每个字段默认取 `package.json` 中的同名字段;此处的值会覆盖它,而 `null` 表示不共享。值格式错误报告 `AB4014`。 | -| `version` | **已废弃。** | ### 共享描述元数据 @@ -112,9 +111,8 @@ export default defineConfig({ Claude Code 的 `.claude-plugin/plugin.json` 不携带描述字段——其固定 schema 不允许——且其 `author.name` 仍是生成的插件名。共享值经由 marketplace 条目到达 Claude Code。 -`package.json` 对发布标识具有权威性。发布版本只在那里声明:与 `package.json` 不一致的 `plugin.version` -会报告 `AB4008` 警告,而任何地方都没有版本的发布构建会被 `AB4013` 拒绝,而不是交付 -`0.0.0-dev.` 这个开发期回退值。该字段仅为兼容而保留,并将按照正常的破坏性变更策略移除。 +`package.json` 对发布标识具有权威性。发布版本只在那里声明。若 `package.json` 没有有效版本, +发布构建会被 `AB4013` 拒绝,而不是交付 `0.0.0-dev.` 这个开发期回退值。 ## targets diff --git a/website/docs/zh/reference/targets-artifacts.mdx b/website/docs/zh/reference/targets-artifacts.mdx index 071468ffb..eca9aab48 100644 --- a/website/docs/zh/reference/targets-artifacts.mdx +++ b/website/docs/zh/reference/targets-artifacts.mdx @@ -136,7 +136,7 @@ npm 安装不会自动将插件注册到宿主。 ## agent-bundle.manifest.json -当前封闭产物契约为 **`manifestVersion: 5`**。 +当前封闭产物契约为 **`manifestVersion: 6`**。 它是构建应用的索引,不是另一份需要插件作者维护的文件。 | 部分 | 职责 | From ce0c289c05a90d49a573b8ccd92f4f035f9ed6fa Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 09:16:24 +0000 Subject: [PATCH 02/11] test: repair migrated route fixtures --- .../tests/build-reproducibility.test.ts | 26 +- .../tests/generated-route-server.test.ts | 528 +++++++++--------- .../tests/provider-typegen.test.ts | 18 +- .../agent-bundle/tests/route-graph.test.ts | 14 +- .../tests/route-invocation-dev-server.test.ts | 392 ++++++------- .../tests/route-invocation-service.test.ts | 56 +- .../tests/route-register-typegen.test.ts | 54 +- .../tests/trace-dev-server.test.ts | 28 +- .../workbench-surface-dev-server.test.ts | 32 +- 9 files changed, 568 insertions(+), 580 deletions(-) diff --git a/packages/agent-bundle/tests/build-reproducibility.test.ts b/packages/agent-bundle/tests/build-reproducibility.test.ts index c7f17bc2c..ca2a1bc3a 100644 --- a/packages/agent-bundle/tests/build-reproducibility.test.ts +++ b/packages/agent-bundle/tests/build-reproducibility.test.ts @@ -50,19 +50,19 @@ const writeProject = async (root: string): Promise => { '', ].join('\n')), writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', [ - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "import { Agent } from '@agent-bundle/runtime';", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "export const inputSchema = z.object({ message: z.string().default(\"ready\") }).strict();", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "export const resultSchema = z.object({ message: z.string() }).strict();", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "export default defineTool({", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.',", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', " inputSchema,", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', " resultSchema,", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "}, async (input) => {", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', " return {`Lookup: ${input.message}`};", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', "});", - writeProjectFile(root, 'src/mcp/harness/tools/lookup.tsx', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { z } from 'zod';", + "export const inputSchema = z.object({ message: z.string().default(\"ready\") }).strict();", + "export const resultSchema = z.object({ message: z.string() }).strict();", + "export default defineTool({", + "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.',", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " return {`Lookup: ${input.message}`};", + "});", + ].join('\n')), writeProjectFile(root, 'src/events/session/start.tsx', [ "import { Agent } from '@agent-bundle/runtime';", "export const config = { targets: ['claude', 'codex', 'cursor'] };", diff --git a/packages/agent-bundle/tests/generated-route-server.test.ts b/packages/agent-bundle/tests/generated-route-server.test.ts index fda89b2b8..cd5e02bff 100644 --- a/packages/agent-bundle/tests/generated-route-server.test.ts +++ b/packages/agent-bundle/tests/generated-route-server.test.ts @@ -113,26 +113,26 @@ it('lists and calls a generated filesystem tool through final-only Flight', { re // contract reads: the build has to select the automatic JSX runtime, or // the emitted module calls a `React` factory that is not in scope. writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', [ - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "import { Agent, agent } from '@agent-bundle/runtime';", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "export const inputSchema = z.object({ source: z.string() }).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "export const resultSchema = z.object({ actor: z.unknown(), host: z.unknown(), invocationKind: z.literal('tool'), lineage: z.unknown(), session: z.unknown(), source: z.string(), workspace: z.unknown() }).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "export default defineTool({", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "annotations: { readOnlyHint: true }, description: 'Inspect one source.',", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " inputSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " resultSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "}, async (input, { signal }) => {", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " if (signal.aborted) throw new DOMException('aborted', 'AbortError');", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " const context = await agent();", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " const result = { actor: context.actor, host: context.host, invocationKind: context.invocation.kind, lineage: context.lineage, session: context.session, source: input.source, workspace: context.workspace };", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " return (", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " ", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " {`Inspected **${input.source}**.`}", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " ", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', " );", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', "});", - writeProjectFile(root, 'src/mcp/curator/tools/inspect.tsx', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { z } from 'zod';", + "export const inputSchema = z.object({ source: z.string() }).strict();", + "export const resultSchema = z.object({ actor: z.unknown(), host: z.unknown(), invocationKind: z.literal('tool'), lineage: z.unknown(), session: z.unknown(), source: z.string(), workspace: z.unknown() }).strict();", + "export default defineTool({", + "annotations: { readOnlyHint: true }, description: 'Inspect one source.',", + " inputSchema,", + " resultSchema,", + "}, async (input, { signal }) => {", + " if (signal.aborted) throw new DOMException('aborted', 'AbortError');", + " const context = await agent();", + " const result = { actor: context.actor, host: context.host, invocationKind: context.invocation.kind, lineage: context.lineage, session: context.session, source: input.source, workspace: context.workspace };", + " return (", + " ", + " {`Inspected **${input.source}**.`}", + " ", + " );", + "});", + ].join('\n')), ]); const output = join(root, 'artifact'); @@ -408,37 +408,37 @@ it('augments a generated server from config and projects result _meta and text-o writeProjectFile(root, 'views/panel.ts', "document.body.textContent = 'Curator panel';\n"), // #383: `Agent.Result metadata` is the result-level `_meta`. writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', [ - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { Agent } from '@agent-bundle/runtime';", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { createElement } from 'react';", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "export const inputSchema = z.object({}).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "export default defineTool({", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "_meta: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, description: 'Annotated result.',", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', " inputSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', " resultSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "}, async () => {", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', " return createElement(Agent.Result, { metadata: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', "});", - writeProjectFile(root, 'src/mcp/curator/tools/annotated.tsx', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + "export default defineTool({", + "_meta: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, description: 'Annotated result.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { metadata: { ui: { resourceUri: 'ui://generated-augment-fixture/panel.html' } }, value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", + "});", + ].join('\n')), // A text-only tool declares no object result, so it advertises no // outputSchema and returns no structuredContent. writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', [ - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { Agent } from '@agent-bundle/runtime';", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { createElement } from 'react';", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "export const inputSchema = z.object({}).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "export const resultSchema = z.undefined();", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "export default defineTool({", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "description: 'Text only.',", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', " inputSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', " resultSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "}, async () => {", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', " return createElement(Agent.Result, null, createElement(Agent.Text, null, 'plain text'));", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', "});", - writeProjectFile(root, 'src/mcp/curator/tools/plain.tsx', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.undefined();", + "export default defineTool({", + "description: 'Text only.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, null, createElement(Agent.Text, null, 'plain text'));", + "});", + ].join('\n')), ]); const output = join(root, 'artifact'); @@ -514,21 +514,21 @@ it('compiles appResourceUri() and local literal references to the App route reso // The tool references the App through the compile-time helper (run-time // import from the light routes subpath, never the compiler root). 'src/mcp/curator/tools/open.tsx': [ - 'src/mcp/curator/tools/open.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/open.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/open.tsx': "import { appResourceUri } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/open.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/open.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/open.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/open.tsx': "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - 'src/mcp/curator/tools/open.tsx': "export default defineTool({", - 'src/mcp/curator/tools/open.tsx': "_meta: { ui: { resourceUri: appResourceUri('dashboard') } }, description: 'Open the dashboard.',", - 'src/mcp/curator/tools/open.tsx': " inputSchema,", - 'src/mcp/curator/tools/open.tsx': " resultSchema,", - 'src/mcp/curator/tools/open.tsx': "}, async () => {", - 'src/mcp/curator/tools/open.tsx': " return createElement(Agent.Result, { value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", - 'src/mcp/curator/tools/open.tsx': "});", - 'src/mcp/curator/tools/open.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { appResourceUri } from 'agent-bundle/routes';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + "export default defineTool({", + "_meta: { ui: { resourceUri: appResourceUri('dashboard') } }, description: 'Open the dashboard.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { value: { status: 'ready' } }, createElement(Agent.Text, null, 'ready'));", + "});", + ].join('\n'), // The resource references the same constant module the App reads. 'src/mcp/curator/resources/catalog.tsx': [ "import { Agent } from '@agent-bundle/runtime';", @@ -694,26 +694,26 @@ it('observes one process-lifetime provider across consecutive generated tool cal '', ].join('\n'), 'src/mcp/curator/tools/warmth.tsx': [ - 'src/mcp/curator/tools/warmth.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/warmth.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/warmth.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/warmth.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/warmth.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/warmth.tsx': "export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();", - 'src/mcp/curator/tools/warmth.tsx': "export default defineTool({", - 'src/mcp/curator/tools/warmth.tsx': "annotations: { readOnlyHint: true }, description: 'Observe process lifetime.',", - 'src/mcp/curator/tools/warmth.tsx': " inputSchema,", - 'src/mcp/curator/tools/warmth.tsx': " resultSchema,", - 'src/mcp/curator/tools/warmth.tsx': "}, async () => {", - 'src/mcp/curator/tools/warmth.tsx': " const context = await agent();", - 'src/mcp/curator/tools/warmth.tsx': " const processLifetime = context.process;", - 'src/mcp/curator/tools/warmth.tsx': " if (processLifetime === undefined || typeof processLifetime !== 'object' || processLifetime === null) {", - 'src/mcp/curator/tools/warmth.tsx': " throw new Error('process-lifetime provider was not installed');", - 'src/mcp/curator/tools/warmth.tsx': " }", - 'src/mcp/curator/tools/warmth.tsx': " const value = processLifetime as { hits: number; instanceId: string; pid: number };", - 'src/mcp/curator/tools/warmth.tsx': " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", - 'src/mcp/curator/tools/warmth.tsx': "});", - 'src/mcp/curator/tools/warmth.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();", + "export default defineTool({", + "annotations: { readOnlyHint: true }, description: 'Observe process lifetime.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " const context = await agent();", + " const processLifetime = context.process;", + " if (processLifetime === undefined || typeof processLifetime !== 'object' || processLifetime === null) {", + " throw new Error('process-lifetime provider was not installed');", + " }", + " const value = processLifetime as { hits: number; instanceId: string; pid: number };", + " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", + "});", + ].join('\n'), }); const session = await connectGeneratedServer(root); try { @@ -764,28 +764,28 @@ it('emits notifications/resources/updated for the durable notice inbox to the su '', ].join('\n'), 'src/mcp/curator/tools/notify.tsx': [ - 'src/mcp/curator/tools/notify.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/notify.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/notify.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/notify.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/notify.tsx': "export const inputSchema = z.object({ host: z.string(), message: z.string() }).strict();", - 'src/mcp/curator/tools/notify.tsx': "export const resultSchema = z.object({ noticeId: z.string(), state: z.literal('pending') }).strict();", - 'src/mcp/curator/tools/notify.tsx': "export default defineTool({", - 'src/mcp/curator/tools/notify.tsx': "description: 'Publish a notice to a host-scoped recipient.',", - 'src/mcp/curator/tools/notify.tsx': " inputSchema,", - 'src/mcp/curator/tools/notify.tsx': " resultSchema,", - 'src/mcp/curator/tools/notify.tsx': "}, async (input) => {", - 'src/mcp/curator/tools/notify.tsx': " const context = await agent();", - 'src/mcp/curator/tools/notify.tsx': " if (context.notices === undefined) throw new TypeError('notices unavailable');", - 'src/mcp/curator/tools/notify.tsx': " const published = await context.notices.publish({", - 'src/mcp/curator/tools/notify.tsx': " content: { root: { kind: 'text', text: input.message }, status: 'success', version: 1 },", - 'src/mcp/curator/tools/notify.tsx': " priority: 'normal',", - 'src/mcp/curator/tools/notify.tsx': " recipient: { host: { name: input.host } },", - 'src/mcp/curator/tools/notify.tsx': " }, { idempotencyKey: `notice:${input.host}:${input.message}` });", - 'src/mcp/curator/tools/notify.tsx': " const value = { noticeId: published.notice.id, state: published.notice.state };", - 'src/mcp/curator/tools/notify.tsx': " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `published ${value.noticeId}`));", - 'src/mcp/curator/tools/notify.tsx': "});", - 'src/mcp/curator/tools/notify.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({ host: z.string(), message: z.string() }).strict();", + "export const resultSchema = z.object({ noticeId: z.string(), state: z.literal('pending') }).strict();", + "export default defineTool({", + "description: 'Publish a notice to a host-scoped recipient.',", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " const context = await agent();", + " if (context.notices === undefined) throw new TypeError('notices unavailable');", + " const published = await context.notices.publish({", + " content: { root: { kind: 'text', text: input.message }, status: 'success', version: 1 },", + " priority: 'normal',", + " recipient: { host: { name: input.host } },", + " }, { idempotencyKey: `notice:${input.host}:${input.message}` });", + " const value = { noticeId: published.notice.id, state: published.notice.state };", + " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `published ${value.noticeId}`));", + "});", + ].join('\n'), }); const inboxUri = 'agent-bundle://notices/inbox'; const session = await connectGeneratedServer(root); @@ -853,23 +853,23 @@ it('emits MCP progress notifications only when a progress token is supplied', { roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/progress.tsx': [ - 'src/mcp/curator/tools/progress.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/progress.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/progress.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/progress.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/progress.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/progress.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", - 'src/mcp/curator/tools/progress.tsx': "export default defineTool({", - 'src/mcp/curator/tools/progress.tsx': "annotations: { readOnlyHint: true }, description: 'Report progress.',", - 'src/mcp/curator/tools/progress.tsx': " inputSchema,", - 'src/mcp/curator/tools/progress.tsx': " resultSchema,", - 'src/mcp/curator/tools/progress.tsx': "}, async () => {", - 'src/mcp/curator/tools/progress.tsx': " const context = await agent();", - 'src/mcp/curator/tools/progress.tsx': " await context.progress.report({ completed: 1, message: 'halfway', total: 2 });", - 'src/mcp/curator/tools/progress.tsx': " await context.progress.report({ completed: 2, message: 'done', total: 2 });", - 'src/mcp/curator/tools/progress.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'finished'));", - 'src/mcp/curator/tools/progress.tsx': "});", - 'src/mcp/curator/tools/progress.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + "export default defineTool({", + "annotations: { readOnlyHint: true }, description: 'Report progress.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " const context = await agent();", + " await context.progress.report({ completed: 1, message: 'halfway', total: 2 });", + " await context.progress.report({ completed: 2, message: 'done', total: 2 });", + " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'finished'));", + "});", + ].join('\n'), }); const session = await connectGeneratedServer(root); const notifications: Array<{ readonly message?: string; readonly progress: number; readonly progressToken: string | number; readonly total?: number }> = []; @@ -903,28 +903,28 @@ it('streams an authored Suspense fallback to the stdio client before the render roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/gated.tsx': [ - 'src/mcp/curator/tools/gated.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/gated.tsx': "import { existsSync } from 'node:fs';", - 'src/mcp/curator/tools/gated.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/gated.tsx': "import { createElement, Suspense } from 'react';", - 'src/mcp/curator/tools/gated.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/gated.tsx': "export const inputSchema = z.object({ gate: z.string() }).strict();", - 'src/mcp/curator/tools/gated.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", - 'src/mcp/curator/tools/gated.tsx': "const Gated = async ({ gate, signal }) => {", - 'src/mcp/curator/tools/gated.tsx': " while (!existsSync(gate)) {", - 'src/mcp/curator/tools/gated.tsx': " if (signal.aborted) throw new DOMException('gate abandoned', 'AbortError');", - 'src/mcp/curator/tools/gated.tsx': " await new Promise((resolve) => setTimeout(resolve, 25));", - 'src/mcp/curator/tools/gated.tsx': " }", - 'src/mcp/curator/tools/gated.tsx': " return createElement(Agent.Text, null, 'released');", - 'src/mcp/curator/tools/gated.tsx': "};", - 'src/mcp/curator/tools/gated.tsx': "export default defineTool({", - 'src/mcp/curator/tools/gated.tsx': "annotations: { readOnlyHint: true }, description: 'Wait for a gate.',", - 'src/mcp/curator/tools/gated.tsx': " inputSchema,", - 'src/mcp/curator/tools/gated.tsx': " resultSchema,", - 'src/mcp/curator/tools/gated.tsx': "}, async (input, { signal }) => {", - 'src/mcp/curator/tools/gated.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'waiting', total: 1 }) }, createElement(Gated, { gate: input.gate, signal })));", - 'src/mcp/curator/tools/gated.tsx': "});", - 'src/mcp/curator/tools/gated.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { existsSync } from 'node:fs';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement, Suspense } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({ gate: z.string() }).strict();", + "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + "const Gated = async ({ gate, signal }) => {", + " while (!existsSync(gate)) {", + " if (signal.aborted) throw new DOMException('gate abandoned', 'AbortError');", + " await new Promise((resolve) => setTimeout(resolve, 25));", + " }", + " return createElement(Agent.Text, null, 'released');", + "};", + "export default defineTool({", + "annotations: { readOnlyHint: true }, description: 'Wait for a gate.',", + " inputSchema,", + " resultSchema,", + "}, async (input, { signal }) => {", + " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'waiting', total: 1 }) }, createElement(Gated, { gate: input.gate, signal })));", + "});", + ].join('\n'), }); const session = await connectGeneratedServer(root); const notifications: Array<{ readonly message?: string; readonly progress: number }> = []; @@ -982,18 +982,18 @@ it('projects thrown route errors as the SDK tool error or a JSON-RPC error, neve 'src/mcp/curator/resources/broken.ts': throwing('resource'), 'src/mcp/curator/tools/broken.ts': throwing('tool'), 'src/mcp/curator/tools/fine.tsx': [ - 'src/mcp/curator/tools/fine.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/fine.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/fine.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/fine.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/fine.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/fine.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", - 'src/mcp/curator/tools/fine.tsx': "export default defineTool({", - 'src/mcp/curator/tools/fine.tsx': "description: 'Renders.',", - 'src/mcp/curator/tools/fine.tsx': " inputSchema,", - 'src/mcp/curator/tools/fine.tsx': " resultSchema,", - 'src/mcp/curator/tools/fine.tsx': "}, async () => { return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'fine')); });", - 'src/mcp/curator/tools/fine.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + "export default defineTool({", + "description: 'Renders.',", + " inputSchema,", + " resultSchema,", + "}, async () => { return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'fine')); });", + ].join('\n'), }); const session = await connectGeneratedServer(root); try { @@ -1033,25 +1033,25 @@ it('maps notifications/cancelled into the renderer AbortSignal', { retry: 2, tim roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/hang.tsx': [ - 'src/mcp/curator/tools/hang.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/hang.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/hang.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/hang.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/hang.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/hang.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", - 'src/mcp/curator/tools/hang.tsx': "export default defineTool({", - 'src/mcp/curator/tools/hang.tsx': "annotations: { readOnlyHint: true }, description: 'Hang until cancelled.',", - 'src/mcp/curator/tools/hang.tsx': " inputSchema,", - 'src/mcp/curator/tools/hang.tsx': " resultSchema,", - 'src/mcp/curator/tools/hang.tsx': "}, async (_input, { signal }) => {", - 'src/mcp/curator/tools/hang.tsx': " await new Promise((_, reject) => {", - 'src/mcp/curator/tools/hang.tsx': " const fail = () => reject(new DOMException('aborted', 'AbortError'));", - 'src/mcp/curator/tools/hang.tsx': " if (signal.aborted) { fail(); return; }", - 'src/mcp/curator/tools/hang.tsx': " signal.addEventListener('abort', fail, { once: true });", - 'src/mcp/curator/tools/hang.tsx': " });", - 'src/mcp/curator/tools/hang.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'should not complete'));", - 'src/mcp/curator/tools/hang.tsx': "});", - 'src/mcp/curator/tools/hang.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + "export default defineTool({", + "annotations: { readOnlyHint: true }, description: 'Hang until cancelled.',", + " inputSchema,", + " resultSchema,", + "}, async (_input, { signal }) => {", + " await new Promise((_, reject) => {", + " const fail = () => reject(new DOMException('aborted', 'AbortError'));", + " if (signal.aborted) { fail(); return; }", + " signal.addEventListener('abort', fail, { once: true });", + " });", + " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'should not complete'));", + "});", + ].join('\n'), }); const session = await connectGeneratedServer(root); try { @@ -1070,38 +1070,38 @@ it('fails closed when the generated runtime worker restarts', { retry: 2, timeou roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/warmth.tsx': [ - 'src/mcp/curator/tools/warmth.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/warmth.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/warmth.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/warmth.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/warmth.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/warmth.tsx': "export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();", - 'src/mcp/curator/tools/warmth.tsx': "export default defineTool({", - 'src/mcp/curator/tools/warmth.tsx': "annotations: { readOnlyHint: true }, description: 'Observe process lifetime.',", - 'src/mcp/curator/tools/warmth.tsx': " inputSchema,", - 'src/mcp/curator/tools/warmth.tsx': " resultSchema,", - 'src/mcp/curator/tools/warmth.tsx': "}, async () => {", - 'src/mcp/curator/tools/warmth.tsx': " const context = await agent();", - 'src/mcp/curator/tools/warmth.tsx': " const value = context.process as { hits: number; instanceId: string; pid: number };", - 'src/mcp/curator/tools/warmth.tsx': " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", - 'src/mcp/curator/tools/warmth.tsx': "});", - 'src/mcp/curator/tools/warmth.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ hits: z.number(), instanceId: z.string(), pid: z.number() }).strict();", + "export default defineTool({", + "annotations: { readOnlyHint: true }, description: 'Observe process lifetime.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " const context = await agent();", + " const value = context.process as { hits: number; instanceId: string; pid: number };", + " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `hit ${String(value.hits)}`));", + "});", + ].join('\n'), 'src/mcp/curator/tools/halt.tsx': [ - 'src/mcp/curator/tools/halt.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/halt.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/halt.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/halt.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/halt.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/halt.tsx': "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", - 'src/mcp/curator/tools/halt.tsx': "export default defineTool({", - 'src/mcp/curator/tools/halt.tsx': "description: 'Halt the Flight worker.',", - 'src/mcp/curator/tools/halt.tsx': " inputSchema,", - 'src/mcp/curator/tools/halt.tsx': " resultSchema,", - 'src/mcp/curator/tools/halt.tsx': "}, async () => {", - 'src/mcp/curator/tools/halt.tsx': " process.exit(1);", - 'src/mcp/curator/tools/halt.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'halted'));", - 'src/mcp/curator/tools/halt.tsx': "});", - 'src/mcp/curator/tools/halt.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + "export default defineTool({", + "description: 'Halt the Flight worker.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " process.exit(1);", + " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'halted'));", + "});", + ].join('\n'), 'src/events/session/start.tsx': [ "import { Agent } from '@agent-bundle/runtime';", "import { createElement } from 'react';", @@ -1137,20 +1137,20 @@ it('keeps a second generated server from the same install alive while the first roots.push(root); await writeGeneratedProject(root, { 'src/mcp/curator/tools/ping.tsx': [ - 'src/mcp/curator/tools/ping.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/ping.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/curator/tools/ping.tsx': "import { createElement } from 'react';", - 'src/mcp/curator/tools/ping.tsx': "import { z } from 'zod';", - 'src/mcp/curator/tools/ping.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/curator/tools/ping.tsx': "export const resultSchema = z.object({ pid: z.number() }).strict();", - 'src/mcp/curator/tools/ping.tsx': "export default defineTool({", - 'src/mcp/curator/tools/ping.tsx': "annotations: { readOnlyHint: true }, description: 'Answer from this process.',", - 'src/mcp/curator/tools/ping.tsx': " inputSchema,", - 'src/mcp/curator/tools/ping.tsx': " resultSchema,", - 'src/mcp/curator/tools/ping.tsx': "}, async () => {", - 'src/mcp/curator/tools/ping.tsx': " return createElement(Agent.Result, { value: { pid: process.pid } }, createElement(Agent.Text, null, 'pong'));", - 'src/mcp/curator/tools/ping.tsx': "});", - 'src/mcp/curator/tools/ping.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ pid: z.number() }).strict();", + "export default defineTool({", + "annotations: { readOnlyHint: true }, description: 'Answer from this process.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { value: { pid: process.pid } }, createElement(Agent.Text, null, 'pong'));", + "});", + ].join('\n'), 'src/events/session/start.tsx': [ "import { Agent } from '@agent-bundle/runtime';", "import { createElement } from 'react';", @@ -1225,37 +1225,37 @@ it('renders one tool/after event route through two native thin clients', { retry '', ].join('\n')), writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', [ - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { Agent, agent } from '@agent-bundle/runtime';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { createElement } from 'react';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const inputSchema = z.object({}).strict();", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const resultSchema = z.object({ providerKind: z.literal('tool'), contextFrozen: z.literal(true) }).strict();", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export default defineTool({", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " inputSchema,", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " resultSchema,", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "}, async () => {", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " const context = await agent();", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " const requestValue = (await context.provider(\"requestValue\")) as { kind: string };", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " const value = { providerKind: requestValue.kind, contextFrozen: Object.isFrozen(context) };", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `provider:${requestValue.kind}`));", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "});", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ providerKind: z.literal('tool'), contextFrozen: z.literal(true) }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " const context = await agent();", + " const requestValue = (await context.provider(\"requestValue\")) as { kind: string };", + " const value = { providerKind: requestValue.kind, contextFrozen: Object.isFrozen(context) };", + " return createElement(Agent.Result, { value }, createElement(Agent.Text, null, `provider:${requestValue.kind}`));", + "});", + ].join('\n')), writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', [ - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { Agent, agent } from '@agent-bundle/runtime';", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { createElement } from 'react';", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "export const inputSchema = z.object({}).strict();", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "export default defineTool({", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " inputSchema,", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " resultSchema,", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "}, async () => {", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " await (await agent()).provider('throwing');", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'unreachable'));", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', "});", - writeProjectFile(root, 'src/mcp/runtime/tools/explode.tsx', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " await (await agent()).provider('throwing');", + " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'unreachable'));", + "});", + ].join('\n')), writeProjectFile(root, 'src/providers/request-value.ts', [ "import type { AgentProviderFactory } from 'agent-bundle';", 'const provideRequest: AgentProviderFactory = ({ invocation }) => Object.freeze({ kind: invocation.kind });', @@ -1417,20 +1417,20 @@ it('renders composite root events through each selected host in one warm runtime '', ].join('\n')), writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', [ - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { Agent } from '@agent-bundle/runtime';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { createElement } from 'react';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const inputSchema = z.object({}).strict();", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "export default defineTool({", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "description: 'Keep the shared event runtime warm.',", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " inputSchema,", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " resultSchema,", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "}, async () => {", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'ready'));", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', "});", - writeProjectFile(root, 'src/mcp/runtime/tools/status.tsx', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ok: z.literal(true) }).strict();", + "export default defineTool({", + "description: 'Keep the shared event runtime warm.',", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { value: { ok: true } }, createElement(Agent.Text, null, 'ready'));", + "});", + ].join('\n')), writeProjectFile(root, 'src/events/tool/after.tsx', [ "import { Agent, agent } from '@agent-bundle/runtime';", "import { createElement } from 'react';", diff --git a/packages/agent-bundle/tests/provider-typegen.test.ts b/packages/agent-bundle/tests/provider-typegen.test.ts index 6a6d921d5..f22e8b3a1 100644 --- a/packages/agent-bundle/tests/provider-typegen.test.ts +++ b/packages/agent-bundle/tests/provider-typegen.test.ts @@ -88,15 +88,15 @@ it('types await context.provider(key) from the generated provider declarations', '', ].join('\n')), writeProjectFile(root, 'src/mcp/curator/tools/status.ts', [ - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const inputSchema = z.object({}).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export default defineTool({", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " inputSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " resultSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "}, async () => { return { status: 'ready' as const }; });", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => { return { status: 'ready' as const }; });", + ].join('\n')), writeProjectFile(root, 'assertions.ts', [ "import { agent } from '@agent-bundle/runtime';", "import type { ProviderKey, ProviderValue } from './.agent-bundle/routes.js';", diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index f7675f290..f2d830742 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -1758,15 +1758,11 @@ it('validates the single async route-module authoring contract statically', asyn '', ].join('\n'), 'src/mcp/curator/tools/split.tsx': [ - 'src/mcp/curator/tools/split.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/curator/tools/split.tsx': "export const resultSchema = {};", - 'src/mcp/curator/tools/split.tsx': "export const execute = async () => ({});", - 'src/mcp/curator/tools/split.tsx': "export const render = () => undefined;", - 'src/mcp/curator/tools/split.tsx': "export default defineTool({", - 'src/mcp/curator/tools/split.tsx': " inputSchema,", - 'src/mcp/curator/tools/split.tsx': " resultSchema,", - 'src/mcp/curator/tools/split.tsx': "}, async () => { return undefined; });", - 'src/mcp/curator/tools/split.tsx': ].join('\n'), + "export const resultSchema = {};", + "export const execute = async () => ({});", + "export const render = () => undefined;", + "export default function Split() { return undefined; }", + ].join('\n'), }); const graph = await compileRouteGraph(root, fixtureConfig()); diff --git a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts index 3104c483e..f7d16e0fb 100644 --- a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts +++ b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts @@ -251,164 +251,164 @@ it('invokes compiled tool and event routes through the foreground server', { tim '', ].join('\n'), 'src/mcp/alpha/tools/fail.tsx': [ - 'src/mcp/alpha/tools/fail.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/alpha/tools/fail.tsx': "import { appendFileSync } from 'node:fs';", - 'src/mcp/alpha/tools/fail.tsx': "import { z } from 'zod';", - 'src/mcp/alpha/tools/fail.tsx': "", - 'src/mcp/alpha/tools/fail.tsx': "appendFileSync('.agent-bundle/alpha-worker.marker', 'load\\n');", - 'src/mcp/alpha/tools/fail.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/alpha/tools/fail.tsx': "export const resultSchema = z.object({ failed: z.boolean() }).strict();", - 'src/mcp/alpha/tools/fail.tsx': "", - 'src/mcp/alpha/tools/fail.tsx': "export default defineTool({", - 'src/mcp/alpha/tools/fail.tsx': " inputSchema,", - 'src/mcp/alpha/tools/fail.tsx': " resultSchema,", - 'src/mcp/alpha/tools/fail.tsx': "}, async () => {", - 'src/mcp/alpha/tools/fail.tsx': " appendFileSync('.agent-bundle/alpha-handler.marker', 'run\\n');", - 'src/mcp/alpha/tools/fail.tsx': " throw new Error('Generated route must default-export an async Server Component.');", - 'src/mcp/alpha/tools/fail.tsx': "});", - 'src/mcp/alpha/tools/fail.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { appendFileSync } from 'node:fs';", + "import { z } from 'zod';", + "", + "appendFileSync('.agent-bundle/alpha-worker.marker', 'load\\n');", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ failed: z.boolean() }).strict();", + "", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " appendFileSync('.agent-bundle/alpha-handler.marker', 'run\\n');", + " throw new Error('Generated route must default-export an async Server Component.');", + "});", + ].join('\n'), 'src/mcp/omega/tools/pass.tsx': [ - 'src/mcp/omega/tools/pass.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/omega/tools/pass.tsx': "import { appendFileSync } from 'node:fs';", - 'src/mcp/omega/tools/pass.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/omega/tools/pass.tsx': "import { createElement } from 'react';", - 'src/mcp/omega/tools/pass.tsx': "import { z } from 'zod';", - 'src/mcp/omega/tools/pass.tsx': "", - 'src/mcp/omega/tools/pass.tsx': "appendFileSync('.agent-bundle/omega-worker.marker', 'load\\n');", - 'src/mcp/omega/tools/pass.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/omega/tools/pass.tsx': "export const resultSchema = z.object({ selected: z.literal('omega') }).strict();", - 'src/mcp/omega/tools/pass.tsx': "", - 'src/mcp/omega/tools/pass.tsx': "export default defineTool({", - 'src/mcp/omega/tools/pass.tsx': " inputSchema,", - 'src/mcp/omega/tools/pass.tsx': " resultSchema,", - 'src/mcp/omega/tools/pass.tsx': "}, async () => {", - 'src/mcp/omega/tools/pass.tsx': " return createElement(Agent.Result, { value: { selected: 'omega' } });", - 'src/mcp/omega/tools/pass.tsx': "});", - 'src/mcp/omega/tools/pass.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { appendFileSync } from 'node:fs';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "appendFileSync('.agent-bundle/omega-worker.marker', 'load\\n');", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ selected: z.literal('omega') }).strict();", + "", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { value: { selected: 'omega' } });", + "});", + ].join('\n'), 'src/mcp/importer/tools/fail.tsx': [ - 'src/mcp/importer/tools/fail.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/importer/tools/fail.tsx': "import { appendFileSync } from 'node:fs';", - 'src/mcp/importer/tools/fail.tsx': "import { z } from 'zod';", - 'src/mcp/importer/tools/fail.tsx': "", - 'src/mcp/importer/tools/fail.tsx': "appendFileSync('.agent-bundle/importer-worker.marker', 'load\\n');", - 'src/mcp/importer/tools/fail.tsx': "throw new Error('Generated route must default-export from import.');", - 'src/mcp/importer/tools/fail.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/importer/tools/fail.tsx': "export const resultSchema = z.object({ failed: z.boolean() }).strict();", - 'src/mcp/importer/tools/fail.tsx': "export default defineTool({", - 'src/mcp/importer/tools/fail.tsx': " inputSchema,", - 'src/mcp/importer/tools/fail.tsx': " resultSchema,", - 'src/mcp/importer/tools/fail.tsx': "}, async () => { return undefined; });", - 'src/mcp/importer/tools/fail.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { appendFileSync } from 'node:fs';", + "import { z } from 'zod';", + "", + "appendFileSync('.agent-bundle/importer-worker.marker', 'load\\n');", + "throw new Error('Generated route must default-export from import.');", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ failed: z.boolean() }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => { return undefined; });", + ].join('\n'), 'src/mcp/status/tools/counter.tsx': [ - 'src/mcp/status/tools/counter.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/counter.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/counter.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/counter.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/counter.tsx': "", - 'src/mcp/status/tools/counter.tsx': "export const inputSchema = z.object({ key: z.string() }).strict();", - 'src/mcp/status/tools/counter.tsx': "export const resultSchema = z.object({ count: z.number() }).strict();", - 'src/mcp/status/tools/counter.tsx': "export default defineTool({", - 'src/mcp/status/tools/counter.tsx': " inputSchema,", - 'src/mcp/status/tools/counter.tsx': " resultSchema,", - 'src/mcp/status/tools/counter.tsx': "}, async (input) => {", - 'src/mcp/status/tools/counter.tsx': " const context = await agent();", - 'src/mcp/status/tools/counter.tsx': " if (context.state === undefined) throw new Error('state unavailable');", - 'src/mcp/status/tools/counter.tsx': " const committed = await context.state.dispatch('incremented', { by: 1 }, { idempotencyKey: `${input.key}:${crypto.randomUUID()}` });", - 'src/mcp/status/tools/counter.tsx': " return createElement(Agent.Result, { value: { count: committed.state.count } });", - 'src/mcp/status/tools/counter.tsx': "});", - 'src/mcp/status/tools/counter.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({ key: z.string() }).strict();", + "export const resultSchema = z.object({ count: z.number() }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " const context = await agent();", + " if (context.state === undefined) throw new Error('state unavailable');", + " const committed = await context.state.dispatch('incremented', { by: 1 }, { idempotencyKey: `${input.key}:${crypto.randomUUID()}` });", + " return createElement(Agent.Result, { value: { count: committed.state.count } });", + "});", + ].join('\n'), 'src/mcp/status/tools/refuse.tsx': [ - 'src/mcp/status/tools/refuse.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/refuse.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/refuse.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/refuse.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/refuse.tsx': "", - 'src/mcp/status/tools/refuse.tsx': "export const inputSchema = z.object({ reason: z.string() }).strict();", - 'src/mcp/status/tools/refuse.tsx': "export const resultSchema = z.object({ refused: z.boolean() }).strict();", - 'src/mcp/status/tools/refuse.tsx': "export default defineTool({", - 'src/mcp/status/tools/refuse.tsx': " inputSchema,", - 'src/mcp/status/tools/refuse.tsx': " resultSchema,", - 'src/mcp/status/tools/refuse.tsx': "}, async (input) => {", - 'src/mcp/status/tools/refuse.tsx': " return createElement(Agent.Result, { value: { refused: true } }, createElement(Agent.Error, { code: 'refused' }, `Refused: ${input.reason}`));", - 'src/mcp/status/tools/refuse.tsx': "});", - 'src/mcp/status/tools/refuse.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({ reason: z.string() }).strict();", + "export const resultSchema = z.object({ refused: z.boolean() }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " return createElement(Agent.Result, { value: { refused: true } }, createElement(Agent.Error, { code: 'refused' }, `Refused: ${input.reason}`));", + "});", + ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/report.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/report.tsx': "import { ALIAS_VALUE } from '@fixture/value';", - 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/report.tsx': "import { Panel } from './panel.js';", - 'src/mcp/status/tools/report.tsx': "declare const __ROUTE_INVOCATION_DEFINE__: string;", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({ service: z.string().min(1), source: z.string() }).strict();", - 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ alias: z.string(), define: z.string(), pluginRoot: z.string(), service: z.string(), source: z.string(), stateRoot: z.string() }).strict();", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export default defineTool({", - 'src/mcp/status/tools/report.tsx': "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"}},\"required\":[\"service\",\"source\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.',", - 'src/mcp/status/tools/report.tsx': " inputSchema,", - 'src/mcp/status/tools/report.tsx': " resultSchema,", - 'src/mcp/status/tools/report.tsx': "}, async (input) => {", - 'src/mcp/status/tools/report.tsx': " const context = await agent();", - 'src/mcp/status/tools/report.tsx': " await context.provider(\"clock\");", - 'src/mcp/status/tools/report.tsx': " if (context.plugin.state !== 'available') throw new Error('plugin unavailable');", - 'src/mcp/status/tools/report.tsx': " const value = { alias: ALIAS_VALUE, define: __ROUTE_INVOCATION_DEFINE__, pluginRoot: context.plugin.value.root, service: input.service, source: input.source, stateRoot: context.plugin.value.stateRoot };", - 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value }, createElement(Panel), createElement(Agent.Text, null, './panel.js'), createElement(Agent.Text, null, `Service ${input.service}`));", - 'src/mcp/status/tools/report.tsx': "});", - 'src/mcp/status/tools/report.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { ALIAS_VALUE } from '@fixture/value';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "import { Panel } from './panel.js';", + "declare const __ROUTE_INVOCATION_DEFINE__: string;", + "", + "export const inputSchema = z.object({ service: z.string().min(1), source: z.string() }).strict();", + "export const resultSchema = z.object({ alias: z.string(), define: z.string(), pluginRoot: z.string(), service: z.string(), source: z.string(), stateRoot: z.string() }).strict();", + "", + "export default defineTool({", + "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"}},\"required\":[\"service\",\"source\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.',", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " const context = await agent();", + " await context.provider(\"clock\");", + " if (context.plugin.state !== 'available') throw new Error('plugin unavailable');", + " const value = { alias: ALIAS_VALUE, define: __ROUTE_INVOCATION_DEFINE__, pluginRoot: context.plugin.value.root, service: input.service, source: input.source, stateRoot: context.plugin.value.stateRoot };", + " return createElement(Agent.Result, { value }, createElement(Panel), createElement(Agent.Text, null, './panel.js'), createElement(Agent.Text, null, `Service ${input.service}`));", + "});", + ].join('\n'), 'src/mcp/status/tools/panel.tsx': [ - 'src/mcp/status/tools/panel.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/panel.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/panel.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/panel.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/panel.tsx': "", - 'src/mcp/status/tools/panel.tsx': "export const Panel = () => createElement(Agent.Text, null, 'panel rendered');", - 'src/mcp/status/tools/panel.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/status/tools/panel.tsx': "export const resultSchema = z.object({ panel: z.literal(true) }).strict();", - 'src/mcp/status/tools/panel.tsx': "export default defineTool({", - 'src/mcp/status/tools/panel.tsx': " inputSchema,", - 'src/mcp/status/tools/panel.tsx': " resultSchema,", - 'src/mcp/status/tools/panel.tsx': "}, async () => {", - 'src/mcp/status/tools/panel.tsx': " return createElement(Agent.Result, { value: { panel: true } }, createElement(Panel));", - 'src/mcp/status/tools/panel.tsx': "});", - 'src/mcp/status/tools/panel.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const Panel = () => createElement(Agent.Text, null, 'panel rendered');", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ panel: z.literal(true) }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { value: { panel: true } }, createElement(Panel));", + "});", + ].join('\n'), 'src/mcp/status/tools/crash.tsx': [ - 'src/mcp/status/tools/crash.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/crash.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/crash.tsx': "import { createElement, Suspense } from 'react';", - 'src/mcp/status/tools/crash.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/crash.tsx': "", - 'src/mcp/status/tools/crash.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/status/tools/crash.tsx': "export const resultSchema = z.object({ ok: z.boolean() }).strict();", - 'src/mcp/status/tools/crash.tsx': "const Boom = async () => { throw new Error('render exploded'); };", - 'src/mcp/status/tools/crash.tsx': "", - 'src/mcp/status/tools/crash.tsx': "export default defineTool({", - 'src/mcp/status/tools/crash.tsx': " inputSchema,", - 'src/mcp/status/tools/crash.tsx': " resultSchema,", - 'src/mcp/status/tools/crash.tsx': "}, async () => {", - 'src/mcp/status/tools/crash.tsx': " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'crashing', total: 1 }) }, createElement(Boom)));", - 'src/mcp/status/tools/crash.tsx': "});", - 'src/mcp/status/tools/crash.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement, Suspense } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ok: z.boolean() }).strict();", + "const Boom = async () => { throw new Error('render exploded'); };", + "", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { value: { ok: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'crashing', total: 1 }) }, createElement(Boom)));", + "});", + ].join('\n'), 'src/mcp/status/tools/burst.tsx': [ - 'src/mcp/status/tools/burst.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/burst.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/burst.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/burst.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/burst.tsx': "", - 'src/mcp/status/tools/burst.tsx': "export const inputSchema = z.object({ messageBytes: z.number().int().min(1).max(65_536), ticks: z.number().int().min(1).max(900) }).strict();", - 'src/mcp/status/tools/burst.tsx': "export const resultSchema = z.object({ ticks: z.number() }).strict();", - 'src/mcp/status/tools/burst.tsx': "", - 'src/mcp/status/tools/burst.tsx': "export default defineTool({", - 'src/mcp/status/tools/burst.tsx': " inputSchema,", - 'src/mcp/status/tools/burst.tsx': " resultSchema,", - 'src/mcp/status/tools/burst.tsx': "}, async (input) => {", - 'src/mcp/status/tools/burst.tsx': " const { progress } = await agent();", - 'src/mcp/status/tools/burst.tsx': " for (let step = 1; step <= input.ticks; step += 1) {", - 'src/mcp/status/tools/burst.tsx': " await progress.report({ completed: step, message: `${step}:${'p'.repeat(input.messageBytes)}`, total: input.ticks });", - 'src/mcp/status/tools/burst.tsx': " }", - 'src/mcp/status/tools/burst.tsx': " return createElement(Agent.Result, { value: { ticks: input.ticks } }, createElement(Agent.Text, null, `Reported ${input.ticks} ticks.`));", - 'src/mcp/status/tools/burst.tsx': "});", - 'src/mcp/status/tools/burst.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({ messageBytes: z.number().int().min(1).max(65_536), ticks: z.number().int().min(1).max(900) }).strict();", + "export const resultSchema = z.object({ ticks: z.number() }).strict();", + "", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " const { progress } = await agent();", + " for (let step = 1; step <= input.ticks; step += 1) {", + " await progress.report({ completed: step, message: `${step}:${'p'.repeat(input.messageBytes)}`, total: input.ticks });", + " }", + " return createElement(Agent.Result, { value: { ticks: input.ticks } }, createElement(Agent.Text, null, `Reported ${input.ticks} ticks.`));", + "});", + ].join('\n'), 'src/mcp/status/tools/report.cli.ts': [ "export const config = { command: ['report'], confirm: true, flags: { service: { name: 'name' }, source: { required: false } } };", "export const mapInput = (input) => ({ ...input, source: input.source ?? 'cli-projection' });", @@ -416,18 +416,18 @@ it('invokes compiled tool and event routes through the foreground server', { tim ].join('\n'), 'src/mcp/status/tools/plain.cli.ts': "export const config = { command: ['plain-tool'] };\n", 'src/mcp/status/tools/plain.ts': [ - 'src/mcp/status/tools/plain.ts': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/plain.ts': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/plain.ts': "import { createElement } from 'react';", - 'src/mcp/status/tools/plain.ts': "import { z } from 'zod';", - 'src/mcp/status/tools/plain.ts': "", - 'src/mcp/status/tools/plain.ts': "export const inputSchema = z.object({}).strict();", - 'src/mcp/status/tools/plain.ts': "export const resultSchema = z.object({ selected: z.literal('plain-tool') }).strict();", - 'src/mcp/status/tools/plain.ts': "export default defineTool({", - 'src/mcp/status/tools/plain.ts': " inputSchema,", - 'src/mcp/status/tools/plain.ts': " resultSchema,", - 'src/mcp/status/tools/plain.ts': "}, async () => { return createElement(Agent.Result, { value: { selected: 'plain-tool' } }); });", - 'src/mcp/status/tools/plain.ts': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ selected: z.literal('plain-tool') }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => { return createElement(Agent.Result, { value: { selected: 'plain-tool' } }); });", + ].join('\n'), 'src/providers/clock.ts': [ 'export default () => ({ now: 0 });', '', @@ -1603,24 +1603,24 @@ it('enforces compiled handler, MCP schemas, and operator env across production s '', ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/report.tsx': "import { writeFileSync } from 'node:fs';", - 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", - 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ operator: z.string(), service: z.string() }).strict();", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export default defineTool({", - 'src/mcp/status/tools/report.tsx': "inputJsonSchema: { type: 'object', additionalProperties: false, properties: { service: { type: 'string' } }, required: ['service'] },", - 'src/mcp/status/tools/report.tsx': " inputSchema,", - 'src/mcp/status/tools/report.tsx': " resultSchema,", - 'src/mcp/status/tools/report.tsx': "}, async (input) => {", - 'src/mcp/status/tools/report.tsx': " writeFileSync('.agent-bundle/handler-ran', 'yes');", - 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value: { operator: process.env.OPERATOR_VALUE ?? 'missing', service: input.service } });", - 'src/mcp/status/tools/report.tsx': "});", - 'src/mcp/status/tools/report.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { writeFileSync } from 'node:fs';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", + "export const resultSchema = z.object({ operator: z.string(), service: z.string() }).strict();", + "", + "export default defineTool({", + "inputJsonSchema: { type: 'object', additionalProperties: false, properties: { service: { type: 'string' } }, required: ['service'] },", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " writeFileSync('.agent-bundle/handler-ran', 'yes');", + " return createElement(Agent.Result, { value: { operator: process.env.OPERATOR_VALUE ?? 'missing', service: input.service } });", + "});", + ].join('\n'), }, prefix: 'agent-bundle-route-parity-', }); @@ -1737,22 +1737,22 @@ it('publishes invocation routes only after a successful initial or recovered bui files: { 'package.json': '{"dependencies":{"@agent-bundle/runtime":"workspace:*","react":"19.2.8","zod":"4.5.4"},"type":"module"}\n', 'src/mcp/status/tools/report.tsx': [ - 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/report.tsx': "import './missing.js';", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ version: z.string() }).strict();", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export default defineTool({", - 'src/mcp/status/tools/report.tsx': " inputSchema,", - 'src/mcp/status/tools/report.tsx': " resultSchema,", - 'src/mcp/status/tools/report.tsx': "}, async () => {", - 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value: { version: 'published' } }, createElement(Agent.Text, null, 'Published route.'));", - 'src/mcp/status/tools/report.tsx': "});", - 'src/mcp/status/tools/report.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "import './missing.js';", + "", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ version: z.string() }).strict();", + "", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Result, { value: { version: 'published' } }, createElement(Agent.Text, null, 'Published route.'));", + "});", + ].join('\n'), }, prefix: 'agent-bundle-route-invocation-publication-gate-', }); diff --git a/packages/agent-bundle/tests/route-invocation-service.test.ts b/packages/agent-bundle/tests/route-invocation-service.test.ts index d404fc21a..4c9740427 100644 --- a/packages/agent-bundle/tests/route-invocation-service.test.ts +++ b/packages/agent-bundle/tests/route-invocation-service.test.ts @@ -1310,19 +1310,15 @@ const tsxSiblingProject = async (): Promise => routeProject( '', ].join('\n'), 'src/mcp/fixture/tools/report.tsx': [ - 'src/mcp/fixture/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/fixture/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/fixture/tools/report.tsx': "import { createElement } from 'react';", - 'src/mcp/fixture/tools/report.tsx': "", - 'src/mcp/fixture/tools/report.tsx': "import { Panel } from './panel.js';", - 'src/mcp/fixture/tools/report.tsx': "", - 'src/mcp/fixture/tools/report.tsx': "export default defineTool({", - 'src/mcp/fixture/tools/report.tsx': " inputSchema,", - 'src/mcp/fixture/tools/report.tsx': " resultSchema,", - 'src/mcp/fixture/tools/report.tsx': "}, async () => {", - 'src/mcp/fixture/tools/report.tsx': " return createElement(Agent.Result, null, createElement(Panel), createElement(Agent.Text, null, './panel.js'));", - 'src/mcp/fixture/tools/report.tsx': "});", - 'src/mcp/fixture/tools/report.tsx': ].join('\n'), + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "", + "import { Panel } from './panel.js';", + "", + "export default async function Report() {", + " return createElement(Agent.Result, null, createElement(Panel), createElement(Agent.Text, null, './panel.js'));", + "}", + ].join('\n'), }, ); @@ -1352,25 +1348,21 @@ const burstRouteProject = async (): Promise => routeProject( 'burst', { 'src/mcp/fixture/tools/burst.tsx': [ - 'src/mcp/fixture/tools/burst.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/fixture/tools/burst.tsx': "import { Agent, agent } from '@agent-bundle/runtime';", - 'src/mcp/fixture/tools/burst.tsx': "import { createElement, Suspense } from 'react';", - 'src/mcp/fixture/tools/burst.tsx': "", - 'src/mcp/fixture/tools/burst.tsx': "const Chunk = async ({ index }) => {", - 'src/mcp/fixture/tools/burst.tsx': " await new Promise((resolve) => setTimeout(resolve, 20 * (index + 1)));", - 'src/mcp/fixture/tools/burst.tsx': " return createElement(Agent.Text, null, `${index}:${'x'.repeat(150 * 1024)}`);", - 'src/mcp/fixture/tools/burst.tsx': "};", - 'src/mcp/fixture/tools/burst.tsx': "", - 'src/mcp/fixture/tools/burst.tsx': "export default defineTool({", - 'src/mcp/fixture/tools/burst.tsx': " inputSchema,", - 'src/mcp/fixture/tools/burst.tsx': " resultSchema,", - 'src/mcp/fixture/tools/burst.tsx': "}, async () => {", - 'src/mcp/fixture/tools/burst.tsx': " const { progress } = await agent();", - 'src/mcp/fixture/tools/burst.tsx': " for (let step = 1; step <= 400; step += 1) await progress.report({ completed: step, total: 400 });", - 'src/mcp/fixture/tools/burst.tsx': " return createElement(Agent.Result, null, ...Array.from({ length: 5 }, (_, index) =>", - 'src/mcp/fixture/tools/burst.tsx': " createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: index, total: 5 }), key: index }, createElement(Chunk, { index }))));", - 'src/mcp/fixture/tools/burst.tsx': "});", - 'src/mcp/fixture/tools/burst.tsx': ].join('\n'), + "import { Agent, agent } from '@agent-bundle/runtime';", + "import { createElement, Suspense } from 'react';", + "", + "const Chunk = async ({ index }) => {", + " await new Promise((resolve) => setTimeout(resolve, 20 * (index + 1)));", + " return createElement(Agent.Text, null, `${index}:${'x'.repeat(150 * 1024)}`);", + "};", + "", + "export default async function Burst() {", + " const { progress } = await agent();", + " for (let step = 1; step <= 400; step += 1) await progress.report({ completed: step, total: 400 });", + " return createElement(Agent.Result, null, ...Array.from({ length: 5 }, (_, index) =>", + " createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: index, total: 5 }), key: index }, createElement(Chunk, { index }))));", + "}", + ].join('\n'), }, ); diff --git a/packages/agent-bundle/tests/route-register-typegen.test.ts b/packages/agent-bundle/tests/route-register-typegen.test.ts index 7e0e86ea7..db01eacb4 100644 --- a/packages/agent-bundle/tests/route-register-typegen.test.ts +++ b/packages/agent-bundle/tests/route-register-typegen.test.ts @@ -92,36 +92,36 @@ it('types every route-aware public surface from the generated route registration '', ].join('\n')), writeProjectFile(root, 'src/mcp/curator/tools/status.ts', [ - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const inputSchema = z.object({}).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "export default defineTool({", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " inputSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', " resultSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', "}, async () => { return { status: 'ready' as const }; });", - writeProjectFile(root, 'src/mcp/curator/tools/status.ts', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { z } from 'zod';", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ status: z.literal('ready') }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => { return { status: 'ready' as const }; });", + ].join('\n')), writeProjectFile(root, 'src/mcp/curator/tools/find.ts', [ - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "export const inputSchema = z.object({ query: z.string() }).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "export const resultSchema = z.object({ hits: z.number() }).strict();", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "export default defineTool({", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', " inputSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', " resultSchema,", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', "}, async () => { return { hits: 1 }; });", - writeProjectFile(root, 'src/mcp/curator/tools/find.ts', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { z } from 'zod';", + "export const inputSchema = z.object({ query: z.string() }).strict();", + "export const resultSchema = z.object({ hits: z.number() }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => { return { hits: 1 }; });", + ].join('\n')), // A second server registering the same tool name: the wire helpers see the union of both inputs. writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', [ - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "import { defineTool } from 'agent-bundle/routes';", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "import { z } from 'zod';", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "export const inputSchema = z.object({ isbn: z.string() }).strict();", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "export const resultSchema = z.object({ shelved: z.boolean() }).strict();", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "export default defineTool({", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', " inputSchema,", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', " resultSchema,", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', "}, async () => { return { shelved: true }; });", - writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', ].join('\n')), + "import { defineTool } from 'agent-bundle/routes';", + "import { z } from 'zod';", + "export const inputSchema = z.object({ isbn: z.string() }).strict();", + "export const resultSchema = z.object({ shelved: z.boolean() }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => { return { shelved: true }; });", + ].join('\n')), writeProjectFile(root, 'src/mcp/curator/prompts/brief.ts', [ "import { z } from 'zod';", 'export const inputSchema = z.object({ topic: z.string() }).strict();', diff --git a/packages/agent-bundle/tests/trace-dev-server.test.ts b/packages/agent-bundle/tests/trace-dev-server.test.ts index 63d232bb2..2f6f09402 100644 --- a/packages/agent-bundle/tests/trace-dev-server.test.ts +++ b/packages/agent-bundle/tests/trace-dev-server.test.ts @@ -51,20 +51,20 @@ it('serves replay and live trace entries and lowers build failures', { timeout: '', ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({}).strict();", - 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ ready: z.boolean() }).strict();", - 'src/mcp/status/tools/report.tsx': "export default defineTool({", - 'src/mcp/status/tools/report.tsx': " inputSchema,", - 'src/mcp/status/tools/report.tsx': " resultSchema,", - 'src/mcp/status/tools/report.tsx': "}, async () => {", - 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Text, null, 'Ready.');", - 'src/mcp/status/tools/report.tsx': "});", - 'src/mcp/status/tools/report.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({}).strict();", + "export const resultSchema = z.object({ ready: z.boolean() }).strict();", + "export default defineTool({", + " inputSchema,", + " resultSchema,", + "}, async () => {", + " return createElement(Agent.Text, null, 'Ready.');", + "});", + ].join('\n'), }, prefix: 'agent-bundle-trace-dev-server-', }); diff --git a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts index e0850a0f9..4a0c0e137 100644 --- a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts +++ b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts @@ -52,22 +52,22 @@ it('matches the route manifest and lifecycle inventory a real dev server serves' '', ].join('\n'), 'src/mcp/status/tools/report.tsx': [ - 'src/mcp/status/tools/report.tsx': "import { defineTool } from 'agent-bundle/routes';", - 'src/mcp/status/tools/report.tsx': "import { Agent } from '@agent-bundle/runtime';", - 'src/mcp/status/tools/report.tsx': "import { createElement } from 'react';", - 'src/mcp/status/tools/report.tsx': "import { z } from 'zod';", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", - 'src/mcp/status/tools/report.tsx': "export const resultSchema = z.object({ service: z.string() }).strict();", - 'src/mcp/status/tools/report.tsx': "", - 'src/mcp/status/tools/report.tsx': "export default defineTool({", - 'src/mcp/status/tools/report.tsx': "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.',", - 'src/mcp/status/tools/report.tsx': " inputSchema,", - 'src/mcp/status/tools/report.tsx': " resultSchema,", - 'src/mcp/status/tools/report.tsx': "}, async (input) => {", - 'src/mcp/status/tools/report.tsx': " return createElement(Agent.Result, { value: { service: input.service } }, createElement(Agent.Text, null, input.service));", - 'src/mcp/status/tools/report.tsx': "});", - 'src/mcp/status/tools/report.tsx': ].join('\n'), + "import { defineTool } from 'agent-bundle/routes';", + "import { Agent } from '@agent-bundle/runtime';", + "import { createElement } from 'react';", + "import { z } from 'zod';", + "", + "export const inputSchema = z.object({ service: z.string().min(1) }).strict();", + "export const resultSchema = z.object({ service: z.string() }).strict();", + "", + "export default defineTool({", + "inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.',", + " inputSchema,", + " resultSchema,", + "}, async (input) => {", + " return createElement(Agent.Result, { value: { service: input.service } }, createElement(Agent.Text, null, input.service));", + "});", + ].join('\n'), 'src/providers/clock.ts': [ 'export default () => ({ now: 0 });', '', From cecaf70c4815e6f9e39832759c61c31559612b6b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 09:18:52 +0000 Subject: [PATCH 03/11] test: migrate remaining legacy config callers --- .changeset/remove-legacy-apis.md | 2 +- packages/agent-bundle/README.md | 2 +- packages/workbench/tests/mcp-app-real.e2e.test.ts | 8 ++++---- packages/workbench/tests/mcp-session-timeout.e2e.test.ts | 4 ++-- packages/workbench/tests/overview.e2e.test.ts | 4 ++-- packages/workbench/tests/streaming-render.e2e.test.ts | 4 ++-- scripts/measure-hook-cold-start.mjs | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.changeset/remove-legacy-apis.md b/.changeset/remove-legacy-apis.md index d60946133..0c7ce766d 100644 --- a/.changeset/remove-legacy-apis.md +++ b/.changeset/remove-legacy-apis.md @@ -1,7 +1,7 @@ --- "agent-bundle": minor "@agent-bundle/runtime": minor -"rsc-markdown-stream": patch +"rsc-markdown-stream": minor --- Remove `plugin.version` and diagnostics AB4001/AB4008, the install `--force` alias, split tool exports, `ServedApp`, generated CLI `isTty`, and legacy MCP App template resolution; remove runtime `Hook`/`Mcp` lowerers, `createRscRequestContext`, and `AgentDocumentSnapshot`; require React 19 element and context contracts (#PR) diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index 2240303de..94693d2e1 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -135,7 +135,7 @@ manifests at files inside those payloads without compiling them. Payload files c | --- | --- | | `agent-bundle build` | Build a validated artifact from source, plus the declared `dist/` package build. | | `agent-bundle prepack` | Run the release build, dry-run npm packing without scripts, and verify packaged outputs, artifact hashes, bins, and versions (`--output` and `--json` supported). | -| `agent-bundle install ` | Install a built bundle into Claude, Codex, or Cursor (`--from`, `--scope`, `--replace`/`--force`, `--mode local\|marketplace` for Cursor, and `--json` supported). Same-version content drift of an agent-bundle-managed install is replaced automatically; identical reruns are a no-op. | +| `agent-bundle install ` | Install a built bundle into Claude, Codex, or Cursor (`--from`, `--scope`, `--replace`, `--mode local\|marketplace` for Cursor, and `--json` supported). Same-version content drift of an agent-bundle-managed install is replaced automatically; identical reruns are a no-op. | | `agent-bundle uninstall ` | Remove a receipt-owned install and nothing else: the receipt's files and directories, the host registrations it recorded (`claude plugin uninstall --keep-data` + `marketplace remove`, `codex plugin remove` + `marketplace remove`, the Cursor local directory or staged marketplace). `--plan` prints the exact paths without changing anything; the effective framework state root, web-data, and legacy `state/` are kept unless `--purge-data --confirm-purge`; a missing receipt or content mismatch is refused unless `--force`; a rerun is `not-installed`. | | `agent-bundle doctor` | Read-only host inspection: host probes, installed inventory, effective and legacy state roots with existence and writability, store receipts cross-checked against the host, and, with `--from`, the installed copy compared against the built artifact by version and content hash (`current`, `stale`, `version-mismatch`, `foreign`, `not-installed`) plus the lifecycle stage (placed → registered → enabled → active, unobservable stages typed `unavailable`). | | `agent-bundle validate` | Validate project source, or an artifact with `--artifact`. | diff --git a/packages/workbench/tests/mcp-app-real.e2e.test.ts b/packages/workbench/tests/mcp-app-real.e2e.test.ts index a8510ccbd..bd3ab21ce 100644 --- a/packages/workbench/tests/mcp-app-real.e2e.test.ts +++ b/packages/workbench/tests/mcp-app-real.e2e.test.ts @@ -79,7 +79,7 @@ const writeRealAppProject = async (root: string): Promise => { symlink(join(workbenchNodeModules, 'zod'), join(root, 'node_modules', 'zod'), 'dir'), ]); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'server.ts'), [ "import { McpServer } from '@modelcontextprotocol/server';", "import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';", @@ -104,7 +104,7 @@ const writeRealAppProject = async (root: string): Promise => { '', 'export default defineConfig({', " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", - " plugin: { name: 'real-app-e2e-fixture', version: '1.0.0' },", + " plugin: { name: 'real-app-e2e-fixture' },", " skills: ['src/skills/review'],", " targets: ['portable'],", '});', @@ -121,7 +121,7 @@ const writeBundledAppProject = async (root: string): Promise => { symlink(join(workbenchNodeModules, 'zod'), join(root, 'node_modules', 'zod'), 'dir'), ]); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'views', 'dashboard.css'), '#view { color: rgb(18, 52, 86); font-weight: 700; }\n'), writeFile(join(root, 'views', 'dashboard.ts'), [ "import { createAppClient } from 'agent-bundle/app';", @@ -179,7 +179,7 @@ const writeBundledAppProject = async (root: string): Promise => { " apps: { dashboard: { entry: './views/dashboard.ts', resourceUri: 'ui://packed-release/dashboard.html', template: './views/shell.html' } },", " entry: './src/server.ts',", ' } } },', - " plugin: { name: 'bundled-app-e2e-fixture', version: '1.0.0' },", + " plugin: { name: 'bundled-app-e2e-fixture' },", " skills: ['src/skills/review'],", " targets: ['portable'],", '});', diff --git a/packages/workbench/tests/mcp-session-timeout.e2e.test.ts b/packages/workbench/tests/mcp-session-timeout.e2e.test.ts index f29cb73a8..7d32c5535 100644 --- a/packages/workbench/tests/mcp-session-timeout.e2e.test.ts +++ b/packages/workbench/tests/mcp-session-timeout.e2e.test.ts @@ -19,7 +19,7 @@ const writeTimeoutProject = async (root: string): Promise => { symlink(join(agentBundleNodeModules, '@modelcontextprotocol'), join(root, 'node_modules', '@modelcontextprotocol'), 'dir'), ]); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'server.ts'), [ "import { McpServer } from '@modelcontextprotocol/server';", "import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';", @@ -34,7 +34,7 @@ const writeTimeoutProject = async (root: string): Promise => { '', 'export default defineConfig({', " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", - " plugin: { name: 'timeout-e2e-fixture', version: '1.0.0' },", + " plugin: { name: 'timeout-e2e-fixture' },", " targets: ['portable'],", '});', '', diff --git a/packages/workbench/tests/overview.e2e.test.ts b/packages/workbench/tests/overview.e2e.test.ts index 074d2b8b9..cdf0a375c 100644 --- a/packages/workbench/tests/overview.e2e.test.ts +++ b/packages/workbench/tests/overview.e2e.test.ts @@ -33,7 +33,7 @@ const writeMcpPlaygroundProject = async (root: string): Promise => { symlink(join(workbenchNodeModules, 'zod'), join(root, 'node_modules', 'zod'), 'dir'), ]); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'src', 'server.ts'), [ "import { McpServer } from '@modelcontextprotocol/server';", "import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';", @@ -61,7 +61,7 @@ const writeMcpPlaygroundProject = async (root: string): Promise => { ' codex: {},', " mcp: { servers: { fixture: { entry: './src/server.ts', env: { NO_COLOR: '1', SECRET_TOKEN: 'fixture-secret' } } } },", " portable: { fixtureMarker: 'artifact-extension-initial' },", - " plugin: { name: 'workbench-mcp-fixture', version: '1.0.0' },", + " plugin: { name: 'workbench-mcp-fixture' },", " skills: ['src/skills/review'],", " targets: ['portable'],", '});', diff --git a/packages/workbench/tests/streaming-render.e2e.test.ts b/packages/workbench/tests/streaming-render.e2e.test.ts index 91c94f71f..311ecdff0 100644 --- a/packages/workbench/tests/streaming-render.e2e.test.ts +++ b/packages/workbench/tests/streaming-render.e2e.test.ts @@ -33,14 +33,14 @@ e2e('renders a compiled MCP tool\'s Suspense fallback before its gated child rel createProject: () => createProjectFixture({ config: [ 'export default {', - " plugin: { name: 'streaming-render-e2e', version: '1.0.0' },", + " plugin: { name: 'streaming-render-e2e' },", " targets: ['portable'],", '};', '', ].join('\n'), files: { ...gatedRouteFiles, - 'package.json': '{"dependencies":{"@agent-bundle/runtime":"workspace:*","react":"19.2.8","zod":"4.5.4"},"type":"module"}\n', + 'package.json': '{"dependencies":{"@agent-bundle/runtime":"workspace:*","react":"19.2.8","zod":"4.5.4"},"type":"module","version":"1.0.0"}\n', }, prefix: 'agent-bundle-streaming-render-e2e-', }), diff --git a/scripts/measure-hook-cold-start.mjs b/scripts/measure-hook-cold-start.mjs index a69da22de..cde6b8771 100644 --- a/scripts/measure-hook-cold-start.mjs +++ b/scripts/measure-hook-cold-start.mjs @@ -72,13 +72,13 @@ const measure = async () => { try { await mkdir(join(root, 'src', 'hooks'), { recursive: true }); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"0.0.0"}\n'), writeFile( join(root, 'agent-bundle.config.ts'), [ 'export default {', ' hooks: { sessionStart: { handler: "./src/hooks/session-start.ts" } },', - " plugin: { name: 'cold-start', version: '0.0.0' },", + " plugin: { name: 'cold-start' },", " targets: ['claude'],", '};', '', From ab38b35defaebb7c4a132d42a4e4f54366a70d52 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 09:20:59 +0000 Subject: [PATCH 04/11] docs: finish defineTool migration guidance --- website/docs/en/guide/authoring/mcp.mdx | 63 ++++++++++++------------- website/docs/zh/guide/authoring/mcp.mdx | 61 ++++++++++++------------ 2 files changed, 61 insertions(+), 63 deletions(-) diff --git a/website/docs/en/guide/authoring/mcp.mdx b/website/docs/en/guide/authoring/mcp.mdx index 5bbe20fb2..2641a614d 100644 --- a/website/docs/en/guide/authoring/mcp.mdx +++ b/website/docs/en/guide/authoring/mcp.mdx @@ -88,9 +88,10 @@ same way under both, `$defs` is reached through JSON-pointer `$ref`s under eithe 2020-12-only keyword such as `unevaluatedProperties` or `dependentRequired` is ignored by a lax draft-07 validator rather than misapplied. -Runtime schemas may be imported or built by factories. Optional literal `config.inputJsonSchema` -provides execution-free forms and CLI flag metadata; `tools/list` and runtime validation use the -actual schema. See [input metadata](./package-entries.mdx#share-one-schema-between-mcp-and-cli). +Runtime schemas may be imported or built by factories. The optional literal `inputJsonSchema` +property in `defineTool` provides execution-free forms and CLI flag metadata; `tools/list` and +runtime validation use the actual schema. See +[input metadata](./package-entries.mdx#share-one-schema-between-mcp-and-cli). A generated tool can also expose an idiomatic CLI command without a second route: a colocated `.cli.ts` projection module is a CLI surface projection of the same operation (`tool:/`), never a route. A route observes `kind: 'cli'` whenever it runs from @@ -98,16 +99,8 @@ the generated CLI executable, whichever projection mechanism produced the comman generated MCP server still passes `kind: 'tool'`. See [Project one tool as an idiomatic command](./package-entries.mdx#project-one-tool-as-an-idiomatic-command). -`ToolConfig` and `ToolRouteProps` are public types: - -```ts twoslash -import type { ToolConfig } from 'agent-bundle'; - -export const config = { - annotations: { readOnlyHint: true }, - description: 'Read runtime status.', -} satisfies ToolConfig; -``` +`ToolConfig` describes the metadata and execution options accepted by `defineTool`; +`ToolRouteProps` describes the component that `defineTool` produces. Both are public types. Call `await agent()` only when the route needs context. The handle exposes the invocation plus the `host`, `session`, `actor`, `workspace`, and `plugin` identity axes. Each axis is *observed*: a @@ -296,11 +289,12 @@ framework's streaming progress surface on MCP, in the rendered CLI, and in the W // src/mcp/curator/tools/search.tsx import React, { Suspense } from 'react'; import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; -export default async function Search( - { input, signal }: ToolRouteProps, -) { - return ( +export default defineTool({ + inputSchema, + resultSchema, +}, async (input, { signal }) => ( @@ -308,8 +302,7 @@ export default async function Search( > - ); -} +)); ``` On MCP, the projector reads the `Agent.Progress` node out of each streamed `shell` and `replace` @@ -343,21 +336,23 @@ that sets `_meta.progressToken` on `callTool` receives them over the real protoc Every call renders inside one render session bounded by the runtime's default limits, and the wall-clock one is 60 seconds (`DEFAULT_AGENT_RENDER_LIMITS.maxElapsedMs`). A route whose legitimate work outlives that — awaiting a build, long-polling a daemon — declares its own budget -in `config.render` instead of clamping its waits and asking the caller to poll: +in `defineTool`'s `render` option instead of clamping its waits and asking the caller to poll: -```ts twoslash -import type { ToolConfig } from 'agent-bundle'; +```ts +import { defineTool } from 'agent-bundle/routes'; -export const config = { +export default defineTool({ annotations: { readOnlyHint: true }, description: 'Wait for a ticket to settle.', + inputSchema, render: { maxElapsedMs: 7_200_000 }, -} satisfies ToolConfig; + resultSchema, +}, handler); ``` `maxElapsedMs` is a positive integer of milliseconds up to the framework ceiling of 24 hours (`MAX_ROUTE_RENDER_ELAPSED_MS`, exported from `agent-bundle`); anything else is `AB4835` at build -time. The compiler reads the value statically with the rest of `config`, so the generated MCP +time. The compiler reads the value statically with the rest of the `defineTool` options, so the generated MCP server applies it to that route's render session on every `tools/call`, `resources/read`, and `prompts/get`; a `routes.mcpCommands` projection of the tool and a rendered `src/cli/**` command carry the same key on the compiled command (`config.render` on `CliRouteConfig`; a plain `.ts` @@ -380,17 +375,19 @@ The render budget bounds how long a render may run; it does not change that an o `tools/call` blocks the client until the render ends. The MCP `2025-11-25` Tasks utility is the complementary mechanism: a client that asks for task-augmented execution gets a task handle at once and collects the result later, so its own request deadline stops mattering for the render. A -tool route opts in with `config.execution.taskSupport`, the tool's `execution` block as +tool route opts in with `defineTool`'s `execution.taskSupport`, the tool's `execution` block as `tools/list` advertises it: -```ts twoslash -import type { ToolConfig } from 'agent-bundle'; +```ts +import { defineTool } from 'agent-bundle/routes'; -export const config = { +export default defineTool({ annotations: { readOnlyHint: true }, description: 'Convert the selected audiobook.', execution: { taskSupport: 'optional' }, -} satisfies ToolConfig; + inputSchema, + resultSchema, +}, handler); ``` `taskSupport` is `'forbidden'` (the wire default when the key is absent: every call is an @@ -505,7 +502,9 @@ shell, `_meta`, `structuredContent`, and the `[code] message` text all survive, carries `isError: true`: ```tsx -export default async function Lookup({ input }: ToolRouteProps) { +import { defineTool } from 'agent-bundle/routes'; + +export default defineTool({ inputSchema, resultSchema }, async (input) => { const hit = await catalog.find(input.id); if (hit === undefined) { return ( @@ -515,7 +514,7 @@ export default async function Lookup({ input }: ToolRouteProps, -) { - return ( +export default defineTool({ + inputSchema, + resultSchema, +}, async (input, { signal }) => ( @@ -271,8 +264,7 @@ export default async function Search( > - ); -} +)); ``` 在 MCP 上,投影器会从每个流式传输的 `shell` 与 `replace` 文档中读取 `Agent.Progress` 节点,并向请求携带了 @@ -300,20 +292,23 @@ route-unit 与 `mcp-in-memory` 两个测试层级无需宿主即可证明这一 每次调用都在一个受运行时默认上限约束的渲染会话中进行,其中挂钟上限是 60 秒 (`DEFAULT_AGENT_RENDER_LIMITS.maxElapsedMs`)。一条合理工作时间超过这一上限的路由——等待一次构建、 -对守护进程长轮询——应在 `config.render` 中声明自己的预算,而不是截短自己的等待再让调用方反复轮询: +对守护进程长轮询——应在 `defineTool` 的 `render` 选项中声明自己的预算,而不是截短自己的等待再让 +调用方反复轮询: -```ts twoslash -import type { ToolConfig } from 'agent-bundle'; +```ts +import { defineTool } from 'agent-bundle/routes'; -export const config = { +export default defineTool({ annotations: { readOnlyHint: true }, description: 'Wait for a ticket to settle.', + inputSchema, render: { maxElapsedMs: 7_200_000 }, -} satisfies ToolConfig; + resultSchema, +}, handler); ``` `maxElapsedMs` 是一个以毫秒计的正整数,上限为框架天花板 24 小时(从 `agent-bundle` 导出的 -`MAX_ROUTE_RENDER_ELAPSED_MS`);其他任何值都会在构建时报 `AB4835`。编译器随 `config` 的其余部分静态读取 +`MAX_ROUTE_RENDER_ELAPSED_MS`);其他任何值都会在构建时报 `AB4835`。编译器随 `defineTool` 的其余选项静态读取 该值,因此生成式 MCP 服务器会在该路由的每次 `tools/call`、`resources/read` 与 `prompts/get` 上把它应用到 渲染会话;该工具的 `routes.mcpCommands` 投影以及渲染式 `src/cli/**` 命令在编译后的命令上携带同一个键 (`CliRouteConfig` 上的 `config.render`;纯 `.ts` 命令没有渲染会话,会拒绝它)。`renderRoute` 与 @@ -331,17 +326,19 @@ Claude Code 默认的单次调用挂钟约为 28 小时(`MCP_TOOL_TIMEOUT`, 渲染预算约束的是一次渲染最多能跑多久;它并不改变普通 `tools/call` 会一直阻塞客户端直到渲染结束这一点。 MCP `2025-11-25` 的 Tasks 工具是与之互补的机制:请求任务增强执行的客户端会立刻拿到一个任务句柄,稍后再 -取结果,于是它自身的请求期限不再影响渲染。工具路由通过 `config.execution.taskSupport` 选择加入,这正是 +取结果,于是它自身的请求期限不再影响渲染。工具路由通过 `defineTool` 的 `execution.taskSupport` 选择加入,这正是 `tools/list` 宣告的工具 `execution` 块: -```ts twoslash -import type { ToolConfig } from 'agent-bundle'; +```ts +import { defineTool } from 'agent-bundle/routes'; -export const config = { +export default defineTool({ annotations: { readOnlyHint: true }, description: 'Convert the selected audiobook.', execution: { taskSupport: 'optional' }, -} satisfies ToolConfig; + inputSchema, + resultSchema, +}, handler); ``` `taskSupport` 取 `'forbidden'`(省略该键时的线上默认值:每次调用都是普通请求)、`'optional'`(客户端 @@ -441,7 +438,9 @@ export default function Layout({ children, route }: AgentLayoutProps) { `[code] message` 文本都得以保留,线上结果携带 `isError: true`: ```tsx -export default async function Lookup({ input }: ToolRouteProps) { +import { defineTool } from 'agent-bundle/routes'; + +export default defineTool({ inputSchema, resultSchema }, async (input) => { const hit = await catalog.find(input.id); if (hit === undefined) { return ( @@ -451,7 +450,7 @@ export default async function Lookup({ input }: ToolRouteProps Date: Wed, 16 Sep 2026 09:53:24 +0000 Subject: [PATCH 05/11] test: finish legacy route fixture migrations --- packages/agent-bundle/tests/api.test.ts | 4 ++ packages/agent-bundle/tests/cli.test.ts | 4 +- .../agent-bundle/tests/dev-workbench.test.ts | 8 +++- .../tests/generated-route-server.test.ts | 39 ++++++++++----- .../tests/helpers/gated-routes.ts | 5 +- .../tests/helpers/project-fixture.ts | 13 ++++- .../tests/helpers/workspace-paths.ts | 1 + .../agent-bundle/tests/layout-build.test.ts | 20 +++++--- .../tests/provider-typegen.test.ts | 2 +- .../agent-bundle/tests/public-api.test.ts | 2 +- .../tests/route-caller-input-types.test.ts | 28 ++++++++--- .../tests/route-contract-imports.test.ts | 23 ++++++--- .../tests/route-invocation-dev-server.test.ts | 47 ++++++++++++------- .../tests/route-register-typegen.test.ts | 6 +-- .../tests/support/dev-contract-project.ts | 10 ++-- .../tests/trace-dev-server.test.ts | 4 +- .../workbench-surface-dev-server.test.ts | 4 +- .../workbench/tests/mcp-app-real.e2e.test.ts | 17 ++++--- .../tests/streaming-render.e2e.test.ts | 4 +- 19 files changed, 164 insertions(+), 77 deletions(-) diff --git a/packages/agent-bundle/tests/api.test.ts b/packages/agent-bundle/tests/api.test.ts index 27f15d7a0..3126a9c49 100644 --- a/packages/agent-bundle/tests/api.test.ts +++ b/packages/agent-bundle/tests/api.test.ts @@ -1553,6 +1553,7 @@ it('returns an output-independent project context without absolute project paths 'configDigest', 'configPath', 'modelDigest', + 'packageVersion', 'revision', 'sourceInputs', ]); @@ -1840,6 +1841,7 @@ it('builds conventional src/scripts modules beside explicit entries', async () = const root = join(parent, 'project with spaces'); await mkdir(join(root, 'src', 'scripts'), { recursive: true }); await Promise.all([ + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile( join(root, 'agent-bundle.config.ts'), [ @@ -1940,6 +1942,7 @@ it('copies every supported top-level script output suffix byte-for-byte with sou const output = join(root, 'artifact'); await mkdir(join(root, 'src'), { recursive: true }); await Promise.all([ + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile( join(root, 'agent-bundle.config.ts'), [ @@ -2039,6 +2042,7 @@ it('canonicalizes copied script extensions in emitted artifact paths', async () const output = join(root, 'artifact'); await mkdir(join(root, 'src'), { recursive: true }); await Promise.all([ + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile(join(root, 'agent-bundle.config.ts'), [ 'export default {', " plugin: { name: 'uppercase-script-fixture' },", diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index db3c4522e..b80992b60 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -80,7 +80,7 @@ const createCliProject = async ( await mkdir(join(root, 'src', 'skills', 'review'), { recursive: true }); if (mcpApp) await mkdir(join(root, 'views'), { recursive: true }); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile( join(root, 'agent-bundle.config.ts'), [ @@ -118,7 +118,7 @@ const createServiceProject = async (): Promise => { const root = join(parent, 'project with spaces'); await mkdir(join(root, 'src'), { recursive: true }); await Promise.all([ - writeFile(join(root, 'package.json'), '{"type":"module"}\n'), + writeFile(join(root, 'package.json'), '{"type":"module","version":"1.0.0"}\n'), writeFile( join(root, 'agent-bundle.config.ts'), [ diff --git a/packages/agent-bundle/tests/dev-workbench.test.ts b/packages/agent-bundle/tests/dev-workbench.test.ts index c1323b715..d1a568552 100644 --- a/packages/agent-bundle/tests/dev-workbench.test.ts +++ b/packages/agent-bundle/tests/dev-workbench.test.ts @@ -588,7 +588,13 @@ it('publishes routes created and deleted below source roots absent at startup', await mkdir(join(project.root, 'src', 'mcp', 'probe', 'tools'), { recursive: true }); await writeFile( toolPath, - 'export const inputSchema = {}; export const resultSchema = {}; export default async () => undefined;\n', + [ + "import { defineTool } from 'agent-bundle/routes';", + 'export const inputSchema = {};', + 'export const resultSchema = {};', + 'export default defineTool({ inputSchema, resultSchema }, async () => undefined);', + '', + ].join('\n'), ); await publication(3); const withTool = await activeManifest(); diff --git a/packages/agent-bundle/tests/generated-route-server.test.ts b/packages/agent-bundle/tests/generated-route-server.test.ts index cd5e02bff..f8b9b252c 100644 --- a/packages/agent-bundle/tests/generated-route-server.test.ts +++ b/packages/agent-bundle/tests/generated-route-server.test.ts @@ -362,15 +362,19 @@ const generatedAppClientHtml = '
waiti
 
 const generatedPingTool = [
   "import { Agent } from '@agent-bundle/runtime';",
-  "import { appResourceUri } from 'agent-bundle/routes';",
+  "import { appResourceUri, defineTool } from 'agent-bundle/routes';",
   "import { createElement } from 'react';",
   "import { z } from 'zod';",
-  "export const config = { _meta: { ui: { resourceUri: appResourceUri('dashboard') } }, description: 'Ping from the App.' };",
   'export const inputSchema = z.object({ note: z.string() }).strict();',
   'export const resultSchema = z.object({ note: z.string() }).strict();',
-  'export default async function Ping({ input }: { input: z.infer }) {',
+  'export default defineTool({',
+  "  _meta: { ui: { resourceUri: appResourceUri('dashboard') } },",
+  "  description: 'Ping from the App.',",
+  '  inputSchema,',
+  '  resultSchema,',
+  '}, async (input) => {',
   "  return createElement(Agent.Result, { value: { note: input.note } }, createElement(Agent.Text, null, input.note));",
-  '}',
+  '});',
   '',
 ].join('\n');
 
@@ -962,14 +966,25 @@ it('streams an authored Suspense fallback to the stdio client before the render
 it('projects thrown route errors as the SDK tool error or a JSON-RPC error, never as a layout-wrapped document', { retry: 2, timeout: 60_000 }, async () => {
   const root = await mkdtemp(join(tmpdir(), 'agent-bundle-generated-thrown-'));
   roots.push(root);
-  const throwing = (kind: string) => [
-    "import { z } from 'zod';",
-    `export const config = ${kind === 'resource' ? "{ mimeType: 'text/plain', uri: 'curator://broken' }" : "{ description: 'Throws.' }"};`,
-    `export const inputSchema = ${kind === 'resource' ? 'z.object({ uri: z.string() })' : 'z.object({}).strict()'};`,
-    'export const resultSchema = z.object({}).passthrough();',
-    `export default async function Broken() { throw new Error('${kind} route threw'); }`,
-    '',
-  ].join('\n');
+  const throwing = (kind: string) => kind === 'tool'
+    ? [
+        "import { defineTool } from 'agent-bundle/routes';",
+        "import { z } from 'zod';",
+        'export const inputSchema = z.object({}).strict();',
+        'export const resultSchema = z.object({}).passthrough();',
+        "export default defineTool({ description: 'Throws.', inputSchema, resultSchema }, async () => {",
+        "  throw new Error('tool route threw');",
+        '});',
+        '',
+      ].join('\n')
+    : [
+        "import { z } from 'zod';",
+        `export const config = ${kind === 'resource' ? "{ mimeType: 'text/plain', uri: 'curator://broken' }" : "{ description: 'Throws.' }"};`,
+        `export const inputSchema = ${kind === 'resource' ? 'z.object({ uri: z.string() })' : 'z.object({}).strict()'};`,
+        'export const resultSchema = z.object({}).passthrough();',
+        `export default async function Broken() { throw new Error('${kind} route threw'); }`,
+        '',
+      ].join('\n');
   await writeGeneratedProject(root, {
     // A layout that would stamp `_meta` on every document, to show it is absent when the route throws.
     'src/layout.tsx': [
diff --git a/packages/agent-bundle/tests/helpers/gated-routes.ts b/packages/agent-bundle/tests/helpers/gated-routes.ts
index b39804163..6f17833b0 100644
--- a/packages/agent-bundle/tests/helpers/gated-routes.ts
+++ b/packages/agent-bundle/tests/helpers/gated-routes.ts
@@ -18,6 +18,7 @@ export const gatedRouteFiles: Readonly> = Object.freeze({
     '',
   ].join('\n'),
   'src/mcp/status/tools/live.tsx': [
+    "import { defineTool } from 'agent-bundle/routes';",
     "import { Agent } from '@agent-bundle/runtime';",
     "import { createElement, Suspense } from 'react';",
     "import { z } from 'zod';",
@@ -30,9 +31,9 @@ export const gatedRouteFiles: Readonly> = Object.freeze({
     "  return createElement(Agent.Text, null, 'stream complete');",
     '};',
     '',
-    'export default async function Live({ input, signal }) {',
+    'export default defineTool({ inputSchema, resultSchema }, async (input, { signal }) => {',
     "  return createElement(Agent.Result, { value: { done: true } }, createElement(Suspense, { fallback: createElement(Agent.Progress, { completed: 0, message: 'streaming', total: 1 }) }, createElement(Slow, { gate: input.gate, signal })));",
-    '}',
+    '});',
     '',
   ].join('\n'),
 });
diff --git a/packages/agent-bundle/tests/helpers/project-fixture.ts b/packages/agent-bundle/tests/helpers/project-fixture.ts
index 1d4b39c1a..38d969552 100644
--- a/packages/agent-bundle/tests/helpers/project-fixture.ts
+++ b/packages/agent-bundle/tests/helpers/project-fixture.ts
@@ -30,6 +30,10 @@ const sourceEntryPoint = resolve(
   process.cwd(),
   'packages/agent-bundle/src/core/types.ts',
 );
+const routesEntryPoint = resolve(
+  process.cwd(),
+  'packages/agent-bundle/src/routes/definitions.ts',
+);
 
 export const createProjectFixture = async (
   options: ProjectFixtureOptions = {},
@@ -100,13 +104,20 @@ export const createProjectFixture = async (
       JSON.stringify({
         name: 'agent-bundle',
         type: 'module',
-        exports: './index.ts',
+        exports: {
+          '.': './index.ts',
+          './routes': './routes.ts',
+        },
       }),
     ),
     writeFile(
       join(root, 'node_modules/agent-bundle/index.ts'),
       `export { defineConfig } from ${JSON.stringify(sourceEntryPoint)};\n`,
     ),
+    writeFile(
+      join(root, 'node_modules/agent-bundle/routes.ts'),
+      `export { defineTool } from ${JSON.stringify(routesEntryPoint)};\n`,
+    ),
     writeFile(
       configPath,
       [
diff --git a/packages/agent-bundle/tests/helpers/workspace-paths.ts b/packages/agent-bundle/tests/helpers/workspace-paths.ts
index 32e62dced..78c19cca6 100644
--- a/packages/agent-bundle/tests/helpers/workspace-paths.ts
+++ b/packages/agent-bundle/tests/helpers/workspace-paths.ts
@@ -4,5 +4,6 @@ const workspaceRoot = join(import.meta.dirname, '..', '..', '..', '..');
 
 export const agentBundlePackageRoot = join(workspaceRoot, 'packages', 'agent-bundle');
 export const agentBundleNodeModules = join(agentBundlePackageRoot, 'node_modules');
+export const exampleNodeModules = join(workspaceRoot, 'examples', 'audiobook-curator', 'node_modules');
 export const workbenchNodeModules = join(workspaceRoot, 'packages', 'workbench', 'node_modules');
 export const workspaceNodeModules = join(workspaceRoot, 'node_modules');
diff --git a/packages/agent-bundle/tests/layout-build.test.ts b/packages/agent-bundle/tests/layout-build.test.ts
index 134b93645..2bad91528 100644
--- a/packages/agent-bundle/tests/layout-build.test.ts
+++ b/packages/agent-bundle/tests/layout-build.test.ts
@@ -24,27 +24,35 @@ const writeProjectFile = async (root: string, path: string, contents: string): P
 };
 
 const lookupRoute = [
+  "import { defineTool } from 'agent-bundle/routes';",
   "import { Agent, agent } from '@agent-bundle/runtime';",
   "import { z } from 'zod';",
-  "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.' };",
   'export const inputSchema = z.object({ message: z.string().default("ready") }).strict();',
   "export const resultSchema = z.object({ invocation: z.enum(['cli', 'tool']), message: z.string() }).strict();",
-  'export default async function Lookup({ input }) {',
+  'export default defineTool({',
+  "  inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"message\":{\"type\":\"string\",\"default\":\"ready\"}},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Looks up one value.',",
+  '  inputSchema,',
+  '  resultSchema,',
+  '}, async (input) => {',
   '  const context = await agent();',
   '  const result = { invocation: context.invocation.kind, message: input.message };',
   '  return {`Lookup: ${input.message}`};',
-  '}',
+  '});',
   '',
 ].join('\n');
 
 const explodeRoute = [
+  "import { defineTool } from 'agent-bundle/routes';",
   "import { z } from 'zod';",
-  "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Throws before rendering.' };",
   'export const inputSchema = z.object({}).strict();',
   'export const resultSchema = z.object({ ok: z.boolean() }).strict();',
-  'export default async function Explode() {',
+  'export default defineTool({',
+  "  inputJsonSchema: {\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Throws before rendering.',",
+  '  inputSchema,',
+  '  resultSchema,',
+  '}, async () => {',
   "  throw new Error('lookup exploded');",
-  '}',
+  '});',
   '',
 ].join('\n');
 
diff --git a/packages/agent-bundle/tests/provider-typegen.test.ts b/packages/agent-bundle/tests/provider-typegen.test.ts
index f22e8b3a1..4cde18ecd 100644
--- a/packages/agent-bundle/tests/provider-typegen.test.ts
+++ b/packages/agent-bundle/tests/provider-typegen.test.ts
@@ -95,7 +95,7 @@ it('types await context.provider(key) from the generated provider declarations',
     "export default defineTool({",
     "  inputSchema,",
     "  resultSchema,",
-    "}, async () => { return { status: 'ready' as const }; });",
+    "}, async () => undefined);",
     ].join('\n')),
     writeProjectFile(root, 'assertions.ts', [
       "import { agent } from '@agent-bundle/runtime';",
diff --git a/packages/agent-bundle/tests/public-api.test.ts b/packages/agent-bundle/tests/public-api.test.ts
index b1bc1fe9b..79451294c 100644
--- a/packages/agent-bundle/tests/public-api.test.ts
+++ b/packages/agent-bundle/tests/public-api.test.ts
@@ -79,7 +79,7 @@ const createBuildProject = async (root: string): Promise<{ readonly output: stri
   const output = join(project, 'manifest-version-artifact');
   await mkdir(join(project, 'src', 'skills', 'review'), { recursive: true });
   await Promise.all([
-    writeFile(join(project, 'package.json'), '{"type":"module"}\n'),
+    writeFile(join(project, 'package.json'), '{"type":"module","version":"1.0.0"}\n'),
     writeFile(
       join(project, 'agent-bundle.config.ts'),
       "export default { plugin: { name: 'manifest-version-fixture' }, targets: ['portable'] };\n",
diff --git a/packages/agent-bundle/tests/route-caller-input-types.test.ts b/packages/agent-bundle/tests/route-caller-input-types.test.ts
index a83faa49a..b0eddb642 100644
--- a/packages/agent-bundle/tests/route-caller-input-types.test.ts
+++ b/packages/agent-bundle/tests/route-caller-input-types.test.ts
@@ -8,6 +8,11 @@ import { afterEach, expect, it } from '@rstest/core';
 import ts from 'typescript-5';
 
 import { build, validate } from '../src/api.ts';
+import {
+  agentBundleNodeModules,
+  agentBundlePackageRoot,
+  workspaceNodeModules,
+} from './helpers/workspace-paths.ts';
 
 const roots: string[] = [];
 
@@ -30,19 +35,22 @@ const equality = [
 
 /** The tool records every invocation it actually ran, so a rejected input provably ran nothing. */
 const tool = (name: string, schema: string, result: string, body: string): string => [
+  "import { defineTool } from 'agent-bundle/routes';",
   "import { Agent } from '@agent-bundle/runtime';",
-  "import type { ToolRouteProps } from 'agent-bundle';",
   "import { appendFile } from 'node:fs/promises';",
   "import { createElement } from 'react';",
   "import { z } from 'zod';",
-  `export const config = { description: '${name}' };`,
   `export const inputSchema = ${schema};`,
   `export const resultSchema = ${result};`,
-  `export default async function Route({ input }: ToolRouteProps) {`,
+  'export default defineTool({',
+  `  description: '${name}',`,
+  '  inputSchema,',
+  '  resultSchema,',
+  '}, async (input) => {',
   `  await appendFile(process.env['INVOCATIONS']!, JSON.stringify({ input, tool: '${name}' }) + '\\n');`,
   `  const value = ${body};`,
   "  return createElement(Agent.Result, { value }, createElement(Agent.Text, null, JSON.stringify(value)));",
-  '}',
+  '});',
   '',
 ].join('\n');
 
@@ -57,7 +65,7 @@ const tsconfig = (include: readonly string[], lib: readonly string[]): string =>
     skipLibCheck: true,
     strict: true,
     target: 'ES2022',
-    types: [],
+    types: ['node'],
   },
   include,
 }, null, 2)}\n`;
@@ -98,7 +106,15 @@ const callTool = async (client: Client, name: string, input: Record {
   const root = await mkdtemp(join(tmpdir(), 'agent-bundle-caller-input-types-'));
   roots.push(root);
-  await symlink(join(process.cwd(), 'examples', 'audiobook-curator', 'node_modules'), join(root, 'node_modules'), 'dir');
+  await mkdir(join(root, 'node_modules', '@types'), { recursive: true });
+  await Promise.all([
+    symlink(join(agentBundleNodeModules, '@agent-bundle'), join(root, 'node_modules', '@agent-bundle'), 'dir'),
+    symlink(join(workspaceNodeModules, '@types', 'node'), join(root, 'node_modules', '@types', 'node')),
+    symlink(join(agentBundleNodeModules, '@types', 'react'), join(root, 'node_modules', '@types', 'react')),
+    symlink(join(agentBundleNodeModules, 'react'), join(root, 'node_modules', 'react'), 'dir'),
+    symlink(agentBundlePackageRoot, join(root, 'node_modules', 'agent-bundle'), 'dir'),
+    symlink(join(agentBundleNodeModules, 'zod'), join(root, 'node_modules', 'zod'), 'dir'),
+  ]);
   const invocations = join(root, 'invocations.ndjson');
   await Promise.all([
     writeProjectFile(root, 'package.json', JSON.stringify({
diff --git a/packages/agent-bundle/tests/route-contract-imports.test.ts b/packages/agent-bundle/tests/route-contract-imports.test.ts
index 196dfbbf9..a11ec651d 100644
--- a/packages/agent-bundle/tests/route-contract-imports.test.ts
+++ b/packages/agent-bundle/tests/route-contract-imports.test.ts
@@ -104,31 +104,40 @@ const cliRoute = (variant: FixtureVariant): string => variant === 'inline'
 
 const toolRoute = (variant: FixtureVariant): string => variant === 'inline'
   ? [
+      "import { defineTool } from 'agent-bundle/routes';",
       "import { Agent } from '@agent-bundle/runtime';",
       "import { z } from 'zod';",
       '',
-      "export const config = { inputJsonSchema: { type: 'object', additionalProperties: false, properties: { laneKey: { type: 'string' }, limit: { type: 'number', description: 'Recent rows' }, statuses: { type: 'array', items: { type: 'string', enum: ['requested', 'queued', 'running', 'done'] } }, tickets: { type: 'array', items: { type: 'string' } } } }, description: 'Show the queue.' };",
       `export const inputSchema = ${inlineInputSchema};`,
       "export const resultSchema = z.object({ filters: inputSchema, operation: z.literal('status') });",
-      'export default async function HaulerStatus({ input }: { input: z.infer }) {',
+      'export default defineTool({',
+      "  inputJsonSchema: { type: 'object', additionalProperties: false, properties: { laneKey: { type: 'string' }, limit: { type: 'number', description: 'Recent rows' }, statuses: { type: 'array', items: { type: 'string', enum: ['requested', 'queued', 'running', 'done'] } }, tickets: { type: 'array', items: { type: 'string' } } } },",
+      "  description: 'Show the queue.',",
+      '  inputSchema,',
+      '  resultSchema,',
+      '}, async (input) => {',
       "  const result = { filters: input, operation: 'status' };",
       '  return Queue status.;',
-      '}',
+      '});',
       '',
     ].join('\n')
   : [
+      "import { defineTool } from 'agent-bundle/routes';",
       "import { Agent } from '@agent-bundle/runtime';",
-      "import type { z } from 'zod';",
       '',
       "import { statusInputSchema, statusResultSchema } from '../../../lib/protocol-schemas.js';",
       '',
-      "export const config = { inputJsonSchema: { type: 'object', additionalProperties: false, properties: { laneKey: { type: 'string' }, limit: { type: 'number', description: 'Recent rows' }, statuses: { type: 'array', items: { type: 'string', enum: ['requested', 'queued', 'running', 'done'] } }, tickets: { type: 'array', items: { type: 'string' } } } }, description: 'Show the queue.' };",
       'export const inputSchema = statusInputSchema;',
       'export const resultSchema = statusResultSchema;',
-      'export default async function HaulerStatus({ input }: { input: z.infer }) {',
+      'export default defineTool({',
+      "  inputJsonSchema: { type: 'object', additionalProperties: false, properties: { laneKey: { type: 'string' }, limit: { type: 'number', description: 'Recent rows' }, statuses: { type: 'array', items: { type: 'string', enum: ['requested', 'queued', 'running', 'done'] } }, tickets: { type: 'array', items: { type: 'string' } } } },",
+      "  description: 'Show the queue.',",
+      '  inputSchema,',
+      '  resultSchema,',
+      '}, async (input) => {',
       "  const result = { filters: input, operation: 'status' };",
       '  return Queue status.;',
-      '}',
+      '});',
       '',
     ].join('\n');
 
diff --git a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts
index f7d16e0fb..69e792b57 100644
--- a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts
+++ b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts
@@ -24,7 +24,7 @@ import { createWorkbenchAssetSource } from '../src/dev/workbench-assets.ts';
 import { startDevServer } from '../src/dev/workbench-server.ts';
 import { gatedRouteFiles } from './helpers/gated-routes.ts';
 import { createProjectFixture } from './helpers/project-fixture.ts';
-import { agentBundleNodeModules } from './helpers/workspace-paths.ts';
+import { exampleNodeModules } from './helpers/workspace-paths.ts';
 import { replaceWatchedSourceAndAwaitRebuild } from './support/watched-files.ts';
 import { runNodeScript } from './support/run-node-script.ts';
 
@@ -461,7 +461,7 @@ it('invokes compiled tool and event routes through the foreground server', { tim
   let server: Awaited> | undefined;
   await mkdir(assetsRoot, { recursive: true });
   await Promise.all([
-    symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir'),
+    symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir'),
     writeFile(join(assetsRoot, 'index.html'), 'Route invocation'),
   ]);
   try {
@@ -1394,20 +1394,25 @@ it('invokes compiled tool and event routes through the foreground server', { tim
       project.root,
       reportRoutePath,
       [
+        "import { defineTool } from 'agent-bundle/routes';",
         "import { Agent } from '@agent-bundle/runtime';",
         "import { createElement } from 'react';",
         "import { z } from 'zod';",
         "import './missing.js';",
         '',
-        "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.' };",
-        "export const config = { inputJsonSchema: { type: 'object', additionalProperties: false, properties: { service: { type: 'string' } }, required: ['service'] } };",
         "export const inputSchema = z.object({ service: z.string().min(1) }).strict();",
         'export const resultSchema = z.object({ service: z.string() }).strict();',
         '',
-        'export default async function Report({ input }) {',
+        'export default defineTool({',
+        "  inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"},",
+        "  annotations: { readOnlyHint: true },",
+        "  description: 'Reports one service.',",
+        '  inputSchema,',
+        '  resultSchema,',
+        '}, async (input) => {',
         '  const service = `rebuilt-${input.service}`;',
         '  return createElement(Agent.Result, { value: { service } }, createElement(Agent.Text, null, `Service ${service}`));',
-        '}',
+        '});',
         '',
       ].join('\n'),
       { timeoutMs: 20_000 },
@@ -1439,18 +1444,24 @@ it('invokes compiled tool and event routes through the foreground server', { tim
       project.root,
       reportRoutePath,
       [
+        "import { defineTool } from 'agent-bundle/routes';",
         "import { Agent } from '@agent-bundle/runtime';",
         "import { createElement } from 'react';",
         "import { z } from 'zod';",
         '',
-        "export const config = { inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"}, annotations: { readOnlyHint: true }, description: 'Reports one service.' };",
         "export const inputSchema = z.object({ service: z.string().min(1), source: z.string().optional() }).strict();",
         'export const resultSchema = z.object({ service: z.string() }).strict();',
         '',
-        'export default async function Report({ input }) {',
+        'export default defineTool({',
+        "  inputJsonSchema: {\"additionalProperties\":false,\"properties\":{\"service\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"}},\"required\":[\"service\"],\"type\":\"object\"},",
+        "  annotations: { readOnlyHint: true },",
+        "  description: 'Reports one service.',",
+        '  inputSchema,',
+        '  resultSchema,',
+        '}, async (input) => {',
         '  const service = `rebuilt-${input.service}`;',
         '  return createElement(Agent.Result, { value: { service } }, createElement(Agent.Text, null, `Service ${service}`));',
-        '}',
+        '});',
         '',
       ].join('\n'),
       { timeoutMs: 20_000 },
@@ -1509,7 +1520,7 @@ it('fails closed when a valid host is ineligible for the compiled event route',
   let server: Awaited> | undefined;
   await mkdir(assetsRoot, { recursive: true });
   await Promise.all([
-    symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir'),
+    symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir'),
     writeFile(join(assetsRoot, 'index.html'), 'Route invocation host binding'),
   ]);
   try {
@@ -1628,7 +1639,7 @@ it('enforces compiled handler, MCP schemas, and operator env across production s
   let server: Awaited> | undefined;
   await mkdir(assetsRoot, { recursive: true });
   await Promise.all([
-    symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir'),
+    symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir'),
     writeFile(join(assetsRoot, 'index.html'), 'Route parity'),
   ]);
   try {
@@ -1761,7 +1772,7 @@ it('publishes invocation routes only after a successful initial or recovered bui
   let server: Awaited> | undefined;
   await mkdir(assetsRoot, { recursive: true });
   await Promise.all([
-    symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir'),
+    symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir'),
     writeFile(join(assetsRoot, 'index.html'), 'Route publication gate'),
   ]);
   try {
@@ -1805,6 +1816,7 @@ it('publishes invocation routes only after a successful initial or recovered bui
       project.root,
       reportRoutePath,
       [
+        "import { defineTool } from 'agent-bundle/routes';",
         "import { Agent } from '@agent-bundle/runtime';",
         "import { createElement } from 'react';",
         "import { z } from 'zod';",
@@ -1812,9 +1824,9 @@ it('publishes invocation routes only after a successful initial or recovered bui
         'export const inputSchema = z.object({}).strict();',
         'export const resultSchema = z.object({ version: z.string() }).strict();',
         '',
-        'export default async function Report() {',
+        'export default defineTool({ inputSchema, resultSchema }, async () => {',
         "  return createElement(Agent.Result, { value: { version: 'published' } }, createElement(Agent.Text, null, 'Published route.'));",
-        '}',
+        '});',
         '',
       ].join('\n'),
       { timeoutMs: 10_000 },
@@ -1835,6 +1847,7 @@ it('publishes invocation routes only after a successful initial or recovered bui
     await server.close();
     server = undefined;
     await writeFile(reportRoutePath, [
+      "import { defineTool } from 'agent-bundle/routes';",
       "import { Agent } from '@agent-bundle/runtime';",
       "import { createElement } from 'react';",
       "import { z } from 'zod';",
@@ -1843,9 +1856,9 @@ it('publishes invocation routes only after a successful initial or recovered bui
       'export const inputSchema = z.object({}).strict();',
       'export const resultSchema = z.object({ version: z.string() }).strict();',
       '',
-      'export default async function Report() {',
+      'export default defineTool({ inputSchema, resultSchema }, async () => {',
       "  return createElement(Agent.Result, { value: { version: 'unpublished' } }, createElement(Agent.Text, null, 'Unpublished route.'));",
-      '}',
+      '});',
       '',
     ].join('\n'));
     server = await startDevServer({
@@ -1941,7 +1954,7 @@ it('bounds the render history a compiled child produces by count and bytes acros
   let server: Awaited> | undefined;
   await mkdir(assetsRoot, { recursive: true });
   await Promise.all([
-    symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir'),
+    symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir'),
     writeFile(join(assetsRoot, 'index.html'), 'Route invocation retention'),
   ]);
   try {
diff --git a/packages/agent-bundle/tests/route-register-typegen.test.ts b/packages/agent-bundle/tests/route-register-typegen.test.ts
index db01eacb4..128f4fa0a 100644
--- a/packages/agent-bundle/tests/route-register-typegen.test.ts
+++ b/packages/agent-bundle/tests/route-register-typegen.test.ts
@@ -99,7 +99,7 @@ it('types every route-aware public surface from the generated route registration
     "export default defineTool({",
     "  inputSchema,",
     "  resultSchema,",
-    "}, async () => { return { status: 'ready' as const }; });",
+    "}, async () => undefined);",
     ].join('\n')),
     writeProjectFile(root, 'src/mcp/curator/tools/find.ts', [
     "import { defineTool } from 'agent-bundle/routes';",
@@ -109,7 +109,7 @@ it('types every route-aware public surface from the generated route registration
     "export default defineTool({",
     "  inputSchema,",
     "  resultSchema,",
-    "}, async () => { return { hits: 1 }; });",
+    "}, async () => undefined);",
     ].join('\n')),
     // A second server registering the same tool name: the wire helpers see the union of both inputs.
     writeProjectFile(root, 'src/mcp/shelf/tools/find.ts', [
@@ -120,7 +120,7 @@ it('types every route-aware public surface from the generated route registration
     "export default defineTool({",
     "  inputSchema,",
     "  resultSchema,",
-    "}, async () => { return { shelved: true }; });",
+    "}, async () => undefined);",
     ].join('\n')),
     writeProjectFile(root, 'src/mcp/curator/prompts/brief.ts', [
       "import { z } from 'zod';",
diff --git a/packages/agent-bundle/tests/support/dev-contract-project.ts b/packages/agent-bundle/tests/support/dev-contract-project.ts
index 9a2e979c3..a7124d144 100644
--- a/packages/agent-bundle/tests/support/dev-contract-project.ts
+++ b/packages/agent-bundle/tests/support/dev-contract-project.ts
@@ -18,16 +18,20 @@ const fixtureNodeModules = join(import.meta.dirname, '..', '..', '..', '..', 'ex
 
 export const devContractToolSource = (version: string, projectRoot: string): string => [
   `// fixture: ${projectRoot}`,
+  "import { defineTool } from 'agent-bundle/routes';",
   "import { createElement } from 'react';",
   "import { z } from 'zod';",
   '',
-  "export const config = { description: 'Reports the generated epoch version.' };",
   'export const inputSchema = z.object({ token: z.string() });',
   'export const resultSchema = z.object({ version: z.string() });',
   '',
-  'export default async function Version() {',
+  'export default defineTool({',
+  "  description: 'Reports the generated epoch version.',",
+  '  inputSchema,',
+  '  resultSchema,',
+  '}, async () => {',
   `  return createElement('agent-result', { value: { version: ${JSON.stringify(version)} } }, createElement('agent-text', null, ${JSON.stringify(version)}));`,
-  '}',
+  '});',
   '',
 ].join('\n');
 
diff --git a/packages/agent-bundle/tests/trace-dev-server.test.ts b/packages/agent-bundle/tests/trace-dev-server.test.ts
index 2f6f09402..cc16f74cf 100644
--- a/packages/agent-bundle/tests/trace-dev-server.test.ts
+++ b/packages/agent-bundle/tests/trace-dev-server.test.ts
@@ -11,7 +11,7 @@ import type { EventTraceReceipt } from '../src/events/trace-receipt.ts';
 import { createWorkbenchAssetSource } from '../src/dev/workbench-assets.ts';
 import { startDevServer } from '../src/dev/workbench-server.ts';
 import { createProjectFixture } from './helpers/project-fixture.ts';
-import { agentBundleNodeModules } from './helpers/workspace-paths.ts';
+import { exampleNodeModules } from './helpers/workspace-paths.ts';
 import { replaceWatchedSourceAndAwaitRebuild } from './support/watched-files.ts';
 
 const runHook = (
@@ -74,7 +74,7 @@ it('serves replay and live trace entries and lowers build failures', { timeout:
   let trace: TraceHub | undefined;
   await mkdir(assetsRoot, { recursive: true });
   await Promise.all([
-    symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir'),
+    symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir'),
     writeFile(join(assetsRoot, 'index.html'), 'Trace'),
   ]);
   try {
diff --git a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts
index 4a0c0e137..be03232b4 100644
--- a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts
+++ b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts
@@ -9,7 +9,7 @@ import { createWorkbenchAssetSource } from '../src/dev/workbench-assets.ts';
 import { startDevServer } from '../src/dev/workbench-server.ts';
 import { inspectWorkbenchSurface, workbenchLeafPath } from '../src/test/index.ts';
 import { createProjectFixture } from './helpers/project-fixture.ts';
-import { agentBundleNodeModules } from './helpers/workspace-paths.ts';
+import { exampleNodeModules } from './helpers/workspace-paths.ts';
 
 /**
  * The workbench-surface level claims to hand a consumer exactly what the dev
@@ -79,7 +79,7 @@ it('matches the route manifest and lifecycle inventory a real dev server serves'
   let server: Awaited> | undefined;
   await mkdir(assetsRoot, { recursive: true });
   await Promise.all([
-    symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir'),
+    symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir'),
     writeFile(join(assetsRoot, 'index.html'), 'Workbench surface'),
   ]);
   try {
diff --git a/packages/workbench/tests/mcp-app-real.e2e.test.ts b/packages/workbench/tests/mcp-app-real.e2e.test.ts
index bd3ab21ce..957af1c97 100644
--- a/packages/workbench/tests/mcp-app-real.e2e.test.ts
+++ b/packages/workbench/tests/mcp-app-real.e2e.test.ts
@@ -217,19 +217,18 @@ const writeLifecycleServer = async (root: string): Promise => {
     writeFile(join(server, 'tools', 'show-lifecycle.tsx'), [
       "import { Agent } from '@agent-bundle/runtime';",
       "import React from 'react';",
-      "import type { ToolConfig, ToolRouteProps } from 'agent-bundle';",
-      "import { appResourceUri } from 'agent-bundle/routes';",
+      "import { appResourceUri, defineTool } from 'agent-bundle/routes';",
       "import { z } from 'zod';",
       '',
-      'export const config = {',
-      "  _meta: { ui: { resourceUri: appResourceUri('lifecycle') } },",
-      "  description: 'Waits as long as it is told, then succeeds or fails on request.',",
-      '} satisfies ToolConfig;',
-      '',
       'export const inputSchema = z.object({ delayMs: z.number().int().min(0), fail: z.boolean().optional() });',
       'export const resultSchema = z.object({ waited: z.number() });',
       '',
-      'export default async function ShowLifecycle({ input }: ToolRouteProps) {',
+      'export default defineTool({',
+      "  _meta: { ui: { resourceUri: appResourceUri('lifecycle') } },",
+      "  description: 'Waits as long as it is told, then succeeds or fails on request.',",
+      '  inputSchema,',
+      '  resultSchema,',
+      '}, async (input) => {',
       '  await new Promise((resolve) => setTimeout(resolve, input.delayMs));',
       '  return (',
       '    ',
@@ -238,7 +237,7 @@ const writeLifecycleServer = async (root: string): Promise => {
       '        : {`Lifecycle waited ${String(input.delayMs)}ms.`}}',
       '    ',
       '  );',
-      '}',
+      '});',
       '',
     ].join('\n')),
   ]);
diff --git a/packages/workbench/tests/streaming-render.e2e.test.ts b/packages/workbench/tests/streaming-render.e2e.test.ts
index 311ecdff0..e9a057790 100644
--- a/packages/workbench/tests/streaming-render.e2e.test.ts
+++ b/packages/workbench/tests/streaming-render.e2e.test.ts
@@ -7,7 +7,7 @@ import { expect } from '@rstest/playwright';
 import { inspectWorkbenchSurface } from '../../agent-bundle/src/test/index.ts';
 import { gatedRouteFiles } from '../../agent-bundle/tests/helpers/gated-routes.ts';
 import { createProjectFixture, removeProjectFixture } from '../../agent-bundle/tests/helpers/project-fixture.ts';
-import { agentBundleNodeModules } from '../../agent-bundle/tests/helpers/workspace-paths.ts';
+import { exampleNodeModules } from '../../agent-bundle/tests/helpers/workspace-paths.ts';
 import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts';
 import { applicationLeafForRouteId } from '../src/application/application-tree-model.ts';
 import {
@@ -47,7 +47,7 @@ e2e('renders a compiled MCP tool\'s Suspense fallback before its gated child rel
     dispose: (project) => removeProjectFixture(project.root),
     setup: async (project) => {
       await mkdir(join(project.root, '.agent-bundle'), { recursive: true });
-      await symlink(agentBundleNodeModules, join(project.root, 'node_modules'), 'dir');
+      await symlink(exampleNodeModules, join(project.root, 'node_modules'), 'dir');
     },
     start: (project) => startWorkbenchDevServer(project),
   }, async (server, project) => {

From 5b364556524c585a6b0e88ce3dc389a318d65a30 Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy 
Date: Wed, 16 Sep 2026 10:05:18 +0000
Subject: [PATCH 06/11] test: migrate packed legacy fixtures

---
 .../tests/dev-workbench-packaging.test.ts           | 13 +++++++++----
 .../tests/packed-consumer-typescript.test.ts        |  4 ++--
 packages/agent-bundle/tests/packed-consumer.test.ts |  2 +-
 .../tests/packed-stdio-projection.test.ts           | 11 ++++++++---
 .../agent-bundle/tests/public-api-packed.test.ts    |  2 +-
 5 files changed, 21 insertions(+), 11 deletions(-)

diff --git a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts
index 9d9aa497c..081740210 100644
--- a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts
+++ b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts
@@ -84,7 +84,7 @@ it('serves prebuilt workbench assets from an installed tarball without the repos
     await execFile('npm', ['install', ...cachedNpmInstallArguments, tarball], { cwd: consumer, env: installedEnvironment() });
     await mkdir(join(project, 'skills', 'review'), { recursive: true });
     await Promise.all([
-      writeFile(join(project, 'package.json'), '{"type":"module"}\n'),
+      writeFile(join(project, 'package.json'), '{"type":"module","version":"1.0.0"}\n'),
       writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-workbench' }, targets: ['portable'] };\n"),
       writeFile(join(project, 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n'),
     ]);
@@ -138,17 +138,22 @@ it('packages both react-server render children and renders a route invocation fr
       writeFile(join(project, 'package.json'), '{"type":"module"}\n'),
       writeFile(join(project, 'agent-bundle.config.ts'), "export default { plugin: { name: 'packed-route-invocation' }, targets: ['claude'] };\n"),
       writeFile(join(project, 'src', 'mcp', 'status', 'tools', 'report.tsx'), [
+        "import { defineTool } from 'agent-bundle/routes';",
         "import { Agent } from '@agent-bundle/runtime';",
         "import { createElement } from 'react';",
         "import { z } from 'zod';",
         '',
-        "export const config = { annotations: { readOnlyHint: true }, description: 'Reports one service.' };",
         'export const inputSchema = z.object({ service: z.string().min(1) }).strict();',
         'export const resultSchema = z.object({ service: z.string() }).strict();',
         '',
-        'export default async function Report({ input }) {',
+        'export default defineTool({',
+        "  annotations: { readOnlyHint: true },",
+        "  description: 'Reports one service.',",
+        '  inputSchema,',
+        '  resultSchema,',
+        '}, async (input) => {',
         '  return createElement(Agent.Result, { value: { service: input.service } }, createElement(Agent.Text, null, `Service ${input.service}`));',
-        '}',
+        '});',
         '',
       ].join('\n')),
     ]);
diff --git a/packages/agent-bundle/tests/packed-consumer-typescript.test.ts b/packages/agent-bundle/tests/packed-consumer-typescript.test.ts
index 610bb4ceb..8d1c98464 100644
--- a/packages/agent-bundle/tests/packed-consumer-typescript.test.ts
+++ b/packages/agent-bundle/tests/packed-consumer-typescript.test.ts
@@ -70,10 +70,10 @@ it('never shadows the consumer\'s tsc bin from a packed npm install', async () =
         '',
       ].join('\n')),
       writeFile(routePath, [
-        "export const config = { annotations: { readOnlyHint: true }, description: 'Read status.' } satisfies { description: string };",
+        "import { defineTool } from 'agent-bundle/routes';",
         'export const inputSchema = {};',
         'export const resultSchema = {};',
-        'export default async () => undefined;',
+        "export default defineTool({ annotations: { readOnlyHint: true }, description: 'Read status.', inputSchema, resultSchema }, async () => undefined);",
         '',
       ].join('\n')),
     ]);
diff --git a/packages/agent-bundle/tests/packed-consumer.test.ts b/packages/agent-bundle/tests/packed-consumer.test.ts
index 315c1850d..1e4bb7f70 100644
--- a/packages/agent-bundle/tests/packed-consumer.test.ts
+++ b/packages/agent-bundle/tests/packed-consumer.test.ts
@@ -377,7 +377,7 @@ it('uses only an installed tarball after source deletion', async () => {
     const frameworkRoot = join(consumerRoot, 'framework-build-project');
     await mkdir(join(frameworkRoot, 'src', 'mcp'), { recursive: true });
     await Promise.all([
-      writeFile(join(frameworkRoot, 'package.json'), '{"name":"framework-build-fixture","type":"module","private":true}\n'),
+      writeFile(join(frameworkRoot, 'package.json'), '{"name":"framework-build-fixture","type":"module","private":true,"version":"1.0.0"}\n'),
       writeFile(join(frameworkRoot, 'tsconfig.json'), JSON.stringify({
         compilerOptions: {
           module: 'esnext',
diff --git a/packages/agent-bundle/tests/packed-stdio-projection.test.ts b/packages/agent-bundle/tests/packed-stdio-projection.test.ts
index 68d92b9b7..00d9f296d 100644
--- a/packages/agent-bundle/tests/packed-stdio-projection.test.ts
+++ b/packages/agent-bundle/tests/packed-stdio-projection.test.ts
@@ -69,21 +69,26 @@ it.each([
     // shared fixture's lists stay untouched, and the packed level is the one
     // that spawns the real entry whose shell applies the layer.
     await writeFile(join(project, 'src', 'mcp', 'harness', 'tools', 'env-probe.tsx'), [
+      "import { defineTool } from 'agent-bundle/routes';",
       "import { Agent } from '@agent-bundle/runtime';",
       "import { z } from 'zod';",
       '',
-      "export const config = { annotations: { readOnlyHint: true }, description: 'Reports one environment variable as the server process sees it.' };",
       'export const inputSchema = z.object({ name: z.string().min(1) }).strict();',
       'export const resultSchema = z.object({ name: z.string(), value: z.string().nullable() }).strict();',
       '',
-      'export default async function EnvProbe({ input }: { readonly input: z.infer }) {',
+      'export default defineTool({',
+      "  annotations: { readOnlyHint: true },",
+      "  description: 'Reports one environment variable as the server process sees it.',",
+      '  inputSchema,',
+      '  resultSchema,',
+      '}, async (input) => {',
       '  const value = process.env[input.name] ?? null;',
       '  return (',
       '    ',
       '      {`${input.name}: ${value ?? "(unset)"}`}',
       '    ',
       '  );',
-      '}',
+      '});',
       '',
     ].join('\n'));
     await execFile('npm', ['install', ...cachedNpmInstallArguments,
diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts
index 427f27b31..607f92843 100644
--- a/packages/agent-bundle/tests/public-api-packed.test.ts
+++ b/packages/agent-bundle/tests/public-api-packed.test.ts
@@ -137,7 +137,7 @@ const createBuildProject = async (root: string): Promise<{ readonly output: stri
   const output = join(project, 'manifest-version-artifact');
   await mkdir(join(project, 'src', 'skills', 'review'), { recursive: true });
   await Promise.all([
-    writeFile(join(project, 'package.json'), '{"type":"module"}\n'),
+    writeFile(join(project, 'package.json'), '{"type":"module","version":"1.0.0"}\n'),
     writeFile(
       join(project, 'agent-bundle.config.ts'),
       "export default { plugin: { name: 'manifest-version-fixture' }, targets: ['portable'] };\n",

From d4c2a119825b8927c555976974e4e0f6e8362d47 Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy 
Date: Wed, 16 Sep 2026 10:10:13 +0000
Subject: [PATCH 07/11] test: allow packed config typecheck time

---
 packages/agent-bundle/tests/public-api-packed.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts
index 607f92843..68d91d649 100644
--- a/packages/agent-bundle/tests/public-api-packed.test.ts
+++ b/packages/agent-bundle/tests/public-api-packed.test.ts
@@ -378,7 +378,7 @@ it('imports the externalized config entry from a packed npm consumer', async ()
   } finally {
     await rm(consumerRoot, { force: true, recursive: true });
   }
-}, 30_000);
+}, 60_000);
 
 it('runs the packed App client through a dynamic-origin parent', async () => {
   const { tarball } = await sharedPackedTarball('agent-bundle');

From 62c314972fc42ae2aea1eaa57cfe9129c419b30d Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy 
Date: Wed, 16 Sep 2026 10:20:03 +0000
Subject: [PATCH 08/11] docs: link legacy removals to PR 810

---
 .changeset/remove-legacy-apis.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.changeset/remove-legacy-apis.md b/.changeset/remove-legacy-apis.md
index 0c7ce766d..e4a769fc7 100644
--- a/.changeset/remove-legacy-apis.md
+++ b/.changeset/remove-legacy-apis.md
@@ -4,4 +4,4 @@
 "rsc-markdown-stream": minor
 ---
 
-Remove `plugin.version` and diagnostics AB4001/AB4008, the install `--force` alias, split tool exports, `ServedApp`, generated CLI `isTty`, and legacy MCP App template resolution; remove runtime `Hook`/`Mcp` lowerers, `createRscRequestContext`, and `AgentDocumentSnapshot`; require React 19 element and context contracts (#PR)
+Remove `plugin.version` and diagnostics AB4001/AB4008, the install `--force` alias, split tool exports, `ServedApp`, generated CLI `isTty`, and legacy MCP App template resolution; remove runtime `Hook`/`Mcp` lowerers, `createRscRequestContext`, and `AgentDocumentSnapshot`; require React 19 element and context contracts (#810)

From 1ff792a97a56f72d4314c95cff8f789b286b339e Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy 
Date: Wed, 16 Sep 2026 10:36:19 +0000
Subject: [PATCH 09/11] test: allow packed manifest build time

---
 packages/agent-bundle/tests/public-api-packed.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts
index 68d91d649..a47c621f5 100644
--- a/packages/agent-bundle/tests/public-api-packed.test.ts
+++ b/packages/agent-bundle/tests/public-api-packed.test.ts
@@ -259,7 +259,7 @@ it('writes the package version as the producer of a packed CLI manifest', async
   } finally {
     await rm(consumerRoot, { force: true, recursive: true });
   }
-}, 30_000);
+}, 60_000);
 
 /**
  * The compiler (`@rslib/core`) and MCP Apps (`@rsbuild/core`) must run one

From 5a4ae0a782afca3e264b42c07a7c3f3866ce074f Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy 
Date: Wed, 16 Sep 2026 11:01:12 +0000
Subject: [PATCH 10/11] fix(config): reject retired plugin version key

---
 .changeset/remove-legacy-apis.md              |  2 +-
 docs/diagnostics.md                           | 10 ++++++----
 packages/agent-bundle/src/config/validate.ts  |  9 +++++++++
 packages/agent-bundle/src/install/install.ts  |  2 +-
 .../tests/host-install-proof.test.ts          |  1 -
 .../tests/package-identity.test.ts            | 20 +++++++++++++++++++
 website/docs/en/reference/configuration.mdx   |  3 ++-
 website/docs/zh/reference/configuration.mdx   |  3 ++-
 8 files changed, 41 insertions(+), 9 deletions(-)

diff --git a/.changeset/remove-legacy-apis.md b/.changeset/remove-legacy-apis.md
index e4a769fc7..7a708181b 100644
--- a/.changeset/remove-legacy-apis.md
+++ b/.changeset/remove-legacy-apis.md
@@ -4,4 +4,4 @@
 "rsc-markdown-stream": minor
 ---
 
-Remove `plugin.version` and diagnostics AB4001/AB4008, the install `--force` alias, split tool exports, `ServedApp`, generated CLI `isTty`, and legacy MCP App template resolution; remove runtime `Hook`/`Mcp` lowerers, `createRscRequestContext`, and `AgentDocumentSnapshot`; require React 19 element and context contracts (#810)
+Remove `plugin.version` and mismatch diagnostic AB4008, the install `--force` alias, split tool exports, `ServedApp`, generated CLI `isTty`, and legacy MCP App template resolution; reject the retired version key with AB4001; remove runtime `Hook`/`Mcp` lowerers, `createRscRequestContext`, and `AgentDocumentSnapshot`; require React 19 element and context contracts (#810)
diff --git a/docs/diagnostics.md b/docs/diagnostics.md
index 77d36451e..7fbd0aca5 100644
--- a/docs/diagnostics.md
+++ b/docs/diagnostics.md
@@ -14,7 +14,7 @@ even when no error diagnostic was reported.
 | Family | Area |
 | --- | --- |
 | `AB30xx` | Skill documents: Markdown parsing (`AB3000`–`AB3002`: unreadable, missing or malformed frontmatter), rendered-skill compilation (`AB3003`: module failed to load, `AB3004`: missing/invalid default component or `frontmatter` export, `AB3005`: content outside the supported Markdown element subset), and the Skill IR (`AB3006`: unknown frontmatter field; `AB3008`–`AB3010`: per-host lowering of tokens and frontmatter); see below. |
-| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`: name; `AB4002`–`AB4007`: Skill fields; `AB4009`–`AB4011` and `AB4013`: release identity; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project; `AB4014`/`AB4015`: the shared descriptive metadata every host projection reads); see below. |
+| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`: name; `AB4001`: retired `plugin.version`; `AB4002`–`AB4007`: Skill fields; `AB4009`–`AB4011` and `AB4013`: release identity; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project; `AB4014`/`AB4015`: the shared descriptive metadata every host projection reads); see below. |
 | `AB41xx` | Normalized model invariants (`AB4100`–`AB4102`: unknown targets — the retired `plugin` name included — duplicate IDs and outputs; `AB4103`, `AB4105`, `AB4106`: the composite-root checks — same path with different bytes across selected projections, a host-scoped component leaking through conventional discovery, an advanced-registry adapter selected beside another target; see below). |
 | `AB42xx` | Hook configuration and native hook sources (`AB4200`–`AB4212`; see below). |
 | `AB43xx` | MCP server and MCP App configuration (`AB4300`–`AB4339`, see below; `AB4340`: a declaration for a route-generated server redeclares `entry`/`command`/`url`; `AB4341`: the `web` exposure/policy key; see below). |
@@ -92,18 +92,20 @@ and is never lowered. `AB3007` is not assigned.
 
 ## Plugin metadata and Skill source validation (`AB4000`–`AB4007`, `AB4012`)
 
-`plugin.name` is the host-native slug every manifest carries; `plugin.logo`
-is an optional project-relative image. Each discovered skill is checked
+`plugin.name` is the host-native slug every manifest carries; `plugin.version`
+is retired and rejected because `package.json` is the sole release-version
+source; `plugin.logo` is an optional project-relative image. Each discovered skill is checked
 against the pinned Agent Skills frontmatter schema
 (`schemas/agent-skills/frontmatter.schema.json`: `name` and `description`
 required, closed portable field shapes), its directory, its resources, and
 the other skills. `AB4009`–`AB4011` and `AB4013` are the release
 identity codes (see "Release identity" below). Every row is reported on the
-config file (`AB4000`, `AB4012`) or the skill source.
+config file (`AB4000`, `AB4001`, `AB4012`) or the skill source.
 
 | Code | Severity | Trigger | Recovery |
 | --- | --- | --- | --- |
 | `AB4000` | error | `plugin.name` is missing or is not a nonempty string. | Declare `plugin: { name: '' }`. |
+| `AB4001` | error | The retired `plugin.version` key is present. Config loading does not run `tsc`, so accepting it as an unknown key would silently ignore the value and stamp `package.json.version` instead. | Remove `plugin.version` and declare the release version only in `package.json`. |
 | `AB4002` | error | Skill frontmatter `name` fails the pinned schema: missing, not a string, empty, longer than 64 characters, or not lowercase kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`): `Skill frontmatter name .` | Declare a kebab-case `name` of at most 64 characters. |
 | `AB4003` | error | Skill frontmatter `description` fails the pinned schema: missing, not a string, blank, or longer than 1024 characters. | Declare a nonblank `description` of at most 1024 characters. |
 | `AB4004` | error | Skill frontmatter `name` differs from the skill directory's name: `Skill name "" must match directory "".` | Rename the directory or the `name` so they agree. |
diff --git a/packages/agent-bundle/src/config/validate.ts b/packages/agent-bundle/src/config/validate.ts
index 3f423de1e..c09f92ff2 100644
--- a/packages/agent-bundle/src/config/validate.ts
+++ b/packages/agent-bundle/src/config/validate.ts
@@ -2416,6 +2416,15 @@ export const validateSource = (
       sourceDiagnostic('AB4000', 'Plugin metadata must define a nonempty name.', loaded.configPath),
     );
   }
+  if (pluginRecord !== undefined && Object.hasOwn(pluginRecord, 'version')) {
+    diagnostics.push(
+      sourceDiagnostic(
+        'AB4001',
+        'Plugin metadata no longer accepts version; declare the release version in package.json.',
+        loaded.configPath,
+      ),
+    );
+  }
   diagnostics.push(...validatePluginLogo(loaded, pluginRecord));
 
   const skillNames = new Map();
diff --git a/packages/agent-bundle/src/install/install.ts b/packages/agent-bundle/src/install/install.ts
index cf4179ec5..f9e74a429 100644
--- a/packages/agent-bundle/src/install/install.ts
+++ b/packages/agent-bundle/src/install/install.ts
@@ -87,7 +87,7 @@ export interface InstallBundleOptions {
   readonly host: InstallHost;
   /**
    * Replace an existing agent-bundle install of this plugin even when its
-   * version differs (`--replace` / `--force`). Same-version content drift is
+   * version differs (`--replace`). Same-version content drift is
    * replaced automatically; foreign directories are always refused.
    */
   readonly replace?: boolean;
diff --git a/packages/agent-bundle/tests/host-install-proof.test.ts b/packages/agent-bundle/tests/host-install-proof.test.ts
index 8e721f982..7697513f6 100644
--- a/packages/agent-bundle/tests/host-install-proof.test.ts
+++ b/packages/agent-bundle/tests/host-install-proof.test.ts
@@ -62,7 +62,6 @@ beforeAll(async () => {
           '  plugin: {',
           "    description: 'Proves an MCP-only installed host artifact.',",
           "    name: 'host-install-mcp-only-proof',",
-          "    version: '1.0.0',",
           '  },',
           '  routes: { mcpCommands: true },',
           "  targets: ['claude', 'codex', 'cursor'],",
diff --git a/packages/agent-bundle/tests/package-identity.test.ts b/packages/agent-bundle/tests/package-identity.test.ts
index bc1e389cd..e6b062a69 100644
--- a/packages/agent-bundle/tests/package-identity.test.ts
+++ b/packages/agent-bundle/tests/package-identity.test.ts
@@ -197,6 +197,26 @@ it('infers the plugin version from package.json when the config omits it', async
   });
 });
 
+it('rejects the removed plugin.version key instead of silently ignoring it', async () => {
+  await withProject(JSON.stringify({ name: '@scope/pkg', version: '2.3.4' }), async (root) => {
+    const loaded = loadedProject(root);
+    const withRemovedVersion: LoadedConfig = {
+      ...loaded,
+      config: {
+        plugin: { name: 'identity-fixture', version: '9.9.9' },
+      } as unknown as AgentBundleConfig,
+    };
+    expect(validateSource(withRemovedVersion, { skills: [] }, registry)).toMatchObject([
+      {
+        code: 'AB4001',
+        message: 'Plugin metadata no longer accepts version; declare the release version in package.json.',
+        severity: 'error',
+        sourcePath: loaded.configPath,
+      },
+    ]);
+  });
+});
+
 it('keeps the development fallback for an unpackaged project with no declared version', async () => {
   await withProject(undefined, async (root) => {
     const loaded = loadedProject(root);
diff --git a/website/docs/en/reference/configuration.mdx b/website/docs/en/reference/configuration.mdx
index 24b434b15..c3c6fe524 100644
--- a/website/docs/en/reference/configuration.mdx
+++ b/website/docs/en/reference/configuration.mdx
@@ -125,7 +125,8 @@ Claude Code through the marketplace entry.
 
 `package.json` is authoritative for release identity. Declare the release version there. A
 release build with no valid `package.json` version is refused with `AB4013` rather than shipping
-the `0.0.0-dev.` development fallback.
+the `0.0.0-dev.` development fallback. The retired `plugin.version` key is
+rejected with `AB4001` so an un-typechecked config cannot silently ignore it.
 
 ## targets
 
diff --git a/website/docs/zh/reference/configuration.mdx b/website/docs/zh/reference/configuration.mdx
index d3112e2e8..72695c0a5 100644
--- a/website/docs/zh/reference/configuration.mdx
+++ b/website/docs/zh/reference/configuration.mdx
@@ -112,7 +112,8 @@ Claude Code 的 `.claude-plugin/plugin.json` 不携带描述字段——其固
 `author.name` 仍是生成的插件名。共享值经由 marketplace 条目到达 Claude Code。
 
 `package.json` 对发布标识具有权威性。发布版本只在那里声明。若 `package.json` 没有有效版本,
-发布构建会被 `AB4013` 拒绝,而不是交付 `0.0.0-dev.` 这个开发期回退值。
+发布构建会被 `AB4013` 拒绝,而不是交付 `0.0.0-dev.` 这个开发期回退值。已退役的
+`plugin.version` 键会被 `AB4001` 拒绝,避免未经过类型检查的配置悄悄忽略它。
 
 ## targets
 

From 19ab901221cb80ad83b917c7cdbf5eea4e3f3901 Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy 
Date: Wed, 16 Sep 2026 13:43:32 -0700
Subject: [PATCH 11/11] fix(examples): drop duplicate package.json version keys

AB5000 rejected examples/mcp-app (and the same duplicate landed in
hooks-and-scripts / skills-starter) after the plugin.version migration.
---
 examples/hooks-and-scripts/package.json | 1 -
 examples/mcp-app/package.json           | 1 -
 examples/skills-starter/package.json    | 1 -
 3 files changed, 3 deletions(-)

diff --git a/examples/hooks-and-scripts/package.json b/examples/hooks-and-scripts/package.json
index 423ab33c5..fecc60079 100644
--- a/examples/hooks-and-scripts/package.json
+++ b/examples/hooks-and-scripts/package.json
@@ -4,7 +4,6 @@
   "private": true,
   "license": "Apache-2.0",
   "type": "module",
-  "version": "1.0.0",
   "scripts": {
     "build": "agent-bundle build",
     "check": "pnpm validate && pnpm build && pnpm typecheck",
diff --git a/examples/mcp-app/package.json b/examples/mcp-app/package.json
index 70da99ec5..86c2f02f8 100644
--- a/examples/mcp-app/package.json
+++ b/examples/mcp-app/package.json
@@ -4,7 +4,6 @@
   "private": true,
   "license": "Apache-2.0",
   "type": "module",
-  "version": "1.0.0",
   "scripts": {
     "build": "agent-bundle build",
     "check": "pnpm validate && pnpm build && pnpm typecheck",
diff --git a/examples/skills-starter/package.json b/examples/skills-starter/package.json
index a9ae28098..2e25b3396 100644
--- a/examples/skills-starter/package.json
+++ b/examples/skills-starter/package.json
@@ -4,7 +4,6 @@
   "private": true,
   "license": "Apache-2.0",
   "type": "module",
-  "version": "1.0.0",
   "scripts": {
     "build": "agent-bundle build",
     "check": "pnpm validate && pnpm build",