Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/portable-normative-rules-fail-closed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

portable: fail closed on the Agent Plugins 1.0.0 normative MCP rules during ordinary builds (#307 review follow-up) — the command-form, cwd-containment, env-key-placeholder, URL-form, and header rules the pinned `mcp.schema.json` cannot express now run at plan time (`portable.mcp.{command,cwd,env,url,headers}.standard`, errors) and the Agent Plugins byte lane (`AB6035`–`AB6037`) runs over every emitted `portable/` tree in `build` and `validate --artifact`, so a standard-invalid `mcp.json` (for example `command: "bin/server"`, an escaping `cwd`, duplicate case-insensitive headers, or plain HTTP off loopback) can no longer be published, whether or not `--host-validation` is requested (the flag now only adds the `portable` host report and its `AB6038` provenance note). Containment is checked with platform-independent POSIX semantics: a `./` command or `cwd` containing backslashes or NUL is refused so a bundle built on POSIX cannot resolve outside the root on Windows. Header values are now rejected when they contain any character outside visible ASCII, space, horizontal tab, and obs-text bytes (RFC 9110 §5.5; matches Node's `validateHeaderValue`), not only CR/LF/NUL. Backward compatibility: portable bundles whose MCP servers already satisfy the standard emit byte-identical output; servers that relied on path-bearing or whitespace-bearing `command` values, escaping `cwd`, or plain-HTTP remote URLs now fail the build with a field-scoped diagnostic instead of being published.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ npx agent-bundle dev --root . # local workbench with live rebu

`targets: ['plugin']` emits one multi-host bundle at `dist/plugin/`: `.claude-plugin/`, `.codex-plugin/`, and `.cursor-plugin/` manifests over shared `skills/`, `hooks/`, `mcp/`, and `scripts/` directories. The bundle's generated `AGENTS.md` explains how to install it into each host. Per-host layouts are available as the `claude`, `codex`, `cursor`, and `portable` targets.

The `portable` target is the [Agent Plugins open standard](https://agent-plugins.org/specification) (specification 1.0.0) adapter — the default target, and the layout Cursor, Codex, VS Code, GitHub Copilot, Kiro, and ChatGPT load natively (Claude Code consumes it only through CLI translation). It emits the closed root `plugin.json` (canonical `$schema`, `name`, `version`, `description`, plus `author`, `homepage`, `repository`, `license`, `keywords`, and reverse-domain `extensions` authored under the `portable` config key), `skills/<name>/SKILL.md`, and `mcp.json` with stdio and Streamable HTTP servers whose `args`, `env` values, and `cwd` use the standard's `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` placeholders. Rules, commands, hooks, marketplaces, and client extension directories are honestly unavailable there because the v1 standard packages only skills and MCP servers. Both documents are validated against the vendored, hash-pinned 1.0.0 schemas and the normative text at plan time, after every build (`AB6011`/`AB6012`), under `validate --artifact --host-validation` (`AB6035`–`AB6038`), and by `agent-bundle doctor` for installed Cursor local plugins that declare the standard's `$schema` (`AB7320`); see [Diagnostics](docs/diagnostics.md#agent-plugins-portable-validation-ab6035ab6038). Pins live in `packages/agent-bundle/src/adapters/schemas/portable/PROVENANCE.json`; the capability table `packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json` carries a dated row for every standard feature.
The `portable` target is the [Agent Plugins open standard](https://agent-plugins.org/specification) (specification 1.0.0) adapter — the default target, and the layout Cursor, Codex, VS Code, GitHub Copilot, Kiro, and ChatGPT load natively (Claude Code consumes it only through CLI translation). It emits the closed root `plugin.json` (canonical `$schema`, `name`, `version`, `description`, plus `author`, `homepage`, `repository`, `license`, `keywords`, and reverse-domain `extensions` authored under the `portable` config key), `skills/<name>/SKILL.md`, and `mcp.json` with stdio and Streamable HTTP servers whose `args`, `env` values, and `cwd` use the standard's `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` placeholders. Rules, commands, hooks, marketplaces, and client extension directories are honestly unavailable there because the v1 standard packages only skills and MCP servers. Both documents are validated against the vendored, hash-pinned 1.0.0 schemas and the normative text at plan time (`portable.mcp.*.standard`), after every ordinary build and `validate --artifact` (`AB6011`/`AB6012` plus the Agent Plugins byte lane `AB6035`–`AB6037`), under `validate --artifact --host-validation` (same lane with the `AB6038` provenance note), and by `agent-bundle doctor` for installed Cursor local plugins that declare the standard's `$schema` (`AB7320`); see [Diagnostics](docs/diagnostics.md#agent-plugins-portable-validation-ab6035ab6038). Pins live in `packages/agent-bundle/src/adapters/schemas/portable/PROVENANCE.json`; the capability table `packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json` carries a dated row for every standard feature.

Claude Code language servers are declared under `claude.lspServers`; the `claude` target and the Claude half of `plugin` emit the record as plugin-root `.lsp.json`. Agent Bundle expands path tokens only in `command`, `args`, `env`, and `workspaceFolder`, and it does not include the language-server binary — install that separately so the declared command is available on `PATH`. Codex, Cursor, and the portable format do not currently receive this host-scoped configuration.

Expand Down
21 changes: 15 additions & 6 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,31 @@ Validation happens at three moments, all fail-closed:
1. **Plan time** (`agent-bundle build`/`validate`): the emitted `plugin.json`
and `mcp.json` are validated against the pinned schemas before they are
written (`portable.schema.plugin`, `portable.schema.mcp`), authored manifest
metadata is checked field by field (`portable.manifest.<field>.invalid`), and
metadata is checked field by field (`portable.manifest.<field>.invalid`),
MCP path tokens are refused where the standard forbids them
(`portable.mcp.token.*`). These target-scoped codes are errors.
(`portable.mcp.token.*`), and the normative MCP rules the schemas cannot
express are applied to each server as it will be written
(`portable.mcp.{command,cwd,env,url,headers}.standard`: command form, cwd
containment, env-key placeholders, URL form, header names/values/casing).
These target-scoped codes are errors, so a standard-invalid server never
reaches an artifact.
2. **Artifact time** (`agent-bundle build`, `validate --artifact`): the generic
target-contract pass reports a missing required document as `AB6011` and a
pinned-schema rejection as `AB6012`; `validate --artifact --host-validation`
additionally runs the byte lane below and returns a `portable` host
validation report.
pinned-schema rejection as `AB6012`, and the Agent Plugins byte lane below
(`AB6035`–`AB6037`) runs over every tree emitted by the built-in portable
adapter, so a standard-invalid layout fails the ordinary build before
publication (a tree already carrying a symlink or other unsupported entry
is reported as `AB6013` and never read by this lane).
`validate --artifact --host-validation` additionally returns the same lane
as a `portable` host validation report with the `AB6038` provenance note.
3. **Installed bytes** (`agent-bundle doctor`): a Cursor local plugin whose root
`plugin.json` declares an Agent Plugins `$schema` is validated with the same
byte lane and reported under `AB7320` (an error marks the entry `corrupt`).

| Code | Severity | Meaning | Recovery |
| --- | --- | --- | --- |
| `AB6035` | error | The root `plugin.json` is missing, or a present `plugin.json`/`mcp.json` is unreadable, not valid JSON, or rejected by its pinned Agent Plugins 1.0.0 schema (closed manifest fields, plugin name constraints, reserved `PLUGIN_ROOT`/`PLUGIN_DATA` env keys, closed server variants). | Repair the generated Agent Plugins document so it satisfies the pinned 1.0.0 schema, then rebuild. |
| `AB6036` | error | A normative-text rule the schemas cannot express is violated: `plugin.json` and `mcp.json` declare different Agent Plugins versions (§10.1); a stdio `command` is neither a bare executable name nor a bundled plugin-relative `./` file, or carries a placeholder (§7.2.1); a `./`, `${PLUGIN_ROOT}`, or `${PLUGIN_DATA}` `cwd` escapes its root after resolution (§4.1/§7.2.1); a remote `url` is not an absolute HTTP(S) URL, carries user information or a fragment, uses plain HTTP against a non-loopback host, or carries a placeholder; header names are invalid, repeat under different casing, or carry placeholders (§7.2.1); an `env` key carries a placeholder (§9.2); `skills/` or `mcp.json` is present with the wrong filesystem kind (§6.2); or a `skills/<name>/` directory has no regular `SKILL.md` (§7.1). | Repair the generated portable layout or MCP entry to satisfy the Agent Plugins 1.0.0 normative text, then rebuild. |
| `AB6036` | error | A normative-text rule the schemas cannot express is violated: `plugin.json` and `mcp.json` declare different Agent Plugins versions (§10.1); a stdio `command` is neither a bare executable name nor a bundled plugin-relative `./` file, or carries a placeholder (§7.2.1); a `./`, `${PLUGIN_ROOT}`, or `${PLUGIN_DATA}` `cwd` escapes its root after resolution (§4.1/§7.2.1); a remote `url` is not an absolute HTTP(S) URL, carries user information or a fragment, uses plain HTTP against a non-loopback host, or carries a placeholder; header names are invalid, repeat under different casing, or carry placeholders, or a header value contains anything other than visible ASCII, space, horizontal tab, or obs-text bytes (§7.2.1, RFC 9110 §5.5); an `env` key carries a placeholder (§9.2); `skills/` or `mcp.json` is present with the wrong filesystem kind (§6.2); or a `skills/<name>/` directory has no regular `SKILL.md` (§7.1). | Repair the generated portable layout or MCP entry to satisfy the Agent Plugins 1.0.0 normative text, then rebuild. |
| `AB6037` | error | A symlink inside the plugin resolves outside the plugin root, or cannot be resolved at all (§4.1 containment). | Replace the escaping symlink with a file or a link that resolves inside the plugin root, then rebuild. |
| `AB6038` | info | Every portable host-validation report states that Agent Plugins publishes no reference validator and names the pinned schema provenance (specification repository commit, retrieval and re-verification dates) used for local validation. | Review the pinned Agent Plugins provenance before changing the local validator contract. |

Expand Down
10 changes: 6 additions & 4 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,12 @@ metadata (`author`, `homepage`, `repository`, `license`, `keywords`) and
reverse-domain `extensions` are authored under the `portable` config key and
land in the root `plugin.json`; omitting them leaves the manifest exactly as
before. Emitted bytes are validated against the pinned schemas and the
normative text at plan time, after every build, under
`validate --artifact --host-validation`, and by `doctor` for installed Cursor
local plugins that declare the standard's `$schema`
(`AB6035`–`AB6038`, `AB7320`; see `docs/diagnostics.md`). A dogfood proof
normative text at plan time (`portable.mcp.*.standard`), by the Agent Plugins
byte lane after every ordinary build and `validate --artifact`
(`AB6035`–`AB6037`), under `validate --artifact --host-validation` (same lane
plus the `AB6038` provenance note), and by `doctor` for installed Cursor
local plugins that declare the standard's `$schema` (`AB7320`; see
`docs/diagnostics.md`). A dogfood proof
against the real Cursor IDE plugin loader (discovery, skill listing, MCP
launch, and three observed Cursor 3.18.25 placeholder-expansion conformance
gaps) is recorded in `docs/audits/2026-09-02-agent-plugins-cursor-ide-proof.md`.
Expand Down
Loading
Loading