diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d63d464 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @showxu diff --git a/.github/RELEASE.md b/.github/RELEASE.md deleted file mode 100644 index eaffab1..0000000 --- a/.github/RELEASE.md +++ /dev/null @@ -1,59 +0,0 @@ -# Release publication and catalog notification - -Publish only an accepted plugin package bound to its reviewed source/tag and exact archive digest. -Keep an existing public tag and archive immutable. A candidate, draft or notification receipt does -not establish authenticated vendor or installed-host acceptance. - -The Validate and package workflow produces candidate artifacts; it does not make a release public. After the accepted release becomes public, -`notify-catalog.yml` requests a complete catalog reconciliation. It also observes public edits, -channel promotion, unpublishing and deletion; those events never authorize catalog withdrawal by -themselves. The central publisher retains verified history and applies its reviewed withdrawal -policy. It verifies actual GitHub release sources rather than trusting an event payload. - -## Notification authority - -The workflow pins the website's central notification action to a reviewed full commit. Publish that -central commit before enabling a plugin workflow that references it. Review and update this pin -when adopting changes to the notification contract. The caller checks its immutable repository ID, -does not check out package code, and grants its own job token no repository permissions. - -Supply `CATALOG_DISPATCH_TOKEN` using existing reviewed authority with Actions write access to -`computer-mcp/computer-mcp.github.io` only. Website Contents write access is unnecessary. The action -can also receive an existing temporary token directly from a publishing job. Neither workflow -creates or persists credentials. Missing or rejected authority fails visibly; the -publisher's independent schedule still reconciles missed notifications. - -## Publication and retry - -A manual public release emits the release event. Publication performed with a repository's -`GITHUB_TOKEN` does not trigger ordinary release-event workflows. After that publication succeeds, -its automation must explicitly call this reusable workflow as a dependent job: - -```yaml -notify-catalog: - needs: publish - uses: ./.github/workflows/notify-catalog.yml - secrets: - CATALOG_DISPATCH_TOKEN: ${{ secrets.CATALOG_DISPATCH_TOKEN }} -``` - -Here `publish` is the job that actually makes the accepted release public, not the candidate-build -or draft-upload job. When using an existing short-lived token within that publishing job, invoke -the same pinned central action directly after publication instead. Keep token values out of command -arguments, printed output and release metadata. - -For an operator-driven publication or a missed/failed notification, explicitly dispatch: - -```sh -gh workflow run notify-catalog.yml --repo computer-mcp/plugin-cursor --ref master -``` - -This schedules notification using its configured authority; it does not publish or rewrite a -release. Inspect the notification run and its returned central `run_url`. A successful dispatch -proves request acceptance only. Verify the central run completed successfully and the public index -contains the exact expected release identities and generation. If the release is already public and -notification fails, retry notification without changing or republishing the release. Complete -reconciliation is idempotent and repairs duplicate/missed events. - -See the central [catalog publication and notification contract](https://github.com/computer-mcp/computer-mcp.github.io/blob/master/docs/plugin-catalog.md) -for provenance, credentials, retry bounds and deployment semantics. diff --git a/.github/workflows/validate.yml b/.github/workflows/ci.yml similarity index 98% rename from .github/workflows/validate.yml rename to .github/workflows/ci.yml index 02335fe..1fe09b5 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,8 @@ -name: Validate and package +name: CI on: pull_request: push: + branches: [master] workflow_dispatch: permissions: contents: read diff --git a/AGENTS.md b/AGENTS.md index c12b8a0..abcd9b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,154 @@ # plugin-cursor Agent Guide +Read `README.md` first for repository purpose and entry points. +Apply this guide before making changes. + +## First-Principles Work + +Before changing code, docs, schemas, scripts, templates, examples, governance, +or automation, reduce the task to observable behavior, root cause, invariant, +owner, data flow, and validation. + +- Do not silently choose among plausible interpretations. State assumptions, + surface conflicts, and ask when the decision materially changes the result. +- Deliver the complete requested behavior with bounded design. Do not stop at a + toy result when production behavior is requested, and do not add abstraction, + configurability, workflow machinery, or future-facing features unless the + request, source evidence, or owning invariant requires them. +- Change the owning layer, not the nearest convenient file. +- Keep changes traceable to the request, source evidence, or owning invariant. +- Do not clean up, reformat, rename, or refactor unrelated nearby material + unless it is required by the request or owning invariant. +- Use the strongest feasible validation for the result. If validation is + skipped, say what was skipped and why. + +Use this check: + +1. What behavior is wrong, missing, or at risk? +2. What root cause explains it? +3. What invariant must hold? +4. Which artifact, layer, or workflow owns it? +5. What data or decision flows into that owner? +6. What remains variable, configurable, or case-local? +7. What evidence proves the result beyond one literal case? + +## Canonical Artifacts + +- Treat conversation, review feedback, plans, and intermediate attempts as + editing input. Recompute the complete accepted result before finalizing. +- Active artifacts depend only on that result and their repository role, not + on the editing path. Apply this to code, symbols, files, wrappers, branches, + configuration, schemas, defaults, generated sources, scripts, templates, + automation, comments, DocC, diagrams, tests, fixtures, snapshots, examples, + and normative docs. +- If an intermediate result is `A + B` and the accepted result is `A`, express + `A` directly. Remove `B` and its residual surface rather than retaining names + such as `AOnly` or `AWithoutB`, or prose such as "B was removed." +- Normalize by semantic identity and artifact role, not by token. A rejected + current capability does not invalidate a distinct historical fact, + migration, ownership record, or safety boundary that uses the same term. +- Keep a negative constraint only when excluding `B` is independently required + by a current compatibility, safety, or ownership invariant. +- A disabled B flag, skipped B test, dead B branch, retained B fixture, or + "do not add B" rule is residue when it exists only because B was attempted; + disabled state alone is not an invariant. +- Keep change history only in commits, pull requests, changelogs, release + records, migrations, archives, or accepted decision records with durable + value. Do not create a history artifact merely to preserve a correction. +- Preserve role-owned facts unless separate evidence changes them; do not + rewrite history or ownership merely to make a rejected term disappear. +- Leave an already-correct history, migration, provenance, ownership, or safety + artifact unchanged when the task does not change its facts. Do not polish or + restate it merely because it is relevant to the current edit. +- Comments explain non-obvious current semantics and invariants, not the + sequence of edits. +- Before handoff, verify that a new agent with no editing conversation can + derive the complete current behavior, boundaries, and operating guidance + without mentally subtracting a rejected concept. + +## Task Route + +- Before changing versions, dependencies, packaging or release workflows, read + `Documentation/Architecture/VersioningAndRelease.md` and use its existing project check entry points. + +- For repository-native documentation placement, read `Documentation/README.md` + before editing. +- For current canonical structure, read + `Documentation/Architecture/README.md` and the relevant architecture files. +- For design-in-progress, use `Documentation/Proposals/*` when that subtree is + present. +- For change history, consult `Documentation/Decisions/*`, + `Documentation/Migrations/*`, and `Documentation/Archive/*` when those + subtrees are present. +- For GitHub-facing collaboration files, use `.github/` and root governance + files. +- Stop and clarify before mixing route instructions into `README` files or + index text into `AGENTS.md`. + +## Authority + +- `AGENTS.md` is the agent guide: first-principles guardrails, task + route, authority boundaries, and boundary guardrails. +- `README`-class files index scope and placement. +- `Documentation/Architecture/*` is current truth. +- `Documentation/Proposals/*` is proposal space when that subtree is present. +- `Documentation/Decisions/*`, `Documentation/Migrations/*`, and + `Documentation/Archive/*` are history when those subtrees are present. +- `.github/*` is GitHub-facing governance. + +## Boundary Guardrails + +After the owner and invariant are clear, classify concrete values by stability, +variability, and ownership before writing reusable artifacts. + +Do not promote context-bound values into reusable artifacts. A value is +context-bound if it depends on the current machine, local workspace, current +input, one fixture, one runtime run, one user-specific path, or temporary +execution state. + +Keep shipped documentation focused on current product facts, supported behavior, +and operating guidance. Keep temporary implementation notes, local evidence, +local paths, run-specific artifacts, and historical comparison notes out of +README files, Reference docs, API docs, and bundled user-facing skills. Promote +only accepted decision records into +`Documentation/Decisions/*` and durable transition or cutover records into +`Documentation/Migrations/*`. + +Use this decision test: + +- If a value changes by input, get it from input, spec, config, parameters, or + an explicit user decision. +- If a value changes by environment, get it from configuration, runtime state, + environment variables, or local execution notes. +- If a value belongs only to one example, fixture, or run, keep it there. Do + not generalize it into reusable docs, schemas, templates, scripts, + validation rules, or automation. +- If the artifact being edited is not the source of truth for the value, do not + hardcode it there. Pass it in, derive it, configure it, or link to the + owning artifact. +- Only stable invariants and values owned by the current artifact may be fixed + in reusable artifacts. + +Classify concrete values before writing: + +1. Name the variable parts. +2. Decide which artifact owns each variable. +3. Replace context-bound literals with placeholders, parameters, config keys, + derived values, or links to the owning artifact. +4. Keep concrete literals only inside the artifact that owns them. + +When in doubt, use a placeholder, parameter, configuration key, or repo-owned +source of truth instead of a literal value. + +## Operating Notes + +- Keep Agent Guide and Index separate. +- Keep current truth out of proposal and history subtrees when they are + present. +- Keep GitHub collaboration configuration out of `Documentation/`. + +## Repository Guardrails + Read `README.md`, `computer-mcp-plugin.toml`, and `Documentation/Reference/Interface.md` before editing. Keep vendor installation, authentication, subscription usage, and updates outside this repository. The external Cursor Agent is a dependency, not bundled code. Do not run authenticated model prompts merely to validate package structure. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6674fe4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable user-visible changes to the Cursor plugin are documented here. + +## Unreleased + +## 0.1.1 β€” 2026-09-29 + +- Preserve ACP session ownership across background prompts, interactive requests + and uncertain cleanup. +- Bind permission, question and plan replies to the current native request. +- Expose owned sessions through the standard MCP work resource, so a compatible + host can account for work after a tool reply and across live configuration + changes. +- Bound events and continuation handling while keeping native session identity + explicit. + +## 0.1.0 β€” 2026-09-24 + +- First release: a canonical CLI tree for the headless Cursor Agent surface, a + stdio MCP adapter with twelve tools for ACP sessions, resumed conversations, + synchronous and background prompts, events, cancellation and explicit + permission, question and plan responses, and usage Skills. +- Requires Computer MCP 1.2.2 or later on Apple Silicon, Python 3.13 or newer, + and Cursor Agent 2026.05.04-08e5280. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3c7df7..b7a27e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,13 @@ Read the [architecture](Documentation/Architecture/README.md) and [interface contract](Documentation/Reference/Interface.md). Keep vendor-specific behavior in this repository and preserve the host manifest/CLITree/MCP boundaries. -Run `python3 -m unittest discover -s Tests -p 'test_*.py' -v` and package into a new output directory with `python3 Scripts/build_package.py`. When changing the native interface, inspect the matching real executable and run `Scripts/validate_native.py --executable PATH`. Never use authenticated model calls as ordinary unit tests. +CI runs these checks on Python 3.13 and 3.14; run them before opening a pull request: -Do not commit caches, local credentials, generated runtime state or `.agent` evidence. Publishing a repository or release is a separate explicit action. +```sh +python3 -m unittest discover -s Tests -p 'test_*.py' -v +python3 Scripts/build_package.py /new/output/directory +``` + +CI also runs `python3 Tests/check_runtime_rejection.py` on Python 3.11 and 3.12 to confirm that unsupported interpreters are refused, and the organization brand check. When changing the native interface, inspect the matching real executable and run `Scripts/validate_native.py --executable PATH`. Never use authenticated model calls as ordinary unit tests. + +Do not commit caches, local credentials or generated runtime state. Publishing a repository or release is a separate explicit action. diff --git a/Documentation/Architecture/README.md b/Documentation/Architecture/README.md index 5babf0c..1c96f6f 100644 --- a/Documentation/Architecture/README.md +++ b/Documentation/Architecture/README.md @@ -1,3 +1,5 @@ # Architecture [Package](Package.md) defines ownership, runtime and distribution boundaries. +[Versioning and Release](VersioningAndRelease.md) defines version authority, +acceptance and publication. diff --git a/Documentation/Architecture/VersioningAndRelease.md b/Documentation/Architecture/VersioningAndRelease.md new file mode 100644 index 0000000..35b3c3b --- /dev/null +++ b/Documentation/Architecture/VersioningAndRelease.md @@ -0,0 +1,80 @@ +# 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). + +This document owns the current version and release rules for plugin-cursor. + +## 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. + +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. + +## 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. + +`cli-tree.json` pins the exact Cursor Agent version through its +`executable_checks`, which the host applies before every CLI and adapter call. A +different vendor version requires a reviewed update of the tree, verified with +`Scripts/validate_native.py` against the real executable. + +## Derived Metadata and Drift Checks + +`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. + +## 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. +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. + +## Evidence Reuse and Invalidation + +The CI artifact name binds the source commit and Python version, and the release +receipt binds the commit, CI run, artifact digest and archive SHA-256. An archive +whose digest matches the accepted one keeps its acceptance. Any change to a +packaged file produces a new archive that needs the affected checks again. + +## Entry Points and Artifact Retention + +| Operation | Existing command or explicit manual procedure | Required access | +| --- | --- | --- | +| 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 ` | 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 | +| 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. diff --git a/Documentation/README.md b/Documentation/README.md index e923be0..21978df 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,5 +1,7 @@ # Documentation - [Architecture](Architecture/README.md): current component ownership and lifecycle. +- [Versioning and Release](Architecture/VersioningAndRelease.md): version authority, acceptance and publication rules. - [Interface](Reference/Interface.md): CLI and MCP contracts, error and retention behavior. - [Installation](Reference/Installation.md): dependencies, grants, packaging and validation. +- [Release](Reference/Release.md): publishing a release and notifying the plugin catalog. diff --git a/Documentation/Reference/Release.md b/Documentation/Reference/Release.md new file mode 100644 index 0000000..daafbf7 --- /dev/null +++ b/Documentation/Reference/Release.md @@ -0,0 +1,74 @@ +# 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. + +## 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--3.13` artifact. Rebuild the same + commit with `python3 Scripts/build_package.py ` and confirm the + two `cursor.zip` files are byte-identical. +3. Run the native, 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. + +`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. + +## Catalog notification + +`notify-catalog.yml` runs when a release is published, edited, released, +unpublished or deleted, and on manual dispatch. It asks the website to reconcile +the complete catalog. The website verifies the actual GitHub releases rather +than the event payload, keeps verified history, and applies its own withdrawal +policy; an event never withdraws a release by itself. + +The workflow authenticates as the receiver-scoped catalog GitHub App through +the `CATALOG_APP_CLIENT_ID` variable and the `CATALOG_APP_PRIVATE_KEY` secret. +The App needs Actions write access to `computer-mcp/computer-mcp.github.io` only. +The job checks this repository's immutable ID, does not check out package code, +and grants its own token no repository permissions. Missing or rejected +authority fails the run visibly. + +The website's notification action is pinned to a reviewed full commit. Publish +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 }} +``` + +## Retry + +For an operator-driven publication or a missed or failed notification, dispatch: + +```sh +gh workflow run notify-catalog.yml --repo computer-mcp/plugin-cursor --ref master +``` + +A successful dispatch only proves the request was accepted. Follow the run's +`run_url` output to the website run, confirm it succeeded, and confirm the public +index lists the expected release. Retrying never requires changing or +republishing the release: reconciliation is idempotent, and the website's +hourly schedule also repairs missed notifications. + +The website's +[catalog publication and notification contract](https://github.com/computer-mcp/computer-mcp.github.io/blob/master/docs/plugin-catalog.md) +covers provenance, credentials, retry bounds and deployment. diff --git a/Scripts/validate_host.py b/Scripts/validate_host.py index c4d4009..7956557 100644 --- a/Scripts/validate_host.py +++ b/Scripts/validate_host.py @@ -165,10 +165,9 @@ def validate(host, archive, output, expected_host_version, require_work_ownershi projected = [tool for tool in catalog if tool.get('_meta', {}).get('cli', {}).get('command') == 'print'] require(len(projected) == 1, 'Expected one real host-projected print tool') native_names = [tool['name'] for tool in catalog if tool['name'].startswith(vendor + '.')] - require(len(native_names) == (12 if vendor == 'cursor' else 6), 'Adapter catalog missing required tools') + require(len(native_names) == 12, 'Adapter catalog missing required tools') checks['catalog'] = {'status':'passed', 'cli_tools':len(projected), 'mcp_tools':len(native_names)} if require_work_ownership: - require(vendor == 'cursor', 'Work ownership acceptance requires the Cursor session contract') require(all(key not in tool.get('_meta',{}) for tool in catalog for key in ('io.github.computer-mcp/work','io.github.computer-mcp/continuation')), 'Gateway exports advertise downstream-only ownership metadata') @@ -182,8 +181,6 @@ def observe(): and value['unsettled_invocation_count']==0 and not value['observation_pending']) prompt = "--leading 'quotes' δΈ­ζ–‡\nnot-a-shell-command" arguments = {'prompt': prompt} - if vendor == 'claude': - arguments['permission_mode'] = 'plan' result = checked(client, projected[0]['name'], arguments) argv = result['data']['argv'] require(argv[-2:] == ['--', prompt], 'Host did not preserve exact argv argument boundaries') @@ -199,32 +196,24 @@ def observe(): require('error' in bypass or bypass['result'].get('isError'), 'Raw execution bypassed the CLI tree') checks['cli_negative_contracts'] = 'passed' - if vendor == 'cursor': - session = checked(client, 'cursor.acp.session.open', {'permission_policy':'manual'})['session'] - work_evidence = {'opened':work_status(1)} if require_work_ownership else None - run = checked(client, 'cursor.acp.session.prompt.start', {'session':session,'prompt':'permission'})['prompt_id'] - pending = wait_for(lambda: checked(client, 'cursor.acp.requests.list', {'session':session}), lambda v: bool(v['requests']))['requests'][0] - if work_evidence is not None: work_evidence['waiting_for_input'] = work_status(1) - checked(client, 'cursor.acp.requests.respond', {'session':session,'request_id':pending['request_id'], - 'response':{'outcome':{'outcome':'selected','optionId':'opaque-no'}}}) - completed = wait_for(lambda: checked(client, 'cursor.acp.session.prompt.result', {'session':session,'prompt_id':run}), lambda v:v.get('completed')) - require(not completed.get('is_error'), 'Background ACP fixture failed') - if work_evidence is not None: work_evidence['idle_session'] = work_status(1) - checked(client, 'cursor.acp.events.read', {'session':session,'max_bytes':2048}) - checked(client, 'cursor.acp.session.close', {'session':session}) - require(not checked(client, 'cursor.acp.session.list')['sessions'], 'Closed ACP session remains live') - if work_evidence is not None: - work_evidence['released'] = work_status(0) - require(len({state['instance_id'] for state in work_evidence.values()})==1, - 'Provider connection changed during session ownership acceptance') - checks['provider_work'] = work_evidence - else: - run = checked(client, 'claude.run.start', {'prompt':'hello','permission_mode':'plan'})['run_id'] - completed = wait_for(lambda: checked(client, 'claude.run.result', {'run_id':run}), lambda v:v.get('completed')) - require(completed['result'] == 'hello', 'Native final result was not preserved') - checked(client, 'claude.run.events', {'run_id':run,'max_bytes':2048}) - invalid = client.call('claude.run', {'prompt':'not-executed','permission_mode':'bypassPermissions'}) - require(invalid['result'].get('isError'), 'Bypass mode was admitted') + session = checked(client, 'cursor.acp.session.open', {'permission_policy':'manual'})['session'] + work_evidence = {'opened':work_status(1)} if require_work_ownership else None + run = checked(client, 'cursor.acp.session.prompt.start', {'session':session,'prompt':'permission'})['prompt_id'] + pending = wait_for(lambda: checked(client, 'cursor.acp.requests.list', {'session':session}), lambda v: bool(v['requests']))['requests'][0] + if work_evidence is not None: work_evidence['waiting_for_input'] = work_status(1) + checked(client, 'cursor.acp.requests.respond', {'session':session,'request_id':pending['request_id'], + 'response':{'outcome':{'outcome':'selected','optionId':'opaque-no'}}}) + completed = wait_for(lambda: checked(client, 'cursor.acp.session.prompt.result', {'session':session,'prompt_id':run}), lambda v:v.get('completed')) + require(not completed.get('is_error'), 'Background ACP fixture failed') + if work_evidence is not None: work_evidence['idle_session'] = work_status(1) + checked(client, 'cursor.acp.events.read', {'session':session,'max_bytes':2048}) + checked(client, 'cursor.acp.session.close', {'session':session}) + require(not checked(client, 'cursor.acp.session.list')['sessions'], 'Closed ACP session remains live') + if work_evidence is not None: + work_evidence['released'] = work_status(0) + require(len({state['instance_id'] for state in work_evidence.values()})==1, + 'Provider connection changed during session ownership acceptance') + checks['provider_work'] = work_evidence checks['mcp_execution_and_events'] = 'passed' finally: client.close() @@ -241,7 +230,7 @@ def observe(): restricted = configuration(package, workspace, cli_fixture, ROOT / 'Tests/Fixtures/vendor.py', vendor) restricted = restricted.replace('mode = "local-full-access"', 'mode = "workspace-operations"') restricted = restricted.replace('full_shell_enabled = true', 'full_shell_enabled = false') - execution_tool = 'cursor.acp.prompt' if vendor == 'cursor' else 'claude.run' + execution_tool = 'cursor.acp.prompt' # An explicit low host risk must not bypass the publisher's execution floor. restricted += '\n[mcp.servers.tool_risks]\n' + json.dumps(execution_tool) + ' = "read-only"\n' config.write_text(restricted) @@ -249,8 +238,7 @@ def observe(): try: tools = client.request('tools/list')['result']['tools'] require(execution_tool not in {tool['name'] for tool in tools}, 'Restricted profile exposed arbitrary vendor execution') - inspection = 'cursor.acp.session.list' if vendor == 'cursor' else 'claude.run.list' - checked(client, inspection) + checked(client, 'cursor.acp.session.list') for name, arguments in [(execution_tool, {'prompt':'must-not-execute'}), ('mcp.tools.call', {'server':'fixture-adapter','tool':execution_tool,'arguments':{'prompt':'must-not-execute'}})]: denied = client.call(name, arguments)