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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/portable-agent-plugins-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

portable: pin Agent Plugins 1.0.0 adoption evidence — specification repository commit, native-client roster, a repeatable installer/filesystem/pinned-schema conformance proof against an isolated Cursor home, and a Cursor 3.18.25 IDE plugin-loader dogfood audit (discovery, skill and MCP surfacing, stdio handshake, and three observed placeholder-expansion conformance gaps) — and refresh the install surface wording.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions docs/audits/2026-09-02-agent-plugins-cursor-ide-proof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Cursor IDE dogfood proof — portable Agent Plugins target

Date: 2026-09-02. Observed client: Cursor 3.18.25 stable, commit
`280eca2911f1774689696e5f1efa5a4f97a87af3`, Linux x64 AppImage, run as an
isolated instance (dedicated `HOME`, dedicated `--user-data-dir`, Xvfb
display). Package under test: the `portable` target artifact of the
host-install fixture (`plugin.json` name `host-install-proof`, version
`1.0.0`), installed with the artifact's own emitted `install.mjs` into the
isolated home's `~/.cursor/plugins/local/host-install-proof`.

This audit records what the real Cursor IDE plugin loader was observed doing.
It is separate from — and stronger on loader behavior than — the repeatable
`host-install` test proof, which validates installer execution, filesystem
shape, and pinned-schema conformance only and deliberately does not claim any
IDE observation.

## What the IDE established

1. **Discovery as an Agent Plugin.** After a restart the Customize page lists
the package as plugin "Host Install Proof" with a `Local` badge; searching
`host-install` returns the plugin and its skill.
![Customize search discovery](../assets/agent-plugins-cursor-proof/2026-09-02-customize-search-discovery.png)
2. **Skill discovery.** The `probe` skill is listed with the exact
description string from the emitted `skills/probe/SKILL.md` frontmatter.
3. **MCP configuration discovery and launch attempt.** The plugin detail page
shows `MCPs 1 (probe)` and `Skills 1 (probe)`; Cursor did not merely parse
`mcp.json`, it spawned the configured stdio server.
![Plugin detail](../assets/agent-plugins-cursor-proof/2026-09-02-plugin-detail-mcp-skill.png)
4. **Honest absence of unsupported surfaces.** The detail page renders only
MCPs and Skills sections for this plugin; no rules, commands, hooks, or
subagents are attributed to it, matching the portable capability table.
![Plugins panel](../assets/agent-plugins-cursor-proof/2026-09-02-customize-plugins-panel.png)
5. **Successful stdio handshake.** With a launchable server configuration
(see gaps below) the IDE log records `Successfully connected to stdio
server` / `connection:connect_success` with a stable heartbeat
(`~/.config/Cursor/logs/<session>/mcp-server-plugin-host-install-proof-probe.log`).

## Cursor 3.18.25 conformance gaps against Agent Plugins 1.0

All three were isolated by mutating only the installed copy's `mcp.json`
between IDE restarts and reading the per-server IDE logs. Spec citations are
to <https://agent-plugins.org/specification> (1.0.0, repository commit
`ff8ab5e392cc87bd88d87c060815a87490e51003`).

1. **`${PLUGIN_ROOT}` is not expanded in `cwd`.** §7.2.1 requires `args`,
`env`, and `cwd` to support `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` expansion,
and the specification's own stdio example uses `"cwd": "${PLUGIN_ROOT}"`.
Cursor passes the literal string as the working directory, so spawn fails
with a misleading `spawn node ENOENT` — even for an absolute `command`
with an existing binary.
2. **`${PLUGIN_ROOT}` is not expanded in `args`.** With `cwd` removed the
server process starts, but Node receives the literal
`${PLUGIN_ROOT}/mcp/<entry>.mjs` and exits with `MODULE_NOT_FOUND`
(the literal resolves against the home directory).
3. **Omitted `cwd` does not default to the plugin root.** §7.2.1: "When
`cwd` is omitted, clients MUST use the plugin root as the subprocess
working directory." Observed default was the user's home directory.

