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.
This commit is contained in:
KeyInfo Bot
2026-06-12 17:42:38 +08:00
parent 9d6c8cded3
commit fca5c80516
9 changed files with 237 additions and 105 deletions
+54 -16
View File
@@ -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: