feat(install): expand Agent Plugins placeholders for the Cursor copy at install time; doctor AB7326 (#426) - #482
Conversation
🦋 Changeset detectedLatest commit: 50679e3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ac5bbfe25
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| launch: Object.freeze({ servers: names, state: 'unexpanded' }), | ||
| }); | ||
| } | ||
| const issues = await driftIssues(pluginRoot, expansion, servers, caseInsensitive); |
There was a problem hiding this comment.
Compare installed MCP bytes with the recorded expansion
When a receipt is present, driftIssues examines only the current servers for generic properties and never verifies that they equal the transformation of the recorded pre-expansion document. Consequently, after a normal install, editing a server's bare command, removing a server (or all servers), or replacing mcp.json with unparsable JSON can still make plain doctor --host cursor report expanded or omit AB7325 entirely; AB7320 also validates the receipt's original document instead of these installed bytes. Recompute the expected expanded document from the receipt and compare it with the installed file so the documented “edited → drifted/corrupt” behavior is actually enforced.
AGENTS.md reference: AGENTS.md:L78-L81
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 0d837cd. Doctor now reproduces the installer's expansion (expandAgentPluginsMcpForCursor, mirrored line for line from the emitted install.mjs) from the receipt's recorded document and its recorded pluginRoot/pluginData, and requires the installed mcp.json to be byte-identical to that output before anything else: an edit (a bare command renamed), a replaced or unparsable document, a removed mcp.json, or a receipt whose recorded document has no stdio server all report drifted (error, entry corrupt). The recorded document is handed to the AB7320 byte lane only when the bytes match; a drifted copy is validated as the bytes on disk (so the §9.1 env keys and absolute cwd surface as AB6035). The path/env checks stay on top for the moved-copy case. Tests added to the AB7325 doctor test (steps 5–6: edited → drifted + AB7320 errors + corrupt; removed → drifted); the step that installs through the real emitted install.mjs pins the two implementations to each other. docs/diagnostics.md updated.
8fc4b01 to
5bae7fc
Compare
commit: |
0d837cd to
60b7a96
Compare
60b7a96 to
a8266cd
Compare
a8266cd to
50679e3
Compare
…and doctor activation states (#101) (#452) * feat(install): receipt-owned uninstall lifecycle, format/2 receipts, and doctor activation states (#101) Squash of the #452 branch (review rounds 1-7 folded in) ahead of the rebase onto main, which now carries #482's AB7326. * fix(install): roll back a marketplace this install created when the plugin install fails; exact standalone marketplace plans - install: when marketplace add succeeded but plugin install / receipt write fails, remove the marketplace this run registered (its claim lived only in memory); a pre-existing marketplace is never touched - install.mjs --uninstall --mode marketplace --plan names the receipt store, marketplaces root, and agent-bundle namespace it would prune, matching the completed run * fix(install): refuse dirty staged marketplaces, reverse the plugin on receipt-write failure, aggregate Claude lifecycle across scopes - uninstall (core + install.mjs): a staged Cursor marketplace whose working tree differs from the receipted commit (or cannot be verified) is refused with AB7007 until --force; HEAD alone proved only the commit - install: when the host install succeeded but the receipt write failed, the plugin registration is reversed before the marketplace this run created - doctor AB7328: Claude multi-scope rows aggregate — a stage holds only for every listed copy; evidence names the disabled/unplaced/flagless scopes - tests and docs for each * chore(adapters): advance the Claude (1.27.0), Cursor (1.13.0), and portable (1.10.0) adapter revisions for the lifecycle capability rows and install-surface changes * fix(install): treat the Cursor PLUGIN_DATA directory as receipt-owned durable state during uninstall Uninstalling a local Cursor copy (core uninstallBundle and the emitted install.mjs --uninstall) now keeps a written PLUGIN_DATA directory behind a remnant receipt that carries the cursorExpansion so a later --purge-data --confirm-purge still finds it, prunes an empty installer-created one together with its agent-bundle parents, and never touches a recorded path outside this home's plugin-data. Doctor's AB7307 remnant diagnostic names the PLUGIN_DATA directory. Restores the byte-identical host-install proofs for cursor/portable and documents the behaviour in docs/diagnostics.md. * fix(uninstall): read Claude's plugins/installed_plugins.json so hand-made project/local installs elsewhere retain the marketplace and refuse a shared purge * docs(uninstall): document the PLUGIN_DATA data policy and the Claude installed_plugins.json dependents in INSTALL.md, README, site (en/zh), and the changeset
Summary
Framework-side support for the Cursor half of #426: Cursor 3.18.25 loads Agent Plugins 1.0.0 packages from
~/.cursor/plugins/local/<name>but performs none of the specification's §7.2.1 / §9 path resolution, so every spec-shaped stdio server fails to spawn there. The emitted portableinstall.mjsnow performs that resolution itself, for the Cursor copy only, and Doctor proves it. Nothing is claimed as Cursor behaviour (provenancederived), and nothing is sent to the vendor.Per the maintainer's direction (support host-side gaps from inside the framework where possible; no vendor feedback).
Gap (captured evidence)
docs/audits/2026-09-03-agent-plugins-cursor-ide-proof.md§3, Cursor 3.18.25 (realCommit 280eca29…af3), isolatedHOME/--user-data-dir, five probe plugins:"cwd": "${PLUGIN_ROOT}"Connection failed: spawn node ENOENT— the literal string is handed tospawnas the working directory"args": ["${PLUGIN_ROOT}/mcp/report.mjs"]Cannot find module '/home/<user>/${PLUGIN_ROOT}/mcp/report.mjs'cwdcwd = $HOME, not the plugin root"env": { "X": "${PLUGIN_ROOT}" }PLUGIN_ROOT/PLUGIN_DATA(§9.1)undefinedin the child"command": "./mcp/launch.sh"spawn <workspace>/mcp/launch.sh ENOENT${CURSOR_PLUGIN_ROOT}connect_success, stable heartbeatThe control row is what makes this feasible: Cursor hands
command,args,cwdandenvtospawnverbatim, so absolute paths written at install time launch.What changes
install.mjs(emitted withportablebundles) —packages/agent-bundle/src/install/surface.ts: detects an Agent Plugins pack (rootplugin.jsonwith anagent-plugins.org$schema, no.cursor-plugin/plugin.json) and rewritesmcp.jsonin the~/.cursor/plugins/local/<name>copy, everystdioserver:${PLUGIN_ROOT}→ absolute plugin root;${PLUGIN_DATA}→~/.cursor/agent-bundle/plugin-data/<name>(created before install, §9.1); omittedcwd→ plugin root;./incommand/cwd→ resolved against the plugin root;PLUGIN_ROOT/PLUGIN_DATAadded toenv. Remote servers, skills-only packs andcursor-target bundles are untouched. The artifact inventory hashes the expanded form, so reruns areAlready installed, same-version rebuildsReplaced, and an older unexpanded receipt-managed copy is replaced on the next run. The receipt gainscursorExpansion { pluginRoot, pluginData, documents: { "mcp.json": <shipped text> } }.install/cursor-agent-plugins-launch.ts,AB7326:expanded(info; recorded root matches, data directory exists, no placeholder left, absolutecwd/ plugin-rootcommand/argspaths exist, env carries the recorded values),unexpanded(warning; spec forms Cursor cannot launch, listed per server),drifted(error, entrycorrupt; moved / duplicated / edited copy).AB7320validates the Agent Plugins contract against the receipt's shipped document (validatePortablePluginFiles({ documents })), since the expanded copy is conformant only in that form.portable-1.0.0.json:plugin.cursorLocalInstall.placeholderExpansion(supported,provenance: derived) and a datedmcp.evidenceline; the hosts page renders it. Also drops the reference to the audit file removed in docs(audits): remove the vendor-feedback docs and sections #467.docs/diagnostics.md(AB7326),website/docs/{en,zh}/guide/distribution/installation.mdx,website/docs/{en,zh}/reference/cli.mdx,docs/framework-mode.md, package README, emittedINSTALL.md(“Cursor placeholder expansion”), audit §5.adapterRevision: cursor 1.12.0, plugin 1.28.0, portable 1.9.0 (the shared installer source changed).Tests
tests/install-surface.test.ts: the observations' pack (./command,${PLUGIN_ROOT}cwd/args,${PLUGIN_DATA}env/args, remote server) — bundle untouched, copy expanded, data dir created, receipt records the shipped document, idempotent rerun, same-version re-expansion, older unexpanded copy repaired, remote-only pack byte-identical, Cursor Plugin bundle never rewritten.tests/doctor.test.ts:AB7326unexpanded → expanded (driven by the real emittedinstall.mjs) → drifted (data dir and script removed) → moved copy;AB7320stays clean throughout.tests/host-install-proof.test.ts/tests/support/host-install.ts(pnpm test:host-install:build): the built portable fixture in an isolated Cursor home — installed copy expanded, receipt verbatim, DoctorAB7326info, byte lane clean against the shipped document.pnpm typecheck,pnpm lint,pnpm test:unit,pnpm docs:site:buildgreen locally.Review status
6ac5bbf(PR opened): one P2 thread — Doctor did not compare the installedmcp.jsonwith the recorded expansion. Addressed in0d837cde:AB7326now recomputes the expansion from the receipt (expandAgentPluginsMcpForCursor, mirrored from the emitted installer) and requires the installed bytes to match before trusting the recorded document; edited/replaced/removed documents reportdrifted/corrupt(tests: AB7326 doctor test steps 5–6).5bae7fc1(changeset ref),0d837cde(the fix above). Per maintainer instruction no review re-request is posted; merging on green CI.Refs #426.
60b7a969(rebase onto main after fix(install): surfaceclaude plugin list --jsonerrors in doctor/install (AB7325, AB7006); reject the auto-loaded hooks path in the Claude manifest schema #479): the launch proof is renumberedAB7325→AB7326because fix(install): surfaceclaude plugin list --jsonerrors in doctor/install (AB7325, AB7006); reject the auto-loaded hooks path in the Claude manifest schema #479 assignedAB7325to the Claude load-refusal check; no behavior change. Not re-reviewed by Codex.50679e38: rebased ontomainatc6db0ffe1(feat(lineage): tell concurrent Cursor MCP calls apart by the arguments the pre-tool hook recorded (#424) #483, feat(lineage): confirm Claude subagent parent edges from the parent's Agent PostToolUse (#422) #486, chore(ci): remove the release audit gate #487 — the release-audit gate is gone from the base); no conflicts, no code change. Targeted suites re-run locally on the rebase: doctor / install-surface / adapter-metadata / lineage-registry (82 passed), host-install proof (9 passed),pnpm docs:site:buildgreen.