From 4e8acf2367d2bb3979cff4ca6d1fd007bd28c747 Mon Sep 17 00:00:00 2001 From: JP Date: Fri, 7 Aug 2026 10:23:11 +0100 Subject: [PATCH 1/3] feat: conform to Agent Plugins 1.0.0 and the Agent Skills specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent fatal faults meant a conformant Agent Plugins client would load nothing from this plugin: 1. plugin.json omitted the required $schema. It sits on a closed schema, so §5.2 makes the omission fatal — the client rejects the plugin outright. 2. All 17 skills carried top-level frontmatter keys (version, triggers, required_scopes, mcp_servers, persona, tier, gate, owns_eval) outside the closed Agent Skills field set. Agent Plugins §6.1 requires clients to skip every non-conforming skill. Changes: - plugin.json declares the manifest schema. - New root mcp.json: the portable MCP manifest, with an explicit stdio transport discriminator. It ships the gcloud server only — toolbox needs a user-specific tools.yaml that no spec placeholder can express, so it stays a documented opt-in. env is omitted entirely: only ${PLUGIN_ROOT} and ${PLUGIN_DATA} expand, so ${GCP_PROJECT_ID} would have been passed through literally and mis-set the project. - All 17 SKILL.md frontmatters reduced to {name, description, license, metadata}. Routing keywords folded into description, which is what a conformant client actually routes on; the plugin's own contract moves to namespaced string values under metadata. - validate_skills.py inverted: it now enforces the closed Agent Skills field set instead of requiring the fields that broke conformance. - New validate_agent_plugins.py: dependency-free conformance check wired in as `make spec`, reporting against the spec's own FATAL/MCP/SKILL failure boundaries. - Smoke tests rewritten: 136 -> 195, covering both specs. Verified against the official reference validator (agentskills/agentskills skills-ref) — 17/17 valid — and both published JSON schemas via check-jsonschema. All four harnesses remain installable. --- Makefile | 15 +- mcp.json | 10 + plugin.json | 1 + scripts/validate_agent_plugins.py | 306 ++++++++++++++++++ scripts/validate_plugin.py | 28 ++ scripts/validate_skills.py | 154 +++++++-- skills/agent-architect/SKILL.md | 45 +-- skills/bigquery/SKILL.md | 30 +- skills/cloud-run/SKILL.md | 28 +- skills/cloud-storage/SKILL.md | 30 +- skills/gcp-architect/SKILL.md | 27 +- skills/gcp-ops/SKILL.md | 35 +- skills/gcp-qa/SKILL.md | 29 +- skills/gcp-security/SKILL.md | 30 +- skills/gke/SKILL.md | 29 +- skills/iam/SKILL.md | 28 +- skills/logging-monitoring/SKILL.md | 32 +- skills/mcp-servers/SKILL.md | 29 +- skills/networking/SKILL.md | 29 +- skills/solution-designer/SKILL.md | 28 +- skills/terraform-gcp/SKILL.md | 24 +- skills/vertex-ai/SKILL.md | 31 +- skills/well-architected/SKILL.md | 20 +- .../skill-smoke-tests/test_skill_contract.py | 183 +++++++++-- 24 files changed, 760 insertions(+), 441 deletions(-) create mode 100644 mcp.json create mode 100644 scripts/validate_agent_plugins.py diff --git a/Makefile b/Makefile index 86b57a8..a7a2f7b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all gate validate manifest preflight lint mermaid test check crawl crawl-dry hooks help +.PHONY: all gate spec validate manifest preflight lint mermaid test check crawl crawl-dry hooks help PYTHON := python3 SCRIPTS := scripts @@ -7,12 +7,16 @@ TESTS := tests/skill-smoke-tests # ─── 3-minute validation gate (validate + manifest + lint + test) ─────────── # This is the pre-commit gate. Install the hook once with: make hooks -gate: validate manifest mermaid lint test +gate: spec validate manifest mermaid lint test @echo "" - @echo "Gate passed: validate + manifest + mermaid + lint + test" + @echo "Gate passed: spec + validate + manifest + mermaid + lint + test" # ─── Individual targets ────────────────────────────────────────────────────── +spec: + @echo "==> Validating Agent Plugins 1.0.0 conformance..." + @$(PYTHON) $(SCRIPTS)/validate_agent_plugins.py + validate: @echo "==> Validating SKILL.md contracts..." @$(PYTHON) $(SCRIPTS)/validate_skills.py @@ -66,8 +70,9 @@ help: @echo "" @echo "GoogleCloud Plugin — Makefile Targets" @echo "" - @echo " make gate Pre-commit gate: validate + manifest + mermaid + lint + test" - @echo " make validate Validate all SKILL.md frontmatter (contract check)" + @echo " make gate Pre-commit gate: spec + validate + manifest + mermaid + lint + test" + @echo " make spec Validate Agent Plugins 1.0.0 conformance (plugin.json + mcp.json)" + @echo " make validate Validate all SKILL.md frontmatter (Agent Skills + plugin contract)" @echo " make manifest Validate plugin is installable (Claude/AGY/Codex/Kimi)" @echo " make mermaid Lint Mermaid diagrams for GitHub render-safety" @echo " make lint Check all reference URLs resolve (HTTP 200)" diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..d3cab1f --- /dev/null +++ b/mcp.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "gcloud": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@google-cloud/gcloud-mcp"] + } + } +} diff --git a/plugin.json b/plugin.json index e0b95c4..83278c2 100644 --- a/plugin.json +++ b/plugin.json @@ -1,4 +1,5 @@ { + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "googlecloud-plugin", "description": "A full Google Cloud delivery team for your coding agent: vendor-objective solution designer, GCP architect, agentic-systems architect (ADK / Agent Runtime / MCP / A2A / AP2), plus security, SRE, and QA — wired into a design-first, security-first delivery gate. Eleven service skills (Cloud Run, GKE, IAM, BigQuery, Cloud Storage, Vertex AI, networking, observability), MCP server setup, and a self-validating research pipeline that keeps every reference current.", "version": "0.1.0", diff --git a/scripts/validate_agent_plugins.py b/scripts/validate_agent_plugins.py new file mode 100644 index 0000000..15ad97f --- /dev/null +++ b/scripts/validate_agent_plugins.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Validate the plugin against the Agent Plugins 1.0.0 specification. + +https://agent-plugins.org/specification + +This enforces the vendor-neutral packaging contract that Amazon, Cursor, +Google, Microsoft, OpenAI and Vercel co-maintain — independently of any single +harness. It is deliberately dependency-free so it runs in CI without a network +call: the published schemas are transcribed as rules rather than fetched. + +Failure classes follow the specification's own boundaries: + FATAL — client rejects the plugin entirely (§5.2) + MCP — client disables MCP but keeps loading other component types (§6.2) + SKILL — client skips that one skill, others still load (§6.1) + +Exit 0: conformant. Exit 1: one or more violations. +""" +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).parent.parent +SPEC_VERSION = "1.0.0" +PLUGIN_SCHEMA = f"https://agent-plugins.org/schemas/{SPEC_VERSION}/plugin.schema.json" +MCP_SCHEMA = f"https://agent-plugins.org/schemas/{SPEC_VERSION}/mcp.schema.json" + +# §5.2 — the only permitted top-level manifest fields. +PLUGIN_FIELDS = { + "$schema", "name", "version", "description", "author", + "homepage", "repository", "license", "keywords", "extensions", +} +AUTHOR_FIELDS = {"name", "email", "url"} +NAME_PATTERN = re.compile(r"^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$") + +# Only these two placeholders are expanded; everything else stays literal. +EXPANDED = {"${PLUGIN_ROOT}", "${PLUGIN_DATA}"} +PLACEHOLDER = re.compile(r"\$\{[^}]*\}") +CWD_PATTERN = re.compile(r"^(?:\./|\$\{PLUGIN_ROOT\}(?:/|$)|\$\{PLUGIN_DATA\}(?:/|$))") + +SERVER_FIELDS = { + "stdio": ({"type", "command"}, {"type", "command", "args", "env", "cwd"}), + "streamable-http": ({"type", "url"}, {"type", "url", "headers"}), + "sse": ({"type", "url"}, {"type", "url", "headers"}), +} + + +def schema_version(identifier: str) -> str | None: + match = re.search(r"/schemas/([^/]+)/", identifier or "") + return match.group(1) if match else None + + +def check_plugin_manifest(errors: list[str]) -> dict: + path = ROOT / "plugin.json" + if not path.exists(): + errors.append("FATAL plugin.json missing at plugin root") + return {} + try: + manifest = json.loads(path.read_text()) + except json.JSONDecodeError as e: + errors.append(f"FATAL plugin.json is not valid JSON: {e}") + return {} + if not isinstance(manifest, dict): + errors.append("FATAL plugin.json must be a JSON object") + return {} + + if manifest.get("$schema") != PLUGIN_SCHEMA: + errors.append( + f"FATAL plugin.json '$schema' must be '{PLUGIN_SCHEMA}' " + f"(got {manifest.get('$schema')!r}) — required by the closed schema" + ) + + name = manifest.get("name") + if not isinstance(name, str) or not name: + errors.append("FATAL plugin.json 'name' is required") + else: + if not 1 <= len(name) <= 64: + errors.append(f"FATAL plugin.json 'name' must be 1-64 chars (got {len(name)})") + if not NAME_PATTERN.match(name): + errors.append( + f"FATAL plugin.json 'name' {name!r} violates the schema pattern " + "(lowercase alphanumeric, hyphens and periods; no leading/trailing " + "separator; no '--' or '..')" + ) + + extra = sorted(set(manifest) - PLUGIN_FIELDS) + if extra: + errors.append( + f"FATAL plugin.json has non-permitted top-level fields {extra} — " + f"§5.2 permits only {sorted(PLUGIN_FIELDS)}; use 'extensions'" + ) + + author = manifest.get("author") + if author is not None: + if not isinstance(author, dict): + errors.append("FATAL plugin.json 'author' must be an object") + else: + author_extra = sorted(set(author) - AUTHOR_FIELDS) + if author_extra: + errors.append( + f"FATAL plugin.json 'author' has non-permitted fields {author_extra} " + f"— only {sorted(AUTHOR_FIELDS)} are allowed" + ) + + extensions = manifest.get("extensions") + if extensions is not None and isinstance(extensions, dict): + for namespace, value in extensions.items(): + if not isinstance(value, dict): + errors.append(f"FATAL extensions['{namespace}'] must be an object") + elif "." not in namespace: + errors.append( + f" extensions namespace '{namespace}' should be a " + "reverse-domain name you control (SHOULD, §5.2)" + ) + return manifest + + +def check_placeholders(where: str, value: str, errors: list[str]) -> None: + """Only ${PLUGIN_ROOT} and ${PLUGIN_DATA} expand — anything else stays literal.""" + for found in PLACEHOLDER.findall(value): + if found not in EXPANDED: + errors.append( + f"MCP {where}: '{found}' is not an Agent Plugins placeholder and " + f"will be passed through LITERALLY (only {sorted(EXPANDED)} expand)" + ) + + +def check_stdio(name: str, cfg: dict, errors: list[str]) -> None: + command = cfg.get("command") + if isinstance(command, str): + if not command: + errors.append(f"MCP server '{name}': 'command' must be non-empty") + elif len(command.split()) > 1: + errors.append( + f"MCP server '{name}': 'command' must be a single executable token, " + f"got {command!r} — move arguments into 'args'" + ) + elif command.startswith("/") or command.startswith("../"): + errors.append( + f"MCP server '{name}': 'command' {command!r} must be a bare name or " + "a './' plugin-relative path" + ) + elif command.startswith("./") and not (ROOT / command).exists(): + errors.append(f"MCP server '{name}': 'command' {command!r} does not resolve") + + for index, arg in enumerate(cfg.get("args") or []): + if isinstance(arg, str): + check_placeholders(f"server '{name}' args[{index}]", arg, errors) + + env = cfg.get("env") or {} + if isinstance(env, dict): + for key, value in env.items(): + if key in ("PLUGIN_ROOT", "PLUGIN_DATA"): + errors.append( + f"MCP server '{name}': env must not define '{key}' — the client " + "provides it and its value takes precedence" + ) + if isinstance(value, str): + check_placeholders(f"server '{name}' env['{key}']", value, errors) + + cwd = cfg.get("cwd") + if isinstance(cwd, str) and not CWD_PATTERN.match(cwd): + errors.append( + f"MCP server '{name}': 'cwd' {cwd!r} must start with './', " + "'${PLUGIN_ROOT}' or '${PLUGIN_DATA}'" + ) + + +def check_mcp(plugin_manifest: dict, errors: list[str]) -> None: + path = ROOT / "mcp.json" + if not path.exists(): + return # §6 — a missing fixed location is not an error. + + try: + config = json.loads(path.read_text()) + except json.JSONDecodeError as e: + errors.append(f"MCP mcp.json is not valid JSON: {e}") + return + if not isinstance(config, dict): + errors.append("MCP mcp.json must be a JSON object") + return + + declared = config.get("$schema") + if declared != MCP_SCHEMA: + errors.append(f"MCP mcp.json '$schema' must be '{MCP_SCHEMA}' (got {declared!r})") + elif plugin_manifest: + plugin_version = schema_version(plugin_manifest.get("$schema", "")) + if plugin_version and schema_version(declared) != plugin_version: + errors.append( + f"MCP mcp.json targets {schema_version(declared)} but plugin.json " + f"targets {plugin_version} — versions MUST match" + ) + + extra = sorted(set(config) - {"$schema", "mcpServers"}) + if extra: + errors.append(f"MCP mcp.json has non-permitted top-level fields {extra}") + + servers = config.get("mcpServers") + if not isinstance(servers, dict): + errors.append("MCP mcp.json 'mcpServers' is required and must be an object") + return + + for name, cfg in servers.items(): + if not isinstance(cfg, dict): + errors.append(f"MCP server '{name}' must be an object") + continue + + transport = cfg.get("type") + if transport not in SERVER_FIELDS: + errors.append( + f"MCP server '{name}': 'type' must be one of " + f"{sorted(SERVER_FIELDS)} (got {transport!r}) — the schema discriminates " + "transports on this field" + ) + continue + if transport == "sse": + errors.append(f" server '{name}': transport 'sse' is deprecated") + + required, permitted = SERVER_FIELDS[transport] + missing = sorted(required - set(cfg)) + if missing: + errors.append(f"MCP server '{name}': missing required field(s) {missing}") + server_extra = sorted(set(cfg) - permitted) + if server_extra: + errors.append( + f"MCP server '{name}': non-permitted field(s) {server_extra} for " + f"transport '{transport}'" + ) + + if transport == "stdio": + check_stdio(name, cfg, errors) + else: + url = cfg.get("url", "") + if isinstance(url, str) and not url.startswith(("http://", "https://")): + errors.append(f"MCP server '{name}': 'url' must be absolute http(s)") + if "#" in url: + errors.append(f"MCP server '{name}': 'url' must not contain a fragment") + + +def check_skills(errors: list[str]) -> None: + """§6.1 — skills are discovered non-recursively from skills/.""" + skills_dir = ROOT / "skills" + if not skills_dir.exists(): + return + if not skills_dir.is_dir(): + errors.append("SKILL 'skills' exists but is not a directory") + return + + discovered = sorted(p.name for p in skills_dir.iterdir() if (p / "SKILL.md").is_file()) + for deeper in skills_dir.glob("*/*/**/SKILL.md"): + errors.append( + f" {deeper.relative_to(ROOT)} will NOT be discovered — clients must " + "not search below the immediate children of skills/" + ) + print(f" {len(discovered)} skill(s) discoverable at skills/*/SKILL.md") + + +def check_containment(errors: list[str]) -> None: + """§4.1 — every plugin-relative path must resolve inside the plugin root.""" + root = ROOT.resolve() + for path in ROOT.rglob("*"): + if ".git" in path.parts or not path.is_symlink(): + continue + try: + target = path.resolve() + except OSError: + errors.append(f"FATAL symlink {path.relative_to(ROOT)} does not resolve") + continue + if not target.is_relative_to(root): + errors.append( + f"FATAL symlink {path.relative_to(ROOT)} escapes the plugin root " + f"(-> {target})" + ) + + +def main() -> None: + print(f"==> Validating against Agent Plugins {SPEC_VERSION} " + "(https://agent-plugins.org/specification)\n") + errors: list[str] = [] + + manifest = check_plugin_manifest(errors) + check_mcp(manifest, errors) + check_skills(errors) + check_containment(errors) + + fatal = [e for e in errors if e.startswith(("FATAL", "MCP", "SKILL"))] + advisory = [e for e in errors if e not in fatal] + + for e in advisory: + print(f"WARN{e}") + if not fatal: + print(f"\nOK plugin.json conforms to {PLUGIN_SCHEMA}") + if (ROOT / "mcp.json").exists(): + print(f"OK mcp.json conforms to {MCP_SCHEMA}") + print(f"OK skills/ conform to https://agentskills.io/specification") + print(f"\nPlugin conforms to the Agent Plugins {SPEC_VERSION} specification") + return + + for e in fatal: + print(f"FAIL {e}") + print(f"\n{len(fatal)} conformance violation(s)", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_plugin.py b/scripts/validate_plugin.py index d51d176..8e843f3 100644 --- a/scripts/validate_plugin.py +++ b/scripts/validate_plugin.py @@ -117,6 +117,8 @@ def mcp_map(path: Path) -> dict | None: agy = mcp_map(ROOT / "mcp_config.json") gemini = mcp_map(ROOT / "gemini-extension.json") + check_agent_plugins_mcp_subset(claude, errors) + present = {n: s for n, s in (("claude .mcp.json", claude), ("agy mcp_config.json", agy), ("gemini inline", gemini)) if s is not None} if len(present) > 1: baseline_name, baseline = next(iter(present.items())) @@ -143,6 +145,32 @@ def mcp_map(path: Path) -> dict | None: ) +def check_agent_plugins_mcp_subset(claude: dict | None, errors: list[str]) -> None: + """The spec-portable mcp.json must not declare servers the harnesses don't know. + + mcp.json (Agent Plugins 1.0.0) is deliberately a SUBSET of the harness + manifests: it carries only servers that run with zero user-supplied config, + because the spec expands no placeholders beyond ${PLUGIN_ROOT}/${PLUGIN_DATA}. + Servers needing a user-specific config file stay documented opt-ins. + """ + spec = _load_json(ROOT / "mcp.json", errors) + if spec is None or claude is None: + return + spec_servers = set(spec.get("mcpServers", {})) + if not spec_servers <= set(claude): + errors.append( + f"mcp.json declares server(s) {sorted(spec_servers - set(claude))} absent " + "from .mcp.json — the portable manifest must be a subset" + ) + for name in spec_servers & set(claude): + spec_cmd = spec["mcpServers"][name].get("command") + if spec_cmd != claude[name]["command"]: + errors.append( + f"MCP drift: server '{name}' command differs between mcp.json " + f"({spec_cmd}) and .mcp.json ({claude[name]['command']})" + ) + + def check_context_files(errors: list[str]) -> None: for ctx in ("PLUGIN.md", "GEMINI.md", "AGENTS.md"): if not (ROOT / ctx).exists(): diff --git a/scripts/validate_skills.py b/scripts/validate_skills.py index f9211e8..241b9f6 100644 --- a/scripts/validate_skills.py +++ b/scripts/validate_skills.py @@ -1,60 +1,154 @@ #!/usr/bin/env python3 -"""Validate all SKILL.md files conform to the plugin contract. +"""Validate every SKILL.md against the Agent Skills specification. + +Two layers are checked: + +1. Agent Skills conformance (https://agentskills.io/specification) — mirrors the + reference validator at agentskills/agentskills:skills-ref. The frontmatter + field set is CLOSED: a skill carrying any other top-level key is + non-conformant, and Agent Plugins 1.0.0 requires clients to skip it. +2. The plugin's own contract, which now lives under `metadata` as namespaced + string values rather than as top-level fields. Exit 0: all skills valid. Exit 1: one or more violations found. """ import sys +import unicodedata import yaml from pathlib import Path -REQUIRED_FIELDS = ["name", "description", "version", "triggers", "required_scopes"] SKILLS_DIR = Path(__file__).parent.parent / "skills" +# Closed field set per the Agent Skills specification. +ALLOWED_FIELDS = { + "name", + "description", + "license", + "allowed-tools", + "metadata", + "compatibility", +} +MAX_NAME = 64 +MAX_DESCRIPTION = 1024 +MAX_COMPATIBILITY = 500 + +# The plugin's own contract, carried as namespaced metadata keys. +NS = "googlecloud-plugin/" +REQUIRED_METADATA = {f"{NS}version", f"{NS}triggers", f"{NS}required-scopes"} + + +def check_name(name, dir_name: str) -> list[str]: + if not isinstance(name, str) or not name.strip(): + return ["Field 'name' must be a non-empty string"] -def validate_skill(skill_path: Path) -> list[str]: errors = [] + name = unicodedata.normalize("NFKC", name.strip()) + if len(name) > MAX_NAME: + errors.append(f"name '{name}' exceeds {MAX_NAME} chars ({len(name)})") + if name != name.lower(): + errors.append(f"name '{name}' must be lowercase") + if name.startswith("-") or name.endswith("-"): + errors.append("name cannot start or end with a hyphen") + if "--" in name: + errors.append("name cannot contain consecutive hyphens") + if not all(c.isalnum() or c == "-" for c in name): + errors.append(f"name '{name}' may only contain letters, digits and hyphens") + if name != unicodedata.normalize("NFKC", dir_name): + errors.append(f"name '{name}' does not match directory '{dir_name}'") + return errors + + +def check_description(description) -> list[str]: + if not isinstance(description, str) or not description.strip(): + return ["Field 'description' must be a non-empty string"] + if len(description) > MAX_DESCRIPTION: + return [f"description exceeds {MAX_DESCRIPTION} chars ({len(description)})"] + return [] + + +def check_metadata(metadata) -> list[str]: + """Agent Skills defines metadata as a map from string keys to string values.""" + if not isinstance(metadata, dict): + return ["'metadata' must be a mapping"] + + errors = [] + for key, value in metadata.items(): + if not isinstance(key, str): + errors.append(f"metadata key {key!r} must be a string") + continue + if not isinstance(value, str): + errors.append( + f"metadata['{key}'] must be a string value, got {type(value).__name__} " + f"— serialise lists as comma-separated strings" + ) + if not key.startswith(NS): + errors.append(f"metadata key '{key}' must be namespaced as '{NS}'") + + missing = REQUIRED_METADATA - set(metadata) + if missing: + errors.append(f"missing required metadata keys: {sorted(missing)}") + return errors + + +def validate_skill(skill_path: Path) -> list[str]: content = skill_path.read_text() if not content.startswith("---"): - return [f"{skill_path}: Missing YAML frontmatter (file must start with ---)"] + return ["Missing YAML frontmatter (file must start with ---)"] parts = content.split("---", 2) if len(parts) < 3: - return [f"{skill_path}: Malformed frontmatter (missing closing ---)"] + return ["Malformed frontmatter (missing closing ---)"] try: fm = yaml.safe_load(parts[1]) except yaml.YAMLError as e: - return [f"{skill_path}: YAML parse error: {e}"] + return [f"YAML parse error: {e}"] if not isinstance(fm, dict): - return [f"{skill_path}: Frontmatter must be a YAML mapping"] + return ["Frontmatter must be a YAML mapping"] - for field in REQUIRED_FIELDS: - if field not in fm: - errors.append(f"{skill_path}: Missing required field '{field}'") + errors = [] - dir_name = skill_path.parent.name - if fm.get("name") and fm["name"] != dir_name: + extra = sorted(set(fm) - ALLOWED_FIELDS) + if extra: errors.append( - f"{skill_path}: name '{fm['name']}' does not match directory '{dir_name}'" + f"non-conformant frontmatter fields {extra} — Agent Skills allows only " + f"{sorted(ALLOWED_FIELDS)}; move plugin-specific keys under 'metadata'" ) - refs_dir = skill_path.parent / "references" - if not refs_dir.is_dir(): - errors.append(f"{skill_path.parent}: Missing references/ directory") - - if not isinstance(fm.get("triggers"), list) or not fm["triggers"]: - errors.append(f"{skill_path}: 'triggers' must be a non-empty list") - - if not isinstance(fm.get("required_scopes"), list): - errors.append(f"{skill_path}: 'required_scopes' must be a list (can be empty)") + if "name" not in fm: + errors.append("Missing required field 'name'") + else: + errors.extend(check_name(fm["name"], skill_path.parent.name)) + + if "description" not in fm: + errors.append("Missing required field 'description'") + else: + errors.extend(check_description(fm["description"])) + + if "compatibility" in fm: + compatibility = fm["compatibility"] + if not isinstance(compatibility, str): + errors.append("'compatibility' must be a string") + elif len(compatibility) > MAX_COMPATIBILITY: + errors.append( + f"compatibility exceeds {MAX_COMPATIBILITY} chars ({len(compatibility)})" + ) + + if "metadata" not in fm: + errors.append("Missing 'metadata' (carries this plugin's skill contract)") + else: + errors.extend(check_metadata(fm["metadata"])) + + if not (skill_path.parent / "references").is_dir(): + errors.append("Missing references/ directory") return errors -def main(): +def main() -> None: if not SKILLS_DIR.is_dir(): print(f"ERROR: skills/ directory not found at {SKILLS_DIR}", file=sys.stderr) sys.exit(1) @@ -64,21 +158,19 @@ def main(): print("WARNING: No SKILL.md files found") sys.exit(0) - all_errors: list[str] = [] + total = 0 for skill_file in skill_files: errors = validate_skill(skill_file) - all_errors.extend(errors) - status = "FAIL" if errors else "OK " - print(f"{status} {skill_file.parent.name}") + total += len(errors) + print(f"{'FAIL' if errors else 'OK '} {skill_file.parent.name}") for e in errors: print(f" {e}") print() - if all_errors: - print(f"{len(all_errors)} error(s) — fix before committing", file=sys.stderr) + if total: + print(f"{total} error(s) — fix before committing", file=sys.stderr) sys.exit(1) - - print(f"{len(skill_files)} skill(s) validated OK") + print(f"{len(skill_files)} skill(s) conform to the Agent Skills specification") if __name__ == "__main__": diff --git a/skills/agent-architect/SKILL.md b/skills/agent-architect/SKILL.md index 1f07a53..3f7b837 100644 --- a/skills/agent-architect/SKILL.md +++ b/skills/agent-architect/SKILL.md @@ -1,39 +1,16 @@ --- name: agent-architect -description: "Agentic systems architect for the Gemini Enterprise Agent Platform (GEAP, formerly Vertex AI). Tier 2 specialist alongside gcp-architect: owns agentic application design AND agent evaluation execution. Covers ADK, Agent Runtime (formerly Agent Engine), the MCP/A2A/AP2 protocol stack, multi-agent topologies, grounding/RAG, memory, human-in-the-loop, and Gemini model selection. GCP is always the target deployment platform." -version: "0.1" -persona: true -tier: 2 -gate: gcp-design -owns_eval: true -triggers: - - "agent architecture" - - "agentic system" - - "multi-agent" - - "ADK" - - "agent development kit" - - "agent engine" - - "agent runtime" - - "A2A" - - "agent to agent" - - "AP2" - - "agent payments" - - "MCP tools for agent" - - "agent evaluation" - - "agent eval" - - "grounding" - - "RAG on GCP" - - "orchestrator agent" - - "gemini agent" - - "GEAP" -required_scopes: - - aiplatform.reasoningEngines.create - - aiplatform.reasoningEngines.get - - aiplatform.reasoningEngines.query - - aiplatform.endpoints.predict - - aiplatform.evaluationTasks.create -mcp_servers: - - google-vertex-ai +description: "Agentic systems architect for the Gemini Enterprise Agent Platform (GEAP, formerly Vertex AI). Tier 2 specialist alongside gcp-architect: owns agentic application design AND agent evaluation execution. Covers ADK, Agent Runtime (formerly Agent Engine), the MCP/A2A/AP2 protocol stack, multi-agent topologies, grounding/RAG, memory, human-in-the-loop, and Gemini model selection. GCP is always the target deployment platform. Use when the user mentions: agent architecture, agent development kit, agent to agent, agent payments, MCP tools for agent, RAG on GCP, orchestrator agent, gemini agent." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" + "googlecloud-plugin/tier": "2" + "googlecloud-plugin/gate": "gcp-design" + "googlecloud-plugin/owns-eval": "true" + "googlecloud-plugin/triggers": "agent architecture, agentic system, multi-agent, ADK, agent development kit, agent engine, agent runtime, A2A, agent to agent, AP2, agent payments, MCP tools for agent, agent evaluation, agent eval, grounding, RAG on GCP, orchestrator agent, gemini agent, GEAP" + "googlecloud-plugin/required-scopes": "aiplatform.reasoningEngines.create, aiplatform.reasoningEngines.get, aiplatform.reasoningEngines.query, aiplatform.endpoints.predict, aiplatform.evaluationTasks.create" + "googlecloud-plugin/mcp-servers": "google-vertex-ai" --- # Agent Architect diff --git a/skills/bigquery/SKILL.md b/skills/bigquery/SKILL.md index c4b0681..5af1e16 100644 --- a/skills/bigquery/SKILL.md +++ b/skills/bigquery/SKILL.md @@ -1,28 +1,12 @@ --- name: bigquery -description: "Query and manage data in Google BigQuery. Covers dataset and table management, IAM, cost-safe querying (dry-run first), partitioning, clustering, and bq CLI patterns. Always estimates cost before executing queries — BigQuery bills by bytes processed." -version: "0.1" -triggers: - - "BigQuery" - - "bigquery query" - - "bq" - - "dataset" - - "analytics on GCP" - - "BigQuery IAM" - - "BigQuery cost" - - "bigquery partition" - - "bigquery schema" - - "sql on GCP" -required_scopes: - - bigquery.datasets.create - - bigquery.datasets.get - - bigquery.jobs.create - - bigquery.jobs.get - - bigquery.tables.create - - bigquery.tables.getData - - bigquery.tables.get -mcp_servers: - - google-bigquery +description: "Query and manage data in Google BigQuery. Covers dataset and table management, IAM, cost-safe querying (dry-run first), partitioning, clustering, and bq CLI patterns. Always estimates cost before executing queries — BigQuery bills by bytes processed. Use when the user mentions: bigquery query, analytics on GCP, BigQuery IAM, BigQuery cost, bigquery partition, bigquery schema, sql on GCP." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "BigQuery, bigquery query, bq, dataset, analytics on GCP, BigQuery IAM, BigQuery cost, bigquery partition, bigquery schema, sql on GCP" + "googlecloud-plugin/required-scopes": "bigquery.datasets.create, bigquery.datasets.get, bigquery.jobs.create, bigquery.jobs.get, bigquery.tables.create, bigquery.tables.getData, bigquery.tables.get" + "googlecloud-plugin/mcp-servers": "google-bigquery" --- # BigQuery diff --git a/skills/cloud-run/SKILL.md b/skills/cloud-run/SKILL.md index 9a05e66..7815c76 100644 --- a/skills/cloud-run/SKILL.md +++ b/skills/cloud-run/SKILL.md @@ -1,26 +1,12 @@ --- name: cloud-run -description: "Deploy and manage containerized workloads on Cloud Run. Covers service creation, traffic splitting, IAM, VPC connectivity, secrets integration, auto-scaling, and gcloud CLI patterns. Warns before billable deployments." -version: "0.1" -triggers: - - "deploy to cloud run" - - "create cloud run service" - - "cloud run" - - "serverless container" - - "scale cloud run" - - "cloud run IAM" - - "cloud run VPC" - - "cloud run secrets" - - "gcloud run" -required_scopes: - - run.services.create - - run.services.delete - - run.services.get - - run.services.list - - run.services.update - - run.routes.invoke -mcp_servers: - - google-cloud-run +description: "Deploy and manage containerized workloads on Cloud Run. Covers service creation, traffic splitting, IAM, VPC connectivity, secrets integration, auto-scaling, and gcloud CLI patterns. Warns before billable deployments. Use when the user mentions: deploy to cloud run, create cloud run service, serverless container, scale cloud run, cloud run IAM, cloud run VPC, cloud run secrets, gcloud run." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "deploy to cloud run, create cloud run service, cloud run, serverless container, scale cloud run, cloud run IAM, cloud run VPC, cloud run secrets, gcloud run" + "googlecloud-plugin/required-scopes": "run.services.create, run.services.delete, run.services.get, run.services.list, run.services.update, run.routes.invoke" + "googlecloud-plugin/mcp-servers": "google-cloud-run" --- # Cloud Run diff --git a/skills/cloud-storage/SKILL.md b/skills/cloud-storage/SKILL.md index 6a754a3..8b1cb1a 100644 --- a/skills/cloud-storage/SKILL.md +++ b/skills/cloud-storage/SKILL.md @@ -1,28 +1,12 @@ --- name: cloud-storage -description: "Manage object storage on Google Cloud Storage. Covers bucket creation, IAM, uniform access control, lifecycle policies, signed URLs, and gsutil/gcloud CLI patterns. Enforces private-by-default: never creates public buckets without explicit intent and gcp-security sign-off." -version: "0.1" -triggers: - - "cloud storage" - - "GCS" - - "bucket" - - "gsutil" - - "object storage" - - "upload to GCP" - - "storage IAM" - - "signed URL" - - "lifecycle policy" - - "storage class" -required_scopes: - - storage.buckets.create - - storage.buckets.get - - storage.buckets.getIamPolicy - - storage.buckets.setIamPolicy - - storage.objects.create - - storage.objects.get - - storage.objects.list -mcp_servers: - - google-storage +description: "Manage object storage on Google Cloud Storage. Covers bucket creation, IAM, uniform access control, lifecycle policies, signed URLs, and gsutil/gcloud CLI patterns. Enforces private-by-default: never creates public buckets without explicit intent and gcp-security sign-off. Use when the user mentions: GCS, upload to GCP, storage IAM, lifecycle policy, storage class." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "cloud storage, GCS, bucket, gsutil, object storage, upload to GCP, storage IAM, signed URL, lifecycle policy, storage class" + "googlecloud-plugin/required-scopes": "storage.buckets.create, storage.buckets.get, storage.buckets.getIamPolicy, storage.buckets.setIamPolicy, storage.objects.create, storage.objects.get, storage.objects.list" + "googlecloud-plugin/mcp-servers": "google-storage" --- # Cloud Storage diff --git a/skills/gcp-architect/SKILL.md b/skills/gcp-architect/SKILL.md index f53bfae..ca25487 100644 --- a/skills/gcp-architect/SKILL.md +++ b/skills/gcp-architect/SKILL.md @@ -1,23 +1,14 @@ --- name: gcp-architect -description: "100% Google Cloud focused architect. Receives the GCP-scoped portion of the solution-designer HLD. Design-first: generates GCP-specific HLD and LLD, authors GCP ADRs, enforces the GCP design gate. Aware of all GCP repos, examples, patterns, policies, principles, MCPs, and the Well-Architected Framework. Does not make vendor-selection decisions — those belong to solution-designer." -version: "0.1" -persona: true -tier: 2 -gate: gcp-design -triggers: - - "GCP design" - - "architect on GCP" - - "GCP HLD" - - "GCP architecture" - - "design on google cloud" - - "ADR for GCP" - - "GCP LLD" - - "google cloud architecture" - - "GCP patterns" - - "well-architected GCP" -required_scopes: [] -mcp_servers: [] +description: "100% Google Cloud focused architect. Receives the GCP-scoped portion of the solution-designer HLD. Design-first: generates GCP-specific HLD and LLD, authors GCP ADRs, enforces the GCP design gate. Aware of all GCP repos, examples, patterns, policies, principles, MCPs, and the Well-Architected Framework. Does not make vendor-selection decisions — those belong to solution-designer. Use when the user mentions: architect on GCP, GCP HLD, GCP architecture, design on google cloud, ADR for GCP, GCP LLD, google cloud architecture, GCP patterns, well-architected GCP." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" + "googlecloud-plugin/tier": "2" + "googlecloud-plugin/gate": "gcp-design" + "googlecloud-plugin/triggers": "GCP design, architect on GCP, GCP HLD, GCP architecture, design on google cloud, ADR for GCP, GCP LLD, google cloud architecture, GCP patterns, well-architected GCP" + "googlecloud-plugin/required-scopes": "" --- # GCP Architect diff --git a/skills/gcp-ops/SKILL.md b/skills/gcp-ops/SKILL.md index bb35e43..4e391bd 100644 --- a/skills/gcp-ops/SKILL.md +++ b/skills/gcp-ops/SKILL.md @@ -1,31 +1,14 @@ --- name: gcp-ops -description: "GCP Operations and SRE persona. Defines SLOs, alerting policy, runbooks, and incident response. Knows what healthy looks like in production for every GCP service. Validates that observability exists before any production release. Owns the operational readiness gate." -version: "0.1" -persona: true -tier: 3 -gate: operational-readiness -triggers: - - "SLO" - - "SLA" - - "alerting" - - "monitoring setup" - - "runbook" - - "incident response" - - "observability" - - "dashboard" - - "on-call" - - "production readiness" - - "operational readiness" - - "cloud logging setup" - - "cloud monitoring setup" - - "error budget" -required_scopes: - - monitoring.alertPolicies.create - - monitoring.dashboards.create - - logging.sinks.create - - logging.logMetrics.create -mcp_servers: [] +description: "GCP Operations and SRE persona. Defines SLOs, alerting policy, runbooks, and incident response. Knows what healthy looks like in production for every GCP service. Validates that observability exists before any production release. Owns the operational readiness gate. Use when the user mentions: SLA, monitoring setup, dashboard, on-call, production readiness, cloud logging setup, cloud monitoring setup, error budget." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" + "googlecloud-plugin/tier": "3" + "googlecloud-plugin/gate": "operational-readiness" + "googlecloud-plugin/triggers": "SLO, SLA, alerting, monitoring setup, runbook, incident response, observability, dashboard, on-call, production readiness, operational readiness, cloud logging setup, cloud monitoring setup, error budget" + "googlecloud-plugin/required-scopes": "monitoring.alertPolicies.create, monitoring.dashboards.create, logging.sinks.create, logging.logMetrics.create" --- # GCP Operations / SRE diff --git a/skills/gcp-qa/SKILL.md b/skills/gcp-qa/SKILL.md index 80f6971..71c4ddb 100644 --- a/skills/gcp-qa/SKILL.md +++ b/skills/gcp-qa/SKILL.md @@ -1,25 +1,14 @@ --- name: gcp-qa -description: "GCP QA and review persona. Critiques and evaluates designs, implementations, and release candidates against acceptance criteria. Owns linting, freshness checks, link validation, and smoke tests. Raises blockers before release. Quality gate authority for both the plugin itself and solutions built with it." -version: "0.1" -persona: true -tier: 3 -gate: quality -triggers: - - "QA review" - - "quality gate" - - "validate implementation" - - "lint" - - "smoke test" - - "acceptance criteria" - - "release check" - - "skill validation" - - "check links" - - "freshness" - - "is this ready to ship" - - "pre-release review" -required_scopes: [] -mcp_servers: [] +description: "GCP QA and review persona. Critiques and evaluates designs, implementations, and release candidates against acceptance criteria. Owns linting, freshness checks, link validation, and smoke tests. Raises blockers before release. Quality gate authority for both the plugin itself and solutions built with it. Use when the user mentions: QA review, validate implementation, release check, skill validation, check links, is this ready to ship, pre-release review." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" + "googlecloud-plugin/tier": "3" + "googlecloud-plugin/gate": "quality" + "googlecloud-plugin/triggers": "QA review, quality gate, validate implementation, lint, smoke test, acceptance criteria, release check, skill validation, check links, freshness, is this ready to ship, pre-release review" + "googlecloud-plugin/required-scopes": "" --- # GCP QA / Review diff --git a/skills/gcp-security/SKILL.md b/skills/gcp-security/SKILL.md index 6e7c4cb..ace4268 100644 --- a/skills/gcp-security/SKILL.md +++ b/skills/gcp-security/SKILL.md @@ -1,26 +1,14 @@ --- name: gcp-security -description: "GCP security enforcer. Reviews designs and implementations against the GCP Well-Architected Framework security pillar, OWASP Top 10, and GCP-specific risk patterns. Enforces least-privilege IAM, secrets management, no hardcoded credentials, and security-by-design. Also reviews solution-designer output for cross-cloud security gaps. Must clear the security gate before any implementation begins." -version: "0.1" -persona: true -tier: 3 -gate: security -triggers: - - "security review" - - "IAM review" - - "least privilege" - - "secrets management" - - "GCP security" - - "security gate" - - "vulnerability" - - "hardcoded credentials" - - "public bucket" - - "default service account" - - "security posture" - - "compliance GCP" - - "OWASP GCP" -required_scopes: [] -mcp_servers: [] +description: "GCP security enforcer. Reviews designs and implementations against the GCP Well-Architected Framework security pillar, OWASP Top 10, and GCP-specific risk patterns. Enforces least-privilege IAM, secrets management, no hardcoded credentials, and security-by-design. Also reviews solution-designer output for cross-cloud security gaps. Must clear the security gate before any implementation begins. Use when the user mentions: security review, IAM review, least privilege, vulnerability, public bucket, default service account, security posture, compliance GCP, OWASP GCP." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" + "googlecloud-plugin/tier": "3" + "googlecloud-plugin/gate": "security" + "googlecloud-plugin/triggers": "security review, IAM review, least privilege, secrets management, GCP security, security gate, vulnerability, hardcoded credentials, public bucket, default service account, security posture, compliance GCP, OWASP GCP" + "googlecloud-plugin/required-scopes": "" --- # GCP Security diff --git a/skills/gke/SKILL.md b/skills/gke/SKILL.md index 6b5b4ae..f65137d 100644 --- a/skills/gke/SKILL.md +++ b/skills/gke/SKILL.md @@ -1,27 +1,12 @@ --- name: gke -description: "Deploy and manage Kubernetes workloads on Google Kubernetes Engine (GKE). Covers Autopilot and Standard modes, Workload Identity, node pool management, networking, security, and gcloud/kubectl CLI patterns. Warns before cluster creation (billable)." -version: "0.1" -triggers: - - "GKE" - - "kubernetes on GCP" - - "google kubernetes engine" - - "create GKE cluster" - - "workload identity" - - "node pool" - - "GKE autopilot" - - "kubectl GCP" - - "GKE networking" - - "GKE security" -required_scopes: - - container.clusters.create - - container.clusters.delete - - container.clusters.get - - container.clusters.list - - container.clusters.update - - container.nodes.list -mcp_servers: - - google-gke +description: "Deploy and manage Kubernetes workloads on Google Kubernetes Engine (GKE). Covers Autopilot and Standard modes, Workload Identity, node pool management, networking, security, and gcloud/kubectl CLI patterns. Warns before cluster creation (billable). Use when the user mentions: kubernetes on GCP, create GKE cluster, GKE autopilot, kubectl GCP, GKE networking, GKE security." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "GKE, kubernetes on GCP, google kubernetes engine, create GKE cluster, workload identity, node pool, GKE autopilot, kubectl GCP, GKE networking, GKE security" + "googlecloud-plugin/required-scopes": "container.clusters.create, container.clusters.delete, container.clusters.get, container.clusters.list, container.clusters.update, container.nodes.list" + "googlecloud-plugin/mcp-servers": "google-gke" --- # GKE — Google Kubernetes Engine diff --git a/skills/iam/SKILL.md b/skills/iam/SKILL.md index d9583fc..c1f9802 100644 --- a/skills/iam/SKILL.md +++ b/skills/iam/SKILL.md @@ -1,26 +1,12 @@ --- name: iam -description: "Google Cloud IAM — identity, access management, service accounts, and policy authoring. Enforces least-privilege by default. Covers roles, conditions, service account patterns, Workload Identity Federation, and gcloud CLI. Never grants owner or editor roles." -version: "0.1" -triggers: - - "IAM" - - "iam policy" - - "grant access" - - "service account" - - "roles" - - "least privilege" - - "IAM binding" - - "who has access" - - "iam conditions" - - "workload identity federation" - - "gcloud iam" -required_scopes: - - iam.roles.get - - iam.roles.list - - resourcemanager.projects.getIamPolicy - - resourcemanager.projects.setIamPolicy -mcp_servers: - - google-iam +description: "Google Cloud IAM — identity, access management, service accounts, and policy authoring. Enforces least-privilege by default. Covers roles, conditions, service account patterns, Workload Identity Federation, and gcloud CLI. Never grants owner or editor roles. Use when the user mentions: iam policy, grant access, least privilege, IAM binding, who has access, iam conditions, gcloud iam." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "IAM, iam policy, grant access, service account, roles, least privilege, IAM binding, who has access, iam conditions, workload identity federation, gcloud iam" + "googlecloud-plugin/required-scopes": "iam.roles.get, iam.roles.list, resourcemanager.projects.getIamPolicy, resourcemanager.projects.setIamPolicy" + "googlecloud-plugin/mcp-servers": "google-iam" --- # IAM — Identity and Access Management diff --git a/skills/logging-monitoring/SKILL.md b/skills/logging-monitoring/SKILL.md index 571d274..3a53d3e 100644 --- a/skills/logging-monitoring/SKILL.md +++ b/skills/logging-monitoring/SKILL.md @@ -1,30 +1,12 @@ --- name: logging-monitoring -description: "Configure observability on GCP using Cloud Logging, Cloud Monitoring, Cloud Trace, and Cloud Profiler. Covers log sinks, log-based metrics, alerting policies, dashboards, and uptime checks. Owns the operational readiness evidence for gcp-ops." -version: "0.1" -triggers: - - "cloud logging" - - "cloud monitoring" - - "logging setup" - - "monitoring setup" - - "alerting policy" - - "log sink" - - "dashboard GCP" - - "uptime check" - - "cloud trace" - - "error reporting" - - "log-based metric" - - "SLO monitoring" -required_scopes: - - logging.sinks.create - - logging.sinks.get - - logging.logMetrics.create - - monitoring.alertPolicies.create - - monitoring.dashboards.create - - monitoring.uptimeCheckConfigs.create -mcp_servers: - - google-logging - - google-monitoring +description: "Configure observability on GCP using Cloud Logging, Cloud Monitoring, Cloud Trace, and Cloud Profiler. Covers log sinks, log-based metrics, alerting policies, dashboards, and uptime checks. Owns the operational readiness evidence for gcp-ops. Use when the user mentions: logging setup, monitoring setup, alerting policy, dashboard GCP, error reporting, SLO monitoring." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "cloud logging, cloud monitoring, logging setup, monitoring setup, alerting policy, log sink, dashboard GCP, uptime check, cloud trace, error reporting, log-based metric, SLO monitoring" + "googlecloud-plugin/required-scopes": "logging.sinks.create, logging.sinks.get, logging.logMetrics.create, monitoring.alertPolicies.create, monitoring.dashboards.create, monitoring.uptimeCheckConfigs.create" + "googlecloud-plugin/mcp-servers": "google-logging, google-monitoring" --- # Logging + Monitoring diff --git a/skills/mcp-servers/SKILL.md b/skills/mcp-servers/SKILL.md index c711b59..7dadd77 100644 --- a/skills/mcp-servers/SKILL.md +++ b/skills/mcp-servers/SKILL.md @@ -1,27 +1,12 @@ --- name: mcp-servers -description: "Configure, install, and maintain Google-managed and self-hosted MCP servers for GCP. Covers setup, auth (ADC and SA key), capability map, troubleshooting, and version tracking. Every MCP entry includes a gcloud CLI fallback." -version: "0.1" -triggers: - - "MCP server" - - "MCP setup" - - "google MCP" - - "model context protocol GCP" - - "MCP install" - - "MCP auth" - - "genai toolbox" - - "MCP tools GCP" - - "configure MCP" -required_scopes: [] -mcp_servers: - - google-cloud-run - - google-bigquery - - google-gke - - google-storage - - google-vertex-ai - - google-iam - - google-logging - - google-monitoring +description: "Configure, install, and maintain Google-managed and self-hosted MCP servers for GCP. Covers setup, auth (ADC and SA key), capability map, troubleshooting, and version tracking. Every MCP entry includes a gcloud CLI fallback. Use when the user mentions: MCP setup, google MCP, model context protocol GCP, MCP install, MCP auth, genai toolbox, MCP tools GCP, configure MCP." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "MCP server, MCP setup, google MCP, model context protocol GCP, MCP install, MCP auth, genai toolbox, MCP tools GCP, configure MCP" + "googlecloud-plugin/required-scopes": "" + "googlecloud-plugin/mcp-servers": "google-cloud-run, google-bigquery, google-gke, google-storage, google-vertex-ai, google-iam, google-logging, google-monitoring" --- # MCP Servers — Google Cloud diff --git a/skills/networking/SKILL.md b/skills/networking/SKILL.md index 6f905d5..4fa025a 100644 --- a/skills/networking/SKILL.md +++ b/skills/networking/SKILL.md @@ -1,28 +1,11 @@ --- name: networking -description: "Design and manage GCP networking: VPC, subnets, firewall rules, Cloud Load Balancing, Cloud Armor, Private Google Access, and Shared VPC. Deny-by-default firewall posture. Warns before creating external load balancers (billable)." -version: "0.1" -triggers: - - "VPC" - - "firewall" - - "networking GCP" - - "cloud load balancer" - - "cloud armor" - - "private google access" - - "shared VPC" - - "subnet" - - "VPN GCP" - - "cloud NAT" - - "network design GCP" -required_scopes: - - compute.firewalls.create - - compute.firewalls.get - - compute.firewalls.list - - compute.networks.create - - compute.networks.get - - compute.subnetworks.create - - compute.subnetworks.get -mcp_servers: [] +description: "Design and manage GCP networking: VPC, subnets, firewall rules, Cloud Load Balancing, Cloud Armor, Private Google Access, and Shared VPC. Deny-by-default firewall posture. Warns before creating external load balancers (billable). Use when the user mentions: networking GCP, cloud load balancer, VPN GCP, cloud NAT, network design GCP." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "VPC, firewall, networking GCP, cloud load balancer, cloud armor, private google access, shared VPC, subnet, VPN GCP, cloud NAT, network design GCP" + "googlecloud-plugin/required-scopes": "compute.firewalls.create, compute.firewalls.get, compute.firewalls.list, compute.networks.create, compute.networks.get, compute.subnetworks.create, compute.subnetworks.get" --- # Networking diff --git a/skills/solution-designer/SKILL.md b/skills/solution-designer/SKILL.md index e8f7387..f1cde9d 100644 --- a/skills/solution-designer/SKILL.md +++ b/skills/solution-designer/SKILL.md @@ -1,24 +1,14 @@ --- name: solution-designer -description: "Vendor-agnostic solution authority. Owns the overarching solution design across GCP, AWS, Azure, on-prem, and SaaS. Produces the master HLD that scopes each cloud domain. Objective: not GCP-biased — will recommend another cloud when warranted. Researches, proves, and validates that the proposed solution works across all mentioned vendors." -version: "0.1" -persona: true -tier: 1 -gate: solution -triggers: - - "design a solution" - - "what should we build" - - "multi-cloud" - - "which cloud for" - - "solution architecture" - - "overarching design" - - "vendor recommendation" - - "cross-cloud" - - "hybrid cloud" - - "compare GCP vs AWS" - - "compare GCP vs Azure" -required_scopes: [] -mcp_servers: [] +description: "Vendor-agnostic solution authority. Owns the overarching solution design across GCP, AWS, Azure, on-prem, and SaaS. Produces the master HLD that scopes each cloud domain. Objective: not GCP-biased — will recommend another cloud when warranted. Researches, proves, and validates that the proposed solution works across all mentioned vendors. Use when the user mentions: design a solution, what should we build, multi-cloud, which cloud for, solution architecture, overarching design, vendor recommendation, cross-cloud, hybrid cloud, compare GCP vs AWS, compare GCP vs Azure." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" + "googlecloud-plugin/tier": "1" + "googlecloud-plugin/gate": "solution" + "googlecloud-plugin/triggers": "design a solution, what should we build, multi-cloud, which cloud for, solution architecture, overarching design, vendor recommendation, cross-cloud, hybrid cloud, compare GCP vs AWS, compare GCP vs Azure" + "googlecloud-plugin/required-scopes": "" --- # Solution Designer diff --git a/skills/terraform-gcp/SKILL.md b/skills/terraform-gcp/SKILL.md index f842731..f68d37b 100644 --- a/skills/terraform-gcp/SKILL.md +++ b/skills/terraform-gcp/SKILL.md @@ -1,23 +1,11 @@ --- name: terraform-gcp -description: "Terraform patterns for Google Cloud using the official google and google-beta providers and Cloud Foundation Toolkit modules. Covers project structure, state management, IAM, and CFT blueprint usage. Warns before terraform apply (billable and potentially destructive)." -version: "0.1" -triggers: - - "terraform GCP" - - "terraform google" - - "IaC GCP" - - "cloud foundation toolkit" - - "CFT" - - "terraform plan GCP" - - "terraform apply GCP" - - "GCP modules" - - "landing zone terraform" - - "terraform google provider" -required_scopes: - - resourcemanager.projects.get - - resourcemanager.projects.setIamPolicy - - serviceusage.services.enable -mcp_servers: [] +description: "Terraform patterns for Google Cloud using the official google and google-beta providers and Cloud Foundation Toolkit modules. Covers project structure, state management, IAM, and CFT blueprint usage. Warns before terraform apply (billable and potentially destructive). Use when the user mentions: terraform GCP, terraform google, IaC GCP, terraform plan GCP, terraform apply GCP, GCP modules, landing zone terraform, terraform google provider." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "terraform GCP, terraform google, IaC GCP, cloud foundation toolkit, CFT, terraform plan GCP, terraform apply GCP, GCP modules, landing zone terraform, terraform google provider" + "googlecloud-plugin/required-scopes": "resourcemanager.projects.get, resourcemanager.projects.setIamPolicy, serviceusage.services.enable" --- # Terraform — GCP diff --git a/skills/vertex-ai/SKILL.md b/skills/vertex-ai/SKILL.md index 9281e84..c873cb5 100644 --- a/skills/vertex-ai/SKILL.md +++ b/skills/vertex-ai/SKILL.md @@ -1,29 +1,12 @@ --- name: vertex-ai -description: "Build and deploy ML models and generative AI applications on Vertex AI. Covers Model Garden, Gemini API, custom training, endpoint deployment, Agent Builder, and IAM. Warns before deploying endpoints (billable). Integrates with Vertex AI MCP server." -version: "0.1" -triggers: - - "vertex AI" - - "Gemini on GCP" - - "model garden" - - "deploy ML model" - - "vertex endpoint" - - "generative AI GCP" - - "agent builder" - - "vertex training" - - "PaLM" - - "Gemini API" - - "vertex AI pipeline" -required_scopes: - - aiplatform.endpoints.create - - aiplatform.endpoints.get - - aiplatform.endpoints.predict - - aiplatform.models.get - - aiplatform.models.list - - aiplatform.trainingPipelines.create - - aiplatform.trainingPipelines.get -mcp_servers: - - google-vertex-ai +description: "Build and deploy ML models and generative AI applications on Vertex AI. Covers Model Garden, Gemini API, custom training, endpoint deployment, Agent Builder, and IAM. Warns before deploying endpoints (billable). Integrates with Vertex AI MCP server. Use when the user mentions: Gemini on GCP, vertex endpoint, generative AI GCP, vertex training, PaLM, vertex AI pipeline." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "vertex AI, Gemini on GCP, model garden, deploy ML model, vertex endpoint, generative AI GCP, agent builder, vertex training, PaLM, Gemini API, vertex AI pipeline" + "googlecloud-plugin/required-scopes": "aiplatform.endpoints.create, aiplatform.endpoints.get, aiplatform.endpoints.predict, aiplatform.models.get, aiplatform.models.list, aiplatform.trainingPipelines.create, aiplatform.trainingPipelines.get" + "googlecloud-plugin/mcp-servers": "google-vertex-ai" --- # Vertex AI diff --git a/skills/well-architected/SKILL.md b/skills/well-architected/SKILL.md index 47e029d..9f653c6 100644 --- a/skills/well-architected/SKILL.md +++ b/skills/well-architected/SKILL.md @@ -1,19 +1,11 @@ --- name: well-architected -description: "Google Cloud Well-Architected Framework reference. Six pillars: operational excellence, security, reliability, cost optimization, performance, and sustainability. Referenced by gcp-architect and gcp-security for every design review." -version: "0.1" -triggers: - - "well-architected" - - "GCP framework" - - "architecture pillars" - - "reliability GCP" - - "cost optimization GCP" - - "performance GCP" - - "sustainability GCP" - - "operational excellence" - - "WAF review" -required_scopes: [] -mcp_servers: [] +description: "Google Cloud Well-Architected Framework reference. Six pillars: operational excellence, security, reliability, cost optimization, performance, and sustainability. Referenced by gcp-architect and gcp-security for every design review. Use when the user mentions: GCP framework, architecture pillars, reliability GCP, cost optimization GCP, performance GCP, sustainability GCP, WAF review." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "well-architected, GCP framework, architecture pillars, reliability GCP, cost optimization GCP, performance GCP, sustainability GCP, operational excellence, WAF review" + "googlecloud-plugin/required-scopes": "" --- # GCP Well-Architected Framework diff --git a/tests/skill-smoke-tests/test_skill_contract.py b/tests/skill-smoke-tests/test_skill_contract.py index 8957e5e..def2dba 100644 --- a/tests/skill-smoke-tests/test_skill_contract.py +++ b/tests/skill-smoke-tests/test_skill_contract.py @@ -1,13 +1,36 @@ -"""Smoke tests — every SKILL.md must conform to the plugin contract. +"""Smoke tests — the plugin must conform to Agent Plugins 1.0.0 and Agent Skills. + +Specs under test: + https://agent-plugins.org/specification + https://agentskills.io/specification Run: make test (or: python -m pytest tests/skill-smoke-tests/ -v) """ +import json +import re import pytest import yaml from pathlib import Path -SKILLS_DIR = Path(__file__).parent.parent.parent / "skills" -REQUIRED_FIELDS = ["name", "description", "version", "triggers", "required_scopes"] +ROOT = Path(__file__).parent.parent.parent +SKILLS_DIR = ROOT / "skills" + +SPEC_VERSION = "1.0.0" +PLUGIN_SCHEMA = f"https://agent-plugins.org/schemas/{SPEC_VERSION}/plugin.schema.json" +MCP_SCHEMA = f"https://agent-plugins.org/schemas/{SPEC_VERSION}/mcp.schema.json" + +# Closed field sets — anything outside these makes the artefact non-conformant. +ALLOWED_SKILL_FIELDS = { + "name", "description", "license", "allowed-tools", "metadata", "compatibility", +} +ALLOWED_PLUGIN_FIELDS = { + "$schema", "name", "version", "description", "author", + "homepage", "repository", "license", "keywords", "extensions", +} +NS = "googlecloud-plugin/" +REQUIRED_METADATA = {f"{NS}version", f"{NS}triggers", f"{NS}required-scopes"} +MAX_DESCRIPTION = 1024 +EXPANDED = {"${PLUGIN_ROOT}", "${PLUGIN_DATA}"} def skill_files() -> list[Path]: @@ -24,16 +47,27 @@ def parse_frontmatter(path: Path) -> dict: return fm +# ─── Agent Skills conformance ──────────────────────────────────────────────── + @pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) def test_frontmatter_parses(skill_file: Path) -> None: - fm = parse_frontmatter(skill_file) - assert fm, f"{skill_file}: Frontmatter is empty" + assert parse_frontmatter(skill_file), f"{skill_file}: Frontmatter is empty" + + +@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) +def test_no_non_conformant_fields(skill_file: Path) -> None: + """A skill carrying extra top-level keys MUST be skipped by a conformant client.""" + extra = sorted(set(parse_frontmatter(skill_file)) - ALLOWED_SKILL_FIELDS) + assert not extra, ( + f"{skill_file}: non-conformant frontmatter fields {extra} — " + f"Agent Skills allows only {sorted(ALLOWED_SKILL_FIELDS)}" + ) @pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) def test_required_fields_present(skill_file: Path) -> None: fm = parse_frontmatter(skill_file) - missing = [f for f in REQUIRED_FIELDS if f not in fm] + missing = [f for f in ("name", "description") if f not in fm] assert not missing, f"{skill_file}: Missing required fields: {missing}" @@ -47,43 +81,130 @@ def test_name_matches_directory(skill_file: Path) -> None: @pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) -def test_triggers_non_empty(skill_file: Path) -> None: - fm = parse_frontmatter(skill_file) - triggers = fm.get("triggers", []) - assert isinstance(triggers, list) and len(triggers) > 0, ( - f"{skill_file}: 'triggers' must be a non-empty list" - ) +def test_name_format(skill_file: Path) -> None: + name = parse_frontmatter(skill_file)["name"] + assert len(name) <= 64, f"{skill_file}: name exceeds 64 chars" + assert name == name.lower(), f"{skill_file}: name must be lowercase" + assert not name.startswith("-") and not name.endswith("-") + assert "--" not in name, f"{skill_file}: consecutive hyphens in name" + assert all(c.isalnum() or c == "-" for c in name) @pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) -def test_required_scopes_is_list(skill_file: Path) -> None: - fm = parse_frontmatter(skill_file) - scopes = fm.get("required_scopes") - assert isinstance(scopes, list), ( - f"{skill_file}: 'required_scopes' must be a list (can be empty [])" +def test_description_within_limit(skill_file: Path) -> None: + description = parse_frontmatter(skill_file)["description"] + assert description.strip(), f"{skill_file}: description must be non-empty" + assert len(description) <= MAX_DESCRIPTION, ( + f"{skill_file}: description is {len(description)} chars, limit {MAX_DESCRIPTION}" ) +# ─── Plugin contract, now carried under metadata ───────────────────────────── + +@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) +def test_metadata_is_string_map(skill_file: Path) -> None: + """Agent Skills defines metadata as a map from string keys to string values.""" + metadata = parse_frontmatter(skill_file).get("metadata") + assert isinstance(metadata, dict), f"{skill_file}: 'metadata' must be a mapping" + for key, value in metadata.items(): + assert isinstance(key, str), f"{skill_file}: metadata key {key!r} must be a string" + assert isinstance(value, str), ( + f"{skill_file}: metadata['{key}'] must be a string, got {type(value).__name__}" + ) + + +@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) +def test_metadata_namespaced(skill_file: Path) -> None: + metadata = parse_frontmatter(skill_file)["metadata"] + unnamespaced = sorted(k for k in metadata if not k.startswith(NS)) + assert not unnamespaced, f"{skill_file}: metadata keys not namespaced: {unnamespaced}" + + +@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) +def test_required_metadata_present(skill_file: Path) -> None: + metadata = parse_frontmatter(skill_file)["metadata"] + missing = sorted(REQUIRED_METADATA - set(metadata)) + assert not missing, f"{skill_file}: missing metadata keys {missing}" + + +@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) +def test_triggers_non_empty(skill_file: Path) -> None: + triggers = parse_frontmatter(skill_file)["metadata"][f"{NS}triggers"] + assert triggers.strip(), f"{skill_file}: triggers must be non-empty" + + @pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) def test_references_directory_exists(skill_file: Path) -> None: refs = skill_file.parent / "references" - assert refs.is_dir(), f"{skill_file.parent.name}: Missing references/ directory" + assert refs.is_dir(), f"{skill_file.parent}: Missing references/ directory" -@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) -def test_version_is_string_or_number(skill_file: Path) -> None: - fm = parse_frontmatter(skill_file) - version = fm.get("version") - assert version is not None, f"{skill_file}: 'version' is required" - assert isinstance(version, (str, int, float)), ( - f"{skill_file}: 'version' must be a string or number" +# ─── Agent Plugins manifest conformance ────────────────────────────────────── + +def test_plugin_manifest_declares_schema() -> None: + manifest = json.loads((ROOT / "plugin.json").read_text()) + assert manifest.get("$schema") == PLUGIN_SCHEMA, ( + "plugin.json must declare the Agent Plugins manifest schema — it is a " + "required field on a closed schema, so omitting it is a FATAL violation" ) -@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) -def test_description_non_empty(skill_file: Path) -> None: - fm = parse_frontmatter(skill_file) - desc = fm.get("description", "").strip() - assert len(desc) > 20, ( - f"{skill_file}: 'description' must be a meaningful string (>20 chars)" +def test_plugin_manifest_fields_permitted() -> None: + manifest = json.loads((ROOT / "plugin.json").read_text()) + extra = sorted(set(manifest) - ALLOWED_PLUGIN_FIELDS) + assert not extra, f"plugin.json has non-permitted top-level fields {extra}" + + +def test_plugin_name_pattern() -> None: + name = json.loads((ROOT / "plugin.json").read_text())["name"] + assert re.match(r"^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$", name) + assert 1 <= len(name) <= 64 + + +def test_mcp_config_conforms() -> None: + config = json.loads((ROOT / "mcp.json").read_text()) + assert config.get("$schema") == MCP_SCHEMA + assert sorted(config) == ["$schema", "mcpServers"], ( + "mcp.json permits only '$schema' and 'mcpServers' at the top level" + ) + assert config["mcpServers"], "mcp.json declares no servers" + + +def test_mcp_servers_declare_transport() -> None: + """The schema discriminates transports on 'type' — a server without it is skipped.""" + servers = json.loads((ROOT / "mcp.json").read_text())["mcpServers"] + for name, cfg in servers.items(): + assert cfg.get("type") in ("stdio", "streamable-http", "sse"), ( + f"mcp.json server '{name}' must declare a supported 'type'" + ) + + +def test_mcp_uses_only_expandable_placeholders() -> None: + """Only ${PLUGIN_ROOT} and ${PLUGIN_DATA} expand; anything else stays literal.""" + servers = json.loads((ROOT / "mcp.json").read_text())["mcpServers"] + for name, cfg in servers.items(): + candidates = list(cfg.get("args") or []) + list((cfg.get("env") or {}).values()) + if isinstance(cfg.get("cwd"), str): + candidates.append(cfg["cwd"]) + for value in candidates: + for found in re.findall(r"\$\{[^}]*\}", str(value)): + assert found in EXPANDED, ( + f"mcp.json server '{name}': placeholder {found} does not expand and " + f"would be passed through literally" + ) + + +def test_mcp_schema_version_matches_plugin() -> None: + plugin = json.loads((ROOT / "plugin.json").read_text())["$schema"] + mcp = json.loads((ROOT / "mcp.json").read_text())["$schema"] + version = lambda s: re.search(r"/schemas/([^/]+)/", s).group(1) + assert version(plugin) == version(mcp), ( + "mcp.json and plugin.json MUST target the same Agent Plugins version" ) + + +def test_skills_discoverable_non_recursively() -> None: + """Clients discover skills only at skills/*/SKILL.md — never deeper.""" + assert skill_files(), "no skills discoverable at skills/*/SKILL.md" + buried = list(SKILLS_DIR.glob("*/*/**/SKILL.md")) + assert not buried, f"SKILL.md below the discovery depth will be ignored: {buried}" From 515e050da74aba8b151b8bcccf06ac42f575cea6 Mon Sep 17 00:00:00 2001 From: JP Date: Fri, 7 Aug 2026 10:30:39 +0100 Subject: [PATCH 2/3] docs: record the Agent Plugins 1.0.0 contract and sharpen skill routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Governance: - ADR-007 records the decision, the alternatives weighed, and the two fatal faults that prompted it. ADR-005 marked superseded — its contract survives, only its location changed. - SPEC.md, shared/conventions.md, plugin.yaml, .claude/CLAUDE.md and the coverage matrix updated to the new frontmatter shape. - README gains a standards-conformance section with commands a reader can run to reproduce the evidence independently. Routing, from cross-model testing: An independent Kimi agent loaded the plugin from a clean sandbox export and routed five requests against it. All 17 skills were discovered and all five routed correctly, but it flagged pairs that could misroute now that routing depends on descriptions alone: gcp-ops/logging-monitoring, iam/gcp-security, vertex-ai/agent-architect, solution-designer/gcp-architect. Its two "concrete regressions" did not survive checking — the keywords it reported missing from cloud-storage and bigquery are present in the description prose, just not in the trailing keyword list. But four other skills did have a trigger recorded only in metadata, where nothing can route on it. So each overlapping pair now states which skill owns the case and names the other, and `make validate` fails if any metadata trigger is absent from its description — keeping that index a derived view rather than a rival source of truth. Also: author URL now points at jpantsjoha.com. The schema permits one author URL, and an owned domain outlives any platform profile. make gate: 212 tests, 101 URLs, all four harnesses installable. --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 2 +- .claude/CLAUDE.md | 30 +++-- .kimi-plugin/plugin.json | 2 +- README.md | 29 ++++- SPEC.md | 70 +++++----- .../decisions/ADR-005-skill-contract.md | 15 ++- .../ADR-007-agent-plugins-conformance.md | 122 ++++++++++++++++++ plugin.json | 2 +- plugin.yaml | 4 + research/coverage-matrix.md | 8 +- scripts/validate_skills.py | 23 ++++ shared/conventions.md | 43 ++++-- skills/gcp-architect/SKILL.md | 2 +- skills/gcp-ops/SKILL.md | 4 +- skills/gcp-security/SKILL.md | 2 +- skills/iam/SKILL.md | 2 +- skills/logging-monitoring/SKILL.md | 2 +- skills/mcp-servers/SKILL.md | 37 +++++- skills/solution-designer/SKILL.md | 2 +- skills/vertex-ai/SKILL.md | 2 +- .../skill-smoke-tests/test_skill_contract.py | 13 ++ 22 files changed, 354 insertions(+), 66 deletions(-) create mode 100644 architecture/decisions/ADR-007-agent-plugins-conformance.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6385c3c..b94c9cb 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -3,7 +3,7 @@ "description": "A full Google Cloud delivery team for coding agents — design-first, security-first, Well-Architected, with agentic (ADK / A2A / AP2 / MCP) coverage.", "owner": { "name": "Jaroslav Pantsjoha", - "url": "https://uk.linkedin.com/in/johas" + "url": "https://jpantsjoha.com" }, "plugins": [ { @@ -13,7 +13,7 @@ "source": "./", "author": { "name": "Jaroslav Pantsjoha", - "url": "https://uk.linkedin.com/in/johas" + "url": "https://jpantsjoha.com" } } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e0b95c4..bc7e35d 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -4,7 +4,7 @@ "version": "0.1.0", "author": { "name": "Jaroslav Pantsjoha", - "url": "https://uk.linkedin.com/in/johas" + "url": "https://jpantsjoha.com" }, "homepage": "https://github.com/jpantsjoha/googlecloud-plugin", "repository": "https://github.com/jpantsjoha/googlecloud-plugin", diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a933dc6..4db0b71 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -32,14 +32,28 @@ Before any skill merges to main: ## Skill Contract -Every `skills//SKILL.md` must have valid YAML frontmatter with: -- `name` — matches directory name exactly -- `description` — >20 chars, meaningful -- `version` — string or number -- `triggers` — non-empty list of routing phrases -- `required_scopes` — list (can be empty `[]` for non-IAM skills) - -Run `make validate` after any SKILL.md edit. +The frontmatter field set is **closed** by the Agent Skills specification. Only +`name`, `description`, `license`, `compatibility`, `metadata` and `allowed-tools` +are permitted — a skill carrying anything else is non-conformant, and Agent +Plugins 1.0.0 requires clients to **skip** it. Never add a top-level field. + +Every `skills//SKILL.md` must have: +- `name` — matches directory name exactly, lowercase, no `--` +- `description` — meaningful, ≤1024 chars, **and carries every routing keyword** +- `license` +- `metadata` — string keys to **string** values, all namespaced + `googlecloud-plugin/…`. Required: `version`, `triggers`, `required-scopes` + (emit `""`, don't omit, when a skill needs no IAM scopes). Personas add + `persona`, `tier`, `gate`. Lists are comma-joined. + +**Routing lives in the description.** No client reads `metadata/triggers` — it is +an index for this repo's own tooling, and `make validate` fails if any trigger is +missing from the description. When two skills overlap, state in each description +which one owns the case and name the other. + +Run `make validate` after any SKILL.md edit, and `make spec` after touching +`plugin.json` or `mcp.json`. See +`architecture/decisions/ADR-007-agent-plugins-conformance.md`. ## Safety Rules (Non-Negotiable) diff --git a/.kimi-plugin/plugin.json b/.kimi-plugin/plugin.json index e866dbe..bc61dcf 100644 --- a/.kimi-plugin/plugin.json +++ b/.kimi-plugin/plugin.json @@ -4,7 +4,7 @@ "description": "A full Google Cloud delivery team for your coding agent: solution designer, GCP architect, agentic-systems architect (ADK / Agent Runtime / MCP / A2A / AP2), plus security, SRE, and QA — wired into a design-first, security-first delivery gate, with eleven service skills and MCP server setup.", "author": { "name": "Jaroslav Pantsjoha", - "url": "https://uk.linkedin.com/in/johas" + "url": "https://jpantsjoha.com" }, "homepage": "https://github.com/jpantsjoha/googlecloud-plugin", "repository": "https://github.com/jpantsjoha/googlecloud-plugin", diff --git a/README.md b/README.md index 9db2aba..f584b34 100644 --- a/README.md +++ b/README.md @@ -146,13 +146,37 @@ The rule underneath every gate: **infer intent, never infer permission.** An AI --- +## Standards conformance + +This plugin conforms to **[Agent Plugins 1.0.0](https://agent-plugins.org/specification)** — the open, vendor-neutral packaging specification co-maintained by Amazon, Cursor, Google, Microsoft, OpenAI and Vercel — and to the **[Agent Skills specification](https://agentskills.io/specification)** it references. + +| Standard | Artefact | Verified by | +|---|---|---| +| Agent Plugins 1.0.0 manifest | `plugin.json` | [`plugin.schema.json`](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json) | +| Agent Plugins 1.0.0 MCP config | `mcp.json` | [`mcp.schema.json`](https://agent-plugins.org/schemas/1.0.0/mcp.schema.json) | +| Agent Skills | `skills/*/SKILL.md` | `skills-ref`, the official reference validator | + +That means any conformant client loads it — no per-harness fork. The Claude Code, Antigravity/Gemini, Codex and Kimi manifests remain in place alongside, so existing installs are unaffected. + +Reproduce the conformance evidence yourself: + +```bash +make spec # Agent Plugins 1.0.0 conformance (plugin.json + mcp.json) + +# independent, third-party checks — nothing in this repo is trusted +uvx --from 'git+https://github.com/agentskills/agentskills.git#subdirectory=skills-ref' \ + skills-ref validate skills/cloud-run +uvx check-jsonschema --schemafile https://agent-plugins.org/schemas/1.0.0/plugin.schema.json plugin.json +``` + ## Validation The plugin validates itself. Every skill conforms to a machine-readable contract; every reference URL is checked live; every source carries a retrieval date and content hash for audit. ```bash -make gate # the 3-minute gate: validate + lint + test -make validate # SKILL.md frontmatter contract +make gate # the gate: spec + validate + manifest + mermaid + lint + test +make spec # Agent Plugins 1.0.0 conformance +make validate # SKILL.md frontmatter contract (Agent Skills + routing invariant) make lint # every reference URL resolves (HTTP 200) make test # skill smoke tests make check # freshness: content-hash drift vs live GCP docs @@ -169,6 +193,7 @@ Created and maintained by **Jaroslav Pantsjoha (JP)** — Technical Director and I built this to make my own Google Cloud work repeatable, then to share it. The model is the easy part; the durable engineering is the harness around it — the skills, rules, MCP servers, gates, and evals. GCP patterns outlast the week's model release, so they are worth encoding once and reusing. This plugin is that baseline aimed squarely at Google Cloud — the GCP companion to my [`join-the-team`](https://github.com/jpantsjoha/ai-native-developer-experience) harness. Part of the **#HarnessEngineering** body of work — the engineering discipline behind the Agentic Enterprise. +- Website: [jpantsjoha.com](https://jpantsjoha.com) - LinkedIn: [uk.linkedin.com/in/johas](https://uk.linkedin.com/in/johas) - Google Developer Expert: [me.developers.google.com/u/jpantsjoha](https://me.developers.google.com/u/jpantsjoha) diff --git a/SPEC.md b/SPEC.md index fdc6f9a..b0e22a4 100644 --- a/SPEC.md +++ b/SPEC.md @@ -268,33 +268,43 @@ Every billable skill includes: **Context:** Agents need to discover and route to skills at runtime. A standard contract ensures consistency. -**Decision:** -Every skill has a `SKILL.md` with YAML frontmatter: +**Decision:** +Every skill has a `SKILL.md` conforming to the [Agent Skills +specification](https://agentskills.io/specification). The frontmatter field set +is closed, so the plugin's own contract lives under `metadata` as namespaced +string values, and routing keywords fold into `description`: + ```yaml --- name: cloud-run -description: "Deploy and manage containerized workloads on Cloud Run" -version: 0.1 -triggers: ["deploy to cloud run", "create cloud run service", "scale cloud run"] -required_scopes: - - run.services.create - - run.services.delete - - run.services.get - - run.services.list - - run.services.update -mcp_servers: [] +description: "Deploy and manage containerized workloads on Cloud Run. Covers + service creation, traffic splitting, IAM, VPC connectivity, secrets + integration, auto-scaling, and gcloud CLI patterns. Warns before billable + deployments. Use when the user mentions: deploy to cloud run, serverless + container, cloud run VPC, gcloud run." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "deploy to cloud run, serverless container, …" + "googlecloud-plugin/required-scopes": "run.services.create, run.routes.invoke, …" + "googlecloud-plugin/mcp-servers": "google-cloud-run" --- ``` Followed by H1 title + comprehensive routing + references. **Consequences:** -- ✅ Machine-readable skill metadata for discovery -- ✅ Agents know required IAM scopes before attempting task +- ✅ A conformant Agent Plugins client loads every skill +- ✅ Agents know required IAM scopes before attempting a task - ✅ Consistent structure across all skills -- ⚠️ Must validate frontmatter format (tool: `validate_skills.py`) +- ⚠️ Routing depends on `description` quality alone — `make validate` enforces + that every `metadata` trigger also appears there - ⚠️ Requires skill authors to understand IAM scope mapping +> Superseded shape: the pre-ADR-007 contract put `version`, `triggers`, +> `required_scopes` and `mcp_servers` at the top level. That is non-conformant — +> see `architecture/decisions/ADR-007-agent-plugins-conformance.md`. + --- ## 5. Constraints & Non-Negotiables @@ -505,23 +515,19 @@ Each persona skill follows the same SKILL.md contract but has an elevated role: # Example: gcp-architect frontmatter --- name: gcp-architect -description: "Design-first GCP architect. Generates HLD/LLD, owns ADRs, enforces design gate. Aware of all GCP repos, patterns, MCPs, and Well-Architected Framework." -version: 0.1 -persona: true -triggers: - - "design a solution" - - "architect this" - - "how should we build" - - "what's the approach for" - - "HLD for" - - "ADR for" -gate: design -required_scopes: [] -mcp_servers: [] -references: - - url: https://cloud.google.com/architecture - title: Google Cloud Architecture Framework - retrieved: 2026-07-23 +description: "Design-first GCP architect — the skill for when GCP IS ALREADY THE + CHOSEN TARGET. Generates HLD/LLD, owns ADRs, enforces the design gate. Aware of + GCP repos, patterns, MCPs and the Well-Architected Framework. If the cloud is + still open, use solution-designer. Use when the user mentions: architect this, + how should we build, HLD for, ADR for." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" + "googlecloud-plugin/tier": "2" + "googlecloud-plugin/gate": "gcp-design" + "googlecloud-plugin/triggers": "architect this, how should we build, HLD for, ADR for" + "googlecloud-plugin/required-scopes": "" --- ``` diff --git a/architecture/decisions/ADR-005-skill-contract.md b/architecture/decisions/ADR-005-skill-contract.md index 4614174..329da3a 100644 --- a/architecture/decisions/ADR-005-skill-contract.md +++ b/architecture/decisions/ADR-005-skill-contract.md @@ -1,8 +1,21 @@ # ADR-005: SKILL.md Frontmatter Contract -**Status:** Accepted +**Status:** Superseded by [ADR-007](ADR-007-agent-plugins-conformance.md) (2026-08-07) **Date:** 2026-07-23 +> **Superseded.** The frontmatter shape below is non-conformant with the Agent +> Skills specification, whose field set is closed. `version`, `triggers`, +> `required_scopes`, `mcp_servers`, `persona`, `tier`, `gate` and `owns_eval` +> are not permitted at the top level, and Agent Plugins 1.0.0 requires clients +> to **skip** any skill that carries them — all 17 skills, in practice. +> +> The contract itself survives; only its location changed. Every field below now +> lives under `metadata` as a namespaced string value, and routing keywords fold +> into `description`. See ADR-007 for the current shape and the reasoning. +> +> Retained for the record: the *motivation* below still holds, and the gate +> discipline it established is what made the drift detectable. + ## Context Agents need machine-readable metadata to route prompts to the correct skill at runtime. Without a standard contract, each skill becomes its own format — discovery breaks, validation is impossible, and the plugin can't enumerate its own capabilities. diff --git a/architecture/decisions/ADR-007-agent-plugins-conformance.md b/architecture/decisions/ADR-007-agent-plugins-conformance.md new file mode 100644 index 0000000..86ffbaa --- /dev/null +++ b/architecture/decisions/ADR-007-agent-plugins-conformance.md @@ -0,0 +1,122 @@ +# ADR-007: Adopt Agent Plugins 1.0.0 as the Packaging Contract + +**Status:** Accepted +**Date:** 2026-08-07 +**Supersedes:** the frontmatter portion of [ADR-005](ADR-005-skill-contract.md) + +## Context + +Agent Plugins 1.0.0 is an open, vendor-neutral specification for packaging Agent +Skills and MCP servers into portable plugins. Amazon, Cursor, Google, Microsoft, +OpenAI and Vercel are Core Maintainers. It removes the reason this repo carries +four near-duplicate manifests: the components were already portable, but the +wrapper around them was not. + +An audit against the specification found two independent **fatal** faults. + +**1. `plugin.json` omitted `$schema`.** The manifest schema is closed +(`additionalProperties: false`) and lists `$schema` in `required`. Specification +§5.2 makes a schema violation fatal — the client rejects the plugin entirely. + +**2. Every skill was non-conformant.** The Agent Skills frontmatter field set is +closed: `name`, `description`, `license`, `compatibility`, `metadata`, +`allowed-tools`. ADR-005 mandated `version`, `triggers`, `required_scopes` and +`mcp_servers` as top-level fields, and personas added `persona`, `tier`, `gate` +and `owns_eval`. Agent Plugins §6.1 requires a client to **skip** each skill that +does not conform to the Agent Skills specification. + +Together these meant a conformant client would load **nothing** — the plugin +would be rejected outright, and had it not been, all 17 skills would have been +silently skipped. The repo's own `make gate` passed throughout, because it +enforced ADR-005 rather than the published specs. + +## Decision + +Conform to Agent Plugins 1.0.0 and the Agent Skills specification it references. +Treat the published schemas, not this repo's conventions, as the authority. + +**Manifest.** `plugin.json` declares +`https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`. Its field set was +already within §5.2's permitted ten, so nothing else moved. + +**MCP.** A new root `mcp.json` is the portable MCP manifest, with an explicit +`"type": "stdio"` transport discriminator — the schema's `oneOf` discriminates +on that field, so a server omitting it is skipped. + +It declares the `gcloud` server **only**, and omits `env` entirely. Two reasons: + +- Only `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` expand. `${GCP_PROJECT_ID}` would + have been passed through *literally*, setting `CLOUDSDK_CORE_PROJECT` to the + 17-character string `${GCP_PROJECT_ID}`. Clients MAY inherit the ambient + environment, and `gcloud-mcp` already reads project and ADC from it. +- `toolbox` needs `--config `, which no spec + placeholder can express. It remains a documented opt-in rather than a server + that fails on every first install. + +`mcp.json` is therefore deliberately a **subset** of the harness manifests. +`validate_plugin.py` enforces that relationship so the two cannot drift. + +**Skills.** Frontmatter reduces to `name`, `description`, `license`, `metadata`. +The routing keywords from `triggers` fold into `description` — which is what a +conformant client actually routes on, and the specification asks descriptions to +carry the discriminating keywords. The plugin's own contract moves under +`metadata` as namespaced string values: + +```yaml +--- +name: cloud-run +description: "Deploy and manage containerized workloads on Cloud Run. … Use when + the user mentions: deploy to cloud run, serverless container, gcloud run, …" +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "deploy to cloud run, serverless container, …" + "googlecloud-plugin/required-scopes": "run.services.create, run.routes.invoke" + "googlecloud-plugin/mcp-servers": "google-cloud-run" +--- +``` + +`metadata` is specified as a map from string keys to string values, so lists are +comma-joined rather than nested. Keys are namespaced because the specification +asks authors to keep key names unique against other clients' metadata. + +`required-scopes` is emitted even when empty — "this skill needs no IAM scopes" +is a claim worth making explicitly, and the security gate reads it. + +## Alternatives considered + +**Keep YAML lists under `metadata`.** More readable and diffable, and the +reference validator does not type-check metadata values today. Rejected: it +contradicts the specification's "string values" prose, and the plugin's entire +pitch is conformance. Passing only because the validator has a gap is not +conformance. + +**Drop the fields entirely.** Leanest frontmatter, but the machine-readable IAM +scope data stops being queryable — `gcp-security` reads it to pre-check +least-privilege before a task runs. + +**Keep `${GCP_PROJECT_ID}` and document the caveat.** Some clients pre-expand +their own environment. Rejected: on a strictly conformant client it actively +mis-sets the project, which is worse than not setting it at all. + +## Consequences + +- ✅ A conformant client loads the plugin and all 17 skills. +- ✅ Verified independently: `skills-ref` (the official reference validator) + reports 17/17 valid; `check-jsonschema` validates both manifests against the + published schemas. +- ✅ `make spec` enforces conformance in CI, reporting against the + specification's own FATAL / MCP / SKILL failure boundaries. +- ✅ No breakage. `.claude-plugin/`, `.kimi-plugin/`, `gemini-extension.json` + and `.agents/` are untouched — the specification closes the `plugin.json` + field set and fixes component locations, but says nothing about sibling + directories. All four harnesses remain installable. +- ⚠️ Routing now depends on description quality alone. A skill whose keywords + are absent from its description will not be found, and no validator can catch + that — only routing tests can. +- ⚠️ `triggers` survives in `metadata` for this plugin's own tooling, but no + client reads it. It must be kept in sync with the description by hand, or it + rots into a second source of truth. +- ⚠️ Agent Plugins 1.0.0 specifies no install mechanism, distribution protocol, + permission model or sandboxing. Marketplace packaging stays harness-specific + until a future version addresses it. diff --git a/plugin.json b/plugin.json index 83278c2..b75223e 100644 --- a/plugin.json +++ b/plugin.json @@ -5,7 +5,7 @@ "version": "0.1.0", "author": { "name": "Jaroslav Pantsjoha", - "url": "https://uk.linkedin.com/in/johas" + "url": "https://jpantsjoha.com" }, "homepage": "https://github.com/jpantsjoha/googlecloud-plugin", "repository": "https://github.com/jpantsjoha/googlecloud-plugin", diff --git a/plugin.yaml b/plugin.yaml index f16eaf0..878b945 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -131,7 +131,11 @@ mcp_servers: # ─── Validation ─────────────────────────────────────────────────────────────── validation: gate_command: make gate + conforms_to: + - https://agent-plugins.org/specification # Agent Plugins 1.0.0 + - https://agentskills.io/specification # Agent Skills scripts: + - scripts/validate_agent_plugins.py - scripts/validate_skills.py - scripts/check_links.py - scripts/freshness_check.py diff --git a/research/coverage-matrix.md b/research/coverage-matrix.md index 41d029b..18d65de 100644 --- a/research/coverage-matrix.md +++ b/research/coverage-matrix.md @@ -6,7 +6,13 @@ Tracks skill status for every first-wave service. Update whenever a skill progre **v0.1 target:** All first-wave services at `validated` before release tag. -Last updated: 2026-07-23 (Phase 1 crawl complete — 75/75 sources resolve, 70 unique URLs live, gate green) +Last updated: 2026-08-07 (Agent Plugins 1.0.0 conformance — all 17 skills migrated to the +Agent Skills frontmatter contract, 101/101 URLs live, gate green at 212 tests) + +**Conformance:** every skill validates against the official `skills-ref` reference +validator (17/17), and `plugin.json` / `mcp.json` validate against the published +Agent Plugins 1.0.0 schemas. See +[ADR-007](../architecture/decisions/ADR-007-agent-plugins-conformance.md). --- diff --git a/scripts/validate_skills.py b/scripts/validate_skills.py index 241b9f6..6595eba 100644 --- a/scripts/validate_skills.py +++ b/scripts/validate_skills.py @@ -91,6 +91,27 @@ def check_metadata(metadata) -> list[str]: return errors +def check_triggers_routable(metadata: dict, description: str) -> list[str]: + """Every trigger must appear in the description, because that is what routes. + + No conformant client reads metadata — it matches on `description` alone. A + trigger recorded only in metadata is therefore unroutable, and the two + copies silently rot apart. This keeps the metadata index a derived view of + the description rather than a competing source of truth. + """ + raw = metadata.get(f"{NS}triggers") if isinstance(metadata, dict) else None + if not isinstance(raw, str): + return [] + lowered = description.lower() + unroutable = [t.strip() for t in raw.split(",") if t.strip() and t.strip().lower() not in lowered] + if unroutable: + return [ + f"trigger(s) {unroutable} appear in metadata but not in 'description' — " + "they are unroutable; add them to the description or drop them" + ] + return [] + + def validate_skill(skill_path: Path) -> list[str]: content = skill_path.read_text() @@ -141,6 +162,8 @@ def validate_skill(skill_path: Path) -> list[str]: errors.append("Missing 'metadata' (carries this plugin's skill contract)") else: errors.extend(check_metadata(fm["metadata"])) + if isinstance(fm.get("description"), str): + errors.extend(check_triggers_routable(fm["metadata"], fm["description"])) if not (skill_path.parent / "references").is_dir(): errors.append("Missing references/ directory") diff --git a/shared/conventions.md b/shared/conventions.md index 20c8107..8ad5a51 100644 --- a/shared/conventions.md +++ b/shared/conventions.md @@ -18,17 +18,44 @@ version: "0.1" persona: true # only for persona skills (solution-designer, gcp-*) tier: 1 # only for persona skills (1, 2, or 3) gate: # only for persona skills -triggers: - - "trigger phrase one" - - "trigger phrase two" -required_scopes: # list of GCP IAM permission strings (empty list [] for non-IAM skills) - - service.resource.verb -mcp_servers: # list of MCP server IDs this skill uses (empty list [] if none) - - server-id +license: MIT +metadata: # Agent Skills defines metadata as string keys -> STRING values + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "trigger phrase one, trigger phrase two" + "googlecloud-plugin/required-scopes": "service.resource.verb" # "" for non-IAM skills + "googlecloud-plugin/mcp-servers": "server-id" # omit if none --- ``` -All fields except `persona`, `tier`, and `gate` are required on every skill. +The frontmatter field set is **closed**. Agent Skills permits only `name`, +`description`, `license`, `compatibility`, `metadata` and `allowed-tools`; a +skill carrying anything else is non-conformant, and Agent Plugins 1.0.0 requires +clients to skip it. Everything this plugin needs therefore lives under +`metadata`, namespaced, with lists comma-joined into strings. See +[ADR-007](../architecture/decisions/ADR-007-agent-plugins-conformance.md). + +Persona skills add `"googlecloud-plugin/persona"`, `"googlecloud-plugin/tier"` +and `"googlecloud-plugin/gate"` under the same namespace. + +`version`, `triggers` and `required-scopes` are required metadata keys on every +skill; `required-scopes` is emitted as `""` rather than omitted when a skill +needs no IAM scopes. + +**Routing lives in `description`.** No client reads `metadata/triggers` — a +conformant client routes on the description alone, so every discriminating +keyword must appear there. Where two skills overlap, say explicitly which one +owns the case and name the other (see `gcp-ops` vs `logging-monitoring`). + +## MCP Server IDs + +`googlecloud-plugin/mcp-servers` names the **logical GCP capability** a skill +reaches for (`google-bigquery`, `google-gke`, …). These are not keys in +`mcp.json`. The portable `mcp.json` declares only servers that run with no +user-supplied configuration — today that is the single `gcloud` server, which +covers most of those capabilities through the `gcloud` CLI. Anything needing a +user-specific config file, such as the GenAI Toolbox and its `tools.yaml`, stays +a documented opt-in in `skills/mcp-servers/`. Do not "fix" the mismatch by +inventing server entries that cannot start. ## References Directory diff --git a/skills/gcp-architect/SKILL.md b/skills/gcp-architect/SKILL.md index ca25487..bf65b62 100644 --- a/skills/gcp-architect/SKILL.md +++ b/skills/gcp-architect/SKILL.md @@ -1,6 +1,6 @@ --- name: gcp-architect -description: "100% Google Cloud focused architect. Receives the GCP-scoped portion of the solution-designer HLD. Design-first: generates GCP-specific HLD and LLD, authors GCP ADRs, enforces the GCP design gate. Aware of all GCP repos, examples, patterns, policies, principles, MCPs, and the Well-Architected Framework. Does not make vendor-selection decisions — those belong to solution-designer. Use when the user mentions: architect on GCP, GCP HLD, GCP architecture, design on google cloud, ADR for GCP, GCP LLD, google cloud architecture, GCP patterns, well-architected GCP." +description: "100% Google Cloud focused architect — the skill for when GCP IS ALREADY THE CHOSEN TARGET. Receives the GCP-scoped portion of the solution-designer HLD, then generates GCP-specific HLD and LLD, authors GCP ADRs and enforces the GCP design gate. Aware of GCP repos, examples, patterns, policies, MCPs and the Well-Architected Framework. Makes no vendor-selection decisions: if the cloud is still open, or another cloud is in play, use solution-designer instead. For agentic systems (ADK, A2A, agent topologies) use agent-architect. Use when the user mentions: GCP design, architect on GCP, GCP HLD, GCP architecture, design on google cloud, ADR for GCP, GCP LLD, google cloud architecture, GCP patterns, well-architected GCP." license: MIT metadata: "googlecloud-plugin/version": "0.1" diff --git a/skills/gcp-ops/SKILL.md b/skills/gcp-ops/SKILL.md index 4e391bd..95e368a 100644 --- a/skills/gcp-ops/SKILL.md +++ b/skills/gcp-ops/SKILL.md @@ -1,13 +1,13 @@ --- name: gcp-ops -description: "GCP Operations and SRE persona. Defines SLOs, alerting policy, runbooks, and incident response. Knows what healthy looks like in production for every GCP service. Validates that observability exists before any production release. Owns the operational readiness gate. Use when the user mentions: SLA, monitoring setup, dashboard, on-call, production readiness, cloud logging setup, cloud monitoring setup, error budget." +description: "GCP Operations and SRE persona. DECIDES operational policy — it does not configure the tooling. Sets SLO and error-budget targets, alerting policy, runbooks, and incident response, and knows what healthy looks like in production for every GCP service. Owns the operational readiness gate and blocks release until observability exists. Hand the resulting policy to logging-monitoring, which implements it. Use when the user mentions: SLO, SLA, error budget, runbook, on-call, incident response, production readiness, operational readiness, is this ready for production." license: MIT metadata: "googlecloud-plugin/version": "0.1" "googlecloud-plugin/persona": "true" "googlecloud-plugin/tier": "3" "googlecloud-plugin/gate": "operational-readiness" - "googlecloud-plugin/triggers": "SLO, SLA, alerting, monitoring setup, runbook, incident response, observability, dashboard, on-call, production readiness, operational readiness, cloud logging setup, cloud monitoring setup, error budget" + "googlecloud-plugin/triggers": "SLO, SLA, alerting, runbook, incident response, observability, on-call, production readiness, operational readiness, error budget" "googlecloud-plugin/required-scopes": "monitoring.alertPolicies.create, monitoring.dashboards.create, logging.sinks.create, logging.logMetrics.create" --- diff --git a/skills/gcp-security/SKILL.md b/skills/gcp-security/SKILL.md index ace4268..4cfc2a1 100644 --- a/skills/gcp-security/SKILL.md +++ b/skills/gcp-security/SKILL.md @@ -1,6 +1,6 @@ --- name: gcp-security -description: "GCP security enforcer. Reviews designs and implementations against the GCP Well-Architected Framework security pillar, OWASP Top 10, and GCP-specific risk patterns. Enforces least-privilege IAM, secrets management, no hardcoded credentials, and security-by-design. Also reviews solution-designer output for cross-cloud security gaps. Must clear the security gate before any implementation begins. Use when the user mentions: security review, IAM review, least privilege, vulnerability, public bucket, default service account, security posture, compliance GCP, OWASP GCP." +description: "GCP security enforcer. REVIEWS and BLOCKS — it audits work rather than authoring it. Judges designs and implementations against the GCP Well-Architected Framework security pillar, OWASP Top 10, and GCP-specific risk patterns, covering least-privilege IAM, secrets management, hardcoded credentials, and security-by-design, and reviews solution-designer output for cross-cloud gaps. Must clear the security gate before implementation begins. To actually write an IAM binding, use the iam skill. Use when the user mentions: security review, IAM review, audit my access, vulnerability, public bucket, default service account, security posture, compliance GCP, OWASP GCP, least privilege." license: MIT metadata: "googlecloud-plugin/version": "0.1" diff --git a/skills/iam/SKILL.md b/skills/iam/SKILL.md index c1f9802..ffc0a95 100644 --- a/skills/iam/SKILL.md +++ b/skills/iam/SKILL.md @@ -1,6 +1,6 @@ --- name: iam -description: "Google Cloud IAM — identity, access management, service accounts, and policy authoring. Enforces least-privilege by default. Covers roles, conditions, service account patterns, Workload Identity Federation, and gcloud CLI. Never grants owner or editor roles. Use when the user mentions: iam policy, grant access, least privilege, IAM binding, who has access, iam conditions, gcloud iam." +description: "AUTHORS and inspects Google Cloud IAM — the skill that actually writes bindings, creates service accounts, and answers who has access to what. Covers roles, conditions, service account patterns, Workload Identity Federation, and gcloud CLI, least-privilege by default, and never grants owner or editor. For an audit or sign-off on someone else's access posture, use gcp-security instead. Use when the user mentions: IAM, iam policy, grant access, revoke access, service account, IAM binding, who has access, iam conditions, workload identity federation, gcloud iam, least privilege." license: MIT metadata: "googlecloud-plugin/version": "0.1" diff --git a/skills/logging-monitoring/SKILL.md b/skills/logging-monitoring/SKILL.md index 3a53d3e..229b855 100644 --- a/skills/logging-monitoring/SKILL.md +++ b/skills/logging-monitoring/SKILL.md @@ -1,6 +1,6 @@ --- name: logging-monitoring -description: "Configure observability on GCP using Cloud Logging, Cloud Monitoring, Cloud Trace, and Cloud Profiler. Covers log sinks, log-based metrics, alerting policies, dashboards, and uptime checks. Owns the operational readiness evidence for gcp-ops. Use when the user mentions: logging setup, monitoring setup, alerting policy, dashboard GCP, error reporting, SLO monitoring." +description: "IMPLEMENTS observability on GCP — the hands-on configuration skill, not the policy owner. Creates log sinks, log-based metrics, alerting policies, dashboards, and uptime checks using Cloud Logging, Cloud Monitoring, Cloud Trace, and Cloud Profiler. Builds the operational readiness evidence that gcp-ops signs off; when the question is what the targets SHOULD be, use gcp-ops instead. Use when the user mentions: cloud logging, cloud monitoring, logging setup, monitoring setup, create an alerting policy, log sink, dashboard GCP, uptime check, cloud trace, error reporting, log-based metric, SLO monitoring." license: MIT metadata: "googlecloud-plugin/version": "0.1" diff --git a/skills/mcp-servers/SKILL.md b/skills/mcp-servers/SKILL.md index 7dadd77..bcbd73f 100644 --- a/skills/mcp-servers/SKILL.md +++ b/skills/mcp-servers/SKILL.md @@ -26,6 +26,34 @@ gcloud auth activate-service-account --key-file=/path/to/sa-key.json gcloud auth application-default login --impersonate-service-account=SA@PROJECT.iam.gserviceaccount.com ``` +## What the plugin ships vs what you opt into + +The portable `mcp.json` (Agent Plugins 1.0.0) declares **one** server — `gcloud` +— because that is the only one that starts with no user-supplied configuration: + +```json +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "gcloud": { "type": "stdio", "command": "npx", "args": ["-y", "@google-cloud/gcloud-mcp"] } + } +} +``` + +Two consequences worth knowing: + +- **No `env` block.** Agent Plugins expands only `${PLUGIN_ROOT}` and + `${PLUGIN_DATA}`. A `${GCP_PROJECT_ID}` placeholder would be passed through + *literally* and mis-set your project. Export it in your own shell instead — + the client passes the ambient environment through: + ```bash + export CLOUDSDK_CORE_PROJECT=your-project-id + gcloud auth application-default login + ``` +- **The capability table below lists logical GCP capabilities**, not entries in + `mcp.json`. Most are reachable through the `gcloud` server; anything needing + its own config file is an opt-in you add to your client's own MCP settings. + ## Google-Managed MCP Servers (v0.1) | Server | Capability | Required Role | @@ -39,10 +67,17 @@ gcloud auth application-default login --impersonate-service-account=SA@PROJECT.i | `google-logging` | Cloud Logging query/ingest | `roles/logging.viewer` | | `google-monitoring` | Metrics, alerting | `roles/monitoring.viewer` | -## GenAI Toolbox (Google's MCP for Databases) +## GenAI Toolbox (Google's MCP for Databases) — opt-in Google's MCP Toolbox for Databases enables LLM agents to query Cloud SQL, AlloyDB, Spanner, BigQuery, and more safely. +> **Why this is not in `mcp.json`.** Toolbox needs `--config `, +> a path only you can supply. No Agent Plugins placeholder can express it, so +> shipping it in the portable manifest would mean a server that fails to start +> on every fresh install. Add it to your own client config once you have a +> `tools.yaml`. (A failing server is isolated by spec — it would not take the +> skills down — but a broken default is still a broken default.) + ```bash # Run the toolbox MCP server (requires a tools.yaml pointing at your database) # Tested 2026-07-23: this is the correct package. Full docs: https://mcp-toolbox.dev diff --git a/skills/solution-designer/SKILL.md b/skills/solution-designer/SKILL.md index f1cde9d..88c9e0f 100644 --- a/skills/solution-designer/SKILL.md +++ b/skills/solution-designer/SKILL.md @@ -1,6 +1,6 @@ --- name: solution-designer -description: "Vendor-agnostic solution authority. Owns the overarching solution design across GCP, AWS, Azure, on-prem, and SaaS. Produces the master HLD that scopes each cloud domain. Objective: not GCP-biased — will recommend another cloud when warranted. Researches, proves, and validates that the proposed solution works across all mentioned vendors. Use when the user mentions: design a solution, what should we build, multi-cloud, which cloud for, solution architecture, overarching design, vendor recommendation, cross-cloud, hybrid cloud, compare GCP vs AWS, compare GCP vs Azure." +description: "Vendor-agnostic solution authority — the skill for when the CLOUD IS NOT YET CHOSEN. Owns the overarching design across GCP, AWS, Azure, on-prem and SaaS, and produces the master HLD that scopes each cloud domain. Objective, not GCP-biased: will recommend another cloud when warranted, and proves the solution works across every vendor mentioned. Once GCP is the settled target, hand the GCP-scoped portion to gcp-architect. Use when the user mentions: design a solution, what should we build, multi-cloud, which cloud for, solution architecture, overarching design, vendor recommendation, cross-cloud, hybrid cloud, compare GCP vs AWS, compare GCP vs Azure." license: MIT metadata: "googlecloud-plugin/version": "0.1" diff --git a/skills/vertex-ai/SKILL.md b/skills/vertex-ai/SKILL.md index c873cb5..ad95448 100644 --- a/skills/vertex-ai/SKILL.md +++ b/skills/vertex-ai/SKILL.md @@ -1,6 +1,6 @@ --- name: vertex-ai -description: "Build and deploy ML models and generative AI applications on Vertex AI. Covers Model Garden, Gemini API, custom training, endpoint deployment, Agent Builder, and IAM. Warns before deploying endpoints (billable). Integrates with Vertex AI MCP server. Use when the user mentions: Gemini on GCP, vertex endpoint, generative AI GCP, vertex training, PaLM, vertex AI pipeline." +description: "Build and deploy ML MODELS on Vertex AI — training, serving and inference, not agent architecture. Covers Model Garden, the Gemini API, custom training, endpoint deployment, pipelines and IAM. Warns before deploying endpoints (billable). Integrates with the Vertex AI MCP server. For designing a multi-agent system, ADK topology, or A2A/AP2 protocol work, use agent-architect instead. Use when the user mentions: vertex AI, Gemini on GCP, model garden, deploy ML model, vertex endpoint, generative AI GCP, agent builder, vertex training, PaLM, Gemini API, vertex AI pipeline." license: MIT metadata: "googlecloud-plugin/version": "0.1" diff --git a/tests/skill-smoke-tests/test_skill_contract.py b/tests/skill-smoke-tests/test_skill_contract.py index def2dba..bf5e45d 100644 --- a/tests/skill-smoke-tests/test_skill_contract.py +++ b/tests/skill-smoke-tests/test_skill_contract.py @@ -133,6 +133,19 @@ def test_triggers_non_empty(skill_file: Path) -> None: assert triggers.strip(), f"{skill_file}: triggers must be non-empty" +@pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) +def test_every_trigger_is_routable(skill_file: Path) -> None: + """A trigger absent from the description is unroutable — no client reads metadata.""" + fm = parse_frontmatter(skill_file) + description = fm["description"].lower() + triggers = [t.strip() for t in fm["metadata"][f"{NS}triggers"].split(",") if t.strip()] + unroutable = [t for t in triggers if t.lower() not in description] + assert not unroutable, ( + f"{skill_file}: trigger(s) {unroutable} appear only in metadata, so a " + f"conformant client routing on 'description' can never match them" + ) + + @pytest.mark.parametrize("skill_file", skill_files(), ids=lambda p: p.parent.name) def test_references_directory_exists(skill_file: Path) -> None: refs = skill_file.parent / "references" From 28f0c9cf01525d65f56c9d106e2ef28dbb7ae831 Mon Sep 17 00:00:00 2001 From: JP Date: Fri, 7 Aug 2026 10:31:47 +0100 Subject: [PATCH 3/3] ci: gate on Agent Plugins conformance, warn on upstream spec drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds validate_agent_plugins.py as a blocking CI step — it is deterministic and offline, since it transcribes the published schemas rather than fetching them. Adds an advisory step that validates against UPSTREAM instead: the official skills-ref reference validator and the live JSON schemas. That is what catches spec drift, but it depends on the network, so it warns rather than blocks. --- .github/workflows/gate.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 7176684..d6ee960 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -22,6 +22,9 @@ jobs: - name: Install deps run: pip install pyyaml pytest + - name: Validate Agent Plugins 1.0.0 conformance + run: python3 scripts/validate_agent_plugins.py + - name: Validate skill contracts run: python3 scripts/validate_skills.py @@ -36,3 +39,20 @@ jobs: - name: Smoke tests run: python3 -m pytest tests/skill-smoke-tests/ -q + + # Advisory: validates against UPSTREAM rather than our transcription of it, + # so it catches spec drift early. Network-dependent, so it warns instead of + # blocking — validate_agent_plugins.py above is the deterministic gate. + - name: Independent conformance check (upstream reference validator) + continue-on-error: true + run: | + pipx install uv >/dev/null 2>&1 || pip install uv + for skill in skills/*/; do + uvx --quiet --from \ + 'git+https://github.com/agentskills/agentskills.git#subdirectory=skills-ref' \ + skills-ref validate "$skill" || exit 1 + done + uvx --quiet check-jsonschema \ + --schemafile https://agent-plugins.org/schemas/1.0.0/plugin.schema.json plugin.json + uvx --quiet check-jsonschema \ + --schemafile https://agent-plugins.org/schemas/1.0.0/mcp.schema.json mcp.json