diff --git a/.github/assets/logo/logo-dark.png b/.github/assets/logo/logo-dark.png new file mode 100644 index 0000000..c7b9640 Binary files /dev/null and b/.github/assets/logo/logo-dark.png differ diff --git a/.github/assets/logo/logo-light.png b/.github/assets/logo/logo-light.png new file mode 100644 index 0000000..c7b9640 Binary files /dev/null and b/.github/assets/logo/logo-light.png differ diff --git a/.github/assets/social-preview/social-preview.png b/.github/assets/social-preview/social-preview.png new file mode 100644 index 0000000..6014bdb Binary files /dev/null and b/.github/assets/social-preview/social-preview.png differ diff --git a/.github/brand/brand.lock.json b/.github/brand/brand.lock.json deleted file mode 100644 index e419c05..0000000 --- a/.github/brand/brand.lock.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "schema_version": 2, - "authority": "https://github.com/computer-mcp/.github", - "revision": "2d9913a442afcb22faca90f5272d3988eb961be6e858b71c5067303cf7c627d6", - "consumer": "plugin-claude", - "files": { - "Documentation/Brand/header-light.png": "03277261db524124c7779e7ce7108d82265f0c19a3ea1934066b688bb0a89306", - "Documentation/Brand/header-dark.png": "2e68e1fc465ca83e294da806e8ec8a455fceeeefd598beda27c08a720bab80b5", - ".github/brand/social.png": "287ac8ebf4123e90fc75487fc68c4a8e819847db3bb87fe46710453d1aacf94d" - }, - "entries": { - "README.md": [ - "Claude Code", - "Documentation/Brand/header-light.png", - "Documentation/Brand/header-dark.png" - ] - } -} diff --git a/.github/brand/social.png b/.github/brand/social.png deleted file mode 100644 index 1e95269..0000000 Binary files a/.github/brand/social.png and /dev/null differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a30177d..1023105 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,15 @@ name: CI on: pull_request: + types: [opened, synchronize, reopened] push: branches: [master] workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: package: runs-on: macos-latest @@ -24,8 +28,8 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: | - python3 -m unittest discover -s Tests -p 'test_*.py' - python3 Scripts/build_package.py dist > package-receipt.json + python3 -m unittest discover -s tests -p 'test_*.py' + python3 scripts/build_package.py dist > package-receipt.json - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: claude-plugin-${{ github.sha }}-${{ matrix.python-version }} @@ -46,8 +50,12 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 with: python-version: ${{ matrix.python-version }} - - run: python3 Tests/check_runtime_rejection.py - brand: - uses: computer-mcp/.github/.github/workflows/brand-check.yml@f402911abcdfb87ef0c41a2759e8c51387b18935 # master + - run: python3 tests/check_runtime_rejection.py runtime: - uses: computer-mcp/.github/.github/workflows/plugin-runtime.yml@35766b350427ed900b456aa3c39098e97d9d3aa1 # master + uses: computer-mcp/github-workflows/.github/workflows/plugin-runtime.yml@d65bc20504f39c3a30232d67043abeca43d2c7b1 + validate: + if: always() + needs: [package, unsupported-runtime, runtime] + uses: computer-mcp/github-workflows/.github/workflows/result.yml@d65bc20504f39c3a30232d67043abeca43d2c7b1 + with: + needs-json: ${{ toJSON(needs) }} diff --git a/.github/workflows/notify-catalog.yml b/.github/workflows/notify-catalog.yml index 471b1fd..a90504c 100644 --- a/.github/workflows/notify-catalog.yml +++ b/.github/workflows/notify-catalog.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Request complete catalog reconciliation id: catalog - uses: computer-mcp/computer-mcp.github.io/.github/actions/notify-catalog@cda692839487f03a2b4740d945f9a8bd818554e5 + uses: computer-mcp/github-workflows/actions/notify-catalog@d65bc20504f39c3a30232d67043abeca43d2c7b1 with: client-id: ${{ vars.CATALOG_APP_CLIENT_ID }} private-key: ${{ secrets.CATALOG_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1e1983..17877ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,11 @@ jobs: permissions: contents: write actions: read - uses: computer-mcp/.github/.github/workflows/plugin-release.yml@35766b350427ed900b456aa3c39098e97d9d3aa1 # master + uses: computer-mcp/github-workflows/.github/workflows/plugin-release.yml@d65bc20504f39c3a30232d67043abeca43d2c7b1 with: ref: ${{ inputs.ref }} publish: ${{ inputs.publish }} - build: python3 Scripts/build_package.py + build: python3 scripts/build_package.py ci_artifact: claude-plugin-${commit}-3.13 notify-catalog: needs: release diff --git a/.github/workflows/source-policy.yml b/.github/workflows/source-policy.yml new file mode 100644 index 0000000..6179997 --- /dev/null +++ b/.github/workflows/source-policy.yml @@ -0,0 +1,17 @@ +name: Source policy checks +on: + pull_request: + types: [opened, synchronize, reopened, edited] + push: + branches: [master] + merge_group: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + policy: + uses: computer-mcp/github-workflows/.github/workflows/policy.yml@d65bc20504f39c3a30232d67043abeca43d2c7b1 + with: + declaration-revision: 5c54a40cb7953db7472ebbefd3db608036b7b3ab diff --git a/AGENTS.md b/AGENTS.md index a6395b0..6ff7e6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,17 +70,17 @@ Use this check: - Before changing versions, dependencies, packaging or release workflows, read the [organization VERSIONING](https://github.com/computer-mcp/.github/blob/master/VERSIONING.md) - for version rules and `Documentation/Reference/Release.md` for release facts + for version rules and `docs/Reference/Release.md` for release facts and check entry points. -- For repository-native documentation placement, read `Documentation/README.md` +- For repository-native documentation placement, read `docs/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 + `docs/Architecture/README.md` and the relevant architecture files. +- For design-in-progress, use `docs/Proposals/*` when that subtree is present. -- For change history, consult `Documentation/Decisions/*`, - `Documentation/Migrations/*`, and `Documentation/Archive/*` when those +- For change history, consult `docs/Decisions/*`, + `docs/Migrations/*`, and `docs/Archive/*` when those subtrees are present. - For GitHub-facing collaboration files, use `.github/` and root governance files. @@ -92,10 +92,10 @@ Use this check: - `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. +- `docs/Architecture/*` is current truth. +- `docs/Proposals/*` is proposal space when that subtree is present. +- `docs/Decisions/*`, `docs/Migrations/*`, and + `docs/Archive/*` are history when those subtrees are present. - `.github/*` is GitHub-facing governance. ## Boundary Guardrails @@ -113,8 +113,8 @@ 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/*`. +`docs/Decisions/*` and durable transition or cutover records into +`docs/Migrations/*`. Use this decision test: @@ -147,11 +147,11 @@ source of truth instead of a literal value. - 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/`. +- Keep GitHub collaboration configuration out of `docs/`. ## Repository Guardrails -Read `README.md`, `computer-mcp-plugin.toml`, and `Documentation/Reference/Interface.md` before editing. +Read `README.md`, `computer-mcp-plugin.toml`, and `docs/Reference/Interface.md` before editing. Keep vendor installation, authentication, subscription/API usage, updates, plugins, and setup tokens outside this repository. The external Claude Code executable is a dependency, not bundled code. Do not run authenticated model prompts merely to validate package structure. @@ -162,13 +162,3 @@ Keep vendor installation, authentication, subscription/API usage, updates, plugi Use Python standard library only unless a dependency demonstrably removes required complexity. Tests must cover MCP initialization, tool discovery, stream-json parsing, permission-boundary rejection, cancellation, deterministic package contents, and executable archive mode. Before handoff run unit tests, the native non-model interface check, deterministic package build, Computer MCP package validation/doctor where available, and git status. Do not publish or create a release implicitly. - -## Brand delivery - -The organization `.github` repository owns the design system and renders every -image; the main Computer MCP repository owns ProductIdentity. Imported -`Documentation/Brand/header-light.png` and `header-dark.png` travel with the -packaged manual; `.github/brand/social.png` supplies the repository preview. -`.github/brand/brand.lock.json` binds them to the organization exports, and CI -verifies it with the shared brand-check workflow. Update imports with the -organization repository's `python3 Brand/brand.py sync`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35cc6c1..db6880d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,15 +1,15 @@ # Contributing -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. +Read the [architecture](docs/Architecture/README.md) and [interface contract](docs/Reference/Interface.md). Keep vendor-specific behavior in this repository and preserve the host manifest/CLITree/MCP boundaries. CI runs these checks on Python 3.13 and 3.14; run them before opening a pull request: ```sh -python3 -m unittest discover -s Tests -p 'test_*.py' -v -python3 Scripts/build_package.py /new/output/directory +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. +CI also runs `python3 tests/check_runtime_rejection.py` on Python 3.11 and 3.12 to confirm that unsupported interpreters are refused. 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/Brand/header-dark.png b/Documentation/Brand/header-dark.png deleted file mode 100644 index 17a0a8f..0000000 Binary files a/Documentation/Brand/header-dark.png and /dev/null differ diff --git a/Documentation/Brand/header-light.png b/Documentation/Brand/header-light.png deleted file mode 100644 index 2376fb9..0000000 Binary files a/Documentation/Brand/header-light.png and /dev/null differ diff --git a/README.md b/README.md index 43c2e84..542d936 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,119 @@ - - - Computer MCP — Claude Code - +

