fix(mcp-server): allow_no_snapshot must not bypass stage policy witho… #452
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| workflow_call: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| validate-manifests: | |
| name: Validate JSON manifests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Parse JSON manifests | |
| run: | | |
| python3 -m json.tool .claude-plugin/marketplace.json > /dev/null | |
| python3 -m json.tool .agents/plugins/marketplace.json > /dev/null | |
| python3 -m json.tool plugins/corezoid/.claude-plugin/plugin.json > /dev/null | |
| python3 -m json.tool plugins/corezoid/.codex-plugin/plugin.json > /dev/null | |
| python3 -m json.tool plugins/corezoid/.kiro-plugin/plugin.json > /dev/null | |
| python3 -m json.tool plugins/corezoid/.mcp.json > /dev/null | |
| python3 -m json.tool plugins/corezoid/.mcp.kiro.json > /dev/null | |
| - name: No dead plugin paths in marketplace manifests | |
| run: | | |
| python3 - <<'EOF' | |
| import json, os, sys | |
| errors = [] | |
| for manifest_path in [".claude-plugin/marketplace.json"]: | |
| with open(manifest_path) as f: | |
| data = json.load(f) | |
| for plugin in data.get("plugins", []): | |
| src = plugin.get("source") | |
| if src and not os.path.exists(src): | |
| errors.append(f"{manifest_path}: source path '{src}' does not exist") | |
| for manifest_path in [".agents/plugins/marketplace.json"]: | |
| with open(manifest_path) as f: | |
| data = json.load(f) | |
| for plugin in data.get("plugins", []): | |
| src = plugin.get("source", {}).get("path") | |
| if src and not os.path.exists(src): | |
| errors.append(f"{manifest_path}: source path '{src}' does not exist") | |
| if errors: | |
| for e in errors: | |
| print("ERROR:", e) | |
| sys.exit(1) | |
| print("All marketplace paths resolve.") | |
| EOF | |
| - name: Version sync across manifests | |
| run: | | |
| python3 - <<'EOF' | |
| import json, re, sys | |
| def read_frontmatter_version(path): | |
| with open(path) as f: | |
| text = f.read() | |
| m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL) | |
| if not m: | |
| raise SystemExit(f"{path}: no YAML frontmatter") | |
| for line in m.group(1).splitlines(): | |
| mv = re.match(r"^version:\s*(.+?)\s*$", line) | |
| if mv: | |
| return mv.group(1).strip().strip('"').strip("'") | |
| raise SystemExit(f"{path}: no version field in frontmatter") | |
| with open("plugins/corezoid/.claude-plugin/plugin.json") as f: | |
| claude_ver = json.load(f)["version"] | |
| with open("plugins/corezoid/.codex-plugin/plugin.json") as f: | |
| codex_ver = json.load(f)["version"] | |
| with open("plugins/corezoid/.kiro-plugin/plugin.json") as f: | |
| kiro_ver = json.load(f)["version"] | |
| with open(".claude-plugin/marketplace.json") as f: | |
| market_ver = json.load(f)["plugins"][0]["version"] | |
| with open(".agents/plugins/marketplace.json") as f: | |
| agents_ver = json.load(f)["plugins"][0]["version"] | |
| power_ver = read_frontmatter_version("POWER.md") | |
| versions = { | |
| "claude plugin.json": claude_ver, | |
| "codex plugin.json": codex_ver, | |
| "kiro plugin.json": kiro_ver, | |
| ".claude-plugin/marketplace.json": market_ver, | |
| ".agents/plugins/marketplace.json": agents_ver, | |
| "POWER.md": power_ver, | |
| } | |
| print("Versions found:", versions) | |
| if len(set(versions.values())) > 1: | |
| print("ERROR: version mismatch across manifests") | |
| for name, ver in versions.items(): | |
| print(f" {name}: {ver}") | |
| sys.exit(1) | |
| print("All versions match:", claude_ver) | |
| EOF | |
| - name: License consistency | |
| run: | | |
| python3 - <<'EOF' | |
| import json, sys | |
| manifests = [ | |
| ("plugins/corezoid/.claude-plugin/plugin.json", lambda d: d.get("license")), | |
| ("plugins/corezoid/.codex-plugin/plugin.json", lambda d: d.get("license")), | |
| (".claude-plugin/marketplace.json", lambda d: d["plugins"][0].get("license")), | |
| (".agents/plugins/marketplace.json", lambda d: d["plugins"][0].get("license")), | |
| ] | |
| expected = "MIT" | |
| errors = [] | |
| for path, getter in manifests: | |
| with open(path) as f: | |
| lic = getter(json.load(f)) | |
| if lic != expected: | |
| errors.append(f"{path}: license is '{lic}', expected '{expected}'") | |
| if errors: | |
| for e in errors: | |
| print("ERROR:", e) | |
| sys.exit(1) | |
| print("License OK:", expected) | |
| EOF | |
| - name: MCP launcher resolves plugin root and preserves workspace | |
| run: | | |
| python3 - <<'EOF' | |
| import json, os, pathlib, stat, subprocess, tempfile | |
| with open("plugins/corezoid/.mcp.json") as f: | |
| cfg = json.load(f) | |
| server = cfg.get("mcpServers", cfg)["corezoid"] | |
| def fake_run_sh(plugin_root): | |
| run = pathlib.Path(plugin_root) / "mcp-server" / "run.sh" | |
| run.parent.mkdir(parents=True, exist_ok=True) | |
| run.write_text("#!/bin/sh\nprintf '%s\n' \"$COREZOID_WORK_DIR\" > \"$PROBE_OUT\"\n") | |
| run.chmod(run.stat().st_mode | stat.S_IXUSR) | |
| return run | |
| def run_case(label, env_extra, home_builder): | |
| with tempfile.TemporaryDirectory() as home, tempfile.TemporaryDirectory() as workspace: | |
| probe_out = pathlib.Path(home) / "probe.txt" | |
| env = { | |
| "HOME": home, | |
| "PATH": os.environ["PATH"], | |
| "PROBE_OUT": str(probe_out), | |
| **env_extra, | |
| } | |
| env.update(home_builder(home, probe_out) or {}) | |
| res = subprocess.run( | |
| [server["command"], *server.get("args", [])], | |
| cwd=workspace, | |
| env=env, | |
| text=True, | |
| capture_output=True, | |
| timeout=10, | |
| ) | |
| if res.returncode != 0: | |
| raise SystemExit(f"{label}: launcher failed\nstdout={res.stdout}\nstderr={res.stderr}") | |
| got = probe_out.read_text().strip() | |
| want = os.path.realpath(workspace) | |
| if got != want: | |
| raise SystemExit(f"{label}: COREZOID_WORK_DIR={got!r}, want {want!r}") | |
| with tempfile.TemporaryDirectory() as claude_root: | |
| fake_run_sh(claude_root) | |
| run_case("claude", {"CLAUDE_PLUGIN_ROOT": claude_root}, lambda home, out: None) | |
| def codex_home_case(home, probe_out): | |
| root = pathlib.Path(home) / ".codex" / "plugins" / "cache" / "corezoid" / "corezoid" / "999.0.0" | |
| fake_run_sh(root) | |
| run_case("codex-home", {}, codex_home_case) | |
| def codex_home_env_case(home, probe_out): | |
| codex_home = pathlib.Path(home) / "custom-codex-home" | |
| root = codex_home / "plugins" / "cache" / "corezoid" / "corezoid" / "999.0.0" | |
| fake_run_sh(root) | |
| return {"CODEX_HOME": str(codex_home)} | |
| run_case("codex-home-env", {}, codex_home_env_case) | |
| print(".mcp.json launcher smoke test passed.") | |
| EOF | |
| - name: No invalid CLAUDE_PLUGIN_ROOT paths in skills | |
| run: | | |
| # ${CLAUDE_PLUGIN_ROOT} points to plugins/corezoid — catch doubled prefix | |
| if grep -r '\${CLAUDE_PLUGIN_ROOT}/plugins/corezoid' plugins/corezoid/skills/; then | |
| echo "ERROR: skills reference \${CLAUDE_PLUGIN_ROOT}/plugins/corezoid — use \${CLAUDE_PLUGIN_ROOT}/... instead" | |
| exit 1 | |
| fi | |
| echo "All CLAUDE_PLUGIN_ROOT paths look correct." | |
| - name: Skills list sync (filesystem ↔ CLAUDE.md ↔ README.md) | |
| run: python3 scripts/check-skills-sync.py | |
| - name: No tracked .env or credential files | |
| run: | | |
| if git ls-files | grep -E '\.env$|credentials\.(json|yaml|yml)$'; then | |
| echo "ERROR: sensitive files are tracked in git" | |
| exit 1 | |
| fi | |
| echo "No sensitive files tracked." | |
| - name: Lint run.sh (syntax check) | |
| run: sh -n plugins/corezoid/mcp-server/run.sh | |
| docs: | |
| name: Docs link check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Check markdown links | |
| uses: gaurav-nelson/github-action-markdown-link-check@v1 | |
| with: | |
| use-quiet-mode: "yes" | |
| config-file: ".github/mlc_config.json" | |
| mcp-server: | |
| name: MCP server build & test | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: plugins/corezoid/mcp-server | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: plugins/corezoid/mcp-server/go.mod | |
| cache: true | |
| cache-dependency-path: plugins/corezoid/mcp-server/go.sum | |
| - name: Build | |
| run: go build ./... | |
| - name: Vet | |
| run: go vet ./... | |
| - name: Test (race + coverage) | |
| run: go test -race -coverprofile=coverage.out ./... | |
| - name: Coverage report | |
| run: go tool cover -func=coverage.out | |
| - name: Smoke test --version | |
| run: go run . --version | |
| govulncheck: | |
| name: Go vulnerability scan | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: plugins/corezoid/mcp-server | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: plugins/corezoid/mcp-server/go.mod | |
| cache: true | |
| cache-dependency-path: plugins/corezoid/mcp-server/go.sum | |
| # Pinned, not @latest: a release gate that silently changes tool version | |
| # between runs cannot be reproduced, and a new govulncheck release can | |
| # turn a green tag into a red one (or the reverse) with no change on our | |
| # side. Bump this deliberately. | |
| - name: Install govulncheck | |
| run: go install golang.org/x/vuln/cmd/govulncheck@v1.7.0 | |
| - name: Run govulncheck | |
| run: govulncheck ./... | |
| dependency-review: | |
| name: Dependency review | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/dependency-review-action@v5 | |
| with: | |
| fail-on-severity: high |