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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- run: python3 Tests/check_runtime_rejection.py
runtime:
uses: computer-mcp/.github/.github/workflows/plugin-runtime.yml@35766b350427ed900b456aa3c39098e97d9d3aa1 # master
brand:
uses: computer-mcp/.github/.github/workflows/brand-check.yml@f402911abcdfb87ef0c41a2759e8c51387b18935 # master
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Release
on:
workflow_dispatch:
inputs:
ref:
description: Version tag, or commit for a dry run
type: string
required: true
publish:
description: Publish the verified release
type: boolean
default: false
permissions: {}
jobs:
release:
permissions:
contents: write
actions: read
uses: computer-mcp/.github/.github/workflows/plugin-release.yml@35766b350427ed900b456aa3c39098e97d9d3aa1 # master
with:
ref: ${{ inputs.ref }}
publish: ${{ inputs.publish }}
build: python3 Scripts/build_package.py
ci_artifact: cursor-plugin-${commit}-3.13
notify-catalog:
needs: release
if: inputs.publish && needs.release.outputs.published == 'true'
permissions: {}
uses: ./.github/workflows/notify-catalog.yml
secrets:
CATALOG_APP_PRIVATE_KEY: ${{ secrets.CATALOG_APP_PRIVATE_KEY }}
67 changes: 37 additions & 30 deletions Documentation/Architecture/VersioningAndRelease.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
# Versioning and Release

Release status: partly manual. CI builds and checks candidate archives on every
pull request and `master` push; tagging, acceptance and publication are manual
steps described in [Release](../Reference/Release.md).
CI builds and checks candidate archives on every pull request and `master`
push. `release.yml` verifies and publishes accepted archives after manual
acceptance, as described in [Release](../Reference/Release.md).

This document owns the current version and release rules for plugin-cursor.
This document owns plugin-cursor's version sources and acceptance checks. The
organization's [VERSIONING.md](https://github.com/computer-mcp/.github/blob/master/VERSIONING.md)
owns shared versioning, plugin compatibility, tags, release contents and
recovery rules.

## Components and Version Authority

| Shipped component | Version meaning | Authoritative source | Derived fields | Update and read-only check |
| --- | --- | --- | --- | --- |
| `cursor.zip` plugin package | SemVer 2.0.0 package version | `version` in `computer-mcp-plugin.toml` | adapter `--version`, builder receipt, tag `vX.Y.Z`, `release-receipt.json`, catalog entry | Edit the manifest; `Tests/test_package.py` checks the packaged adapter's `--version` |

While the version is `0.x`, compatible fixes advance the patch number, and new
features or incompatible changes advance the minor number. Compatibility is
established by review and tests, not by comparing version numbers.
| `cursor.zip` plugin package | Package version under the shared versioning rules | `version` in `computer-mcp-plugin.toml` | adapter `--version`, builder receipt, release tag, `release-receipt.json`, catalog entry | Edit the manifest; `Tests/test_package.py` checks the packaged adapter's `--version` |

The archive contains the manifest, `cli-tree.json`, `bin/`, `skills/`, the README,
contribution guide, license, notices, `Documentation/` and `Examples/`. Any change
to those files ships only under a new version; a published version is never
rebuilt from different bytes. Changes to tests, scripts or workflows alone do not
change the archive and do not require a release.
contribution guide, license, notices, `Documentation/` and `Examples/`. Changes
to these packaged files change the release bytes. Tests, scripts and workflows
are outside the archive.

## Dependencies and Verified Combinations

The package uses the Python standard library only and requires Python 3.13 or
newer on the host launch PATH. CI runs the tests on Python 3.13 and 3.14 and
checks that 3.11 and 3.12 are rejected before any vendor process starts.

`[compatibility]` in the manifest declares the minimum Computer MCP host and the
supported architectures. Raise `minimum_host` only when the package needs a newer
host contract, after validating against that host.
`bin/plugin_runtime.py` is copied from the organization's canonical runtime.
The `runtime` job in `ci.yml` checks it against the pinned shared workflow's
digest. Adapter and probe callers extract the native assertion from the CLI
tree with `expected_version` before calling `verify_version`.

`computer-mcp-plugin.toml` declares the plugin API and host and architecture
compatibility. Validate host-specific requirements against the selected host
before changing the host bounds.

`cli-tree.json` pins the exact Cursor Agent version through its
`executable_checks`, which the host applies before every CLI and adapter call. A
Expand All @@ -41,22 +44,26 @@ different vendor version requires a reviewed update of the tree, verified with

