From 5651b64ef4160587154de8d0169df6f98045d4fb Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:26:17 -0400 Subject: [PATCH 1/2] Document agent skill packaging, add AGENTS.md and package-skill skill README, CONTRIBUTING, and docs/ covered MCP servers exclusively, with no mention of agent skills despite 150+ of them already vendored under skills/. Add docs/adding-skills.md, cross-link it from README/index/ CONTRIBUTING, and extend docs/security.md with the skill-scanner equivalent of the existing MCP scanning section. Also add a root AGENTS.md (there was none), and a package-skill skill mirroring package-mcp-server, so future skill-packaging work in this repo has the same guided workflow MCP servers already have. It lives under .claude/skills/ but is also reachable agent-agnostically via a .agents/skills symlink, since these skills aren't Claude Code-specific. Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- .agents/skills | 1 + .claude/skills/package-skill/SKILL.md | 174 ++++++++++++++ .../references/SPEC-YAML-REFERENCE.md | 96 ++++++++ AGENTS.md | 106 +++++++++ CONTRIBUTING.md | 53 ++++- README.md | 7 +- docs/adding-skills.md | 219 ++++++++++++++++++ docs/index.md | 8 + docs/security.md | 48 +++- 9 files changed, 698 insertions(+), 14 deletions(-) create mode 120000 .agents/skills create mode 100644 .claude/skills/package-skill/SKILL.md create mode 100644 .claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md create mode 100644 AGENTS.md create mode 100644 docs/adding-skills.md diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 00000000..454b8427 --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +../.claude/skills \ No newline at end of file diff --git a/.claude/skills/package-skill/SKILL.md b/.claude/skills/package-skill/SKILL.md new file mode 100644 index 00000000..73a0f18b --- /dev/null +++ b/.claude/skills/package-skill/SKILL.md @@ -0,0 +1,174 @@ +--- +name: package-skill +description: Creates spec.yaml configurations for packaging agent skills as OCI artifacts. Use when adding a new agent skill to Dockyard, creating a skills/*/spec.yaml file, or vendoring a third-party skill repository (e.g. from Anthropic, Datadog, HashiCorp, Hugging Face). Not for writing a brand-new skill's own content, or for MCP server packaging (use package-mcp-server for that). +--- + +# Package Agent Skill for Dockyard + +This skill helps you package agent skills for distribution via Dockyard skill artifacts (OCI artifacts published to `ghcr.io/stacklok/dockyard/skills/{name}`). + +Unlike MCP servers — which Dockyard builds into container images from an npm/PyPI/Go package — skills are repackaged directly from a pinned commit in the upstream skill's git repository. There's no build step; `dockhand` clones the repo, validates `SKILL.md`, and repackages that directory as-is. + +## When to Use This Skill + +Use this skill when: +- Adding a new agent skill to Dockyard +- Creating a `skills/{name}/spec.yaml` configuration file +- Vendoring one or more skills from a third-party repository +- The user mentions "package skill", "add skill", "skills spec.yaml", or asks to package skills from a named GitHub org/repo + +## Prerequisites + +- The skill's `SKILL.md` must exist in a public git repository, reachable over HTTPS +- `go build -o build/dockhand ./cmd/dockhand` (or `task build-setup`) for local validate/build +- `task scan-skill-setup` (installs `cisco-ai-skill-scanner` via `uv`) for local scanning + +## Workflow + +### Step 1: Discover the skill(s) + +If packaging from a repo the user names, clone it shallow and find every `SKILL.md`: + +```bash +git clone --depth 1 https://github.com/{org}/{repo} /tmp/{repo} +find /tmp/{repo} -iname SKILL.md +``` + +For each one, read its YAML frontmatter (`name`, `description`, and any `license`/`version` fields) — that's the source of truth for `metadata.description` and for spotting an upstream `license:` field (see Step 5). + +Get the pinned commit SHA once, up front, and reuse it for every skill from that repo: + +```bash +git -C /tmp/{repo} rev-parse HEAD +``` + +### Step 2: Decide names — check for collisions + +`skills/` is a single flat namespace shared by 150+ skills from many upstream sources. Before naming anything: + +```bash +ls skills/ | grep -x '{candidate-name}' +``` + +- If the skill's own upstream name is already specific and unlikely to collide (`claude-api`, `gh-stack`), use it as-is. +- If the repo ships **multiple** skills whose names are generic on their own (e.g. `provider-docs`, `windows-builder`, `push-to-registry`), prefix **all** of them with a short vendor tag: `hashicorp-provider-docs`, `dd-apm` (Datadog), `hf-cli` (Hugging Face). +- Prefer a single flat vendor prefix over splitting by upstream sub-project (e.g. `hashicorp-` alone, not separate `hashicorp-terraform-`/`hashicorp-packer-` prefixes) — splitting produces stutter whenever a skill's own name already contains the sub-project name (`hashicorp-terraform-terraform-test` reads worse than `hashicorp-terraform-test`). +- If there's any ambiguity in naming (flat vendor prefix vs. per-subproject, or no prefix at all), **ask the user** before creating two dozen directories — renaming later means re-running the whole scan/allowlist pass. + +### Step 3: Create spec.yaml + +One directory per skill: `skills/{name}/spec.yaml`. + +```yaml +# {Skill Name} Skill +# Source: {source-repository-url} +# Will publish as: ghcr.io/stacklok/dockyard/skills/{name}:0.1.0 + +metadata: + name: {name} + description: "{copy verbatim from upstream SKILL.md frontmatter description}" + +spec: + repository: "{https-git-clone-url}" + ref: "{commit-sha}" # main as of {date} — pin an exact commit, never a branch + path: "{path-to-skill-dir}" # omit entirely if SKILL.md is at repo root + version: "0.1.0" # ALWAYS 0.1.0 for a new skill — see Step 4 + +provenance: + repository_uri: "{https-git-clone-url}" + repository_ref: "refs/heads/{branch}" +``` + +Long `description` values often need YAML block scalars (`>-` or a quoted +multi-line string) — let a YAML dumper handle escaping rather than hand- +wrapping; malformed quoting is the most common review-round-trip bug here. + +### Step 4: Version — always start at 0.1.0 + +Do **not** copy an upstream release tag (e.g. a repo-wide `v1.0.0`) into `spec.version`, even if one exists. Dockyard owns semver for every vendored skill independently of upstream (see `docs/skill-versioning.md`) specifically because: +- Most upstream skill repos don't tag individual skills at all. +- Even when a repo does cut a release, it's typically a whole-repo tag that doesn't map to any single skill's actual changes — and several individual `SKILL.md` frontmatter blocks often carry their own, much lower, per-skill `version` (e.g. `0.0.1`, `0.1.0`) that would contradict a `1.0.0` Dockyard tag anyway. + +Every new skill starts at `0.1.0`. If this comes up with the user, it's worth surfacing as a quick recommendation rather than silently picking one — but the answer is almost always 0.1.0. + +### Step 5: Validate + +```bash +build/dockhand validate-skill --config skills/{name}/spec.yaml +``` + +Fix any error before moving on — this clones the repo and actually checks `SKILL.md` exists at the given path. + +### Step 6: Scan and triage findings + +```bash +task scan-skill-setup # once per machine +task scan-skill -- skills/{name} +``` + +This is the expensive, judgment-heavy step. Read `scripts/skill-scan/README.md` if you haven't already. What to expect: + +- **Warnings** (below the `HIGH` block threshold) don't fail the task — you can leave them, though citing the intentionally-accepted ones (like `MANIFEST_MISSING_LICENSE` when the upstream repo has a root-level `LICENSE` but no per-skill frontmatter field) in the allowlist keeps the scan summary self-documenting. +- **Blocking findings** (`HIGH`+, unallowlisted) fail the task and must be triaged one by one. + +For each blocking finding, look at its `file_path`/`line_number`/`message` and decide: genuine issue, or false positive? Skill docs are prose- and example-heavy, so the overwhelming majority are false positives from pattern/keyword rules matching on things like: +- Shell variable expansion (`${TOKEN}`, `$HOME`) in documented setup commands +- Documented, vendor-official install one-liners (`curl -L .../release`, `sudo apt-get install`, an official Chocolatey/PowerShell bootstrap) +- Example IP addresses, placeholder credentials (`CLIENT_SECRET="your-secret"`), or attribute names like `password` in schema/code examples +- Words like "exfil", "override", "skip", "kill", "root" appearing in ordinary explanatory prose, not as an executable instruction + +Add each as a `security.allowed_issues` entry, matching by `rule_id` (exact — prefer this) or `category` (broader, only when many distinct rule_ids share one clear rationale). **Every reason must cite the specific matched text and its file:line**, and explain concretely why it isn't a threat — not just "false positive": + +```yaml +security: + allowed_issues: + - rule_id: ATR_2026_00066 + reason: "FP: matched shell variable expansion (\`${TOKEN}\`) in a + documented setup command (SKILL.md:45) — standard shell syntax, + not injected secrets." +``` + +When packaging many skills from the same repo, findings cluster heavily by `rule_id` — collect all blocking findings across the batch first (group by skill + rule_id), write one templated-but-specific reason per rule_id, then customize per skill using that skill's actual matched text. Don't reuse a reason verbatim across skills without checking the cited location actually matches what's in *that* skill — genuinely different constructs can share a rule_id (e.g. an `iex (...)` PowerShell bootstrap vs. a `sudo apt-get install` line both trip the same "documented install command" rule but need different citations). + +Never set `security.insecure_ignore: true` to bypass this — it disables the gate rather than documenting the finding. + +Re-run `task scan-skill -- skills/{name}` until it exits clean. Repeat for every skill in the batch. + +### Step 7: Build (smoke test) + +```bash +build/dockhand build-skill --config skills/{name}/spec.yaml +``` + +No `--push` needed locally — this just proves the artifact packages successfully end to end. Doing this for at least one representative skill in a batch is enough; running it for all of them is optional but cheap. + +### Step 8: Commit + +```bash +git add skills/{name}/spec.yaml +git commit -s -m "Add {name} skill + +Package {name} from {upstream-repo} at {commit-sha}. +Source: {upstream-repo-url}" +``` + +Use `git commit -s` — DCO Signed-off-by is required (see `CONTRIBUTING.md`). For a batch of skills from one repo, one commit covering all of them is fine. + +## Common Issues + +| Issue | Solution | +|-------|----------| +| `validate-skill` fails: SKILL.md not found | Double check `spec.path` — it's relative to the repo root, not to `skills/{name}/` | +| `scan-skill` times out on a large batch | Run it per-skill in a loop rather than one long-running batch command; each clone+scan takes 5-15s | +| Same `rule_id` fires for unrelated reasons across skills | Verify the actual matched text before reusing a reason — don't copy-paste blind | +| Version bump CI failure after editing `spec.ref` later | Run `go run ./cmd/skillversionbump --base origin/main --write` — see `docs/skill-versioning.md` | +| Tempted to set `insecure_ignore: true` | Don't. Triage the finding instead, or ask the user if it's a genuine risk to accept explicitly with a reason | + +## See Also + +- [docs/adding-skills.md](../../../docs/adding-skills.md) — Complete contribution guide +- [docs/skill-versioning.md](../../../docs/skill-versioning.md) — Why version always starts at 0.1.0 +- [docs/security.md](../../../docs/security.md) — Security scanning and attestation model +- `scripts/skill-scan/README.md` — Scanner wrapper scripts and allowlist mechanics +- [internal/skills/spec.go](../../../internal/skills/spec.go) — The actual validated spec.yaml fields +- [references/SPEC-YAML-REFERENCE.md](references/SPEC-YAML-REFERENCE.md) — Full spec.yaml field reference diff --git a/.claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md b/.claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md new file mode 100644 index 00000000..ef765196 --- /dev/null +++ b/.claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md @@ -0,0 +1,96 @@ +# spec.yaml Full Reference (Agent Skills) + +Complete reference for Dockyard agent skill specification files. Fields are +validated by `internal/skills/spec.go` (`LoadSkillSpec`/`validateSkillSpec`) +— that file is authoritative if this reference and the code ever disagree. + +## File Location + +``` +skills/{skill-name}/spec.yaml +``` + +There are no protocol subdirectories for skills (unlike MCP servers' +`npx/`/`uvx/`/`go/`) — everything lives directly under `skills/`. + +## Full Schema + +```yaml +# Comments documenting the skill (optional but conventional) +# Source: https://github.com/{org}/{repo} +# Will publish as: ghcr.io/stacklok/dockyard/skills/{name}:{version} + +metadata: + name: string # Required: skill identifier — should match the + # directory name (lowercase, hyphens) + description: string # Optional but conventional: copy from upstream + # SKILL.md frontmatter `description` + +spec: + repository: string # Required: HTTPS git clone URL. Must be https:// + # — validated, non-HTTPS URLs are rejected. + ref: string # Required: commit SHA, tag, or branch. Always + # use a commit SHA in practice — a moving + # branch/tag breaks reproducibility and confuses + # skillversionbump's diff-based heuristic. + path: string # Optional: subdirectory within the repo + # containing SKILL.md. Omit if SKILL.md is at + # the repo root. + version: string # Required: Dockyard-owned semver, used as the + # OCI tag. New skills always start at "0.1.0" + # regardless of any upstream version/tag — see + # docs/skill-versioning.md. + +provenance: # Optional but conventional + repository_uri: string # Same as spec.repository, restated for clarity + repository_ref: string # The branch the pinned commit came from, e.g. + # "refs/heads/main" + +security: # Optional — omit entirely if the scan is clean + allowed_issues: + - rule_id: string # Exact finding identifier from skill-scanner + # output (preferred — narrowest match) + # category: string # Alternative to rule_id: matches by category + # instead, broader — use only when many + # distinct rule_ids share one clear rationale + reason: string # Required with either rule_id or category. + # Must cite the specific matched text/location + # and explain concretely why it's not a threat. + insecure_ignore: boolean # Default false. NEVER set true to bypass a + # scan failure — it disables the security gate + # entirely rather than documenting findings. + # Reserved for cases the scanner genuinely + # cannot run against (rare). +``` + +## Derived values + +- **OCI tag**: `ghcr.io/stacklok/dockyard/skills/{metadata.name}:{spec.version}` (lowercased name) +- **Git reference URI** (used internally by ToolHive's git resolver): + `git://{host}/{path}[@{ref}][#{path}]`, built from `spec.repository`/`spec.ref`/`spec.path` + +## Fields that do NOT exist (common mistakes) + +- `protocol` — MCP-server-only field, meaningless for skills +- `package` / `spec.package` — MCP-server-only; skills have no package registry +- `args` / `env` — MCP-server-only (baked into the runtime container); skills + don't run anything, they're just files +- `metadata.version` — version lives under `spec.version`, not `metadata` + +## Minimal valid example + +```yaml +metadata: + name: gh-stack + description: "Manage stacked branches and pull requests with the gh-stack GitHub CLI extension" + +spec: + repository: "https://github.com/github/gh-stack" + ref: "14fc42ed9b6c376a53b2f999f138d3bd26dac546" + path: "skills/gh-stack" + version: "0.1.0" + +provenance: + repository_uri: "https://github.com/github/gh-stack" + repository_ref: "refs/heads/main" +``` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..371a9926 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,106 @@ +# AGENTS.md + +Guidance for AI coding agents working in this repository. Human contributors +should read [CONTRIBUTING.md](CONTRIBUTING.md) instead — this file is a +terser, agent-facing summary of the same conventions plus the exact commands +to run. + +## What this repo is + +Dockyard packages two different kinds of things into signed OCI artifacts +published to `ghcr.io/stacklok/dockyard`: + +- **MCP servers** — built from an npm/PyPI/Go package into a container image. + Live under `npx/`, `uvx/`, `go/`. +- **Agent skills** — repackaged directly from a pinned commit in an upstream + git repository (no build step, no code compiled). Live under `skills/`. + +Each of the two has its own CLI subcommand set (`cmd/dockhand`), spec +validation/build logic (MCP server spec types live in +`cmd/dockhand/main.go`; skill spec types live in `internal/skills/`), +Taskfile task family, GitHub Actions workflow, and scanner. Don't assume +something true of one applies to the other — check which one you're +touching first. + +## Adding something + +- New MCP server → [docs/adding-servers.md](docs/adding-servers.md) +- New agent skill → [docs/adding-skills.md](docs/adding-skills.md), or drive + it with the packaged skill at `.claude/skills/package-skill/SKILL.md` + (also reachable agent-agnostically at `.agents/skills/package-skill/`, + a symlink to `.claude/skills/`) + +Both are `spec.yaml`-driven: create `{npx,uvx,go}/{name}/spec.yaml` or +`skills/{name}/spec.yaml` and open a PR. Neither requires writing application +code — this repo has almost no runtime logic of its own beyond the `dockhand` +CLI and the CI scripts that drive it. + +## Build and test + +```bash +go build -o build/dockhand ./cmd/dockhand # or: task build-setup +go test ./... +``` + +There is no lint/format task wired into Taskfile beyond what `go build`/ +`go vet` catch; run `gofmt -l .` before committing Go changes. + +## Local verification commands + +MCP servers: + +```bash +task scan-setup # once per machine: installs mcp-scanner +task build -- {protocol}/{server-name} # generate Dockerfile +task scan -- {protocol}/{server-name} # mcp-scanner +task test-build -- {protocol}/{server-name} # full build + smoke test +./build/dockhand verify-provenance -c {protocol}/{server-name}/spec.yaml -v +``` + +Agent skills: + +```bash +task scan-skill-setup # once per machine: installs skill-scanner +task validate-skill -- skills/{skill-name} # clone + validate SKILL.md, no scan +task scan-skill -- skills/{skill-name} # skill-scanner +task build-skill -- skills/{skill-name} # build OCI artifact (dry run, no push) +``` + +Both scanners are **blocking**: an unallowlisted finding at or above the +block-severity threshold fails CI. Never work around a failing scan by +setting `security.insecure_ignore: true` — triage the finding and add a +`security.allowed_issues` entry with a specific `reason`, or fix the actual +issue. See [docs/security.md](docs/security.md) for the full model. + +## Skill versioning — read before touching `spec.ref` + +Dockyard owns semver for vendored skills independently of upstream via +`spec.version`. Bumping `spec.ref` without bumping `spec.version` fails CI +(`skill-version-check`). If you change `spec.ref` by hand, run: + +```bash +go run ./cmd/skillversionbump --base origin/main --write +``` + +Full policy: [docs/skill-versioning.md](docs/skill-versioning.md). + +## Conventions + +- **Commits**: DCO Signed-off-by trailer required (`git commit -s`). Subject + line imperative mood, ≤50 chars, capitalized, no trailing period. See + [CONTRIBUTING.md](CONTRIBUTING.md#commit-message-guidelines). +- **Naming collisions**: `skills/` is a single flat namespace shared by 150+ + vendored skills. If an upstream repo's skill names are generic on their + own (`provider-docs`, `windows-builder`), prefix all of them with a short + vendor tag (`hashicorp-provider-docs`) rather than splitting per + upstream sub-project — that avoids stutter when a skill's own name already + contains the sub-project name. +- **Don't hand-roll spec.yaml semantics** — read `internal/skills/spec.go` + (skills) or the spec types in `cmd/dockhand/main.go` (MCP servers) for the + fields that are actually validated before inventing new ones. +- **CI workflow gotcha**: `build-skills.yml` only rebuilds *changed* skills + on a normal push/PR, but rebuilds *all* ~160 skills when `cmd/dockhand/`, + `internal/skills/`, or a `stacklok/toolhive` bump in `go.mod`/`go.sum` + changes — because those are the inputs that can alter every built + artifact. Keep that in mind before touching those paths on a whim; it + fans out into a large CI run. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 125b923e..c0c46a74 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,7 @@ Thank you for your interest in contributing to Dockyard! This document helps you - [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities) - [Ways to Contribute](#ways-to-contribute) - [Adding an MCP Server](#adding-an-mcp-server) +- [Adding an Agent Skill](#adding-an-agent-skill) - [Development Setup](#development-setup) - [Pull Request Process](#pull-request-process) - [Commit Message Guidelines](#commit-message-guidelines) @@ -26,6 +27,10 @@ If you think you have found a security vulnerability in Dockyard, please **DO NO The most common contribution is adding a new MCP server to Dockyard. See [Adding an MCP Server](#adding-an-mcp-server) below. +### Add an Agent Skill + +You can also vendor an agent skill (your own, or a third-party one) as a Dockyard skill artifact. See [Adding an Agent Skill](#adding-an-agent-skill) below. + ### Report Bugs Use [GitHub Issues](https://github.com/stacklok/dockyard/issues) to report bugs. Please include: @@ -67,6 +72,31 @@ spec: version: "1.0.0" ``` +## Adding an Agent Skill + +To add an agent skill to Dockyard: + +1. Create a directory: `skills/{skill-name}/` +2. Add a `spec.yaml` configuration file pointing at the skill's source repo, pinned commit, and path +3. Run `task validate-skill -- skills/{skill-name}` and `task scan-skill -- skills/{skill-name}` locally, triaging any security findings into the allowlist +4. Submit a pull request + +**Full guide:** [Adding Skills](docs/adding-skills.md) + +**Quick example:** + +```yaml +metadata: + name: my-skill + description: "What my skill does" + +spec: + repository: "https://github.com/my-org/my-skill-repo" + ref: "abc1234..." # pinned commit SHA + path: "skills/my-skill" # omit if SKILL.md is at repo root + version: "0.1.0" +``` + ## Development Setup ### Prerequisites @@ -104,11 +134,17 @@ go test ./... 1. **Fork and clone** the repository 2. **Create a branch** for your changes 3. **Make your changes** with clear, focused commits -4. **Test locally** if adding an MCP server: - ```bash - task build -- {protocol}/{server-name} - task scan -- {protocol}/{server-name} - ``` +4. **Test locally**: + - If adding an MCP server: + ```bash + task build -- {protocol}/{server-name} + task scan -- {protocol}/{server-name} + ``` + - If adding an agent skill: + ```bash + task validate-skill -- skills/{skill-name} + task scan-skill -- skills/{skill-name} + ``` 5. **Submit a PR** with a clear description ### PR Requirements @@ -124,6 +160,13 @@ Include in your PR description: - Link to the package registry (npm/PyPI) - Link to the source repository +### For Agent Skill PRs + +Include in your PR description: +- What the skill does +- Link to the source repository and the pinned commit +- A brief note on any `security.allowed_issues` entries you added and why + ## Commit Message Guidelines Follow [Chris Beams' guidelines](https://chris.beams.io/posts/git-commit/): diff --git a/README.md b/README.md index b4055771..a581e906 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![Build Status](https://github.com/stacklok/dockyard/actions/workflows/build-containers.yml/badge.svg)](https://github.com/stacklok/dockyard/actions/workflows/build-containers.yml) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -**A centralized repository for packaging Model Context Protocol (MCP) servers into secure, verified containers.** +**A centralized repository for packaging Model Context Protocol (MCP) servers and agent skills into secure, verified OCI artifacts.** -Dockyard automatically builds, scans, and publishes container images for MCP servers. Every container is security-scanned, signed with Sigstore, and includes full build provenance. +Dockyard automatically builds, scans, and publishes container images for MCP servers, and OCI skill artifacts for agent skills. Every artifact is security-scanned, signed with Sigstore, and includes full build provenance. ## Quick Start @@ -29,6 +29,7 @@ docker run -it ghcr.io/stacklok/dockyard/npx/context7:2.1.0 |--------------|---------| | **Use Dockyard containers** | [Getting Started](docs/getting-started.md) | | **Add my MCP server** | [Adding MCP Servers](docs/adding-servers.md) | +| **Package an agent skill** | [Adding Skills](docs/adding-skills.md) | | **Understand the security model** | [Security Overview](docs/security.md) | | **Verify attestations** | [Container Attestations](docs/attestations.md) | | **Check package provenance** | [Package Provenance](docs/provenance.md) | @@ -43,6 +44,8 @@ docker run -it ghcr.io/stacklok/dockyard/npx/context7:2.1.0 Browse available servers: [npx/](npx/) | [uvx/](uvx/) | [go/](go/) +Browse available skills: [skills/](skills/) + ## Add Your MCP Server Create a `spec.yaml` in the appropriate directory and submit a PR: diff --git a/docs/adding-skills.md b/docs/adding-skills.md new file mode 100644 index 00000000..5e72751e --- /dev/null +++ b/docs/adding-skills.md @@ -0,0 +1,219 @@ +# Adding Skills to Dockyard + +This guide walks you through packaging and contributing an agent skill +to Dockyard. + +## Overview + +Unlike MCP servers (built from an npm/PyPI/Go package into a container), +skills are packaged directly from a git repository: Dockyard clones the +upstream repo at a pinned commit, validates the `SKILL.md` at the given +path, and repackages that directory as an OCI artifact. + +Adding a skill is simple: +1. Create a `spec.yaml` configuration file +2. Submit a pull request +3. CI/CD automatically validates, scans, and publishes the skill artifact + +## Directory Structure + +``` +skills/{skill-name}/spec.yaml +``` + +There is a single `skills/` directory (no protocol subdirectories like +`npx/`/`uvx/`/`go/` — those apply only to MCP servers). + +### Naming + +Pick `{skill-name}` to be collision-safe in a single flat namespace shared +by 150+ skills from many upstream sources: + +- If the upstream skill's own name is already specific (`claude-api`, + `gh-stack`), use it as-is. +- If the upstream repo ships several skills whose names are generic on + their own (e.g. `provider-docs`, `windows-builder`), prefix all of them + with a short vendor tag, e.g. `hashicorp-provider-docs`, + `dd-apm` (Datadog), `hf-cli` (Hugging Face). Prefer a single flat prefix + over splitting by upstream sub-project — it avoids awkward stutter when + a skill's own name already contains the sub-project name (e.g. + `hashicorp-terraform-test`, not `hashicorp-terraform-terraform-test`). +- Check for an existing name collision before picking one: + `ls skills/ | grep -x '{candidate-name}'`. + +## spec.yaml Reference + +```yaml +# {Skill Name} Skill +# Source: {source-repository-url} +# Will publish as: ghcr.io/stacklok/dockyard/skills/{name}:{version} + +metadata: + name: {skill-name} # Required: must match the directory name + description: "{brief description}" # Optional but recommended: copy from the + # upstream SKILL.md frontmatter `description` + +spec: + repository: "{https-git-clone-url}" # Required: HTTPS clone URL + ref: "{commit-sha}" # Required: pinned commit (not a moving + # branch/tag — see "Pinning the ref" below) + path: "{path-to-skill-dir}" # Optional: subdirectory containing + # SKILL.md; omit if SKILL.md is at repo root + version: "0.1.0" # Required: Dockyard-owned semver — + # see docs/skill-versioning.md + +provenance: + repository_uri: "{https-git-clone-url}" + repository_ref: "refs/heads/{branch}" # The branch/ref the pinned commit came from + +security: + allowed_issues: + - rule_id: "{RULE_ID}" # From the skill-scanner report + reason: "FP: ..." # Why this finding is a false positive + # or an accepted risk — see "Security + # Scanning" below +``` + +### Pinning the ref + +`spec.ref` must be a commit SHA, not a branch or tag — this is what makes +the build reproducible. Get the current HEAD of the branch you want to +track: + +```bash +git ls-remote https://github.com/{org}/{repo} HEAD +``` + +Renovate keeps `spec.ref` current automatically once the skill is added +(see `renovate.json`); you generally only need to pin it once, at +creation time. + +### version + +Dockyard owns the semver for every vendored skill independently of +upstream — most upstream skill repos don't tag individual skills, and even +when they cut a repo-wide release, it doesn't map cleanly onto one skill's +changes. Start a new skill at `0.1.0` regardless of any upstream version or +release tag. See [Skill Versioning](skill-versioning.md) for the full +policy and the tooling that bumps this automatically as `spec.ref` +advances. + +## Local Testing + +Build the CLI once: + +```bash +task build-setup +``` + +Then, for a given skill: + +```bash +# Validate spec.yaml and SKILL.md (fast, no scan) +task validate-skill -- skills/{skill-name} + +# Run skill-scanner and apply the security allowlist +task scan-skill -- skills/{skill-name} + +# Build the OCI artifact locally (dry run, no push) +task build-skill -- skills/{skill-name} + +# Build and push (requires registry auth — CI does this, not typically local) +PUSH=true task build-skill -- skills/{skill-name} +``` + +`task scan-skill` requires the scanner once per machine: + +```bash +task scan-skill-setup # uv tool install cisco-ai-skill-scanner +``` + +## Security Scanning + +Every skill is scanned with +[Cisco AI Defense skill-scanner](https://github.com/cisco-ai-defense/skill-scanner) +before packaging. This is **blocking**: any finding at or above `HIGH` +severity that isn't allowlisted fails the build. + +Skill documentation is prose-heavy and full of code examples, so +keyword/pattern rules produce a lot of false positives — shell variable +expansion in documented setup commands (`${TOKEN}`, `$HOME`), install +one-liners (`curl -L .../pup`, `sudo apt-get install`, a vendor's official +Chocolatey/PowerShell bootstrap), example IP addresses, and placeholder +credential values in setup docs are the most common triggers, not real +threats. + +When `task scan-skill` reports an unallowlisted finding: + +1. Read the finding's `file_path`/`line_number` in the skill's actual + source (clone it at the pinned `ref` if you need full context). +2. Decide if it's a genuine issue or a false positive / accepted risk. +3. If it's a false positive or an accepted risk, add it to + `security.allowed_issues` in the skill's `spec.yaml`, matching by + `rule_id` (exact) or `category` (broader). Every entry needs a `reason` + that cites the specific matched text/location and explains why it's + safe — see any existing `skills/*/spec.yaml` for the house style, e.g.: + + ```yaml + security: + allowed_issues: + - rule_id: ATR_2026_00066 + reason: "FP: matched shell variable expansion (`${TOKEN}`) in a + documented setup command (SKILL.md:45) — standard shell syntax, + not injected secrets." + ``` + +4. Re-run `task scan-skill -- skills/{skill-name}` until it passes. + +Never set `security.insecure_ignore: true` to work around a scan failure — +it disables the gate entirely rather than documenting why each finding is +safe. See `scripts/skill-scan/README.md` for the scanner wrapper scripts +and `scripts/skill-scan/global_allowed_issues.yaml` for issues allowlisted +across every skill (promote a per-skill entry there only once you've seen +the same false positive recur across unrelated skills). + +## What CI Does + +On every PR touching `skills/**/*.yaml`, `.github/workflows/build-skills.yml`: + +1. **Validates** the spec and `SKILL.md` (`validate-skills` job) +2. **Scans** the pinned source with skill-scanner and applies the + allowlist (`skill-security-scan` job) — this is the blocking security + gate +3. **Builds** the OCI artifact as a dry run (no push on PRs) + +On merge to `main`, it additionally pushes the artifact to +`ghcr.io/stacklok/dockyard/skills/{name}:{version}`, signs it with +Cosign, and attests SBOM, build provenance, and a SCAI-format security +scan predicate — the same supply-chain guarantees MCP server containers +get. See [Security Overview](security.md) and +[Container Attestations](attestations.md). + +A change to `spec.ref` without a corresponding `spec.version` bump fails +the `skill-version-check` CI job — see +[Skill Versioning](skill-versioning.md) for why and how the bump is +computed. + +## Commit and PR + +```bash +git add skills/{skill-name}/spec.yaml +git commit -s -m "Add {skill-name} skill + +Package {skill-name} from {upstream-repo} at {commit-sha}. +Source: {upstream-repo-url}" +``` + +Include in your PR description what the skill does and a link to its +upstream source, same as an MCP server PR (see +[CONTRIBUTING.md](../CONTRIBUTING.md)). + +## See Also + +- [Skill Versioning](skill-versioning.md) — semver policy and auto-bump tooling +- [Security Overview](security.md) — scanning and attestation model +- `.claude/skills/package-skill/` (also at `.agents/skills/package-skill/`) — + a skill that automates this entire workflow (spec.yaml authoring, + scanning, allowlist triage) +- [Adding MCP Servers](adding-servers.md) — the equivalent guide for MCP + server containers diff --git a/docs/index.md b/docs/index.md index 7bd0c09b..fed18cff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,13 @@ You're an MCP server author who wants to package and distribute your server via - [Adding MCP Servers](adding-servers.md) - Step-by-step contribution guide +### I want to package an agent skill + +You want to vendor an agent skill (your own, or a third-party one) as a Dockyard skill artifact. + +- [Adding Skills](adding-skills.md) - Step-by-step contribution guide +- [Skill Versioning](skill-versioning.md) - Semver policy and auto-bump tooling for skills + ### I want to understand the security model You're evaluating Dockyard for security compliance or want to verify container integrity. @@ -30,6 +37,7 @@ You're evaluating Dockyard for security compliance or want to verify container i |----------|-------------| | [Getting Started](getting-started.md) | How to use Dockyard containers | | [Adding MCP Servers](adding-servers.md) | Contributing your MCP server | +| [Adding Skills](adding-skills.md) | Contributing an agent skill | | [Security Overview](security.md) | Security model and scanning | | [Attestations](attestations.md) | Container attestation details | | [Provenance](provenance.md) | Package provenance verification | diff --git a/docs/security.md b/docs/security.md index 9e2c6c41..ffc99171 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,18 +1,18 @@ # Dockyard Security Overview -Dockyard provides multiple layers of security to ensure safe distribution of MCP server containers. +Dockyard provides multiple layers of security to ensure safe distribution of MCP server containers and agent skill artifacts. ## Security Guarantees -When you use a Dockyard container, you can be confident that: +When you use a Dockyard container or skill artifact, you can be confident that: -1. **Source Integrity** - The image was built from the exact source code in this repository +1. **Source Integrity** - The image/artifact was built from the exact source code in this repository (for skills, from the exact upstream commit pinned in `spec.ref`) 2. **Build Transparency** - Full build provenance is available and verifiable -3. **MCP Security Scanning** - The MCP server was scanned for vulnerabilities before packaging -4. **Container Vulnerability Scanning** - Images are scanned with Trivy for CVEs, secrets, and misconfigurations +3. **Security Scanning** - MCP servers are scanned with mcp-scanner and skills are scanned with skill-scanner before packaging +4. **Container Vulnerability Scanning** - MCP server images are additionally scanned with Trivy for CVEs, secrets, and misconfigurations 5. **Dependency Tracking** - Complete SBOM is available for vulnerability management -6. **Non-repudiation** - Signatures prove the image came from our CI/CD pipeline -7. **Continuous Monitoring** - Weekly scans catch newly disclosed vulnerabilities +6. **Non-repudiation** - Signatures prove the image/artifact came from our CI/CD pipeline +7. **Continuous Monitoring** - Weekly scans catch newly disclosed vulnerabilities in MCP server containers ## MCP Security Scanning @@ -62,6 +62,33 @@ Each allowed issue must include: - `code` - The issue code from mcp-scanner - `reason` - Clear explanation of why it's acceptable +## Agent Skill Security Scanning + +All agent skills are scanned using [Cisco AI Defense skill-scanner](https://github.com/cisco-ai-defense/skill-scanner) at the pinned commit before packaging. This scan is **blocking** at `HIGH` severity and above — skills with unallowlisted findings at or above that threshold cannot be packaged. + +### What We Scan For + +Skill-scanner runs pattern-based (YARA/ATR) and prompt-injection rule packs, plus optional LLM-based semantic and behavioral (AST/taint) analysis, looking for the same broad categories as mcp-scanner (prompt injection, tool/agent poisoning, credential harvesting, PII exposure) applied to a skill's `SKILL.md` and reference files instead of MCP tool descriptions. + +### Allowing Known Issues + +Skill documentation is prose- and example-heavy, so pattern rules produce many false positives (shell variable expansion in documented setup commands, code-fence language tokens, example IP addresses, placeholder credentials). Add them to the allowlist in the skill's `spec.yaml`, matching by `rule_id` (exact) or `category` (broader): + +```yaml +security: + allowed_issues: + - rule_id: ATR_2026_00066 + reason: "FP: matched shell variable expansion (`${TOKEN}`) in a + documented setup command (SKILL.md:45) — standard shell syntax, + not injected secrets." +``` + +Each allowed issue must include: +- `rule_id` (or `category`) - The finding identifier from skill-scanner +- `reason` - Clear explanation, citing the specific matched text/location, of why it's a false positive or an accepted risk + +See [Adding Skills](adding-skills.md#security-scanning) for the full workflow, and `scripts/skill-scan/README.md` for the scanner wrapper scripts. + ## Container Vulnerability Scanning Built containers are scanned with [Trivy](https://trivy.dev/) for: @@ -136,6 +163,13 @@ cosign verify-attestation \ For detailed attestation schemas and policy examples, see [Container Attestations](attestations.md). +> Skill artifacts get the same SBOM, build provenance, and SCAI security-scan +> attestations, signed by the `build-skills.yml` workflow instead of +> `build-containers.yml` — substitute that workflow name in the +> `--certificate-identity-regexp` above and the artifact reference with +> `ghcr.io/stacklok/dockyard/skills/{name}:{version}`. See +> [Adding Skills](adding-skills.md#what-ci-does). + ## Package Provenance Dockyard verifies package provenance for npm and PyPI packages before building: From 84ff863745d6e3362cf5b992478ce0c202ae0e71 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:29:39 -0400 Subject: [PATCH 2/2] Document skill licensing and track Renovate branches --- .claude/skills/package-skill/SKILL.md | 33 ++++++++++++++--- .../references/SPEC-YAML-REFERENCE.md | 7 ++-- AGENTS.md | 5 +++ CONTRIBUTING.md | 1 + docs/adding-skills.md | 36 ++++++++++++++----- renovate.json | 3 +- 6 files changed, 68 insertions(+), 17 deletions(-) diff --git a/.claude/skills/package-skill/SKILL.md b/.claude/skills/package-skill/SKILL.md index 73a0f18b..0198e3cc 100644 --- a/.claude/skills/package-skill/SKILL.md +++ b/.claude/skills/package-skill/SKILL.md @@ -27,14 +27,39 @@ Use this skill when: ### Step 1: Discover the skill(s) -If packaging from a repo the user names, clone it shallow and find every `SKILL.md`: +If packaging from a repo the user names, clone it shallow and enumerate candidate `SKILL.md` files: ```bash git clone --depth 1 https://github.com/{org}/{repo} /tmp/{repo} find /tmp/{repo} -iname SKILL.md ``` -For each one, read its YAML frontmatter (`name`, `description`, and any `license`/`version` fields) — that's the source of truth for `metadata.description` and for spotting an upstream `license:` field (see Step 5). +Treat the results as candidates, not an automatic package list: exclude test +fixtures, examples, and templates that are not intended for end users. For each +real skill, read its YAML frontmatter (`name`, `description`, and any +`license`/`version` fields). The upstream description is the source of truth +for `metadata.description`. + +#### Verify redistribution rights + +Before creating any specs, find an explicit license that covers the skill +content. Check the skill directory and repository root for `LICENSE`/`COPYING` +files and inspect any SPDX-style `license:` value in `SKILL.md` frontmatter. +The license must grant the rights needed to copy, modify, and redistribute the +skill in a public OCI artifact. + +Public source is not the same as open source. A repository with no license is +not eligible for packaging unless the copyright holder separately grants the +necessary redistribution rights. Likewise, a link to product, API, developer, +or website terms is not sufficient unless those terms explicitly license the +repository content for redistribution. If the license is missing, ambiguous, +or non-redistributable, stop and surface the issue to the user rather than +creating specs. + +Record the license and where it was found in the PR description. If +skill-scanner later reports `MANIFEST_MISSING_LICENSE` because the license is +at the repository root rather than in `SKILL.md`, cite that verified license in +the allowlist reason. Get the pinned commit SHA once, up front, and reuse it for every skill from that repo: @@ -70,13 +95,13 @@ metadata: spec: repository: "{https-git-clone-url}" - ref: "{commit-sha}" # main as of {date} — pin an exact commit, never a branch + ref: "{commit-sha}" # {branch} as of {date} — pin an exact commit, never a branch path: "{path-to-skill-dir}" # omit entirely if SKILL.md is at repo root version: "0.1.0" # ALWAYS 0.1.0 for a new skill — see Step 4 provenance: repository_uri: "{https-git-clone-url}" - repository_ref: "refs/heads/{branch}" + repository_ref: "refs/heads/{branch}" # Renovate follows this branch ``` Long `description` values often need YAML block scalars (`>-` or a quoted diff --git a/.claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md b/.claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md index ef765196..ce027cff 100644 --- a/.claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md +++ b/.claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md @@ -41,10 +41,11 @@ spec: # regardless of any upstream version/tag — see # docs/skill-versioning.md. -provenance: # Optional but conventional +provenance: # Optional to the parser; required for Renovate + # to keep spec.ref current automatically repository_uri: string # Same as spec.repository, restated for clarity - repository_ref: string # The branch the pinned commit came from, e.g. - # "refs/heads/main" + repository_ref: string # The branch the pinned commit came from and that + # Renovate should follow, e.g. "refs/heads/main" security: # Optional — omit entirely if the scan is clean allowed_issues: diff --git a/AGENTS.md b/AGENTS.md index 371a9926..9cc37e6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,6 +95,11 @@ Full policy: [docs/skill-versioning.md](docs/skill-versioning.md). vendor tag (`hashicorp-provider-docs`) rather than splitting per upstream sub-project — that avoids stutter when a skill's own name already contains the sub-project name. +- **Third-party skill licenses**: public source is not automatically open + source. Before packaging a skill, verify an explicit upstream license grants + redistribution rights. Product or developer terms do not substitute for a + repository-content license unless they explicitly grant those rights; stop + if the license is missing or ambiguous. - **Don't hand-roll spec.yaml semantics** — read `internal/skills/spec.go` (skills) or the spec types in `cmd/dockhand/main.go` (MCP servers) for the fields that are actually validated before inventing new ones. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0c46a74..83f345d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -165,6 +165,7 @@ Include in your PR description: Include in your PR description: - What the skill does - Link to the source repository and the pinned commit +- The license permitting redistribution and where it is declared upstream - A brief note on any `security.allowed_issues` entries you added and why ## Commit Message Guidelines diff --git a/docs/adding-skills.md b/docs/adding-skills.md index 5e72751e..a50d2a52 100644 --- a/docs/adding-skills.md +++ b/docs/adding-skills.md @@ -41,6 +41,25 @@ by 150+ skills from many upstream sources: - Check for an existing name collision before picking one: `ls skills/ | grep -x '{candidate-name}'`. +### Licensing + +Before packaging a third-party skill, verify that an explicit license covers +the skill content and permits copying, modification, and redistribution in a +public OCI artifact. Check both the skill directory and repository root for a +`LICENSE`/`COPYING` file, plus any SPDX-style `license:` field in `SKILL.md`. + +Publicly accessible source is not automatically open source. Do not package a +repository with no license unless the copyright holder has separately granted +the required redistribution rights. A link to product, API, developer, or +website terms is not enough unless those terms explicitly license the +repository content for redistribution. If the license is missing, ambiguous, +or non-redistributable, stop and resolve that before creating the spec. + +Include the license and its location in the PR description. When the repository +has a root license but the individual `SKILL.md` does not, skill-scanner may +report `MANIFEST_MISSING_LICENSE`; an allowlist reason should cite the verified +repository license. + ## spec.yaml Reference ```yaml @@ -64,7 +83,7 @@ spec: provenance: repository_uri: "{https-git-clone-url}" - repository_ref: "refs/heads/{branch}" # The branch/ref the pinned commit came from + repository_ref: "refs/heads/{branch}" # Branch Renovate should follow security: allowed_issues: @@ -77,16 +96,16 @@ security: ### Pinning the ref `spec.ref` must be a commit SHA, not a branch or tag — this is what makes -the build reproducible. Get the current HEAD of the branch you want to -track: +the build reproducible. Resolve the exact branch named by +`provenance.repository_ref`: ```bash -git ls-remote https://github.com/{org}/{repo} HEAD +git ls-remote https://github.com/{org}/{repo} refs/heads/{branch} ``` -Renovate keeps `spec.ref` current automatically once the skill is added -(see `renovate.json`); you generally only need to pin it once, at -creation time. +Renovate reads that branch name from `provenance.repository_ref` and keeps +`spec.ref` current automatically once the skill is added (see +`renovate.json`); you generally only need to pin it once, at creation time. ### version @@ -205,7 +224,8 @@ Source: {upstream-repo-url}" ``` Include in your PR description what the skill does and a link to its -upstream source, same as an MCP server PR (see +upstream source, pinned commit, and redistribution license, same as an MCP +server PR (see [CONTRIBUTING.md](../CONTRIBUTING.md)). ## See Also diff --git a/renovate.json b/renovate.json index 7f257097..5ba3a8b7 100644 --- a/renovate.json +++ b/renovate.json @@ -80,9 +80,8 @@ "/^skills/.*/spec\\.ya?ml$/" ], "matchStrings": [ - "spec:\\s*\\n\\s*repository:\\s*[\"']?https://github\\.com/(?[^\"'\\s]+?)(?:\\.git)?\\s*[\"']?\\s*(?:#.*)?\\n\\s*ref:\\s*[\"']?(?[0-9a-f]{40})[\"']?" + "spec:\\s*\\n\\s*repository:\\s*[\"']?https://github\\.com/(?[^\"'\\s]+?)(?:\\.git)?\\s*[\"']?\\s*(?:#.*)?\\n\\s*ref:\\s*[\"']?(?[0-9a-f]{40})[\"']?[\\s\\S]*?\\n\\s*repository_ref:\\s*[\"']?refs/heads/(?[^\"'\\s#]+)[\"']?" ], - "currentValueTemplate": "main", "datasourceTemplate": "git-refs", "packageNameTemplate": "https://github.com/{{{depName}}}" }