+ + + + +

-# Computer MCP — Claude Code +

Claude Code

-Part of the [Computer MCP](https://computer-mcp.github.io/) family. -**Wherever you chat, your computer is there.** +

Computer MCP family plugin for Claude Code CLI and streaming MCP integration.

-An independent Computer MCP plugin for Claude Code. The same package contributes a canonical CLI Tree, a stdio MCP adapter and client-neutral usage Skills. It does not link Computer MCP Core or require a host release to add vendor behavior. +

+ CI + Python 3.13 or newer + Architecture: arm64 + License: FSL-1.1-ALv2 +

-The CLI contribution describes verified non-interactive commands. The native baseline and its exact version assertion live in `cli-tree.json`. It is deliberately partial: interactive setup, installation and unsupported administrative flows are not represented as callable tools. The adapter separately owns streaming and session/run state; the host owns registration, profile grants, initial working-directory selection and audit. +

+ Install · + Usage · + Interface · + Documentation · + Contributing +

-## MCP capabilities +## Overview -The adapter provides `claude.run`, `claude.run.start`, `claude.run.list`, `claude.run.result`, `claude.run.events`, `claude.run.cancel` and `claude.run.release`. It consumes native print-mode stream-json, including partial messages, and retains the final result, native session ID and explicit error status. Release discards a completed result only after cleanup is confirmed. Discovery does not launch Claude Code. +An independent [Computer MCP](https://computer-mcp.github.io/) plugin for Claude Code. One +package contributes a canonical CLI tree, a stdio MCP adapter that streams runs, and +client-neutral usage skills. It does not link Computer MCP Core, and adding vendor behavior needs +no host release. -The ordinary MCP work resource reports active runs and retained result handles -with their original acquisition reference. Hosts can account for this work -after its creating tool returns, without private Host Services permission. -Cleanup uncertainty remains owned and cannot be released or evicted. +The CLI contribution describes verified non-interactive commands; the native baseline and its +exact version assertion live in `cli-tree.json`. It is deliberately partial: interactive setup, +installation and unsupported administrative flows are not callable tools. The adapter owns +streaming and session and run state. The host owns registration, profile grants, the initial +working directory and audit. -`permission_mode` defaults to `dontAsk`; `plan` and `acceptEdits` are supported explicit choices. The noninteractive contract does not expose permission-bypass modes or fabricate interactive permission responses. Existing vendor configuration continues to apply. Resume/continue/new-session selection is explicit; cancelling a run does not delete its saved native conversation. +## Quick look -## Use and verify +| Contribution | What it adds | +| --- | --- | +| `claude` CLI | Typed tools for the verified non-interactive Claude Code commands | +| `stream` MCP server | `claude.run`, `claude.run.start`, `claude.run.list`, `claude.run.result`, `claude.run.events`, `claude.run.cancel` and `claude.run.release`, plus a work resource listing active runs and retained results | +| `claude-code` skill | Guidance for choosing the CLI or the streaming tools safely | -The external `claude` executable, Python 3.13+ and vendor credentials are user-owned. Install neither through this plugin. Configure the two contributions using the [installation guide](Documentation/Reference/Installation.md) and `Examples/settings.json`. +Install the archive in the host, bind the `claude` executable, and configure both contributions +from `Examples/settings.json`. + +## Install + +1. Install and authenticate Claude Code yourself, and provide Python 3.13 or newer on the + Computer MCP launch `PATH`. The plugin never installs or updates either, logs in, creates setup + tokens or copies credentials. +2. Build the archive: + + ```sh + python3 scripts/build_package.py /new/output/directory + ``` + +3. Install the ZIP through Computer MCP's plugin UI or the owner CLI. Installation is neither + activation nor a permission grant. +4. Review `Examples/settings.json`, replace its placeholder paths, and configure the plugin + through the same UI or `plugins configure`. The CLI dependency binding selects the executable + for the CLI contribution; the MCP launch `args` supply `--executable` for the adapter. Point + both at the same verified installation. + +See the [installation guide](docs/Reference/Installation.md) for revisions, configuration +publication and validation. + +## Usage + +The adapter consumes Claude Code's print-mode stream-json, including partial messages, and keeps +the final result, the native session ID and an explicit error status. Discovery does not launch +Claude Code. Release discards a completed result only after cleanup is confirmed; cleanup +uncertainty stays owned and cannot be released or evicted. The work resource reports each active +run and retained result with its original acquisition reference, so hosts can account for work +after the creating tool returns, without private Host Services permission. + +`permission_mode` defaults to `dontAsk`; `plan` and `acceptEdits` are supported explicit choices. +The non-interactive contract exposes no permission-bypass mode and never fabricates interactive +permission responses, and existing vendor configuration continues to apply. Choosing to resume, +continue or start a session is explicit, and cancelling a run does not delete its saved native +conversation. + +The working directory is not an operating-system sandbox. Vendor configuration, credentials and +tools keep their own authority; host authorization does not silently approve vendor actions. See +the [interface contract](docs/Reference/Interface.md) for retention, cancellation and error +semantics. + +## Requirements + +- Computer MCP on an arm64 computer. +- Claude Code, installed and authenticated separately; its version must match `cli-tree.json`. +- Python 3.13 or newer. + +## Documentation + +The [documentation index](docs/README.md) links the architecture, the interface, the installation +guide and the [release process](docs/Reference/Release.md). To validate a checkout: ```sh -python3 -m unittest discover -s Tests -p 'test_*.py' -v -python3 Scripts/validate_native.py --executable /absolute/path/to/claude -python3 Scripts/build_package.py /new/output/directory +python3 -m unittest discover -s tests -p 'test_*.py' -v +python3 scripts/validate_native.py --executable /absolute/path/to/claude ``` -Normal tests use deterministic peers, not a model. Authenticated execution and production activation are separate acceptance steps. See the [interface contract](Documentation/Reference/Interface.md) for retention, cancellation and error semantics and the [documentation index](Documentation/README.md) for ownership. +Tests use deterministic peers, not a model. Authenticated execution and production activation are +separate acceptance steps. + +## Contributing + +See the [contributor guide](CONTRIBUTING.md). + +## License -The working directory is not an operating-system sandbox. Vendor configuration, credentials and tools retain their own authority; host authorization does not silently approve vendor actions. +Computer MCP-owned code and resources use the +[Functional Source License 1.1, Apache 2.0 Future License](LICENSE) (FSL-1.1-ALv2). Claude Code +and Anthropic services are external; see the [third-party notices](THIRD_PARTY_NOTICES.md). diff --git a/Documentation/Architecture/Package.md b/docs/Architecture/Package.md similarity index 92% rename from Documentation/Architecture/Package.md rename to docs/Architecture/Package.md index b296e53..4f4502d 100644 --- a/Documentation/Architecture/Package.md +++ b/docs/Architecture/Package.md @@ -1,6 +1,6 @@ # Package architecture -This repository owns the Claude Code CLI description and print/stream-json-to-MCP adapter. Computer MCP owns admission, host grants, workspace selection, CLI encoding and audit. The package neither links Computer MCP Core nor modifies host state or requires Host Services. +This repository owns the Claude Code CLI description and print/stream-json-to-MCP adapter. Computer MCP owns admission, host grants, project selection, CLI encoding and audit. The package neither links Computer MCP Core nor modifies host state or requires Host Services. `computer-mcp-plugin.toml` combines a canonical file-backed CLI tree, the stdio adapter and Skills. `cli-tree.json` owns the exact supported native command/version contract. `bin/claude-mcp-adapter` owns run handles, retained native events and final results, cancellation and explicit native permission modes. Native conversation persistence stays with Claude Code; no duplicate vendor session database is created. diff --git a/Documentation/Architecture/README.md b/docs/Architecture/README.md similarity index 100% rename from Documentation/Architecture/README.md rename to docs/Architecture/README.md diff --git a/Documentation/README.md b/docs/README.md similarity index 100% rename from Documentation/README.md rename to docs/README.md diff --git a/Documentation/Reference/Installation.md b/docs/Reference/Installation.md similarity index 83% rename from Documentation/Reference/Installation.md rename to docs/Reference/Installation.md index fcd3e49..73b08c5 100644 --- a/Documentation/Reference/Installation.md +++ b/docs/Reference/Installation.md @@ -10,19 +10,19 @@ Provide Python 3.13 or newer as `python3` on the environment PATH that launches Install the ZIP through Computer MCP's plugin UI or owner CLI. Installation is not activation or a permission grant. Use `plugins list/show` to obtain the current revision and installation identity. Review `Examples/settings.json`, replace its placeholder paths, then configure the plugin through the same UI or `plugins configure` command. The CLI dependency binding selects the vendor for the CLI contribution; the MCP launch `args` independently supply `--executable` for the adapter. These two choices should normally point to the same verified installation. -The example exposes this plugin's complete MCP tool catalog with no extra prefix. It is a host choice, not authorization embedded in the package. Authorize the desired tools/registration and workspace in the calling profile. Obtain projected CLI and MCP names from the actual host `tools/list`, rather than constructing names. `plugins doctor` checks files and configuration; an MCP connection check verifies initialization/catalog, not model execution. +The example exposes this plugin's complete MCP tool catalog with no extra prefix. It is a host choice, not authorization embedded in the package. Authorize the desired tools/registration and project in the calling profile. Obtain projected CLI and MCP names from the actual host `tools/list`, rather than constructing names. `plugins doctor` checks files and configuration; an MCP connection check verifies initialization/catalog, not model execution. Computer MCP publishes plugin configuration changes to connected clients atomically. New calls use the current configuration; existing work keeps its owning runtime until release. Installation does not grant access, and later calls use current authorization. No host binary replacement or host release is required. Do not restart the active development control connection merely to test installation. ## Build ```sh -python3 -m unittest discover -s Tests -p 'test_*.py' -v -python3 Scripts/validate_native.py --executable /absolute/path/to/claude -python3 Scripts/build_package.py /new/output/directory +python3 -m unittest discover -s tests -p 'test_*.py' -v +python3 scripts/validate_native.py --executable /absolute/path/to/claude +python3 scripts/build_package.py /new/output/directory ``` -The builder produces `claude.zip` and prints an ID/version/SHA-256 receipt. Identical source bytes produce identical archive bytes. It refuses to overwrite a different existing artifact. The archive contains the manifest, canonical CLI tree, adapters, Skills, examples, manuals and notices. It excludes tests, build scripts, local evidence, caches and credentials. Only executable entrypoints receive executable mode. +The builder produces `claude.zip` and prints an ID/version/SHA-256 receipt. Identical source bytes produce identical archive bytes. It refuses to overwrite a different existing artifact. The archive contains the manifest, canonical CLI tree, adapters, Skills, examples, manuals and notices. README images are included at `.github/assets/logo/logo-light.png` and `.github/assets/logo/logo-dark.png`, preserving their relative links. These are the only packaged `.github` files. It excludes tests, build scripts, local evidence, caches and credentials. Only executable entrypoints receive executable mode. CI runs deterministic fixture tests and packaging; it does not install a vendor or invoke a model. Native validation checks the actual version/help and required flags only. ACP initialization without authentication is a separate non-model protocol probe for Cursor. Full authenticated prompt execution remains a separately authorized acceptance run because it can consume subscription/API usage and perform vendor actions. @@ -31,7 +31,7 @@ CI runs deterministic fixture tests and packaging; it does not install a vendor After packaging, validate the exact ZIP against the reviewed Computer MCP binary. Select its release version explicitly; a mismatch fails before package extraction: ```sh -python3 Scripts/validate_host.py \ +python3 scripts/validate_host.py \ --host "/absolute/path/to/candidate/computer-mcp" \ --expected-host-version "" \ --archive /output/PLUGIN.zip \ diff --git a/Documentation/Reference/Interface.md b/docs/Reference/Interface.md similarity index 100% rename from Documentation/Reference/Interface.md rename to docs/Reference/Interface.md diff --git a/Documentation/Reference/Release.md b/docs/Reference/Release.md similarity index 89% rename from Documentation/Reference/Release.md rename to docs/Reference/Release.md index b0e10c6..db05202 100644 --- a/Documentation/Reference/Release.md +++ b/docs/Reference/Release.md @@ -9,17 +9,17 @@ The organization [VERSIONING](https://github.com/computer-mcp/.github/blob/maste owns version rules, including tags, version choice and the changelog. This reference records this repository's version sources, compatibility, release procedure and checks. The shared -[release workflow guide](https://github.com/computer-mcp/.github/blob/master/Documentation/Reference/PluginRelease.md) +[release workflow guide](https://github.com/computer-mcp/github-workflows/blob/master/docs/plugin-release.md) describes workflow verification and recovery. ## Components and Version Sources | Shipped component | Version meaning | Version source | Mirrors and derived fields | Update and read-only check | | --- | --- | --- | --- | --- | -| `claude.zip` plugin package | Package version | `version` in [`computer-mcp-plugin.toml`](../../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` | +| `claude.zip` plugin package | Package version | `version` in [`computer-mcp-plugin.toml`](../../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/`. +contribution guide, license, notices, `docs/` and `Examples/`. Tests, build scripts and workflows are excluded from the archive. ## Compatibility Surface @@ -39,13 +39,13 @@ checks that 3.11 and 3.12 are rejected before any vendor process starts. `cli-tree.json` pins the exact Claude Code version through its `executable_checks`, applied before projected CLI calls and adapter execution. A different vendor version requires a reviewed tree update verified with -`Scripts/validate_native.py` against the real executable. Native version/help, +`scripts/validate_native.py` against the real executable. Native version/help, fixture protocol, selected-host interoperability and authenticated execution are separate evidence classes; see [Installation](Installation.md). ## Derived Metadata and Drift Checks -`Scripts/build_package.py` verifies the plugin ID, parses the CLI tree, and +`scripts/build_package.py` verifies the plugin ID, parses the CLI tree, and produces the same archive bytes from the same source. Its builder receipt prints the ID, version and SHA-256. The release caller passes this builder to the shared release workflow. @@ -65,7 +65,7 @@ The shared workflow checks that setting before writing a Release. and that the reviewed `master` commit has a successful `ci.yml` push run. 2. Dispatch `.github/workflows/release.yml` with `ref` set to that full commit SHA and `publish: false` (the default). It rebuilds with - `python3 Scripts/build_package.py`, compares against the exact + `python3 scripts/build_package.py`, compares against the exact `claude-plugin--3.13` CI artifact and uploads `plugin-release--`. Inspect its `assets/` directory for `claude.zip`, `release-receipt.json` and `SHA256SUMS`. An untagged dry run @@ -101,8 +101,8 @@ 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 shared notification action is pinned to a reviewed full commit. Publish +that shared workflow 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 @@ -130,10 +130,10 @@ acceptance record. | 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 dist` | CI or local checkout | -| Native acceptance | `python3 Scripts/validate_native.py --executable ` | User-installed native executable | -| Exact archive host acceptance | `python3 Scripts/validate_host.py --host --expected-host-version X.Y.Z --archive dist/claude.zip --output `; see [Installation](Installation.md#isolated-host-interoperability) | Selected host binary and candidate archive | +| 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 dist` | CI or local checkout | +| Native acceptance | `python3 scripts/validate_native.py --executable ` | User-installed native executable | +| Exact archive host acceptance | `python3 scripts/validate_host.py --host --expected-host-version X.Y.Z --archive dist/claude.zip --output `; see [Installation](Installation.md#isolated-host-interoperability) | Selected host binary and candidate archive | | Acceptance and publication | Manual checks above; dispatch `release.yml`, dry run first | Repository Actions; publication also needs a signing key and release write access | | Status and interrupted-run recovery | Inspect CI or `release.yml`; follow [Retry](#retry) | Repository Actions | | Cleanup | Delete obsolete output and evidence under ignored `dist/`, and `package-receipt.json` | Local checkout | @@ -148,7 +148,7 @@ per-artifact retention override. Retry a failed publication with **Re-run failed jobs**. The shared workflow reuses the verified bundle or a draft's frozen receipt as described in its -[recovery guide](https://github.com/computer-mcp/.github/blob/master/Documentation/Reference/PluginRelease.md#run-and-recover). +[recovery guide](https://github.com/computer-mcp/github-workflows/blob/master/docs/plugin-release.md#run-and-recover). For a missed or failed notification, rerun only the notifier job or dispatch: diff --git a/Scripts/build_package.py b/scripts/build_package.py similarity index 89% rename from Scripts/build_package.py rename to scripts/build_package.py index dfd4779..147688d 100644 --- a/Scripts/build_package.py +++ b/scripts/build_package.py @@ -10,7 +10,13 @@ import tomllib import zipfile +README_ASSETS = ( + ".github/assets/logo/logo-light.png", + ".github/assets/logo/logo-dark.png", +) + INCLUDE = ( + *README_ASSETS, "computer-mcp-plugin.toml", "cli-tree.json", "bin", @@ -19,7 +25,7 @@ "CONTRIBUTING.md", "LICENSE", "THIRD_PARTY_NOTICES.md", - "Documentation", + "docs", "Examples", ) @@ -32,7 +38,10 @@ def package_bytes(root: Path): while pending: path = pending.pop() relative = path.relative_to(root) - if any(part.startswith(".") or part == "__pycache__" for part in relative.parts) or path.suffix in (".pyc", ".pyo"): + if (relative.as_posix() not in README_ASSETS + and any(part.startswith(".") for part in relative.parts)): + continue + if "__pycache__" in relative.parts or path.suffix in (".pyc", ".pyo"): continue count += 1 if count > 512: diff --git a/Scripts/validate_host.py b/scripts/validate_host.py similarity index 93% rename from Scripts/validate_host.py rename to scripts/validate_host.py index 58f4e85..ff22089 100644 --- a/Scripts/validate_host.py +++ b/scripts/validate_host.py @@ -18,7 +18,7 @@ import tomllib ROOT = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(ROOT / 'Tests')) +sys.path.insert(0, str(ROOT / 'tests')) from support import Client @@ -57,7 +57,7 @@ def wait_for(callback, predicate, seconds=15): raise AssertionError('Isolated execution did not reach its postcondition') -def configuration(package, workspace, cli_fixture, vendor_fixture, vendor, readonly=False): +def configuration(package, project, cli_fixture, vendor_fixture, vendor, readonly=False): quote = json.dumps return f'''schema_version = 1 [server] @@ -65,7 +65,7 @@ def configuration(package, workspace, cli_fixture, vendor_fixture, vendor, reado [runtime] caller = "local-mcp" profile = "validation" -workspace_id = "primary" +project_id = "primary" [policy] default_timeout_ms = 15000 max_output_bytes = 1048576 @@ -75,13 +75,13 @@ def configuration(package, workspace, cli_fixture, vendor_fixture, vendor, reado mode = "{'read-only' if readonly else 'local-full-access'}" confirmation_policy = "never" capabilities = ["*"] -workspaces = ["primary"] +projects = ["primary"] allowed_callers = ["local-mcp"] full_shell_enabled = {'false' if readonly else 'true'} -[[workspaces]] +[[projects]] id = "primary" -display_name = "Isolated fixture workspace" -path = {quote(str(workspace))} +display_name = "Isolated fixture project" +path = {quote(str(project))} [[cli.commands]] id = "fixture-cli" executable = {quote(str(cli_fixture))} @@ -145,8 +145,8 @@ def validate(host, archive, output, expected_host_version, require_work_ownershi checks['archive_and_manifest'] = 'passed' require(not list(package.rglob('*.pyc')) and not list(package.rglob('__pycache__')), 'Archive contains Python cache') - workspace = work / 'workspace' - workspace.mkdir() + project = work / 'project' + project.mkdir() version_file = work / 'version.txt' version_file.write_text(expected_version) cli_fixture = work / 'native-cli-fixture' @@ -155,10 +155,10 @@ def validate(host, archive, output, expected_host_version, require_work_ownershi + 'else:\n print(json.dumps({"argv":sys.argv[1:],"stdin":sys.stdin.read()}))\n') cli_fixture.chmod(0o755) config = work / 'gateway.toml' - config.write_text(configuration(package, workspace, cli_fixture, ROOT / 'Tests/Fixtures/vendor.py', vendor)) + config.write_text(configuration(package, project, cli_fixture, ROOT / 'tests/Fixtures/vendor.py', vendor)) checked_config = capture([str(host), 'config', 'validate', '--config', str(config)], work, environment) (output / 'configuration-validation.txt').write_bytes(checked_config) - client = Client(str(workspace), environment, [str(host), 'serve', 'stdio', '--config', str(config)]) + client = Client(str(project), environment, [str(host), 'serve', 'stdio', '--config', str(config)]) try: catalog = client.request('tools/list')['result']['tools'] (output / 'catalog.json').write_text(json.dumps(catalog, indent=2)+'\n') @@ -226,8 +226,8 @@ def observe(): finally: client.close() checks['standalone_shutdown'] = 'passed' - config.write_text(configuration(package, workspace, cli_fixture, ROOT / 'Tests/Fixtures/vendor.py', vendor, readonly=True)) - client = Client(str(workspace), environment, [str(host), 'serve', 'stdio', '--config', str(config)]) + config.write_text(configuration(package, project, cli_fixture, ROOT / 'tests/Fixtures/vendor.py', vendor, readonly=True)) + client = Client(str(project), environment, [str(host), 'serve', 'stdio', '--config', str(config)]) try: readonly_tools = client.request('tools/list')['result']['tools'] require(not any(tool.get('_meta',{}).get('cli') for tool in readonly_tools), 'Read-only profile exposed arbitrary CLI execution') @@ -235,14 +235,14 @@ def observe(): finally: client.close() checks['read_only_profile'] = 'passed' - restricted = configuration(package, workspace, cli_fixture, ROOT / 'Tests/Fixtures/vendor.py', vendor) - restricted = restricted.replace('mode = "local-full-access"', 'mode = "workspace-operations"') + restricted = configuration(package, project, cli_fixture, ROOT / 'tests/Fixtures/vendor.py', vendor) + restricted = restricted.replace('mode = "local-full-access"', 'mode = "project-operations"') restricted = restricted.replace('full_shell_enabled = true', 'full_shell_enabled = false') execution_tool = 'claude.run' # 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) - client = Client(str(workspace), environment, [str(host), 'serve', 'stdio', '--config', str(config)]) + client = Client(str(project), environment, [str(host), 'serve', 'stdio', '--config', str(config)]) 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') diff --git a/Scripts/validate_native.py b/scripts/validate_native.py similarity index 100% rename from Scripts/validate_native.py rename to scripts/validate_native.py diff --git a/Tests/Fixtures/vendor.py b/tests/Fixtures/vendor.py similarity index 100% rename from Tests/Fixtures/vendor.py rename to tests/Fixtures/vendor.py diff --git a/Tests/check_runtime_rejection.py b/tests/check_runtime_rejection.py similarity index 100% rename from Tests/check_runtime_rejection.py rename to tests/check_runtime_rejection.py diff --git a/Tests/support.py b/tests/support.py similarity index 98% rename from Tests/support.py rename to tests/support.py index 2b16b5f..91fc915 100644 --- a/Tests/support.py +++ b/tests/support.py @@ -15,7 +15,7 @@ class Client: def __init__(self, directory, environment=None, command=None): env = dict(os.environ) if command is None else {} env.update(environment or {}) - env['CURSOR_AGENT_EXECUTABLE' if VENDOR=='cursor' else 'CLAUDE_CODE_EXECUTABLE'] = str(ROOT/'Tests/Fixtures/vendor.py') + env['CURSOR_AGENT_EXECUTABLE' if VENDOR=='cursor' else 'CLAUDE_CODE_EXECUTABLE'] = str(ROOT/'tests/Fixtures/vendor.py') self.process = subprocess.Popen(command or [sys.executable,str(ROOT/f'bin/{VENDOR}-mcp-adapter')],cwd=directory,env=env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) self.buffer = bytearray() self.serial = 1 diff --git a/Tests/test_adapter.py b/tests/test_adapter.py similarity index 100% rename from Tests/test_adapter.py rename to tests/test_adapter.py diff --git a/Tests/test_cleanup_receipt.py b/tests/test_cleanup_receipt.py similarity index 100% rename from Tests/test_cleanup_receipt.py rename to tests/test_cleanup_receipt.py diff --git a/Tests/test_completion.py b/tests/test_completion.py similarity index 100% rename from Tests/test_completion.py rename to tests/test_completion.py diff --git a/Tests/test_json.py b/tests/test_json.py similarity index 100% rename from Tests/test_json.py rename to tests/test_json.py diff --git a/Tests/test_lifecycle.py b/tests/test_lifecycle.py similarity index 100% rename from Tests/test_lifecycle.py rename to tests/test_lifecycle.py diff --git a/Tests/test_package.py b/tests/test_package.py similarity index 68% rename from Tests/test_package.py rename to tests/test_package.py index 4b7f68f..6e449db 100644 --- a/Tests/test_package.py +++ b/tests/test_package.py @@ -1,6 +1,7 @@ import importlib.util import io import json +import re from pathlib import Path import shutil import stat @@ -12,7 +13,7 @@ import zipfile ROOT=Path(__file__).resolve().parent.parent -spec=importlib.util.spec_from_file_location('build_package',ROOT/'Scripts/build_package.py') +spec=importlib.util.spec_from_file_location('build_package',ROOT/'scripts/build_package.py') builder=importlib.util.module_from_spec(spec);spec.loader.exec_module(builder) class PackageTests(unittest.TestCase): @@ -24,11 +25,18 @@ def test_reproducible_archive_and_executable_mode(self): self.assertIn('computer-mcp-plugin.toml',names) self.assertIn('cli-tree.json',names) self.assertIn('bin/plugin_runtime.py',names) + for asset in builder.README_ASSETS: + self.assertIn(asset,names) + self.assertEqual(archive.read(asset),(ROOT/asset).read_bytes()) + self.assertEqual({name for name in names if name.startswith('.github/')}, + set(builder.README_ASSETS)) packaged_manifest=tomllib.loads(archive.read('computer-mcp-plugin.toml').decode()) self.assertIs(type(packaged_manifest['plugin_api']),int) self.assertEqual(packaged_manifest['plugin_api'],1) for entry in archive.infolist(): - self.assertFalse(any(p.startswith('.') or p=='__pycache__' for p in Path(entry.filename).parts)) + if entry.filename not in builder.README_ASSETS: + self.assertFalse(any(p.startswith('.') for p in Path(entry.filename).parts)) + self.assertNotIn('__pycache__',Path(entry.filename).parts) self.assertFalse(entry.filename.endswith(('.pyc','.pyo'))) expected=0o755 if entry.filename==f"bin/{manifest['id']}-mcp-adapter" else 0o644 self.assertEqual(stat.S_IMODE(entry.external_attr>>16),expected) @@ -38,6 +46,16 @@ def test_reproducible_archive_and_executable_mode(self): result=subprocess.run(command,capture_output=True,text=True,timeout=3) self.assertEqual(result.returncode,0,result.stderr) self.assertEqual(result.stdout.strip(),manifest['version']) + def test_readme_relative_links_resolve_in_archive(self): + data,_=builder.package_bytes(ROOT) + with zipfile.ZipFile(io.BytesIO(data)) as archive: + readme=archive.read('README.md').decode() + links=re.findall(r'(?:href|src|srcset)="([^"]+)"|\]\(([^)]+)\)',readme) + for html,markdown in links: + target=(html or markdown).split('#',1)[0] + if target and '://' not in target: + self.assertIn(target,archive.namelist()) + def test_bytecode_and_local_metadata_cannot_change_release_bytes(self): before,_=builder.package_bytes(ROOT) with tempfile.TemporaryDirectory() as temp: @@ -45,7 +63,10 @@ def test_bytecode_and_local_metadata_cannot_change_release_bytes(self): for name in builder.INCLUDE: source=ROOT/name if source.is_dir():shutil.copytree(source,copy/name) - else:shutil.copyfile(source,copy/name) + else: + (copy/name).parent.mkdir(parents=True,exist_ok=True) + shutil.copyfile(source,copy/name) + (copy/'.github/assets/logo/unlisted.png').write_bytes(b'not-release-content') cache=copy/'bin/__pycache__';cache.mkdir(exist_ok=True) (cache/'arbitrary.pyc').write_bytes(b'not-release-content') (copy/'bin/.DS_Store').write_bytes(b'not-release-content') diff --git a/Tests/test_protocol_admission.py b/tests/test_protocol_admission.py similarity index 100% rename from Tests/test_protocol_admission.py rename to tests/test_protocol_admission.py diff --git a/Tests/test_review_regressions.py b/tests/test_review_regressions.py similarity index 100% rename from Tests/test_review_regressions.py rename to tests/test_review_regressions.py diff --git a/Tests/test_robustness.py b/tests/test_robustness.py similarity index 100% rename from Tests/test_robustness.py rename to tests/test_robustness.py diff --git a/Tests/test_runtime_requirement.py b/tests/test_runtime_requirement.py similarity index 100% rename from Tests/test_runtime_requirement.py rename to tests/test_runtime_requirement.py diff --git a/Tests/test_work_resources.py b/tests/test_work_resources.py similarity index 100% rename from Tests/test_work_resources.py rename to tests/test_work_resources.py