`Scripts/build_package.py` verifies the plugin ID, parses the CLI tree, and
produces the same archive bytes from the same source. Its receipt prints the ID,
version and SHA-256. Agreement between the manifest version and the release tag
is checked manually when tagging.
version and SHA-256. `release.yml` calls the pinned organization release workflow
with `python3 Scripts/build_package.py` and selects the exact
`cursor-plugin-${commit}-3.13` CI artifact. The shared workflow checks the source
and manifest, rebuilds the archive and compares its bytes with that artifact.

## Candidate, Acceptance and Publication

The candidate is the CI artifact for a reviewed `master` commit. Acceptance
requires a byte-identical local rebuild, the native version and help check, the
ACP initialization probe with `Scripts/probe_acp.py`, and the isolated host check
with `Scripts/validate_host.py` against the selected Computer MCP release.
The candidate is the Python 3.13 CI artifact for a reviewed `master` commit.
Run `release.yml` with `publish: false` first. Manual acceptance requires the
native version and help check with `Scripts/validate_native.py`, the ACP
initialization probe with `Scripts/probe_acp.py`, and the isolated host check
with `Scripts/validate_host.py` against the selected Computer MCP release and
the exact archive verified by the dry run.
Authenticated model execution and production installation are separate checks
and are never implied by these results.

A signed annotated `vX.Y.Z` tag binds the accepted commit, and the GitHub
Release publishes that exact archive with `SHA256SUMS` and
`release-receipt.json`. Published tags and archives are immutable; a defect is
fixed in a new version. Publishing triggers the catalog notification.
Record the manual results and their limits in the draft Release notes. Dispatch
`release.yml` with the accepted version tag and `publish: true` to publish.
Only a reported publication enables its dependent catalog notification job;
only that job receives the catalog secret.

## Evidence Reuse and Invalidation

