Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills
199 changes: 199 additions & 0 deletions .claude/skills/package-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
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 enumerate candidate `SKILL.md` files:

```bash
git clone --depth 1 https://github.com/{org}/{repo} /tmp/{repo}
find /tmp/{repo} -iname SKILL.md
```

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:

```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}" # {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}" # Renovate follows this 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
97 changes: 97 additions & 0 deletions .claude/skills/package-skill/references/SPEC-YAML-REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 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 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 and that
# Renovate should follow, 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"
```
Loading
Loading