From eb4f9f8694aba646e51426c98a2d51187a837925 Mon Sep 17 00:00:00 2001 From: JerryLinLinLin <61572787+JerryLinLinLin@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:57:57 -0500 Subject: [PATCH] Add Claude Code marketplace support --- .claude-plugin/marketplace.json | 35 +++++++++ CONTRIBUTING.md | 22 +++++- README.md | 18 +++++ docs/guides/releases.md | 6 +- .../.claude-plugin/plugin.json | 22 ++++++ plugins/capstone-binary-patching/README.md | 4 +- plugins/frida-use/.claude-plugin/plugin.json | 23 ++++++ plugins/frida-use/README.md | 7 +- .../.claude-plugin/plugin.json | 21 ++++++ .../hyper-v-control/.codex-plugin/mcp.json | 16 ++++ .../hyper-v-control/.codex-plugin/plugin.json | 4 +- plugins/hyper-v-control/.mcp.json | 10 ++- plugins/hyper-v-control/README.md | 2 +- plugins/hyper-v-control/plugin.json | 4 +- .../.claude-plugin/plugin.json | 27 +++++++ plugins/rizin-re-toolkit/README.md | 2 +- scripts/check-marketplace.py | 73 +++++++++++++++---- scripts/package_bundle.py | 2 +- scripts/test-hyperv-launcher.ps1 | 27 ++++++- scripts/test-runtime-packaging.py | 8 +- 20 files changed, 295 insertions(+), 38 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 plugins/capstone-binary-patching/.claude-plugin/plugin.json create mode 100644 plugins/frida-use/.claude-plugin/plugin.json create mode 100644 plugins/hyper-v-control/.claude-plugin/plugin.json create mode 100644 plugins/hyper-v-control/.codex-plugin/mcp.json create mode 100644 plugins/rizin-re-toolkit/.claude-plugin/plugin.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..0cf8d9b --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,35 @@ +{ + "name": "my-plugin-marketplace", + "owner": { + "name": "JerryLinLinLin" + }, + "metadata": { + "description": "Windows reverse engineering and automation for AI agents." + }, + "plugins": [ + { + "name": "rizin-re-toolkit", + "source": "./plugins/rizin-re-toolkit", + "description": "Default native reverse engineering and dump analysis toolkit, routing Ghidra/IDA-style workflows through portable Rizin.", + "category": "Security" + }, + { + "name": "capstone-binary-patching", + "source": "./plugins/capstone-binary-patching", + "description": "Instruction-aware binary patching, diffs and rollback through a uv-managed Capstone CLI and focused workflows.", + "category": "Security" + }, + { + "name": "frida-use", + "source": "./plugins/frida-use", + "description": "Debug, instrument, reverse engineer, and patch running programs with Frida, primarily on Windows.", + "category": "Security" + }, + { + "name": "hyper-v-control", + "source": "./plugins/hyper-v-control", + "description": "Control Hyper-V VMs, graphical consoles, guest automation and user/kernel debugging through a native C# stdio MCP server.", + "category": "Developer Tools" + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8d6c5b..e240425 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,8 @@ Each plugin is an independent product installed through the marketplace. Keep it | Location | Responsibility | | --- | --- | -| `.agents/plugins/marketplace.json` | Discovery catalog: plugin IDs, locations, and categories | +| `.agents/plugins/marketplace.json` | Canonical discovery catalog and Codex entrypoint: plugin IDs, locations, and categories | +| `.claude-plugin/marketplace.json` | Generated Claude Code discovery catalog | | `plugins//` | Installable package: manifest, skill, user documentation, helpers, and licenses | | `src//` | Native application or MCP implementation; currently Hyper-V Control | | `tests/.Tests/` | Native protocol and behavior validation | @@ -21,7 +22,8 @@ Within Hyper-V Control, `HyperVTools*` exposes the MCP contract, `Services/` own ## Sources of truth -- Edit `plugins//plugin.json` for plugin identity, version, and interface metadata. Run `python scripts/check-marketplace.py --sync` to generate `.codex-plugin/plugin.json`; do not maintain the compatibility copy independently. +- Edit `plugins//plugin.json` for plugin identity, version, and interface metadata. Run `python scripts/check-marketplace.py --sync` to generate `.codex-plugin/plugin.json`, `.claude-plugin/plugin.json`, and the root `.claude-plugin/marketplace.json`; do not maintain these compatibility copies independently. Claude marketplace entries come from the canonical catalog and plugin descriptions; marketplace owner/description metadata lives in the checker's `claude_marketplace` function. +- Edit a plugin's portable `mcp.json` for MCP commands, using `${PLUGIN_ROOT}` for paths inside the package. The same sync command generates `.codex-plugin/mcp.json` with `${PLUGIN_ROOT}` and `.mcp.json` with `${CLAUDE_PLUGIN_ROOT}`. Point the canonical `com.openai.mcpServers` field at `./.codex-plugin/mcp.json`. Claude automatically reads the root `.mcp.json`; keeping the client files separate prevents unresolved path variables. Both clients share the same `skills/` and runtime helpers. - Compiled downloads use the plugin's `runtime.json` for the exact artifact and hash. Rizin's independently versioned runtime and build inputs are in `bundle.lock.json`. Keep dependency versions in metadata and lockfiles. - Hyper-V's project, CLI, and protocol versions must match its runtime pin. The plugin version can advance independently for launcher or skill updates. The repository checker rejects inconsistent runtime updates; the build verifies the executable version and hash. - README files explain purpose, setup, and usage. Keep current version numbers, release dates, and changelogs out of them. The [download catalog](docs/guides/releases.md#release-index) is the maintained entrypoint for current releases. @@ -45,7 +47,21 @@ python scripts/check-marketplace.py python scripts/test-runtime-packaging.py ``` -These fast checks validate the catalog, compatibility manifests, bundled skill paths, runtime pins, documentation layout/links, README policy, and runtime packaging. They do not install plugin dependencies or operate a VM. +These fast checks validate both catalogs, Codex/Claude manifests and MCP configurations, bundled skill paths, runtime pins, documentation layout/links, README policy, and runtime packaging. They do not install plugin dependencies or operate a VM. + +When Claude Code is available, also run its native validator against the catalog and each plugin: + +```powershell +claude plugin validate . --strict +Get-ChildItem plugins -Directory | ForEach-Object { + claude plugin validate $_.FullName --strict + if ($LASTEXITCODE -ne 0) { throw "Claude validation failed: $($_.Name)" } +} +``` + +To try a local skill without registering the marketplace, start `claude --plugin-dir ./plugins/rizin-re-toolkit` and invoke `/rizin-re-toolkit:rizin-re-toolkit`. Alternatively, `claude plugin marketplace add .` registers the checkout, after which the install commands in the [README](README.md#claude-code) use its local packages. See Claude's [manifest reference](https://code.claude.com/docs/en/plugins-reference) and [marketplace reference](https://code.claude.com/docs/en/plugins/marketplace-reference) for the supported fields and paths. + +For MCP configuration changes, run `./scripts/test-hyperv-launcher.ps1` on Windows. It exercises both clients' commands from an isolated plugin path containing spaces with a controlled executable, without operating a VM. The single PR workflow runs these checks for all changes. It runs Windows build/protocol/installer checks only when their inputs change. Pure documentation changes avoid native builds, newer commits cancel obsolete runs, and merging or tagging does not repeat CI. Use a manual workflow run when downloadable build artifacts are needed. diff --git a/README.md b/README.md index 9c81984..e54b4a4 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,30 @@ Use them independently or together: understand a program, observe it running, ve ## Install +### Codex + ```powershell codex plugin marketplace add JerryLinLinLin/jerry-plugin-marketplace --ref main ``` Open **Jerry's Plugin Marketplace** in Codex and install the plugins you need. Follow each plugin's setup guide, then describe your task to the agent. +### Claude Code + +Add the marketplace, then run the install commands for the plugins you need: + +```powershell +claude plugin marketplace add JerryLinLinLin/jerry-plugin-marketplace +claude plugin install rizin-re-toolkit@my-plugin-marketplace +claude plugin install frida-use@my-plugin-marketplace +claude plugin install capstone-binary-patching@my-plugin-marketplace +claude plugin install hyper-v-control@my-plugin-marketplace +``` + +In Claude Code, invoke a skill as `/:`, for example `/rizin-re-toolkit:rizin-re-toolkit`, or describe a relevant task. Follow the plugin's setup guide for its runtime dependencies. Hyper-V Control requires a Windows x64 host with Hyper-V; its MCP launcher handles runtime setup on first start. + +For local validation and development, see the [contribution guide](CONTRIBUTING.md#check-a-change). + ## Releases [Releases](https://github.com/JerryLinLinLin/jerry-plugin-marketplace/releases) diff --git a/docs/guides/releases.md b/docs/guides/releases.md index 31fcc0e..f2fd171 100644 --- a/docs/guides/releases.md +++ b/docs/guides/releases.md @@ -1,8 +1,8 @@ # Runtime releases -Install plugins through **Jerry's Plugin Marketplace** in Codex. The marketplace distributes their manifests, skills, launchers, and setup helpers. GitHub Releases distribute native runtimes and their supporting notices, checksums, and build metadata. Do not publish plugin ZIPs or standalone skill files. +Install plugins through **Jerry's Plugin Marketplace** in Codex or Claude Code using the [installation guide](../../README.md#install). The marketplace distributes their manifests, skills, launchers, and setup helpers. GitHub Releases distribute native runtimes and their supporting notices, checksums, and build metadata. Do not publish plugin ZIPs or standalone skill files. -This repository uses the [official marketplace installation workflow](https://developers.openai.com/plugins/build/plugins#add-a-marketplace-from-the-cli). +This repository supports the [Codex marketplace workflow](https://developers.openai.com/plugins/build/plugins#add-a-marketplace-from-the-cli) and [Claude Code marketplace workflow](https://code.claude.com/docs/en/plugin-marketplaces). ## Release index @@ -15,7 +15,7 @@ Frida and Capstone dependencies are installed by their plugin setup workflows. T ## Versions and installation -Plugin versions live in `plugins//plugin.json`. Native runtime versions are independent: Hyper-V records its executable pin in `runtime.json`, and Rizin records its runtime/build inputs in `bundle.lock.json`. A launcher or skill update can ship through the marketplace without rebuilding an unchanged executable. +Plugin versions live in `plugins//plugin.json`. Run `python scripts/check-marketplace.py --sync` after changing them to update both clients' generated manifests. Native runtime versions are independent: Hyper-V records its executable pin in `runtime.json`, and Rizin records its runtime/build inputs in `bundle.lock.json`. A launcher or skill update can ship through the marketplace without rebuilding an unchanged executable. Hyper-V's launcher downloads and verifies a missing runtime before starting the MCP. Rizin's skill invokes its verified runtime installer when needed. Each component uses its own versioned installation directory. Never use the repository-wide Latest release to resolve a runtime: pin an exact asset, or filter and paginate within the intended runtime stream. diff --git a/plugins/capstone-binary-patching/.claude-plugin/plugin.json b/plugins/capstone-binary-patching/.claude-plugin/plugin.json new file mode 100644 index 0000000..c61eeba --- /dev/null +++ b/plugins/capstone-binary-patching/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "capstone-binary-patching", + "version": "0.1.0", + "description": "Instruction-aware binary patching, diffs and rollback through a uv-managed Capstone CLI and focused workflows.", + "author": { + "name": "JerryLinLinLin", + "url": "https://github.com/JerryLinLinLin" + }, + "homepage": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", + "repository": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", + "license": "MIT", + "keywords": [ + "capstone", + "binary-patching", + "binary-diff", + "keystone", + "lief", + "firmware", + "uv" + ], + "skills": "./skills/" +} diff --git a/plugins/capstone-binary-patching/README.md b/plugins/capstone-binary-patching/README.md index 1482ca4..aff1373 100644 --- a/plugins/capstone-binary-patching/README.md +++ b/plugins/capstone-binary-patching/README.md @@ -13,9 +13,9 @@ Capstone decodes instructions, LIEF interprets executable mappings, and optional ## Install and use -Install **Capstone Binary Patching** from **Jerry's Plugin Marketplace** and follow the [setup guide](skills/capstone-binary-patching/references/setup.md). Python and dependencies are managed with **uv**; no MCP server or account connection is required. +Install **Capstone Binary Patching** from **Jerry's Plugin Marketplace** in Codex or Claude Code; see the [marketplace setup](https://github.com/JerryLinLinLin/jerry-plugin-marketplace/blob/main/README.md#install). Follow the [setup guide](skills/capstone-binary-patching/references/setup.md). Python and dependencies are managed with **uv**; no MCP server or account connection is required. -Invoke `$capstone-binary-patching`, or ask your agent to inspect a proposed patch. For direct CLI use: +Invoke `$capstone-binary-patching` in Codex or `/capstone-binary-patching:capstone-binary-patching` in Claude Code, or ask your agent to inspect a proposed patch. For direct CLI use: ```powershell $patchCli = 'C:\path\to\capstone-binary-patching\skills\capstone-binary-patching\scripts\patchkit.py' diff --git a/plugins/frida-use/.claude-plugin/plugin.json b/plugins/frida-use/.claude-plugin/plugin.json new file mode 100644 index 0000000..377420a --- /dev/null +++ b/plugins/frida-use/.claude-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "frida-use", + "version": "1.0.0", + "description": "Debug, instrument, reverse engineer, and patch running programs with Frida, primarily on Windows.", + "author": { + "name": "JerryLinLinLin", + "url": "https://github.com/JerryLinLinLin" + }, + "homepage": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", + "repository": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", + "license": "MIT", + "keywords": [ + "frida", + "debugging", + "windows", + "python", + "instrumentation", + "reverse-engineering", + "malware-analysis", + "patching" + ], + "skills": "./skills/" +} diff --git a/plugins/frida-use/README.md b/plugins/frida-use/README.md index 4a37ac6..0a62a4b 100644 --- a/plugins/frida-use/README.md +++ b/plugins/frida-use/README.md @@ -1,6 +1,6 @@ # Frida Use -A plugin for dynamic instrumentation, debugging, and reverse engineering with Frida, focused on Windows. Both the plugin and skill display name are **Frida Use**, and both IDs are `frida-use`. Invoke the skill as `$frida-use`, or let automatic skill selection match a relevant task. +A plugin for dynamic instrumentation, debugging, and reverse engineering with Frida, focused on Windows. Both the plugin and skill display name are **Frida Use**, and both IDs are `frida-use`. Invoke the skill as `$frida-use` in Codex or `/frida-use:frida-use` in Claude Code, or let automatic skill selection match a relevant task. The plugin includes all documentation, artwork, and licenses from the original project, together with maintained guides and reusable scripts. Installation and use are independent of the original project directory and Git repository. @@ -20,10 +20,13 @@ The plugin includes all documentation, artwork, and licenses from the original p ## Install and use -Install **Frida Use** from Jerry's Plugin Marketplace. Refresh an existing marketplace first: +Install **Frida Use** from Jerry's Plugin Marketplace in Codex or Claude Code; see the [marketplace setup](https://github.com/JerryLinLinLin/jerry-plugin-marketplace/blob/main/README.md#install). Refresh an existing marketplace with the command for your client: ```powershell +# Codex codex plugin marketplace upgrade my-plugin-marketplace +# Claude Code +claude plugin marketplace update my-plugin-marketplace ``` The plugin provides a skill, Python helpers, and JavaScript examples. Install the Frida runtime in the analysis environment. No MCP server or account connection is required. An existing `frida-windows-re` installation is not renamed automatically; remove it after confirming that the new plugin works. diff --git a/plugins/hyper-v-control/.claude-plugin/plugin.json b/plugins/hyper-v-control/.claude-plugin/plugin.json new file mode 100644 index 0000000..1b25dfe --- /dev/null +++ b/plugins/hyper-v-control/.claude-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "hyper-v-control", + "version": "0.1.2", + "description": "Control Hyper-V VMs, graphical consoles, guest automation and user/kernel debugging through a native C# stdio MCP server.", + "author": { + "name": "JerryLinLinLin", + "url": "https://github.com/JerryLinLinLin" + }, + "repository": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", + "license": "MIT", + "keywords": [ + "hyper-v", + "windows", + "virtual-machine", + "debugger", + "windbg", + "mcp" + ], + "skills": "./skills/", + "mcpServers": "./.mcp.json" +} diff --git a/plugins/hyper-v-control/.codex-plugin/mcp.json b/plugins/hyper-v-control/.codex-plugin/mcp.json new file mode 100644 index 0000000..4b84fa0 --- /dev/null +++ b/plugins/hyper-v-control/.codex-plugin/mcp.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "hyper-v-control": { + "command": "powershell.exe", + "args": [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + "${PLUGIN_ROOT}/scripts/start.ps1" + ] + } + } +} diff --git a/plugins/hyper-v-control/.codex-plugin/plugin.json b/plugins/hyper-v-control/.codex-plugin/plugin.json index 1ba5f11..ab53a2a 100644 --- a/plugins/hyper-v-control/.codex-plugin/plugin.json +++ b/plugins/hyper-v-control/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "hyper-v-control", - "version": "0.1.1", + "version": "0.1.2", "description": "Control Hyper-V VMs, graphical consoles, guest automation and user/kernel debugging through a native C# stdio MCP server.", "author": { "name": "JerryLinLinLin", @@ -17,7 +17,7 @@ "mcp" ], "skills": "./skills/", - "mcpServers": "./.mcp.json", + "mcpServers": "./.codex-plugin/mcp.json", "interface": { "displayName": "Hyper-V Control", "shortDescription": "VM lifecycle, computer use, and Windows debugging.", diff --git a/plugins/hyper-v-control/.mcp.json b/plugins/hyper-v-control/.mcp.json index a26e8f6..8733714 100644 --- a/plugins/hyper-v-control/.mcp.json +++ b/plugins/hyper-v-control/.mcp.json @@ -2,7 +2,15 @@ "mcpServers": { "hyper-v-control": { "command": "powershell.exe", - "args": ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", "${PLUGIN_ROOT}/scripts/start.ps1"] + "args": [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + "${CLAUDE_PLUGIN_ROOT}/scripts/start.ps1" + ] } } } diff --git a/plugins/hyper-v-control/README.md b/plugins/hyper-v-control/README.md index 12b315e..bda4562 100644 --- a/plugins/hyper-v-control/README.md +++ b/plugins/hyper-v-control/README.md @@ -14,7 +14,7 @@ Manage a Hyper-V lab from your AI agent. Create and restore experiments, interac Requires a Windows x64 host with Hyper-V. The distributed executable is self-contained and needs no separate .NET or Python installation. -Install **Hyper-V Control** from **Jerry's Plugin Marketplace** in Codex. The plugin includes its skill and launcher. On first start, the launcher downloads and verifies the required executable automatically; subsequent starts reuse the installed runtime. +Install **Hyper-V Control** from **Jerry's Plugin Marketplace** in Codex or Claude Code; see the [marketplace setup](https://github.com/JerryLinLinLin/jerry-plugin-marketplace/blob/main/README.md#install). The plugin includes its skill and launcher. On first start, the launcher downloads and verifies the required executable automatically; subsequent starts reuse the installed runtime. Invoke `$hyper-v-control` in Codex or `/hyper-v-control:hyper-v-control` in Claude Code, or describe a VM task. For another stdio MCP client, download the EXE from the [runtime releases](https://github.com/JerryLinLinLin/jerry-plugin-marketplace/blob/main/docs/guides/releases.md#release-index) and point its configuration at the executable's absolute path. This example assumes you saved it as `C:\Tools\HyperVControl.exe`: diff --git a/plugins/hyper-v-control/plugin.json b/plugins/hyper-v-control/plugin.json index 9cf3d94..db88444 100644 --- a/plugins/hyper-v-control/plugin.json +++ b/plugins/hyper-v-control/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "hyper-v-control", - "version": "0.1.1", + "version": "0.1.2", "description": "Control Hyper-V VMs, graphical consoles, guest automation and user/kernel debugging through a native C# stdio MCP server.", "author": {"name": "JerryLinLinLin", "url": "https://github.com/JerryLinLinLin"}, "repository": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", @@ -10,7 +10,7 @@ "extensions": { "com.openai": { "skills": "./skills/", - "mcpServers": "./.mcp.json", + "mcpServers": "./.codex-plugin/mcp.json", "interface": { "displayName": "Hyper-V Control", "shortDescription": "VM lifecycle, computer use, and Windows debugging.", diff --git a/plugins/rizin-re-toolkit/.claude-plugin/plugin.json b/plugins/rizin-re-toolkit/.claude-plugin/plugin.json new file mode 100644 index 0000000..bfe342a --- /dev/null +++ b/plugins/rizin-re-toolkit/.claude-plugin/plugin.json @@ -0,0 +1,27 @@ +{ + "name": "rizin-re-toolkit", + "version": "0.4.1", + "description": "Default native reverse engineering and dump analysis toolkit, routing Ghidra/IDA-style workflows through portable Rizin.", + "author": { + "name": "JerryLinLinLin", + "url": "https://github.com/JerryLinLinLin" + }, + "homepage": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", + "repository": "https://github.com/JerryLinLinLin/jerry-plugin-marketplace", + "license": "MIT", + "keywords": [ + "rizin", + "reverse-engineering", + "decompiler", + "memory-dump", + "ghidra", + "ida", + "hex-rays", + "binary-ninja", + "radare2", + "cutter", + "yara", + "flirt" + ], + "skills": "./skills/" +} diff --git a/plugins/rizin-re-toolkit/README.md b/plugins/rizin-re-toolkit/README.md index ee4c0f9..237c0f9 100644 --- a/plugins/rizin-re-toolkit/README.md +++ b/plugins/rizin-re-toolkit/README.md @@ -11,7 +11,7 @@ Understand native executables and memory dumps with a portable Windows analysis ## Install -Install **Rizin RE Toolkit** from **Jerry's Plugin Marketplace** in Codex. The included skill guides runtime setup and analysis; invoke it as `$rizin-re-toolkit` or describe a relevant task. +Install **Rizin RE Toolkit** from **Jerry's Plugin Marketplace** in Codex or Claude Code; see the [marketplace setup](https://github.com/JerryLinLinLin/jerry-plugin-marketplace/blob/main/README.md#install). The included skill guides runtime setup and analysis. Invoke `$rizin-re-toolkit` in Codex, `/rizin-re-toolkit:rizin-re-toolkit` in Claude Code, or describe a relevant task. The skill first checks a known installation path, then `%LOCALAPPDATA%\Programs\Rizin\\rizin\bin\rizin.exe`, then PATH, and reuses a working runtime before downloading. The [PowerShell installer](skills/rizin-re-toolkit/scripts/install.ps1) downloads and verifies the runtime when needed. By default it adds `bin` only to the current PowerShell process's PATH; use `-AddToUserPath` for persistent user PATH registration or `-Destination ` for a custom installation root. Across separate shell sessions, use the resolved absolute executable path. diff --git a/scripts/check-marketplace.py b/scripts/check-marketplace.py index 5a43bfa..391f0f7 100644 --- a/scripts/check-marketplace.py +++ b/scripts/check-marketplace.py @@ -9,6 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] SEMVER = re.compile(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?") +CLAUDE_METADATA_FIELDS = ("name", "version", "description", "author", "homepage", "repository", "license", "keywords") def read_json(path): @@ -21,6 +22,36 @@ def compatibility_manifest(manifest): return result +def claude_manifest(manifest): + # OpenAI interface metadata and the portable schema are not Claude fields. + result = {key: manifest[key] for key in CLAUDE_METADATA_FIELDS if key in manifest} + extension = manifest["extensions"]["com.openai"] + result["skills"] = extension["skills"] + if extension.get("mcpServers"): + result["mcpServers"] = "./.mcp.json" + return result + + +def claude_marketplace(catalog, manifests): + return { + "name": catalog["name"], + "owner": {"name": "JerryLinLinLin"}, + "metadata": {"description": "Windows reverse engineering and automation for AI agents."}, + "plugins": [ + {"name": entry["name"], "source": entry["source"]["path"], + "description": manifests[entry["name"]]["description"], "category": entry["category"]} + for entry in catalog["plugins"] + ], + } + + +def mcp_configuration(portable, root_variable): + servers = {name: {key: value for key, value in server.items() if not (key == "type" and value == "stdio")} + for name, server in portable["mcpServers"].items()} + # Replace only the plugin-root token; preserve other environment variables. + return json.loads(json.dumps({"mcpServers": servers}).replace("${PLUGIN_ROOT}", root_variable)) + + def check(sync=False): errors = [] @@ -28,6 +59,17 @@ def require(condition, message): if not condition: errors.append(message) + def generated(path, expected): + contained = path.resolve().is_relative_to(ROOT) + require(contained, f"Generated path escapes the repository: {path}") + if not contained: + return + if sync: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(expected, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", newline="\n") + require(path.is_file() and read_json(path) == expected, + f"{path.relative_to(ROOT)}: compatibility configuration drift; run python scripts/check-marketplace.py --sync") + catalog = read_json(ROOT / ".agents/plugins/marketplace.json") names = [entry["name"] for entry in catalog["plugins"]] require(len(names) == len(set(names)), "Marketplace plugin names must be unique") @@ -40,6 +82,7 @@ def require(condition, message): "Unexpected docs/ directory; follow the documentation index") readmes = [ROOT / "README.md"] repository_urls = set() + manifests = {} for entry in catalog["plugins"]: name = entry["name"] @@ -50,18 +93,21 @@ def require(condition, message): if not plugin.is_dir(): continue manifest = read_json(plugin / "plugin.json") + manifests[name] = manifest if manifest.get("repository"): repository_urls.add(manifest["repository"].rstrip("/") + "/blob/main/") require(manifest["name"] == name, f"{name}: manifest identity mismatch") require(bool(SEMVER.fullmatch(manifest["version"])), f"{name}: invalid semantic version") - expected = compatibility_manifest(manifest) - compat_path = plugin / ".codex-plugin/plugin.json" - if sync: - compat_path.parent.mkdir(parents=True, exist_ok=True) - compat_path.write_text(json.dumps(expected, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - require(compat_path.is_file() and read_json(compat_path) == expected, - f"{name}: compatibility manifest drift; run python scripts/check-marketplace.py --sync") + generated(plugin / ".codex-plugin/plugin.json", compatibility_manifest(manifest)) + generated(plugin / ".claude-plugin/plugin.json", claude_manifest(manifest)) extension = manifest["extensions"]["com.openai"] + if extension.get("mcpServers"): + # Claude automatically loads .mcp.json, so Codex must use its own file. + require(extension["mcpServers"] == "./.codex-plugin/mcp.json", + f"{name}: Codex MCP configuration must use ./.codex-plugin/mcp.json") + portable = read_json(plugin / "mcp.json") + generated(plugin / ".codex-plugin/mcp.json", mcp_configuration(portable, "${PLUGIN_ROOT}")) + generated(plugin / ".mcp.json", mcp_configuration(portable, "${CLAUDE_PLUGIN_ROOT}")) require(entry["category"] == extension["interface"]["category"], f"{name}: category mismatch") for field, relative in (("skills", extension["skills"]), ("logo", extension["interface"].get("logo")), ("mcpServers", extension.get("mcpServers"))): @@ -90,12 +136,9 @@ def require(condition, message): require(bool(re.fullmatch(r"[a-fA-F0-9]{64}", runtime["sha256"])), f"{name}: invalid runtime SHA-256") expected_url = f'{manifest["repository"]}/releases/download/{runtime["tag"]}/{runtime["asset"]}' require(runtime["url"] == expected_url, f"{name}: runtime URL must match its exact tag and asset") - if (plugin / "mcp.json").exists() and (plugin / ".mcp.json").exists(): - portable = read_json(plugin / "mcp.json")["mcpServers"] - compatible = read_json(plugin / ".mcp.json")["mcpServers"] - normalized = {key: {field: value for field, value in server.items() if not (field == "type" and value == "stdio")} - for key, server in portable.items()} - require(normalized == compatible, f"{name}: MCP configurations disagree") + + if len(manifests) == len(names): + generated(ROOT / ".claude-plugin/marketplace.json", claude_marketplace(catalog, manifests)) # Existing native version literals remain part of the published binary identity. # Catch partial version bumps without rewriting an already released executable. @@ -132,13 +175,13 @@ def require(condition, message): for error in errors: print(f"ERROR: {error}", file=sys.stderr) if not errors: - print(f"PASS: {len(names)} plugins; catalog, manifests, runtime pins, README policy, and entrypoint links") + print(f"PASS: {len(names)} plugins; Codex/Claude catalogs, manifests and MCP configs, runtime pins, README policy, and entrypoint links") return bool(errors) if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--sync", action="store_true", help="Regenerate compatibility manifests from each canonical plugin.json before checking") + parser.add_argument("--sync", action="store_true", help="Regenerate Codex/Claude compatibility files from the catalog, plugin.json and mcp.json before checking") args = parser.parse_args() try: sys.exit(check(args.sync)) diff --git a/scripts/package_bundle.py b/scripts/package_bundle.py index 80f6358..8a73423 100644 --- a/scripts/package_bundle.py +++ b/scripts/package_bundle.py @@ -42,7 +42,7 @@ def main(): manifest = json.loads((runtime / 'bundle-manifest.json').read_text(encoding='utf-8-sig')) if manifest['bundleVersion'] != args.version: raise RuntimeError('Runtime and requested versions disagree') - if any((runtime / name).exists() for name in ('skills', 'plugin.json', '.codex-plugin', '.mcp.json', 'mcp.json')): + if any((runtime / name).exists() for name in ('skills', 'plugin.json', '.codex-plugin', '.claude-plugin', '.mcp.json', 'mcp.json')): raise RuntimeError('Runtime contains plugin files; reassemble it without marketplace content') name = f'rizin-windows-x64-bundle-v{args.version}.zip' output.mkdir(parents=True, exist_ok=True) diff --git a/scripts/test-hyperv-launcher.ps1 b/scripts/test-hyperv-launcher.ps1 index 89991b2..c122965 100644 --- a/scripts/test-hyperv-launcher.ps1 +++ b/scripts/test-hyperv-launcher.ps1 @@ -3,9 +3,12 @@ param() $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot $scratch = Join-Path $repo ('build\launcher-test-' + [guid]::NewGuid().ToString('N')) -$plugin = Join-Path $scratch 'plugin' +$plugin = Join-Path $scratch 'plugin with spaces' New-Item -ItemType Directory -Force -Path (Join-Path $plugin 'scripts') | Out-Null Copy-Item -LiteralPath (Join-Path $repo 'plugins\hyper-v-control\scripts\start.ps1'),(Join-Path $repo 'plugins\hyper-v-control\scripts\install.ps1') -Destination (Join-Path $plugin 'scripts') +foreach ($config in @('.codex-plugin', '.claude-plugin', '.mcp.json')) { + Copy-Item -LiteralPath (Join-Path $repo "plugins\hyper-v-control\$config") -Destination $plugin -Recurse +} $runtime = Get-Content -LiteralPath (Join-Path $repo 'plugins\hyper-v-control\runtime.json') -Raw | ConvertFrom-Json $source = Join-Path $scratch 'LauncherProbe.cs' $probeSource = @' @@ -51,6 +54,26 @@ try { if ($LASTEXITCODE -ne 0 -or $second.Count -ne 1 -or $second[0] -ne $expected -or $fixture.calls -ne 1) { throw 'Second launch must reuse the verified runtime.' } + # Execute each client's actual command after cache relocation, outside the plugin directory. + Push-Location $scratch + try { + foreach ($client in @( + @{name='Codex';manifest='.codex-plugin/plugin.json';rootVariable='${PLUGIN_ROOT}'}, + @{name='Claude';manifest='.claude-plugin/plugin.json';rootVariable='${CLAUDE_PLUGIN_ROOT}'} + )) { + $manifest = Get-Content -LiteralPath (Join-Path $plugin $client.manifest) -Raw | ConvertFrom-Json + $config = Get-Content -LiteralPath (Join-Path $plugin $manifest.mcpServers) -Raw | ConvertFrom-Json + $server = $config.mcpServers.'hyper-v-control' + $clientArgs = @($server.args | ForEach-Object { $_.Replace($client.rootVariable, $plugin.Replace('\', '/')) }) + if ($clientArgs -match '\$\{') { throw "$($client.name) MCP args contain an unresolved variable." } + $result = @(& $server.command @clientArgs) + if ($LASTEXITCODE -ne 0 -or $result.Count -ne 1 -or $result[0] -ne $expected) { + throw "$($client.name) MCP configuration failed after relocation." + } + } + } finally { + Pop-Location + } $env:LOCALAPPDATA = Join-Path $scratch 'bad-profile' $fixture.corrupt = $true $rejected = $false @@ -64,4 +87,4 @@ try { } finally { $env:LOCALAPPDATA = $originalLocalAppData } -Write-Output 'PASS: automatic setup, verified reuse, clean MCP stdout, corrupt-download rejection, and retry' +Write-Output 'PASS: automatic setup, verified reuse, Codex/Claude MCP launch from paths with spaces, clean MCP stdout, corrupt-download rejection, and retry' diff --git a/scripts/test-runtime-packaging.py b/scripts/test-runtime-packaging.py index f593a5a..d133822 100644 --- a/scripts/test-runtime-packaging.py +++ b/scripts/test-runtime-packaging.py @@ -48,9 +48,11 @@ def test_rizin(): assert rejected.returncode and 'clean runtime release directory' in rejected.stderr assert all(digest(output / name) == checksum for name, checksum in before.items()) obsolete.unlink() - (runtime / 'skills').mkdir() - rejected = subprocess.run(command, capture_output=True, text=True) - assert rejected.returncode and 'Runtime contains plugin files' in rejected.stderr + for marker in ('skills', '.codex-plugin', '.claude-plugin'): + (runtime / marker).mkdir() + rejected = subprocess.run(command, capture_output=True, text=True) + assert rejected.returncode and 'Runtime contains plugin files' in rejected.stderr + (runtime / marker).rmdir() print('PASS: Rizin runtime-only archive, checksums, mixed-output rejection, and plugin-content rejection')