Expand All @@ -71,10 +78,10 @@ packaged file produces a new archive that needs the affected checks again.
| --- | --- | --- |
| Version update and check | Edit `computer-mcp-plugin.toml`; `python3 -m unittest discover -s Tests -p 'test_*.py'` | Local checkout |
| Candidate validation and build | `ci.yml`; locally `python3 Scripts/build_package.py <new directory>` | CI or local checkout |
| Status and interrupted-run recovery | Rerun the failed CI job or check; outputs go to new directories | Repository Actions |
| Acceptance and publication | Manual steps in [Release](../Reference/Release.md) | Signing key and release write access |
| Status and interrupted-run recovery | Inspect CI or `release.yml`; follow [Release](../Reference/Release.md#retry) | Repository Actions |
| Acceptance and publication | Manual checks and `release.yml` in [Release](../Reference/Release.md) | Signing key and release write access |
| Cleanup | Delete local output and evidence directories | Local checkout |

The builder refuses to overwrite a different archive, and `validate_host.py`
requires a new evidence directory, so earlier results stay intact. Keep local
evidence outside the repository.
requires a new evidence directory. Keep local build and evidence outputs under
ignored `dist/` paths and remove them after use.
71 changes: 43 additions & 28 deletions Documentation/Reference/Release.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
# Release

This guide covers publishing an accepted package and notifying the official
plugin catalog. [Versioning and Release](../Architecture/VersioningAndRelease.md)
owns the version, acceptance and immutability rules.
This guide covers `release.yml` and notification of the official plugin catalog.
[Versioning and Release](../Architecture/VersioningAndRelease.md) owns this
repository's version sources and acceptance checks. The organization's
[VERSIONING.md](https://github.com/computer-mcp/.github/blob/master/VERSIONING.md)
owns shared versioning, release contents, immutability and recovery rules.

The repository owner must enable GitHub release immutability before the first
release. The release workflow refuses publication unless it can verify that
setting; enabling it is a prerequisite to publishing.

## Publish

1. Confirm that `version` in `computer-mcp-plugin.toml` has not been released
and that the reviewed `master` commit has a successful CI run.
2. Download that run's `cursor-plugin-<commit>-3.13` artifact. Rebuild the same
commit with `python3 Scripts/build_package.py <new directory>` and confirm the
two `cursor.zip` files are byte-identical.
3. Run the native, ACP initialization and isolated host checks in
and that the reviewed `master` commit has successful `ci.yml` from a `push`.
Its exact archive artifact name is `cursor-plugin-<full-commit-sha>-3.13`.
2. Dispatch `.github/workflows/release.yml` with `ref` set to that full commit
SHA and `publish: false` (the default). The pinned organization workflow
runs `python3 Scripts/build_package.py` with an output-directory argument,
verifies the CI source and artifact digest, and requires the rebuilt
`cursor.zip` to match the CI archive byte for byte.
3. Inspect the `plugin-release-<run-id>-<attempt>` Actions artifact. Its
`assets/` directory contains `cursor.zip`, the generated
`release-receipt.json` and `SHA256SUMS`. An untagged dry run reports the
signed annotated tag still needed for publication.
4. Run the native version/help, ACP initialization and isolated host checks in
[Installation](Installation.md#isolated-host-interoperability) against that
exact archive.
4. Create a signed annotated tag `vX.Y.Z` on the accepted commit and push it.
5. Create the GitHub Release for the tag with `cursor.zip`, `SHA256SUMS` and
`release-receipt.json`, then publish it.
exact archive. Record the manual acceptance results and their limits in the
draft Release notes. Create and push the signed annotated `vX.Y.Z` tag on
the accepted commit, and target the draft Release at that full commit SHA.
5. Dispatch `release.yml` with `ref` set to that version tag and `publish: true`.
The workflow verifies the tag signature and manifest version, repeats the
source and archive checks, completes and verifies the draft assets, and
publishes. Its dependent notifier runs only when `publish` is true and the
shared workflow reports `published: true`.

`release-receipt.json` is written by the publisher; no script generates it. It
records the plugin ID and version, tag, source commit, archive name, size and
SHA-256, the source CI run and artifact digest, the declared compatibility, and
the validation results with their limits. `SHA256SUMS` covers the archive and the
receipt.
The shared workflow generates the deterministic receipt and checksums under the
organization's [release contents contract](https://github.com/computer-mcp/.github/blob/master/VERSIONING.md#plugin-release-contents).
Manual acceptance evidence belongs in the Release notes.

## Catalog notification

Expand All @@ -44,20 +59,20 @@ that website commit first, then update the pin here when adopting a change to
the notification contract.

A release published with a repository `GITHUB_TOKEN` does not trigger
release-event workflows. Automation that publishes that way calls the workflow
as a dependent of the job that makes the release public:

```yaml
notify-catalog:
needs: publish
uses: ./.github/workflows/notify-catalog.yml
secrets:
CATALOG_APP_PRIVATE_KEY: ${{ secrets.CATALOG_APP_PRIVATE_KEY }}
```
release-event workflows. `release.yml` calls `notify-catalog.yml` as a dependent
job after confirmed publication. Only the notifier receives
`CATALOG_APP_PRIVATE_KEY`; the reusable release workflow receives no catalog
secret. Dry runs do not notify the catalog.

## Retry

For an operator-driven publication or a missed or failed notification, dispatch:
For a failed publication, use **Re-run failed jobs**. Recovery preserves the
receipt's frozen CI provenance and retains matching draft assets. Follow the
organization's [release recovery guide](https://github.com/computer-mcp/.github/blob/master/Documentation/Reference/PluginRelease.md#run-and-recover)
for conflicts or unavailable source artifacts. Published releases are verified
without changing their assets.

For a missed or failed notification, rerun only the notifier or dispatch:

```sh
gh workflow run notify-catalog.yml --repo computer-mcp/plugin-cursor --ref master
Expand Down
4 changes: 2 additions & 2 deletions Scripts/probe_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / 'bin'))
from plugin_runtime import Job, Process, decoded, load_package, resolve_executable, verify_version
from plugin_runtime import Job, Process, decoded, expected_version, load_package, resolve_executable, verify_version


def main():
Expand All @@ -19,7 +19,7 @@ def main():
args = parser.parse_args()
_, tree = load_package(ROOT / 'bin/cursor-mcp-adapter')
executable = resolve_executable(args.executable, 'CURSOR_AGENT_EXECUTABLE', 'agent')
verify_version(executable, tree, Job(), os.getcwd())
verify_version(executable, expected_version(tree), Job(), os.getcwd())
process = Process([executable, 'acp'], os.getcwd())
deadline = time.monotonic()+30
response = None
Expand Down
12 changes: 12 additions & 0 deletions Tests/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
Expand Down Expand Up @@ -47,6 +48,17 @@ def test_version_mismatch_prevents_execution(self):
response=client.call('cursor.acp.prompt' if VENDOR=='cursor' else 'claude.run',{'prompt':'hello'})
self.assertEqual(Client.value(response)['error']['code'],'incompatible_vendor')
finally:client.close()
def test_acp_initialization_probe_uses_native_assertion(self):
with tempfile.TemporaryDirectory() as temp:
report=Path(temp)/'probe.json'
result=subprocess.run([sys.executable,str(ROOT/'Scripts/probe_acp.py'),
'--executable',str(ROOT/'Tests/Fixtures/vendor.py'),
'--output',str(report)],capture_output=True,text=True,timeout=10)
self.assertEqual(result.returncode,0,result.stderr)
evidence=json.loads(report.read_text())
self.assertEqual(evidence['status'],'passed')
self.assertEqual(evidence['response']['result']['protocolVersion'],1)
self.assertEqual(evidence['cleanup'],'confirmed')
def test_event_buffer_reports_exact_retention_and_small_pages(self):
events=EventBuffer(max_events=3,max_bytes=1024)
for i in range(3):events.append({'index':i})
Expand Down
4 changes: 4 additions & 0 deletions Tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import subprocess
import sys
import tempfile
import tomllib
import unittest
import zipfile

Expand All @@ -18,9 +19,12 @@ class PackageTests(unittest.TestCase):
def test_reproducible_archive_and_executable_mode(self):
first,manifest=builder.package_bytes(ROOT)
self.assertEqual(first,builder.package_bytes(ROOT)[0])
self.assertIs(type(manifest['plugin_api']),int)
self.assertEqual(manifest['plugin_api'],1)
with zipfile.ZipFile(io.BytesIO(first)) as archive:
names=archive.namelist()
self.assertIn('computer-mcp-plugin.toml',names)
self.assertEqual(tomllib.loads(archive.read('computer-mcp-plugin.toml').decode()),manifest)
self.assertIn('cli-tree.json',names)
self.assertIn('bin/plugin_runtime.py',names)
for entry in archive.infolist():
Expand Down
12 changes: 12 additions & 0 deletions Tests/test_review_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ def verify(*_):session.close()
self.assertEqual(error.exception.code,'cancelled')
process.assert_not_called()

def test_missing_version_assertion_prevents_native_launch(self):
for tree in ({}, {'executable_checks': []},
{'executable_checks': [{'args': ['--help'], 'stdout': 'usage\n'}]},
{'executable_checks': [{'args': ['--version']}]}):
with self.subTest(tree=tree):
session=adapter.Session('handle','/unused','manual')
with patch.object(adapter,'TREE',tree),patch('plugin_runtime.Process') as probe,patch.object(adapter,'Process') as native:
with self.assertRaises(Failure) as error:session.open({},Job())
self.assertEqual(error.exception.code,'invalid_configuration')
probe.assert_not_called()
native.assert_not_called()

def test_duplicate_and_invalid_native_request_ids_fail_closed(self):
for identifier in (900,True,None,[],{}):
with self.subTest(identifier=identifier):
Expand Down
4 changes: 2 additions & 2 deletions bin/cursor-mcp-adapter
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import time
import uuid

from plugin_runtime import (EventBuffer, Failure, Job, MCPServer, Process, Tool,
decoded, encoded, integer_schema, load_package, resolve_executable, schema,
decoded, encoded, expected_version, integer_schema, load_package, resolve_executable, schema,
text_schema, validate, verify_version)

MANIFEST, TREE = load_package(__file__)
Expand Down Expand Up @@ -135,7 +135,7 @@ class Session:
self.startup_done.clear()
try:
deadline = time.monotonic()+args.get('timeout_seconds',45)
verify_version(self.executable,TREE,job,os.getcwd(),self.retain_version_process)
verify_version(self.executable,expected_version(TREE),job,os.getcwd(),self.retain_version_process)
with self.lock: self.version_process = None
job.check()
with self.lock:
Expand Down
6 changes: 5 additions & 1 deletion bin/plugin_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,10 +414,14 @@ def load_package(entry):
return manifest, tree


def verify_version(executable, tree, job, cwd, retain_process=None):
def expected_version(tree):
expected = next((x.get('stdout') for x in tree.get('executable_checks', []) if x.get('args') == ['--version']), None)
if expected is None:
raise Failure('invalid_configuration', 'Package lacks its native version assertion')
return expected


def verify_version(executable, expected, job, cwd, retain_process=None):
process = Process([executable, '--version'], cwd)
deadline = time.monotonic()+5
output = bytearray()
Expand Down
1 change: 1 addition & 0 deletions computer-mcp-plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
id = "cursor"
name = "Cursor"
version = "0.1.0"
plugin_api = 1
repository = "https://github.com/computer-mcp/plugin-cursor"
description = "Cursor Agent CLI automation plus an ACP-to-MCP adapter."

Expand Down
Loading