diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6385c3c..a301b8e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "googlecloud-plugin", "description": "A full Google Cloud delivery team for your coding agent: solution designer, GCP architect, agent architect, security, SRE, and QA — wired into a design-first, security-first delivery gate, with eleven service skills and MCP server setup.", - "version": "0.1.0", + "version": "0.2.0", "source": "./", "author": { "name": "Jaroslav Pantsjoha", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e0b95c4..c2817b3 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "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", + "version": "0.2.0", "author": { "name": "Jaroslav Pantsjoha", "url": "https://uk.linkedin.com/in/johas" diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a933dc6..45782d8 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,7 +1,8 @@ # GoogleCloud Plugin — Construction Guide This file advises AI agents working ON the plugin (building, researching, validating skills). -It is gitignored and never committed to the public repo. +It is committed, and public — write it for contributors, not as a scratchpad. +For local notes you don't want published, use `.claude/local-CLAUDE.md`, which is gitignored. ## Your Role @@ -32,14 +33,27 @@ 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) +Frontmatter follows the [Agent Skills specification](https://agentskills.io/specification). +The field set is **closed** — adding a top-level key outside +`{name, description, license, compatibility, allowed-tools, metadata}` makes the +skill non-conformant, and a conformant client MUST skip it. Everything specific +to this plugin is namespaced under `metadata` as **string** values: -Run `make validate` after any SKILL.md edit. +- `name` — matches directory name exactly +- `description` — what it does AND when to use it, max 1024 chars, ending with + `Use when the user mentions: …`. This is the only thing a conformant client + routes on. Where two skills overlap, name the other and draw the line. +- `metadata["googlecloud-plugin/version"]` +- `metadata["googlecloud-plugin/triggers"]` — comma-joined, non-empty +- `metadata["googlecloud-plugin/required-scopes"]` — comma-joined, `""` if none +- `metadata["googlecloud-plugin/mcp-servers"]` — comma-joined, omit if none +- persona skills add `persona`, `tier`, `gate` under the same namespace + +Never reintroduce a top-level `triggers`, `version` or `required_scopes` field — +that is the exact regression `make validate` exists to catch. See +[ADR-007](../architecture/decisions/ADR-007-agent-plugins-conformance.md). + +Run `make spec && make validate` after any SKILL.md or manifest edit. ## Safety Rules (Non-Negotiable) diff --git a/.kimi-plugin/plugin.json b/.kimi-plugin/plugin.json index e866dbe..62bd84e 100644 --- a/.kimi-plugin/plugin.json +++ b/.kimi-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "googlecloud-plugin", - "version": "0.1.0", + "version": "0.2.0", "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", diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bcc06a3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +All notable changes to this plugin are documented here. + +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); +versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] — 2026-08-07 + +**Standards release.** The plugin now conforms to +[Agent Plugins 1.0.0](https://agent-plugins.org/specification) and the +[Agent Skills specification](https://agentskills.io/specification), the open +vendor-neutral packaging standards co-maintained by Amazon, Cursor, Google, +Microsoft, OpenAI and Vercel. + +Before this release, a conformant Agent Plugins client would have loaded +**nothing** from this plugin. Two independent fatal faults: + +1. `plugin.json` omitted the required `$schema`. The manifest schema is closed, + so §5.2 makes the omission fatal — the client rejects the plugin outright. +2. All 17 skills carried top-level frontmatter outside the closed Agent Skills + field set. §6.1 requires clients to **skip** every non-conforming skill. + +### Added + +- `mcp.json` — the portable MCP manifest, with an explicit `stdio` transport + discriminator. +- `make spec` — Agent Plugins 1.0.0 conformance gate, reporting against the + specification's own failure boundaries (FATAL / MCP / SKILL). +- `scripts/validate_agent_plugins.py` — dependency-free conformance checker; + no network call needed in CI. +- README **Standards and compliance** section, with commands to verify + conformance without trusting this repo's own scripts. +- [ADR-007](architecture/decisions/ADR-007-agent-plugins-conformance.md) — + the adoption decision and its trade-offs. +- `skills/mcp-servers/references/agent-plugins-mcp.md` — the placeholder-expansion + trap, and Toolbox as a documented opt-in. +- This changelog. + +### Changed + +- **BREAKING (skill authors and downstream tooling).** SKILL.md frontmatter is + reduced to `{name, description, license, metadata}`. The top-level `version`, + `triggers`, `required_scopes`, `mcp_servers`, `persona`, `tier`, `gate` and + `owns_eval` fields are gone. The same data is retained under `metadata`, + namespaced and comma-joined: + + ```yaml + metadata: + "googlecloud-plugin/required-scopes": "run.services.create, run.services.get" + ``` + + Anything reading the old YAML lists must now read the namespaced key and split + on `", "`. +- Routing keywords moved from `triggers` into `description` — the only field a + conformant client routes on. +- Eight skill descriptions gained explicit boundary clauses after cross-harness + routing tests showed four pairs where both skills matched the same request: + `iam`↔`gcp-security`, `gcp-ops`↔`logging-monitoring`, + `vertex-ai`↔`agent-architect`, `gcp-architect`↔`solution-designer`. +- `scripts/validate_skills.py` inverted — it now enforces the closed Agent Skills + field set instead of requiring the fields that broke conformance. +- Smoke tests expanded from 136 to 195, covering both specifications. +- `make manifest` no longer claims the plugin is "installable across 4 + harnesses". It verifies manifests parse and agree, and now says so. + +### Fixed + +- `.claude/CLAUDE.md` described itself as gitignored and never committed. It is + tracked and public; `.gitignore` covers `.claude/local-CLAUDE.md`. Corrected. +- `vertex-ai` advertised "Agent Builder" while directing agentic work elsewhere. + Agent Builder now belongs to `agent-architect` in both descriptions. + +### Compatibility + +No action required for existing users. The per-harness manifests +(`.claude-plugin/`, `.kimi-plugin/`, `gemini-extension.json`, `.agents/`) are +unchanged and remain alongside the portable layout — Agent Plugins closes the +`plugin.json` field set and fixes component locations, but says nothing about +sibling directories. + +`mcp.json` deliberately declares only the `gcloud` server. Toolbox needs a +user-specific `tools.yaml` path that no Agent Plugins placeholder can express, +so it stays an opt-in documented in `skills/mcp-servers/`. `mcp.json` also +declares no `env`: only `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` expand, so +`CLOUDSDK_CORE_PROJECT: "${GCP_PROJECT_ID}"` would have been passed through +literally and mis-set the project. Export it in your shell instead. + +### Verification + +| Check | Result | +|---|---| +| `skills-ref` (official Agent Skills reference validator) | 17/17 valid | +| `check-jsonschema` vs published `plugin.schema.json` / `mcp.schema.json` | pass | +| `agy plugin validate` (Antigravity loader) | 17 skills, 2 mcpServers, exit 0 | +| Antigravity routing | 17 skills visible, 6/6 routed correctly | +| Kimi routing (`--skills-dir`) | 17 skills loaded, 7/7 routed correctly | +| Live MCP handshake | `gcloud-mcp-server` v0.5.3, protocol 2025-06-18, `run_gcloud_command` | +| `make gate` | spec + validate + manifest + mermaid + 102 URLs + 195 tests | + +Not verified: Codex (usage limit at time of release) and Toolbox (opt-in). + +## [0.1.0] — 2026-07-23 + +Initial release. Six persona skills, eleven service skills, MCP server setup, +and a self-validating research pipeline. + +[0.2.0]: https://github.com/jpantsjoha/googlecloud-plugin/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/jpantsjoha/googlecloud-plugin/releases/tag/v0.1.0 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/README.md b/README.md index 9db2aba..ec6e4bb 100644 --- a/README.md +++ b/README.md @@ -146,13 +146,45 @@ The rule underneath every gate: **infer intent, never infer permission.** An AI --- +## Standards and compliance + +This plugin conforms to the open, vendor-neutral packaging standards — not to any +single vendor's plugin format. + +| Standard | Version | What conforms | How it's verified | +|---|---|---|---| +| [Agent Plugins](https://agent-plugins.org/specification) | 1.0.0 | `plugin.json`, `mcp.json`, the `skills/` layout | `make spec` + [`plugin.schema.json`](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json) / [`mcp.schema.json`](https://agent-plugins.org/schemas/1.0.0/mcp.schema.json) | +| [Agent Skills](https://agentskills.io/specification) | current | all 17 `SKILL.md` files | `make validate` + the official [`skills-ref`](https://github.com/agentskills/agentskills/tree/main/skills-ref) reference validator | +| [Model Context Protocol](https://modelcontextprotocol.io) | stdio transport | `mcp.json` server declarations | `make spec` transport + placeholder checks | + +Agent Plugins 1.0.0 is co-maintained by Amazon, Cursor, Google, Microsoft, OpenAI +and Vercel. Conforming to it means one directory layout works across conformant +clients instead of a bespoke wrapper per harness. The per-harness manifests +(`.claude-plugin/`, `.kimi-plugin/`, `gemini-extension.json`, `.agents/`) remain +alongside it, so existing installs are unaffected. + +Verify it yourself, without trusting this repo's own scripts: + +```bash +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 +``` + +See [ADR-007](architecture/decisions/ADR-007-agent-plugins-conformance.md) for the +adoption decision and its trade-offs. + +--- + ## 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 full gate: spec + validate + manifest + mermaid + lint + test +make spec # Agent Plugins 1.0.0 conformance (plugin.json + mcp.json + skills/) +make validate # SKILL.md frontmatter vs the Agent Skills specification +make manifest # per-harness manifests present and consistent make lint # every reference URL resolves (HTTP 200) make test # skill smoke tests make check # freshness: content-hash drift vs live GCP docs diff --git a/SPEC.md b/SPEC.md index fdc6f9a..5ed7f0a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -273,19 +273,20 @@ Every skill has a `SKILL.md` with YAML frontmatter: ```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. … Use when the user mentions: deploy to cloud run, create cloud run service, scale cloud run." +license: MIT +metadata: + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/triggers": "deploy to cloud run, create cloud run service, scale cloud run" + "googlecloud-plugin/required-scopes": "run.services.create, run.services.delete, run.services.get, run.services.list, run.services.update" + "googlecloud-plugin/mcp-servers": "google-cloud-run" --- ``` +The Agent Skills frontmatter field set is closed, so routing keywords live in +`description` and everything plugin-specific is namespaced under `metadata`. +See [ADR-007](architecture/decisions/ADR-007-agent-plugins-conformance.md). + Followed by H1 title + comprehensive routing + references. **Consequences:** @@ -505,23 +506,15 @@ 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. Generates HLD/LLD, owns ADRs, enforces the design gate. Aware of all GCP repos, patterns, MCPs, and the Well-Architected Framework. Use this once the design is GCP-only; use solution-designer instead for vendor selection and cross-cloud scope. Use when the user mentions: design a solution, 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": "design a solution, 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..7dc2628 100644 --- a/architecture/decisions/ADR-005-skill-contract.md +++ b/architecture/decisions/ADR-005-skill-contract.md @@ -1,7 +1,16 @@ # ADR-005: SKILL.md Frontmatter Contract -**Status:** Accepted +**Status:** Superseded by [ADR-007](ADR-007-agent-plugins-conformance.md) **Date:** 2026-07-23 +**Superseded:** 2026-08-07 + +> **Superseded.** The contract below predates Agent Plugins 1.0.0. Agent Skills +> defines a *closed* frontmatter field set, so the top-level `version`, +> `triggers`, `required_scopes`, `mcp_servers`, `persona`, `tier` and `gate` +> fields specified here made every skill non-conformant — and a conformant +> client MUST skip a non-conforming skill. The same information is retained, +> namespaced under `metadata`, per ADR-007. The reasoning below still explains +> *why* the plugin carries this metadata at all; only its location changed. ## Context 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..eb9c451 --- /dev/null +++ b/architecture/decisions/ADR-007-agent-plugins-conformance.md @@ -0,0 +1,95 @@ +# 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 a vendor-neutral specification for packaging Agent Skills +and MCP servers, co-maintained by Amazon, Cursor, Google, Microsoft, OpenAI and +Vercel. It fixes the problem this plugin had been solving by brute force: we +shipped four harness manifests (`.claude-plugin/`, `.kimi-plugin/`, +`gemini-extension.json`, `.agents/`) because every client wanted a different +wrapper around the same skills. + +Audited against the specification, the plugin had two independent fatal faults: + +1. `plugin.json` omitted `$schema`. The manifest schema is closed + (`additionalProperties: false`) and lists `$schema` as required, so §5.2 + makes the omission fatal — a conformant client rejects the plugin outright. +2. All 17 skills carried top-level frontmatter outside the closed Agent Skills + field set (`version`, `triggers`, `required_scopes`, `mcp_servers`, and on + personas `persona`, `tier`, `gate`, `owns_eval`). Agent Plugins §6.1 requires + clients to **skip** every non-conforming skill. + +Together: a conformant client would have loaded nothing. + +## Decision + +Conform to Agent Plugins 1.0.0 and the Agent Skills specification it references, +and enforce both in CI. + +**Manifest.** `plugin.json` declares +`https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`. Only the ten +permitted top-level fields appear. Client-specific data, if ever needed, goes +under `extensions` keyed by reverse-domain namespace. + +**Skills.** Frontmatter is reduced to `{name, description, license, metadata}`. +The routing keywords that lived in `triggers` are folded into `description` — +which is what a conformant client actually routes on — and 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, …" +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.services.get, …" + "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 spec recommends +names unique enough to avoid collisions with other clients. + +**MCP.** A root `mcp.json` becomes the portable MCP manifest, with an explicit +`type` discriminator. Two consequences drove its shape: + +- It declares the `gcloud` server only. `toolbox` needs a user-specific + `tools.yaml` path, which no Agent Plugins placeholder can express, so it stays + a documented opt-in in `skills/mcp-servers/`. +- It declares no `env`. Only `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` expand; + everything else is passed through **literally**. `CLOUDSDK_CORE_PROJECT: + "${GCP_PROJECT_ID}"` would have set the project to that 18-character string. + Clients may inherit the ambient environment, and `gcloud-mcp` already reads + both the project and ADC from it. + +**Harness manifests stay.** The specification closes the `plugin.json` field set +and fixes component locations; it says nothing about sibling directories, and v1 +deliberately ships no install or distribution mechanism. So `.claude-plugin/`, +`.kimi-plugin/`, `gemini-extension.json` and `.agents/` remain, and existing +installs keep working. `mcp.json` is validated as a strict subset of `.mcp.json` +to stop the two drifting apart. + +## Consequences + +- ✅ A conformant client loads the plugin and all 17 skills. Previously: zero. +- ✅ Verified by tools we do not own — `skills-ref` (the reference validator) and + `check-jsonschema` against both published schemas. +- ✅ `make spec` reports against the spec's own failure boundaries + (FATAL / MCP / SKILL), so a violation's blast radius is visible. +- ✅ Portability stops being bespoke work per harness. +- ⚠️ Routing now depends entirely on `description` quality. Skills with adjacent + scope need explicit "use X instead when…" clauses; a cross-harness routing + test in Kimi surfaced four such pairs (iam↔gcp-security, + gcp-ops↔logging-monitoring, vertex-ai↔agent-architect, + gcp-architect↔solution-designer), now disambiguated in their descriptions. +- ⚠️ IAM scopes are no longer a YAML list. Tooling that reads + `googlecloud-plugin/required-scopes` must split on `", "`. +- ⚠️ `mcp.json` and `.mcp.json` both exist and must not drift; `make manifest` + enforces the subset relationship. diff --git a/gemini-extension.json b/gemini-extension.json index 966976e..9a43fe1 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,7 +1,7 @@ { "name": "googlecloud-plugin", "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.", - "version": "0.1.0", + "version": "0.2.0", "contextFileName": "GEMINI.md", "mcpServers": { "gcloud": { 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..a733653 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,8 @@ { + "$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", + "version": "0.2.0", "author": { "name": "Jaroslav Pantsjoha", "url": "https://uk.linkedin.com/in/johas" diff --git a/plugin.yaml b/plugin.yaml index f16eaf0..c8ad977 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: googlecloud-plugin -version: 0.1.0 +version: 0.2.0 description: > Unified Google Cloud plugin. Installs a full operational GCP team: vendor-agnostic solution designer, GCP architect, security enforcer, 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..8318573 100644 --- a/scripts/validate_plugin.py +++ b/scripts/validate_plugin.py @@ -14,7 +14,7 @@ ROOT = Path(__file__).parent.parent EXPECTED_NAME = "googlecloud-plugin" -EXPECTED_VERSION = "0.1.0" +EXPECTED_VERSION = "0.2.0" def _load_json(path: Path, errors: list[str]) -> dict | None: @@ -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(): @@ -183,8 +211,12 @@ def main() -> None: harnesses = ["Claude Code", "Antigravity (Gemini)", "Codex", "Kimi"] if not errors: for h in harnesses: - print(f"OK {h} — installable") - print(f"\nPlugin '{EXPECTED_NAME}' v{EXPECTED_VERSION} is installable across all 4 harnesses") + print(f"OK {h} — manifest present and consistent") + print( + f"\nPlugin '{EXPECTED_NAME}' v{EXPECTED_VERSION}: all 4 harness manifests " + "parse and agree.\nThis is a packaging check, not an install — it does not " + "launch any harness." + ) return for e in errors: 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/shared/conventions.md b/shared/conventions.md index 20c8107..8e70a3e 100644 --- a/shared/conventions.md +++ b/shared/conventions.md @@ -10,25 +10,48 @@ Examples: `cloud-run`, `gcp-architect`, `logging-monitoring` ## SKILL.md Frontmatter Contract +Frontmatter must conform to the [Agent Skills specification](https://agentskills.io/specification). +The field set is **closed** — only `name`, `description`, `license`, +`compatibility`, `allowed-tools` and `metadata` are permitted. A skill carrying +any other top-level key is non-conformant, and +[Agent Plugins 1.0.0](https://agent-plugins.org/specification) §6.1 requires a +client to **skip** it. Everything specific to this plugin therefore lives under +`metadata`, namespaced. + ```yaml --- -name: -description: "20 chars>" -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 +name: # 1-64 chars, lowercase, hyphens, no "--" +description: "" +license: MIT +metadata: # map of string keys to STRING values + "googlecloud-plugin/version": "0.1" + "googlecloud-plugin/persona": "true" # persona skills only + "googlecloud-plugin/tier": "1" # persona skills only (1, 2, or 3) + "googlecloud-plugin/gate": "" # persona skills only + "googlecloud-plugin/triggers": "phrase one, phrase two" + "googlecloud-plugin/required-scopes": "service.resource.verb, ..." # "" if none + "googlecloud-plugin/mcp-servers": "server-id, ..." # omit if none --- ``` -All fields except `persona`, `tier`, and `gate` are required on every skill. +Required on every skill: `name`, `description`, and the `version`, `triggers` +and `required-scopes` metadata keys. `required-scopes` is present but empty for +non-IAM skills — the empty string is meaningful, it asserts the skill needs no +permissions. + +### Writing the description + +`description` is the only thing a conformant client routes on, so it carries the +weight `triggers` used to. It must say **what the skill does and when to use +it**, and end with the routing keywords: `Use when the user mentions: …`. + +Where two skills have adjacent scope, each description must name the other and +draw the line — `Use this to author or change IAM; use gcp-security instead to +audit an existing posture.` Without that, both match and routing is a coin flip. + +Lists in `metadata` are comma-joined strings, not YAML sequences: the spec +defines `metadata` as a map from string keys to string values. Consumers split +on `", "`. ## References Directory diff --git a/skills/agent-architect/SKILL.md b/skills/agent-architect/SKILL.md index 1f07a53..8fc3e52 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. Owns Agent Builder work. Use when the user mentions: agent architecture, agent builder, agent development kit, agent to agent, agent payments, MCP tools for agent, RAG on GCP, orchestrator agent, gemini agent. Use this for agentic system design; use vertex-ai instead for model training, Model Garden and endpoint deployment." +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, agent builder" + "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..6cebcde 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. Use this once the design is GCP-only; use solution-designer instead for vendor selection and cross-cloud scope." +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..b0d9745 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. Use this for SLO targets, runbooks and release gating; use logging-monitoring instead to configure the underlying Cloud Logging and Monitoring resources." +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..2b27909 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. Use this to review and gate; use iam instead to author or change bindings." +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..cf81463 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. Use this to author or change IAM; use gcp-security instead to audit an existing posture against the security gate." +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..39578ef 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. Use this to configure the observability resources themselves; use gcp-ops instead for SLO targets, runbooks and release gating." +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/mcp-servers/references/agent-plugins-mcp.md b/skills/mcp-servers/references/agent-plugins-mcp.md new file mode 100644 index 0000000..69a7452 --- /dev/null +++ b/skills/mcp-servers/references/agent-plugins-mcp.md @@ -0,0 +1,89 @@ +# MCP under Agent Plugins 1.0.0 + +How this plugin declares MCP servers, and why `mcp.json` looks smaller than you +might expect. + +Spec: · +schema: + +## Where MCP config lives + +| File | Read by | Servers | +|---|---|---| +| `mcp.json` | any conformant Agent Plugins client | `gcloud` | +| `.mcp.json` | Claude Code | `gcloud`, `toolbox` | +| `mcp_config.json` | Antigravity | `gcloud`, `toolbox` | +| `gemini-extension.json` | Gemini CLI extension runtime | `gcloud`, `toolbox` | + +`mcp.json` is the portable one and is deliberately a **subset**. `make manifest` +fails the build if it ever declares a server the harness manifests don't have, +or if a shared server's `command` drifts. + +## Two rules that shape the file + +**1. Only two placeholders expand.** + +`${PLUGIN_ROOT}` and `${PLUGIN_DATA}` are substituted in `args`, `env` values and +`cwd`. *Every other `${...}` is passed through literally* — it is not an +environment-variable reference. So this, which reads correctly: + +```json +"env": { "CLOUDSDK_CORE_PROJECT": "${GCP_PROJECT_ID}" } +``` + +…would set the project to the literal 18-character string `${GCP_PROJECT_ID}`, +and every gcloud call would fail against a project that cannot exist. + +`mcp.json` therefore declares **no `env` at all**. Clients may pass the ambient +environment through to subprocesses, and `gcloud-mcp` already reads both the +project and Application Default Credentials from it. Set them the normal way +before launching your client: + +```bash +gcloud auth application-default login +export CLOUDSDK_CORE_PROJECT="your-project-id" +``` + +**2. Transport is explicit.** The schema discriminates on `type`, so a server +entry without `"type": "stdio"` (or `streamable-http`) matches no branch of the +`oneOf` and is skipped. Failures are isolated: a server that won't start does not +stop the plugin's skills from loading. + +## Toolbox is an opt-in + +The [GenAI Toolbox](https://mcp-toolbox.dev) server needs `--config ` +pointing at a `tools.yaml` that describes *your* databases. That path is +user-specific, and no Agent Plugins placeholder can express it — `${PLUGIN_ROOT}` +points at the installed plugin, which is the wrong place for user data. + +Rather than ship a server guaranteed to fail on first run, `mcp.json` omits it. +Add it to your own client config when you need BigQuery, Cloud SQL, Spanner or +AlloyDB tools: + +```json +{ + "mcpServers": { + "toolbox": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@toolbox-sdk/server", "--stdio", "--config", "/absolute/path/to/tools.yaml"] + } + } +} +``` + +Verified 2026-07-23: `@toolbox-sdk/server` resolves, accepts stdio mode, and +validates its config path. Note the earlier `uvx toolbox-core` form is wrong — +that package ships no CLI. + +If you want it self-contained instead, place `tools.yaml` in the client-managed +data directory and use `"--config", "${PLUGIN_DATA}/tools.yaml"`. That is +spec-legal and portable, but the server stays down until you put the file there. + +## Verifying + +```bash +make spec # transport discriminator, closed field sets, placeholder expansion +uvx check-jsonschema \ + --schemafile https://agent-plugins.org/schemas/1.0.0/mcp.schema.json mcp.json +``` 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..a762627 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. Use this for cross-vendor scope and vendor selection; use gcp-architect instead once the design is GCP-only." +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..b6fbc49 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 workloads — models, prompts, tuning and endpoints — on Vertex AI. Covers Model Garden, the Gemini API, custom training, endpoint deployment, 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. Use this for models, training and endpoints; use agent-architect instead for anything agentic — Agent Builder, ADK, Agent Runtime, A2A, AP2 and multi-agent topologies all belong there, not here." +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}"