Supplementary observation: replacing the spec placeholder with Cursor's
proprietary `${CURSOR_PLUGIN_ROOT}` in `args` produced a successful
connection with a stable heartbeat, confirming the loader has a working
expansion pipeline that is simply not wired to the standard's placeholder
names for this format. Bare `command` names (e.g. `node`) resolve normally
once `cwd` is valid.

## Consequence for the portable adapter

The portable emission (`cwd: "${PLUGIN_ROOT}"`, plugin-root-relative `args`)
is exactly what the specification prescribes and stays unchanged. Until
Cursor implements §7.2.1/§9.2 expansion for Agent Plugins, stdio MCP servers
from any spec-conformant portable package fail to launch on Cursor even
though the plugin, its skills, and its MCP configuration are all discovered
and surfaced correctly. This is tracked as adoption-issue evidence, not as an
adapter defect.
12 changes: 12 additions & 0 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ and install through their public plugin CLIs; Cursor bundles use the documented
`~/.cursor/plugins/local/<name>` location because Cursor exposes marketplace
management but no non-interactive plugin install verb.

The `portable` target emits the [Agent Plugins open standard](https://agent-plugins.org)
(specification 1.0.0), with schema hashes and the specification repository
revision pinned in `src/adapters/schemas/portable/PROVENANCE.json`. Cursor loads
this format natively alongside Cursor Plugins; Codex, VS Code, GitHub Copilot,
Kiro, and ChatGPT are native clients too. Claude Code consumes the standard
only through CLI translation, so its dedicated target remains necessary. The
standard packages only skills and MCP servers, leaving rules, commands, and
hooks honestly unavailable on the portable target. 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`.

The framework CLI performs those same operations:

```sh
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"install": {
"evidence": [
"2026-09-01: Cursor loads Agent Plugins natively from ~/.cursor/plugins/local/<name> (https://cursor.com/docs/plugins).",
"2026-09-01: Native launch clients are ChatGPT, Codex, Cursor, GitHub Copilot, Kiro, and VS Code (https://agent-plugins.org).",
"2026-09-01: Claude Code is not a native client; Agent Plugins installations there require CLI translation."
],
"reason": "Portable is a distribution profile, not a host runtime with one universal plugin installation location.",
"source": "https://agent-plugins.org/",
"state": "unavailable"
Expand All @@ -15,6 +20,11 @@
},
"host": "portable",
"mcp": {
"evidence": [
"2026-09-02: Cursor 3.18.25 (stable, 280eca2911f1) discovers the emitted plugin, its skill, and its mcp.json from ~/.cursor/plugins/local and spawns the configured stdio server (docs/audits/2026-09-02-agent-plugins-cursor-ide-proof.md).",
"2026-09-02: Cursor 3.18.25 does not expand ${PLUGIN_ROOT} in cwd or args and does not default an omitted cwd to the plugin root (spec 7.2.1/9.2 MUSTs), so spec-conformant stdio servers fail to launch there; its proprietary ${CURSOR_PLUGIN_ROOT} expands and connects.",
"2026-09-02: With a launchable configuration the Cursor IDE completed the stdio handshake (connect_success with stable heartbeat), so the launch pipeline itself consumes this format."
],
"pathTokens": {
"args": [
"${PLUGIN_DATA}",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-bundle/src/adapters/portable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const schemaValidator = createAdapterValidator();
const validatePlugin = schemaValidator.compile(pluginSchema);
const validateMcp = schemaValidator.compile(mcpSchema);
const metadata = Object.freeze({
adapterRevision: '1.2.0',
adapterRevision: '1.3.0',
observedVersion: capabilityTable.observedSpecificationVersion,
schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.version),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"normativeTextWinsOnConflict": true,
"retrievedAt": "2026-08-14",
"retrievedAt": "2026-09-01",
"schemas": {
"mcp.schema.json": {
"bytes": 3408,
Expand All @@ -13,6 +13,11 @@
"url": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"
}
},
"specRepository": {
"commit": "ff8ab5e392cc87bd88d87c060815a87490e51003",
"committedAt": "2026-08-19T16:34:23Z",
"url": "https://github.com/agentplugins/agent-plugins-spec"
},
"specification": "https://agent-plugins.org/specification",
"version": "1.0.0"
}
6 changes: 4 additions & 2 deletions packages/agent-bundle/src/install/surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ const portableInstructions = (): string[] => [
'## Portable Agent Plugin',
'',
'Portable is a distribution profile, not a host runtime with one universal install location.',
'This bundle follows Agent Plugins 1.0 and can be copied into a compatible host. Cursor supports',
'that format directly, so the bundled installer provides a concrete local install path:',
'This bundle follows the Agent Plugins open standard (Agent Plugins 1.0.0, https://agent-plugins.org).',
'Cursor loads this format natively from `~/.cursor/plugins/local/<name>`; restart Cursor or run',
'`Developer: Reload Window` after copying it. Codex, VS Code, GitHub Copilot, Kiro, and ChatGPT',
'are also native clients. The bundled installer provides the Cursor local copy:',
'',
'```sh',
'node ./install.mjs',
Expand Down
17 changes: 16 additions & 1 deletion packages/agent-bundle/tests/adapter-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ it('records exact immutable metadata for every built-in target', () => {
const registry = createDefaultRegistry();

expect(registryMetadata(registry, 'portable')).toEqual({
adapterRevision: '1.2.0',
adapterRevision: '1.3.0',
observedVersion: '1.0.0',
schemas: [
{
Expand Down Expand Up @@ -214,6 +214,21 @@ it('records observed capability versions and rehashes schema snapshots against p
type: 'string',
});
}
if (target === 'portable') {
expect(capabilityTable.install).toMatchObject({
evidence: [
expect.stringContaining('Cursor loads Agent Plugins natively'),
expect.stringContaining('ChatGPT, Codex, Cursor, GitHub Copilot, Kiro, and VS Code'),
expect.stringContaining('Claude Code is not a native client'),
],
state: 'unavailable',
});
expect(provenance.specRepository).toEqual({
commit: 'ff8ab5e392cc87bd88d87c060815a87490e51003',
committedAt: '2026-08-19T16:34:23Z',
url: 'https://github.com/agentplugins/agent-plugins-spec',
});
}
}
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default {
hooks: {
sessionStart: { handler: './src/hooks/session-start.ts' },
},
mcp: {
servers: {
probe: {},
},
},
plugin: {
description: 'Proves portable installer and filesystem/schema conformance.',
name: 'host-install-portable-proof',
version: '1.0.0',
},
skills: ['skills/probe'],
targets: ['portable'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "host-install-portable-proof-fixture",
"private": true,
"type": "module",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: probe
description: Confirm that the portable host-install proof fixture was discovered.
---

# Probe

Report that the portable host-install proof fixture is available.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default () => ({
additionalContext: 'The portable host-install proof fixture is available.',
outcome: 'continue' as const,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { McpServer } from '@modelcontextprotocol/server';

export default () => new McpServer({
name: 'host-install-portable-proof',
version: '1.0.0',
});
59 changes: 57 additions & 2 deletions packages/agent-bundle/tests/host-install-proof.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { afterAll, beforeAll, expect, it } from '@rstest/core';

import {
buildHostInstallFixture,
buildPortableHostInstallFixture,
disposeHostInstallFixture,
runClaudeHostInstallProof,
runCodexHostInstallProof,
runCursorHostInstallProof,
runPortableHostInstallProof,
type BuiltHostInstallFixture,
type BuiltPortableHostInstallFixture,
} from './support/host-install.ts';
import {
HOST_INSTALL_PROOF_LEVEL,
Expand All @@ -32,20 +35,34 @@ const claudePluginIt = claudeAvailable ? it : it.skip;
const codexPluginIt = codexAvailable ? it : it.skip;

let fixture: BuiltHostInstallFixture | undefined;
let portableFixture: BuiltPortableHostInstallFixture | undefined;

beforeAll(async () => {
fixture = await buildHostInstallFixture({ environment: process.env });
[fixture, portableFixture] = await Promise.all([
buildHostInstallFixture({ environment: process.env }),
buildPortableHostInstallFixture({ environment: process.env }),
]);
}, 180_000);

afterAll(async () => {
if (fixture !== undefined) await disposeHostInstallFixture(fixture);
await Promise.all([
fixture === undefined ? Promise.resolve() : disposeHostInstallFixture(fixture),
portableFixture === undefined ? Promise.resolve() : disposeHostInstallFixture(portableFixture),
]);
});

const builtFixture = (): BuiltHostInstallFixture => {
if (fixture === undefined) throw new Error(`[${proofLabel}] shared fixture build did not complete.`);
return fixture;
};

const builtPortableFixture = (): BuiltPortableHostInstallFixture => {
if (portableFixture === undefined) {
throw new Error(`[${proofLabel}] portable fixture build did not complete.`);
}
return portableFixture;
};

const expectHygienicReport = (report: unknown): void => {
expect(JSON.stringify(report), proofLabel).not.toMatch(
/(?:API_KEY|AUTH_TOKEN|ACCESS_TOKEN|authorization|credential|password|secret|sk-[A-Za-z0-9_-]{16,}|\/home\/|\/Users\/|\/tmp\/|stdout|stderr)/iu,
Expand Down Expand Up @@ -155,3 +172,41 @@ it('installs into an isolated Cursor home, validates schemas, and is idempotent'
});
expectHygienicReport(report);
}, 180_000);

it(
'installs the emitted Agent Plugins 1.0.0 package into an isolated Cursor home and validates it against the pinned spec schemas (filesystem/schema conformance; not an IDE-loading proof)',
async () => {
const report = await runPortableHostInstallProof(
builtPortableFixture(),
{ environment: process.env },
);

expect(report, proofLabel).toEqual({
destination: '.cursor/plugins/local/host-install-portable-proof',
documents: {
mcp: 'schema-valid',
plugin: 'schema-valid',
},
hooks: 'not-emitted',
host: 'cursor',
install: { first: 'installed', second: 'already-installed', version: '1.0.0' },
pluginVariables: {
allowedLocations: 'args/env values/cwd only',
locations: [
'mcp.json#/mcpServers/probe/cwd',
'mcp.json#/mcpServers/probe/env/AGENT_BUNDLE_PLUGIN_ROOT',
],
reservedEnvKeys: 'absent',
resolvedAtInstall: false,
sessionEvidence: 'unavailable: Cursor loads Agent Plugins only at restart or window reload; no non-interactive plugin-loading session surface',
},
proofLevel: 'host-install (emitted install.mjs + isolated Cursor home filesystem + pinned Agent Plugins 1.0.0 schemas; NOT IDE plugin-loader evidence)',
proofScope: 'installer+filesystem+pinned-schema conformance against an isolated Cursor home; IDE plugin-loader behavior not observed by this test',
skill: '.cursor/plugins/local/host-install-portable-proof/skills/probe/SKILL.md',
specVersion: '1.0.0',
status: 'passed',
});
expectHygienicReport(report);
},
180_000,
);
11 changes: 11 additions & 0 deletions packages/agent-bundle/tests/install-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ it('emits a standalone safe-copy installer only for Cursor-compatible fallback p
}
});

it('documents native Agent Plugins clients for the portable profile', () => {
const install = writesFor('portable').get('INSTALL.md');

expect(install).toContain(
'the Agent Plugins open standard (Agent Plugins 1.0.0, https://agent-plugins.org)',
);
expect(install).toContain('`~/.cursor/plugins/local/<name>`');
expect(install).toContain('Developer: Reload Window');
expect(install).toContain('Codex, VS Code, GitHub Copilot, Kiro, and ChatGPT');
});

it('documents every real host path from the composite profile', () => {
const install = writesFor('plugin').get('INSTALL.md');

Expand Down
Loading
Loading