From fca5c80516d15677a21ee630d8d7573b80e12e2e Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Fri, 12 Jun 2026 17:42:38 +0800 Subject: [PATCH] Expose only public MCP support in marketplace Constraint: The public marketplace must not contain internal MCP endpoints, shared keys, project keys, or user-token workflows. Rejected: Publishing every configured KeyInfo MCP server | internal knowledge, Firecrawl, and Outline servers are not public distribution targets. Confidence: high Scope-risk: moderate Directive: Keep MCP publication behind an explicit allowlist and verify hidden plugin metadata before pushing. Tested: python scripts/validate_marketplace.py; temporary CODEX_HOME marketplace list/add/remove for mcp-playwright; hidden rg scan for internal MCP URLs and key markers. Not-tested: Live Playwright MCP browser tool invocation inside a fresh Codex session. --- .agents/plugins/marketplace.json | 12 ++ README.md | 150 ++++++++---------- .../codex/.agents/plugins/marketplace.json | 12 ++ plugins/codex/README.md | 15 +- .../mcp-playwright/.codex-plugin/plugin.json | 38 +++++ .../codex/plugins/mcp-playwright/.mcp.json | 19 +++ .../mcp-playwright/KEYINFO_MCP_SOURCE.json | 7 + .../codex/plugins/mcp-playwright/README.md | 19 +++ scripts/validate_marketplace.py | 70 ++++++-- 9 files changed, 237 insertions(+), 105 deletions(-) create mode 100644 plugins/codex/plugins/mcp-playwright/.codex-plugin/plugin.json create mode 100644 plugins/codex/plugins/mcp-playwright/.mcp.json create mode 100644 plugins/codex/plugins/mcp-playwright/KEYINFO_MCP_SOURCE.json create mode 100644 plugins/codex/plugins/mcp-playwright/README.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 2e7b371a4..2bfee5005 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -255,6 +255,18 @@ "authentication": "ON_INSTALL" }, "category": "文档处理" + }, + { + "name": "mcp-playwright", + "source": { + "source": "local", + "path": "./plugins/codex/plugins/mcp-playwright" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "MCP" } ] } diff --git a/README.md b/README.md index 67a964e9b..95cedf63c 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,31 @@ -# EAPIL Codex Plugin Marketplace +# EAPIL Codex 插件市场 -This repository is a Codex plugin marketplace. Its primary purpose is to give AI agents a stable way to install EAPIL-maintained public skills and third-party Codex plugins. +这是 EAPIL 维护的 Codex 插件市场。首要用途是让 AI agent 能稳定安装公共 skills、MCP 插件和第三方 Codex plugin。 -## For AI Agents +## 给 AI 的安装流程 -When a user asks to install or use an EAPIL skill/plugin, do this first: +当用户要求安装或使用 EAPIL 插件时,优先执行: ```bash codex plugin marketplace add https://git.playones.com/arechen/EapilSkillMarket.git --ref main +codex plugin marketplace upgrade eapil-skill-market codex plugin list --marketplace eapil-skill-market -codex plugin add @eapil-skill-market +codex plugin add @eapil-skill-market ``` -Use the exact plugin id from the plugin table below. Do not guess translated names. +使用 `codex plugin list --marketplace eapil-skill-market` 输出里的精确 `plugin-id`。不要猜测中文名、翻译名或 display name。 -After installation: +如果插件刚安装到当前 Codex 会话中,启动新的 Codex thread 或新的 Codex 调用后再依赖新增 skills/hooks/MCP/app 配置。 + +卸载插件: ```bash -codex plugin list --marketplace eapil-skill-market +codex plugin remove @eapil-skill-market ``` -If the plugin was just installed in the current Codex session, start a new Codex thread or new invocation before relying on newly installed skills being loaded into context. +## 本地调试安装 -To remove a plugin: - -```bash -codex plugin remove @eapil-skill-market -``` - -## Local Debug Install - -From this repository checkout: +从本仓库 checkout 调试: ```bash codex plugin marketplace add . @@ -38,116 +33,105 @@ codex plugin list --marketplace eapil-skill-market codex plugin add docx@eapil-skill-market ``` -For a temporary clean test: +临时干净环境验收: ```bash tmp_home="$(mktemp -d)" CODEX_HOME="$tmp_home" codex plugin marketplace add /path/to/EapilSkillMarket -CODEX_HOME="$tmp_home" codex plugin add @eapil-skill-market -CODEX_HOME="$tmp_home" codex plugin remove @eapil-skill-market +CODEX_HOME="$tmp_home" codex plugin list --marketplace eapil-skill-market +CODEX_HOME="$tmp_home" codex plugin add @eapil-skill-market +CODEX_HOME="$tmp_home" codex plugin remove @eapil-skill-market CODEX_HOME="$tmp_home" codex plugin marketplace remove eapil-skill-market rm -rf "$tmp_home" ``` -## Sparse Checkout +## MCP 插件 -Normal installation should leave sparse paths empty. If a constrained environment requires sparse checkout, include both marketplace metadata and plugin contents: +当前公开仓库只发布 `mcp-playwright`。KeyInfo 主项目里还维护了内部知识库、Firecrawl、Outline 等 MCP,但这些依赖内部地址、项目 Key、shared key 或用户私有 token,不能进入公开 Git 仓库。 -```bash -codex plugin marketplace add https://git.playones.com/arechen/EapilSkillMarket.git --ref main --sparse .agents --sparse plugins/codex +`mcp-playwright` 是本地 MCP 插件: + +- 插件 ID:`mcp-playwright` +- MCP server id:`playwright` +- 本地命令:`bunx @playwright/mcp@latest` +- 鉴权:无 + +安装后如需覆盖启用状态或审批策略,可在用户 Codex 配置里追加: + +```toml +[plugins."mcp-playwright@eapil-skill-market".mcp_servers.playwright] +enabled = true ``` -## Current Plugins +后端自动同步公开 MCP 时只读取 `SKILL_MARKET_PUBLIC_MCP_IDS` allowlist,默认值是 `playwright`。除非确认目标 MCP 可公开、无内部 URL/密钥泄露风险,否则不要加入 allowlist。 -| Plugin id | Display name | Category | Use when | -| --- | --- | --- | --- | -| `docx` | docx | 文档处理 | Create, edit, review, or extract Word documents. | -| `eapil-gpt2-image` | eapil-gpt2-image | 设计 | Generate or edit images through EAPIL's OpenAI-compatible image API. | -| `find-skills` | find-skills | 工具 | Help users discover suitable installable skills. | -| `frontend-slides` | Frontend Slides | 文档处理 | Create animated fixed-stage HTML slides or convert PPT/PPTX to HTML slides. | -| `guizang-ppt-skill` | Guizang PPT Skill | 文档处理 | Create magazine-style or Swiss-style horizontal web PPTs. | -| `html-ppt` | HTML PPT Studio | 文档处理 | Create professional static HTML presentations with themes, layouts, and animations. | -| `karpathy-guidelines` | karpathy-guidelines | 开发工具 | Apply Andrej Karpathy-inspired AI coding and model-training guidance. | -| `literature-review` | literature-review | 文档处理 | Assist with literature review and academic paper drafting workflows. | -| `oh-my-codex` | oh-my-codex | 开发工具 | Add OMX workflows, plugin-scoped hooks, MCP config, and multi-agent orchestration for Codex CLI. | -| `paper2code` | paper2code | 文档处理 | Turn arXiv papers into concise referenced Python implementations. | -| `pdf` | pdf | 文档处理 | Extract, create, merge, split, fill, or analyze PDF files. | -| `postgres` | postgres | 数据分析 | Run read-only PostgreSQL schema inspection and SELECT analysis. | -| `ppt-master` | PPT Master | 文档处理 | Convert source documents into high-quality SVG pages and export PPTX decks. | -| `ppt-translator` | ppt-translator | 文档处理 | Translate PPT/PPTX files while preserving layout through a render-and-verify loop. | -| `pptx` | pptx | 文档处理 | Create, edit, analyze, or annotate PowerPoint presentations. | -| `shadcn` | shadcn/ui | 开发工具 | Work with shadcn/ui projects, components, registries, and presets. | -| `superpowers` | Superpowers | 开发工具 | Use structured planning, TDD, debugging, code review, and delivery workflows. | -| `threejs` | threejs | 设计 | Build Three.js scenes, materials, lighting, animation, postprocessing, and loaders. | -| `ui-ux-pro-max` | UI/UX Pro Max | 设计 | Use a UI/UX design knowledge base for style, typography, accessibility, charts, and frontend stack choices. | -| `xlsx` | xlsx | 文档处理 | Create, edit, analyze, recalculate, or visualize spreadsheets. | -| `patent-application-drafting-skill` | 专利申请撰写 | 文档处理 | Draft Chinese patent technical disclosures, claims, specifications, abstracts, and related Word documents. | +## 官方插件能力 -## Plugin Capabilities +本市场按 Codex 官方插件能力校验和发布,插件可以包含: -Most plugins expose one or more skills under `skills/`. Codex also supports richer plugin components, and this market may publish them when upstream plugins use them: +- `skills`:`./skills/` 下的一个或多个 `SKILL.md`。 +- `mcpServers`:通常是 `./.mcp.json`,允许 MCP-only 插件。 +- `apps`:通常是 `./.app.json`,用于 app connector metadata。 +- `hooks`:插件作用域 Codex hooks,可为路径、路径数组、inline object 或 object 数组。 +- `interface` / `assets`:`interface.logo`、`interface.composerIcon`、`interface.screenshots` 可引用插件内 `assets/` 文件或公开 URL。 -- `hooks`: plugin-scoped Codex hooks, usually declared as `./hooks/hooks.json`. -- `mcpServers`: MCP server metadata, usually declared as `./.mcp.json`. -- `apps`: app connector metadata, usually declared as `./.app.json`. +市场校验器会检查 plugin id、manifest `name` 和目录名必须是 ASCII kebab-case;中文只能用于 `interface.displayName`、说明文案和分类等展示字段。 -`oh-my-codex` currently uses all three. It requires the user's environment to have `oh-my-codex` / `omx` installed and set up for full runtime behavior. Codex may ask the user to trust plugin hooks before running them. - -## Repository Layout +## 仓库结构 ```text .agents/plugins/marketplace.json plugins/codex/ .agents/plugins/marketplace.json - plugins// + plugins// .codex-plugin/plugin.json - skills//SKILL.md - hooks/ # optional - .mcp.json # optional - .app.json # optional + skills//SKILL.md # 可选 + .mcp.json # 可选 + .app.json # 可选 + hooks/ # 可选 + assets/ # 可选 config/external-sources.json scripts/validate_marketplace.py ``` -## Maintenance Rules +## 维护规则 -- This is a distribution repository. Keep the published Git history shallow and small. -- Plugin ids, directory names, and skill `name` values use English kebab-case. -- Preserve upstream display names such as `shadcn/ui`, `oh-my-codex`, and `Guizang PPT Skill`; do not translate proper names. -- User-facing Chinese copy belongs in `description`, `shortDescription`, `longDescription`, `category`, and `defaultPrompt`. -- Update this README whenever a public skill/plugin is added, replaced, renamed, removed, or gains a new major capability such as hooks, MCP, or app metadata. -- Do not sync README-only preview GIFs/screenshots into this repository. Skill/reference images needed by the skill should be compressed to WebP instead. -- Third-party plugin localization belongs in `config/external-sources.json` through `manifestOverrides` and `skillDescriptions`; do not hand-edit generated plugin manifests. +- README 正文统一使用简体中文;插件 ID、命令、配置键和上游专有名称保留原文。 +- 日常发布保留普通线性 Git 历史,使用普通 `git add -A`、`git commit`、`git push`,让 Codex marketplace 刷新获得增量传输收益。 +- 只有误同步大文件或用户明确要求历史清理时,才做一次性历史压缩。 +- 每次新增、替换、重命名、移除 public skill/plugin,或新增 hooks/MCP/apps/assets 等能力后,都要同步更新本 README 和 `plugins/codex/README.md`。 +- 不要同步 README-only 预览 GIF/截图。skill/reference 真正需要的图片保留并压缩为 WebP。 +- 第三方插件本地化写入 `config/external-sources.json` 的 `manifestOverrides` 和 `skillDescriptions`,不要直接手改生成 manifest。 +- KeyInfo 内部 MCP 默认不公开。公开 MCP 必须经过 `SKILL_MARKET_PUBLIC_MCP_IDS` allowlist,并确认无内部 URL、shared key、项目 key 或用户 token 泄露风险。 -## Third-Party Sync +## 第三方同步 -Third-party sources are tracked in `config/external-sources.json`. +第三方来源记录在 `config/external-sources.json`。 -Supported adapters: +支持的适配器: -- `codex-plugin`: upstream is already a Codex plugin. -- `claude-skill`: upstream is a Claude-style skill that is converted into a Codex plugin. +- `codex-plugin`:上游已经是 Codex plugin。 +- `claude-skill`:上游是 Claude-style skill,会转换为 Codex plugin。 -Independent PowerShell sync: +PowerShell 独立同步: ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\sync_external_plugins.ps1 ``` -With proxy: +带代理: ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\sync_external_plugins.ps1 -Proxy http://127.0.0.1:2085 ``` -## Validation - -Before publishing changes: +## 发布前验证 ```bash python scripts/validate_marketplace.py -python /Users/eapil/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py plugins/codex/plugins/ -rg -n "|" README.md plugins/codex/README.md +rg -n "|" README.md plugins/codex/README.md ``` -For new plugins, also install from a temporary `CODEX_HOME` and run the smallest safe invocation that proves Codex can load the skill/plugin. +对包含 `skills` 的插件,可额外运行系统 `validate_plugin.py`。不要用旧 validator 降级官方能力;MCP-only、app-only 或 hooks-only 插件以本仓库 marketplace 校验和 Codex 实装验收为准。 + +对新增插件,还要使用临时 `CODEX_HOME` 安装,并执行最小安全调用,证明 Codex 能加载该插件。MCP-only 插件至少要验证 marketplace list/add 成功,并检查 `.mcp.json` 已进入插件缓存;真实 MCP tool 调用可能依赖本地命令、浏览器权限或 token。 diff --git a/plugins/codex/.agents/plugins/marketplace.json b/plugins/codex/.agents/plugins/marketplace.json index 2a7b7dcf0..1313b7677 100644 --- a/plugins/codex/.agents/plugins/marketplace.json +++ b/plugins/codex/.agents/plugins/marketplace.json @@ -255,6 +255,18 @@ "authentication": "ON_INSTALL" }, "category": "文档处理" + }, + { + "name": "mcp-playwright", + "source": { + "source": "local", + "path": "./plugins/mcp-playwright" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "MCP" } ] } diff --git a/plugins/codex/README.md b/plugins/codex/README.md index 198d72839..d7a78c667 100644 --- a/plugins/codex/README.md +++ b/plugins/codex/README.md @@ -1,24 +1,27 @@ -# Codex Marketplace Sparse Entry +# Codex 插件入口 -This directory is the sparse checkout entry for the EAPIL Codex Plugin Marketplace. AI agents should normally install the marketplace from the repository root URL: +这个目录是 EAPIL Codex 插件市场的 sparse checkout 入口。AI agent 通常应从仓库根 URL 安装市场: ```bash codex plugin marketplace add https://git.playones.com/arechen/EapilSkillMarket.git --ref main +codex plugin marketplace upgrade eapil-skill-market codex plugin list --marketplace eapil-skill-market -codex plugin add @eapil-skill-market +codex plugin add @eapil-skill-market ``` -Use sparse checkout only when the environment needs a smaller checkout: +只有在受限环境需要更小 checkout 时才使用 sparse 参数: ```bash codex plugin marketplace add https://git.playones.com/arechen/EapilSkillMarket.git --ref main --sparse .agents --sparse plugins/codex ``` -Local debug from the repository root: +从仓库根目录本地调试: ```bash codex plugin marketplace add . codex plugin add docx@eapil-skill-market ``` -Each `plugins/` directory is an installable Codex plugin. Plugins can include plain skills plus official Codex plugin components such as `hooks`, `.mcp.json`, and `.app.json`. See the root `README.md` for the full plugin table and maintenance rules. +每个 `plugins/` 目录都是一个可安装 Codex plugin。插件可以包含官方支持的 `skills`、`mcpServers`、`apps`、`hooks`、`interface` 和 `assets`。当前公开 MCP 只包含 `mcp-playwright`;KeyInfo 内部 MCP 不发布到公开仓库。 + +完整插件清单、MCP 说明和维护规则见根目录 `README.md`。 diff --git a/plugins/codex/plugins/mcp-playwright/.codex-plugin/plugin.json b/plugins/codex/plugins/mcp-playwright/.codex-plugin/plugin.json new file mode 100644 index 000000000..522994103 --- /dev/null +++ b/plugins/codex/plugins/mcp-playwright/.codex-plugin/plugin.json @@ -0,0 +1,38 @@ +{ + "name": "mcp-playwright", + "version": "0.1.0", + "description": "Playwright 是由 Microsoft 开发并维护的开源浏览器自动化框架,专为现代 Web 应用设计。它提供了一套统一的 API,支持 Chromium、Firefox 和 WebKit 三大主流浏览器引擎,适用于端到端测试、网页数据采集及自动化操作等场景。", + "author": { + "name": "EAPIL", + "url": "https://git.playones.com/arechen/EapilSkillMarket" + }, + "homepage": "https://git.playones.com/arechen/EapilSkillMarket", + "repository": "https://git.playones.com/arechen/EapilSkillMarket", + "license": "Proprietary", + "keywords": [ + "eapil", + "codex-mcp", + "mcp", + "playwright" + ], + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "playwright浏览器自动化操作", + "shortDescription": "Playwright 是由 Microsoft 开发并维护的开源浏览器自动化框架,专为现代 Web 应用设计。它提供了一套统一的 API,支持 Chromium、Firefox 和 WebKit 三大主流浏览器引擎,适用于端到端测试、网页数据采集及自动化操作等场景。", + "longDescription": "Playwright 是由 Microsoft 开发并维护的开源浏览器自动化框架,专为现代 Web 应用设计。它提供了一套统一的 API,支持 Chromium、Firefox 和 WebKit 三大主流浏览器引擎,适用于端到端测试、网页数据采集及自动化操作等场景。", + "developerName": "EAPIL", + "category": "MCP", + "capabilities": [ + "Interactive", + "Read" + ], + "defaultPrompt": [ + "使用 playwright浏览器自动化操作 MCP 查询或处理这个任务。" + ], + "websiteURL": "https://git.playones.com/arechen/EapilSkillMarket", + "privacyPolicyURL": "https://git.playones.com/arechen/EapilSkillMarket", + "termsOfServiceURL": "https://git.playones.com/arechen/EapilSkillMarket", + "brandColor": "#0F766E", + "screenshots": [] + } +} diff --git a/plugins/codex/plugins/mcp-playwright/.mcp.json b/plugins/codex/plugins/mcp-playwright/.mcp.json new file mode 100644 index 000000000..73e3f71a3 --- /dev/null +++ b/plugins/codex/plugins/mcp-playwright/.mcp.json @@ -0,0 +1,19 @@ +{ + "mcpServers": { + "playwright": { + "command": "bunx", + "args": [ + "@playwright/mcp@latest" + ], + "default_tools_approval_mode": "approve", + "tools": { + "browser_navigate": { + "approval_mode": "approve" + }, + "browser_tabs": { + "approval_mode": "approve" + } + } + } + } +} diff --git a/plugins/codex/plugins/mcp-playwright/KEYINFO_MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/KEYINFO_MCP_SOURCE.json new file mode 100644 index 000000000..bf92557de --- /dev/null +++ b/plugins/codex/plugins/mcp-playwright/KEYINFO_MCP_SOURCE.json @@ -0,0 +1,7 @@ +{ + "id": "playwright", + "name": "playwright浏览器自动化操作", + "version": "20260605", + "keySource": "none", + "syncedAt": "2026-06-12T09:39:35Z" +} diff --git a/plugins/codex/plugins/mcp-playwright/README.md b/plugins/codex/plugins/mcp-playwright/README.md new file mode 100644 index 000000000..eb09a6486 --- /dev/null +++ b/plugins/codex/plugins/mcp-playwright/README.md @@ -0,0 +1,19 @@ +# playwright浏览器自动化操作 + +Codex MCP 插件 ID:`mcp-playwright`。 + +Playwright 是由 Microsoft 开发并维护的开源浏览器自动化框架,专为现代 Web 应用设计。它提供了一套统一的 API,支持 Chromium、Firefox 和 WebKit 三大主流浏览器引擎,适用于端到端测试、网页数据采集及自动化操作等场景。 + +## 运行前提 + +- 本地命令:`bunx @playwright/mcp@latest`。 +- 安装插件不会预先安装本地命令依赖;请先确认命令在当前 Codex 环境可执行。 + +## Codex 配置 + +插件自带 `.mcp.json` 声明传输配置。安装后如需调整启用状态或工具审批策略,可在用户 Codex 配置里覆盖: + +```toml +[plugins."mcp-playwright@eapil-skill-market".mcp_servers.playwright] +enabled = true +``` diff --git a/scripts/validate_marketplace.py b/scripts/validate_marketplace.py index 7ad6074ff..6f5b45108 100644 --- a/scripts/validate_marketplace.py +++ b/scripts/validate_marketplace.py @@ -82,6 +82,39 @@ def validate_hooks_field(plugin_dir: Path, manifest_path: Path, hooks: object) - raise AssertionError(f"{manifest_path} hooks must be a path, path list, inline object, or inline object list") +def validate_skills_component(plugin_dir: Path, manifest_path: Path, skills_rel: object) -> None: + require(isinstance(skills_rel, str) and skills_rel.startswith("./"), f"{manifest_path} skills path must be relative ./...") + skills_dir = validate_manifest_path(plugin_dir, manifest_path, skills_rel, "skills") + require(skills_dir.exists(), f"{manifest_path} skills directory does not exist: {skills_dir}") + skill_dirs = [path for path in skills_dir.iterdir() if path.is_dir()] + require(bool(skill_dirs), f"{manifest_path} skills component must contain at least one skill directory") + for skill_dir in skill_dirs: + require((skill_dir / "SKILL.md").exists(), f"Missing SKILL.md in {skill_dir}") + + +def validate_json_component( + plugin_dir: Path, + manifest_path: Path, + value: object, + field: str, + required_top_level: str, +) -> None: + require(isinstance(value, str), f"{manifest_path} {field} must be a path string") + target = validate_manifest_path(plugin_dir, manifest_path, value, field) + payload = load_json(target) + require(isinstance(payload, dict), f"{target} must be a JSON object") + require(isinstance(payload.get(required_top_level), dict), f"{target} missing object {required_top_level}") + + +def validate_interface_asset(plugin_dir: Path, manifest_path: Path, value: object, field: str) -> None: + if value is None or value == "": + return + require(isinstance(value, str), f"{manifest_path} interface.{field} must be a string") + if value.startswith(("http://", "https://")): + return + validate_manifest_path(plugin_dir, manifest_path, value, f"interface.{field}") + + def validate_manifest(plugin_dir: Path, expected_name: str) -> None: manifest_path = plugin_dir / ".codex-plugin" / "plugin.json" require(manifest_path.exists(), f"Missing manifest: {manifest_path}") @@ -91,26 +124,24 @@ def validate_manifest(plugin_dir: Path, expected_name: str) -> None: require(PLUGIN_NAME_RE.match(expected_name) is not None, f"{manifest_path} plugin name must be ASCII kebab-case") require(SEMVER_RE.match(str(manifest.get("version", ""))) is not None, f"{manifest_path} version must be semver-like") require(bool(manifest.get("description")), f"{manifest_path} missing description") - if "hooks" in manifest: - validate_hooks_field(plugin_dir, manifest_path, manifest["hooks"]) - author = manifest.get("author") require(isinstance(author, dict) and bool(author.get("name")), f"{manifest_path} missing author.name") - skills_rel = manifest.get("skills") - require(isinstance(skills_rel, str) and skills_rel.startswith("./"), f"{manifest_path} skills path must be relative ./...") - skills_dir = validate_manifest_path(plugin_dir, manifest_path, skills_rel, "skills") - require(skills_dir.exists(), f"{manifest_path} skills directory does not exist: {skills_dir}") - skill_dirs = [path for path in skills_dir.iterdir() if path.is_dir()] - require(bool(skill_dirs), f"{manifest_path} must contain at least one skill directory") - for skill_dir in skill_dirs: - require((skill_dir / "SKILL.md").exists(), f"Missing SKILL.md in {skill_dir}") + declared_components = [ + key + for key in ["skills", "mcpServers", "apps", "hooks"] + if key in manifest + ] + require(bool(declared_components), f"{manifest_path} must declare at least one component: skills, mcpServers, apps, or hooks") - for optional_path_field in ["mcpServers", "apps"]: - value = manifest.get(optional_path_field) - if value is not None: - require(isinstance(value, str), f"{manifest_path} {optional_path_field} must be a path string") - validate_manifest_path(plugin_dir, manifest_path, value, optional_path_field) + if "skills" in manifest: + validate_skills_component(plugin_dir, manifest_path, manifest.get("skills")) + if "mcpServers" in manifest: + validate_json_component(plugin_dir, manifest_path, manifest.get("mcpServers"), "mcpServers", "mcpServers") + if "apps" in manifest: + validate_json_component(plugin_dir, manifest_path, manifest.get("apps"), "apps", "apps") + if "hooks" in manifest: + validate_hooks_field(plugin_dir, manifest_path, manifest["hooks"]) interface = manifest.get("interface") require(isinstance(interface, dict), f"{manifest_path} missing interface object") @@ -118,6 +149,13 @@ def validate_manifest(plugin_dir: Path, expected_name: str) -> None: require(bool(interface.get(key)), f"{manifest_path} missing interface.{key}") capabilities = interface.get("capabilities") require(isinstance(capabilities, list), f"{manifest_path} interface.capabilities must be a list") + validate_interface_asset(plugin_dir, manifest_path, interface.get("logo"), "logo") + validate_interface_asset(plugin_dir, manifest_path, interface.get("composerIcon"), "composerIcon") + screenshots = interface.get("screenshots") + if screenshots is not None: + require(isinstance(screenshots, list), f"{manifest_path} interface.screenshots must be a list") + for index, screenshot in enumerate(screenshots): + validate_interface_asset(plugin_dir, manifest_path, screenshot, f"screenshots[{index}]") def resolve_marketplace_plugin_path(marketplace_path: Path, source_path: str) -> Path: