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
11 changes: 11 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@
"skills": [
"./skills/claude-api"
]
},
{
"name": "vastai-skills",
"description": "Canonical Vast.ai renter, host, and host-support skills",
"source": "./",
"strict": false,
"skills": [
"./skills/vastai",
"./skills/vastai-host",
"./skills/vastai-host-support"
]
}
]
}
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Generated snapshots use byte hashes, so canonical skill files must stay LF-only.
skills/** text eol=lf
81 changes: 81 additions & 0 deletions .github/workflows/sync-vastai-wrapper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Sync Vast.ai skills into a harness wrapper

on:
workflow_call:
inputs:
base-branch:
description: Wrapper branch that receives generated skill updates
required: false
type: string
default: main

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Check out harness wrapper
uses: actions/checkout@v7
with:
ref: ${{ inputs.base-branch }}
fetch-depth: 0
- name: Check out canonical skills
uses: actions/checkout@v7
with:
repository: vast-ai/skills
ref: main
path: .canonical-skills
fetch-depth: 1
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Resolve canonical revision
id: source
shell: bash
run: echo "revision=$(git -C .canonical-skills rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Materialize canonical bundle
run: >-
python .canonical-skills/scripts/sync_vastai_bundle.py
--source-root .canonical-skills
--wrapper .
--repository https://github.com/vast-ai/skills
--revision ${{ steps.source.outputs.revision }}
- name: Validate harness wrapper
run: python scripts/validate_plugin.py
- name: Detect generated changes
id: changes
shell: bash
run: |
if [[ -n "$(git status --porcelain -- skills skills.lock.json)" ]]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Open or update sync pull request
if: steps.changes.outputs.changed == 'true'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REVISION: ${{ steps.source.outputs.revision }}
BASE_BRANCH: ${{ inputs.base-branch }}
run: |
branch="automation/sync-vast-skills"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin "$branch:refs/remotes/origin/$branch" 2>/dev/null || true
git switch -C "$branch"
git add skills skills.lock.json
git commit -m "chore: sync canonical Vast.ai skills to ${REVISION:0:12}"
git push --force-with-lease origin "HEAD:$branch"

body="Automated snapshot of [vast-ai/skills](https://github.com/vast-ai/skills) at \`${REVISION}\`.

The checked-in \`skills/\` tree is generated for harness packaging. Edit the canonical repository instead."
existing="$(gh pr list --head "$branch" --base "$BASE_BRANCH" --json number --jq '.[0].number')"
if [[ -n "$existing" ]]; then
gh pr edit "$existing" --title "chore: sync canonical Vast.ai skills" --body "$body"
else
gh pr create --head "$branch" --base "$BASE_BRANCH" --title "chore: sync canonical Vast.ai skills" --body "$body"
fi
43 changes: 43 additions & 0 deletions .github/workflows/validate-vastai.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Validate Vast.ai skills

on:
pull_request:
paths:
- "bundles/vastai.json"
- "skills/vastai/**"
- "skills/vastai-host/**"
- "skills/vastai-host-support/**"
- "scripts/sync_vastai_bundle.py"
- "scripts/validate_vastai_bundle.py"
- ".github/workflows/validate-vastai.yml"
- ".github/workflows/sync-vastai-wrapper.yml"
- ".claude-plugin/marketplace.json"
- ".gitattributes"
push:
paths:
- "bundles/vastai.json"
- "skills/vastai/**"
- "skills/vastai-host/**"
- "skills/vastai-host-support/**"
- "scripts/sync_vastai_bundle.py"
- "scripts/validate_vastai_bundle.py"
- ".github/workflows/validate-vastai.yml"
- ".github/workflows/sync-vastai-wrapper.yml"
- ".claude-plugin/marketplace.json"
- ".gitattributes"

jobs:
validate:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: python scripts/validate_vastai_bundle.py
- run: python -m py_compile scripts/sync_vastai_bundle.py scripts/validate_vastai_bundle.py
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,40 @@ Many skills in this repo are open source (Apache 2.0). We've also included the d
- [./spec](./spec): The Agent Skills specification
- [./template](./template): Skill template

## Vast.ai maintained skills

The `vastai` bundle is the canonical source for Vast.ai agent guidance across
Claude, Codex, Cursor, and future harnesses:

- [`vastai`](./skills/vastai) — renter and instance lifecycle operations
- [`vastai-host`](./skills/vastai-host) — routine GPU-provider operations
- [`vastai-host-support`](./skills/vastai-host-support) — self-test failure diagnostics and safe support evidence

Harness repositories keep their native manifests, commands, prompts, rules, and
assets. Their packaged `skills/` directories are generated snapshots of this
bundle, pinned by `skills.lock.json`. They must not be edited independently.

The reusable workflow at
`.github/workflows/sync-vastai-wrapper.yml` materializes the latest canonical
bundle and opens a wrapper pull request when the snapshot changes. Wrapper
repositories schedule that workflow and retain their own cross-platform
validation and installation tests.

To sync a local wrapper checkout manually:

```bash
python scripts/sync_vastai_bundle.py \
--wrapper /path/to/vast-codex-plugin \
--revision "$(git rev-parse HEAD)"
```

Claude users can also install the skills-only bundle directly:

```text
/plugin marketplace add vast-ai/skills
/plugin install vastai-skills@anthropic-agent-skills
```

# Try in Claude Code, Claude.ai, and the API

## Claude Code
Expand Down
21 changes: 21 additions & 0 deletions bundles/vastai.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"schema_version": 1,
"name": "vastai",
"description": "Canonical Vast.ai renter, host, and host-support Agent Skills.",
"source_repository": "https://github.com/vast-ai/skills",
"minimum_cli_version": "1.4.2",
"skills": [
{
"name": "vastai",
"path": "skills/vastai"
},
{
"name": "vastai-host",
"path": "skills/vastai-host"
},
{
"name": "vastai-host-support",
"path": "skills/vastai-host-support"
}
]
}
169 changes: 169 additions & 0 deletions scripts/sync_vastai_bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Materialize the canonical Vast.ai skill bundle in a harness wrapper."""

from __future__ import annotations

import argparse
import hashlib
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
BUNDLE_FILE = ROOT / "bundles" / "vastai.json"
REVISION_RE = re.compile(r"^[0-9a-f]{40}$")


def digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()


def git_revision(source_root: Path) -> str:
result = subprocess.run(
["git", "-C", str(source_root), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()


def bundle_files(source_root: Path, bundle: dict[str, object]) -> list[tuple[Path, Path]]:
files: list[tuple[Path, Path]] = []
for skill in bundle["skills"]:
name = skill["name"]
source_dir = source_root / skill["path"]
if not source_dir.is_dir():
raise ValueError(f"missing canonical skill directory: {source_dir}")
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name):
raise ValueError(f"unsafe skill name: {name}")
for source_file in sorted(path for path in source_dir.rglob("*") if path.is_file()):
relative = source_file.relative_to(source_dir)
if any(part in {"__pycache__", ".DS_Store"} for part in relative.parts):
continue
files.append((source_file, Path("skills") / name / relative))
return files


def expected_lock(
source_root: Path,
repository: str,
revision: str,
bundle: dict[str, object],
) -> dict[str, object]:
return {
"schema_version": 1,
"bundle": bundle["name"],
"source": {
"repository": repository,
"revision": revision,
},
"files": [
{
"path": target.as_posix(),
"sha256": digest(source),
}
for source, target in bundle_files(source_root, bundle)
],
}


def check_snapshot(
source_root: Path,
wrapper: Path,
repository: str,
revision: str,
bundle: dict[str, object],
) -> list[str]:
errors: list[str] = []
expected = expected_lock(source_root, repository, revision, bundle)
lock_path = wrapper / "skills.lock.json"
try:
actual_lock = json.loads(lock_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
return [f"invalid or missing {lock_path}: {exc}"]
if actual_lock != expected:
errors.append("skills.lock.json does not match the canonical bundle")

expected_paths = {item["path"] for item in expected["files"]}
for source, target in bundle_files(source_root, bundle):
wrapper_file = wrapper / target
if not wrapper_file.is_file():
errors.append(f"missing generated file: {target.as_posix()}")
elif wrapper_file.read_bytes() != source.read_bytes():
errors.append(f"generated file drifted: {target.as_posix()}")

managed_names = {skill["name"] for skill in bundle["skills"]}
actual_paths = {
path.relative_to(wrapper).as_posix()
for name in managed_names
for path in (wrapper / "skills" / name).rglob("*")
if path.is_file()
}
for extra in sorted(actual_paths - expected_paths):
errors.append(f"unexpected generated file: {extra}")
return errors


def write_snapshot(
source_root: Path,
wrapper: Path,
repository: str,
revision: str,
bundle: dict[str, object],
) -> None:
for skill in bundle["skills"]:
name = skill["name"]
source_dir = source_root / skill["path"]
target_dir = wrapper / "skills" / name
if target_dir.exists():
shutil.rmtree(target_dir)
shutil.copytree(source_dir, target_dir)

lock = expected_lock(source_root, repository, revision, bundle)
(wrapper / "skills.lock.json").write_text(
json.dumps(lock, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", type=Path, default=ROOT)
parser.add_argument("--wrapper", type=Path, required=True)
parser.add_argument("--repository")
parser.add_argument("--revision")
parser.add_argument("--check", action="store_true")
args = parser.parse_args()

source_root = args.source_root.resolve()
wrapper = args.wrapper.resolve()
bundle_path = source_root / "bundles" / "vastai.json"
bundle = json.loads(bundle_path.read_text(encoding="utf-8"))
repository = args.repository or bundle["source_repository"]
revision = args.revision or git_revision(source_root)
if not REVISION_RE.fullmatch(revision):
parser.error("--revision must resolve to a full 40-character lowercase Git SHA")
if source_root == wrapper:
parser.error("--wrapper must not be the canonical source repository")

if args.check:
errors = check_snapshot(source_root, wrapper, repository, revision, bundle)
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print(f"Verified {bundle['name']} snapshot in {wrapper.name} at {revision[:12]}")
return 0

write_snapshot(source_root, wrapper, repository, revision, bundle)
print(f"Synced {bundle['name']} into {wrapper.name} at {revision[:12]}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading