Skip to content

fix(mcp-server): allow_no_snapshot must not bypass stage policy witho… #452

fix(mcp-server): allow_no_snapshot must not bypass stage policy witho…

fix(mcp-server): allow_no_snapshot must not bypass stage policy witho… #452

Workflow file for this run

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