diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 87cdc37..43e3e9a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,14 +1,15 @@ { "name": "vast-ai", + "description": "Official Vast.ai plugins for GPU rental and host operations.", "owner": { "name": "Vast.ai", - "url": "https://vast.cloud.ai" + "url": "https://vast.ai" }, "plugins": [ { "name": "vastai", "source": "./", - "description": "Manage Vast.ai GPU instances, volumes, serverless endpoints, and billing from Claude Code. Natural-language driven via the bundled vastai skill, with five slash commands for hot-path operations." + "description": "Manage Vast.ai GPU rentals and host operations from Claude Code, including self-test diagnostics and redacted support bundles." } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 14f80bf..71be5f1 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "vastai", - "version": "0.1.0", - "description": "Manage Vast.ai GPU instances, volumes, serverless endpoints, and billing from Claude Code. Natural-language driven via the bundled vastai skill, with five slash commands for hot-path operations.", + "version": "0.2.0", + "description": "Manage Vast.ai GPU rentals and host operations from Claude Code, including self-test diagnostics and redacted support bundles. Includes three skills and five renter slash commands.", "author": { "name": "Vast.ai", "url": "https://vast.ai" @@ -9,6 +9,5 @@ "homepage": "https://vast.ai", "repository": "https://github.com/vast-ai/vast-claude-plugin", "license": "MIT", - "logo": "assets/logo.svg", - "keywords": ["gpu", "cloud", "ml", "vast", "vastai", "instances", "serverless"] + "keywords": ["gpu", "cloud", "ml", "vast", "vastai", "instances", "serverless", "host", "diagnostics"] } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..43c7821 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Generated snapshots use byte hashes; prevent checkout-time CRLF drift. +skills/** text eol=lf +skills.lock.json text eol=lf diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml new file mode 100644 index 0000000..76ef64a --- /dev/null +++ b/.github/workflows/sync-skills.yml @@ -0,0 +1,17 @@ +name: Sync canonical Vast.ai skills + +on: + schedule: + - cron: "17 4 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + uses: vast-ai/skills/.github/workflows/sync-vastai-wrapper.yml@main + with: + base-branch: main + secrets: inherit diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..06b2434 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,21 @@ +name: Plugin validation + +on: + pull_request: + push: + +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" + - name: Validate manifests and skills + run: python scripts/validate_plugin.py diff --git a/README.md b/README.md index 651e9f2..1f1f1af 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ A [Claude Code](https://claude.com/claude-code) plugin for [Vast.ai](https://vas The plugin combines two layers: - **Five slash commands** wrap the highest-frequency renter operations with safe defaults. -- **Two bundled skills** give Claude full command-reference knowledge of the `vastai` CLI, so anything the slash commands don't cover works through natural language: +- **Three bundled skills** give Claude focused knowledge of the `vastai` CLI, so anything the slash commands don't cover works through natural language: - **`vastai`** — renter operations: search and launch instances, SSH, copy, logs, exec, destroy, volumes, serverless, env vars, billing. - **`vastai-host`** — GPU provider operations: list/unlist machines, pricing, maintenance windows, self-tests, earnings, marketplace metrics. Auto-loads on host-intent prompts. + - **`vastai-host-support`** — host self-test failures, `dump-logs`, redacted support bundles, and safe diagnostic evidence collection. ## Slash commands @@ -17,7 +18,7 @@ The plugin combines two layers: | `/vastai:status [instance-id]` | Snapshot of a single instance or all of yours. Flags terminal states (`exited`, `unknown`, `offline`) so polling loops actually terminate. | | `/vastai:cost` | Account balance, current burn rate ($/hr across active instances), and projected 24-hour spend. Useful as a pre-flight check before launching anything large. | | `/vastai:search [filter]` | Finds the cheapest **rentable** offers matching a filter, sorted by total $/hr. Recognises `"spot"` / `"bid"` in the filter and switches to interruptible bid-mode pricing automatically. | -| `/vastai:launch [image] [--disk N] [--label STR]` | Launches an offer with sensible defaults: `pytorch/pytorch:@vastai-automatic-tag`, 20 GB disk, direct SSH, JSON output. Parses the returned `new_contract` ID so subsequent commands can reference it. | +| `/vastai:launch [image] [--disk N] [--label STR]` | Launches an offer with sensible defaults: `vastai/pytorch:@vastai-automatic-tag`, 20 GB disk, direct SSH, JSON output. Parses the returned `new_contract` ID so subsequent commands can reference it. | Every `vastai` invocation includes `--raw` so responses come back as parseable JSON. @@ -45,14 +46,24 @@ Anything outside the slash-command set works conversationally. The right skill a > > *"What's the going rate for RTX 4090s in the US right now?"* -To force a skill to load explicitly, mention it by name (*"using the vastai skill, …"* or *"using the vastai-host skill, …"*). +**Host support prompts** load `vastai-host-support`: + +> *"My host self-test failed. Create a support bundle and explain what is safe to share."* + +To force a skill to load explicitly, mention `vastai`, `vastai-host`, or `vastai-host-support` by name. + +## Shared skill source + +The three directories under `skills/` are generated snapshots of the canonical skills in [`vast-ai/skills`](https://github.com/vast-ai/skills). `skills.lock.json` pins the exact canonical commit and the SHA-256 digest of every generated file, and validation rejects local drift. + +Make skill-content changes in `vast-ai/skills`, not in this wrapper. A scheduled workflow checks the canonical repository and opens an update PR when its bundle changes. Claude-specific commands and manifests remain in this repository. ## Install ### Prerequisites ```bash -pip install vastai +pip install "vastai>=1.4.2" ``` ### From the Claude Plugins marketplace (recommended) @@ -81,6 +92,7 @@ You'll be prompted for an API key from . T ``` vast-claude-plugin/ ├── .claude-plugin/plugin.json # marketplace manifest +├── .github/workflows/sync-skills.yml # updates the generated skill snapshot ├── commands/ │ ├── setup.md # /vastai:setup │ ├── status.md # /vastai:status @@ -88,8 +100,10 @@ vast-claude-plugin/ │ ├── search.md # /vastai:search │ └── launch.md # /vastai:launch ├── skills/ -│ ├── vastai/SKILL.md # renter skill -│ └── vastai-host/SKILL.md # GPU provider / host skill +│ ├── vastai/ # generated renter skill +│ ├── vastai-host/ # generated GPU provider / host skill +│ └── vastai-host-support/ # generated host support skill +├── skills.lock.json # canonical revision and file digests └── TESTING.md # acceptance test plan ``` diff --git a/SUBMISSION.md b/SUBMISSION.md index a9a6919..105b981 100644 --- a/SUBMISSION.md +++ b/SUBMISSION.md @@ -6,8 +6,8 @@ Claude Code's plugin system supports three install paths: the **Anthropic-manage - [ ] `.claude-plugin/plugin.json` exists with `name` (kebab-case) - [ ] `version` (semver) — bump on every release; users only get updates when this changes -- [ ] `description`, `author`, `license`, `homepage`, `repository`, `logo` all set -- [ ] `skills/vastai/SKILL.md` with YAML frontmatter (`name`, `description`) +- [ ] `description`, `author`, `license`, `homepage`, and `repository` all set +- [ ] All three skill directories contain `SKILL.md` with YAML frontmatter (`name`, `description`) - [ ] Every file under `commands/` has frontmatter (`description`, `argument-hint`, `allowed-tools`) - [ ] `LICENSE` file at repo root - [ ] Public Git repo diff --git a/TESTING.md b/TESTING.md index 1239772..d23f652 100644 --- a/TESTING.md +++ b/TESTING.md @@ -16,7 +16,7 @@ A runbook to verify the plugin end-to-end. Behavioral phases (1–4) are run ins ```bash # 1. vastai CLI -vastai --version # → 1.0.13 or newer +vastai --version # → 1.4.2 or newer # 2. Vast API key in env (must be set BEFORE claude was started) echo "${VAST_API_KEY:?must export VAST_API_KEY first}" @@ -52,7 +52,7 @@ cat > "$OUT/results.md" </dev/null && echo "p5.manifest PASS" || echo "p5.manifest FAIL" -# Both skills present +# All three skills present [ -f "$PLUGIN/skills/vastai/SKILL.md" ] && echo "p5.skill-renter PASS" || echo "p5.skill-renter FAIL" [ -f "$PLUGIN/skills/vastai-host/SKILL.md" ] && echo "p5.skill-host PASS" || echo "p5.skill-host FAIL" +[ -f "$PLUGIN/skills/vastai-host-support/SKILL.md" ] && echo "p5.skill-host-support PASS" || echo "p5.skill-host-support FAIL" # All 5 slash commands present for cmd in setup status cost search launch; do diff --git a/TEST_PLAN.md b/TEST_PLAN.md index 08c99e3..46b3ca0 100644 --- a/TEST_PLAN.md +++ b/TEST_PLAN.md @@ -15,7 +15,7 @@ Pre-flight: ```bash echo "${VAST_API_KEY:?must export VAST_API_KEY first}" # set BEFORE launching claude -vastai --version # 1.0.13+ +vastai --version # 1.4.2+ ``` Confirm the plugin loaded by typing `/vast:` in chat — autocomplete should show `setup`, `cost`, `status`, `launch`, `search`. @@ -27,9 +27,9 @@ A passing run shows the agent reads from the skill and uses the correct CLI shap - **Correct flags first try.** Positional args where the docs say positional; underscores vs hyphens matching the actual command. - **No invented commands.** Agent never types `vastai show templates`, `vastai bid`, or similar non-existent subcommands. - **Surfaces server responses.** On 4xx/5xx, agent quotes the body verbatim and suggests a concrete next step (check scope, check deposit, upgrade CLI), not "let me retry." -- **Destructive ops gated.** Rule #12 (`create team` rebinds API key context) fires before the agent runs it. +- **Mutating ops gated.** Team creation is described as a separate account (not key rebinding), and the agent confirms the team name/credit transfer before running it. Paid host self-tests also require confirmation. -A failing run reaches for `--ssh-key`, `--bid`, `pytorch/pytorch:@vastai-automatic-tag`, `cloud.vast.ai/account`, or runs `create team` without confirmation. +A failing run reaches for `--ssh-key`, `--bid`, `pytorch/pytorch:@vastai-automatic-tag`, `cloud.vast.ai/account`, uses rejected `create-team`, claims team creation rebinds the key, or runs a paid self-test without confirmation. ## Coverage areas (cross-reference `TEST_PROMPTS.txt`) @@ -40,8 +40,8 @@ Walk through `TEST_PROMPTS.txt` top to bottom. The prompts are grouped roughly b 3. **Launch** — Vast-curated default image, `--ssh --direct --cancel-unavail`, materialization re-check, `--bid_price` for spot offers at or above `min_bid`. 4. **Instance ops** — `show instances-v1 -a` (auto-paginate), `ssh` via parsed `ssh-url`, label/change-bid positional/flag forms. 5. **Templates** — `--disk_space` (not `--disk`), `search templates` for discovery, `delete template --template-id `. -6. **Teams & account** — Rule #12 confirmation before `create team`, role lookup before `invite member`, env-vars treated as write-only. -7. **Host routing** — host-side intents load `vastai-host` skill; 401 attributed to scope, not key reset. +6. **Teams & account** — separate-account semantics and spaced `create team` syntax, role lookup before `invite member`, env-vars treated as write-only. +7. **Host routing** — routine host intents load `vastai-host`; self-test failures and support bundles load `vastai-host-support`; 401 is attributed to scope, not key reset. Slash commands at the end of `TEST_PROMPTS.txt` exercise `/vast:setup`, `/vast:cost`, `/vast:status`, `/vast:search`. diff --git a/scripts/validate_plugin.py b/scripts/validate_plugin.py new file mode 100755 index 0000000..54e8d2e --- /dev/null +++ b/scripts/validate_plugin.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Validate the plugin's manifests, skill layout, and corrected CLI guidance.""" + +from __future__ import annotations + +import hashlib +import json +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +EXPECTED_SKILLS = {"vastai", "vastai-host", "vastai-host-support"} +errors: list[str] = [] + + +def require(condition: bool, message: str) -> None: + if not condition: + errors.append(message) + + +manifest_paths = [ + ROOT / ".claude-plugin" / "plugin.json", + ROOT / ".codex-plugin" / "plugin.json", + ROOT / ".cursor-plugin" / "plugin.json", +] +manifest_path = next((path for path in manifest_paths if path.exists()), None) +require(manifest_path is not None, "no ecosystem plugin manifest found") + +manifest: dict[str, object] = {} +if manifest_path is not None: + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + errors.append(f"invalid manifest JSON: {exc}") + +require(manifest.get("name") == "vastai", "manifest name must be vastai") +require(bool(re.fullmatch(r"\d+\.\d+\.\d+", str(manifest.get("version", "")))), "manifest version must be strict semver") +require(bool(manifest.get("description")), "manifest description is required") + +if manifest_path and manifest_path.parent.name == ".codex-plugin": + require(manifest.get("skills") == "./skills/", "Codex manifest must expose ./skills/") + interface = manifest.get("interface") + require(isinstance(interface, dict), "Codex manifest interface is required") + if isinstance(interface, dict): + for key in ("displayName", "shortDescription", "longDescription", "developerName", "category"): + require(bool(interface.get(key)), f"Codex interface.{key} is required") + +skill_dirs = {path.parent.name for path in (ROOT / "skills").glob("*/SKILL.md")} +require(skill_dirs == EXPECTED_SKILLS, f"expected skills {sorted(EXPECTED_SKILLS)}, found {sorted(skill_dirs)}") + +lock_path = ROOT / "skills.lock.json" +lock: dict[str, object] = {} +try: + lock = json.loads(lock_path.read_text(encoding="utf-8")) +except (OSError, json.JSONDecodeError) as exc: + errors.append(f"invalid skills.lock.json: {exc}") + +require(lock.get("schema_version") == 1, "skills.lock.json schema_version must be 1") +require(lock.get("bundle") == "vastai", "skills.lock.json bundle must be vastai") +source = lock.get("source") +require(isinstance(source, dict), "skills.lock.json source is required") +revision = "" +if isinstance(source, dict): + require( + source.get("repository") == "https://github.com/vast-ai/skills", + "skills.lock.json must point to the canonical vast-ai/skills repository", + ) + revision = str(source.get("revision", "")) + require( + re.fullmatch(r"[0-9a-f]{40}", revision) is not None, + "skills.lock.json source revision must be a full Git commit SHA", + ) + +locked_files: dict[str, str] = {} +files = lock.get("files") +require(isinstance(files, list), "skills.lock.json files must be a list") +if isinstance(files, list): + for index, item in enumerate(files): + if not isinstance(item, dict): + errors.append(f"skills.lock.json files[{index}] must be an object") + continue + path = item.get("path") + digest = item.get("sha256") + if not isinstance(path, str) or not isinstance(digest, str): + errors.append(f"skills.lock.json files[{index}] requires path and sha256 strings") + continue + require(path not in locked_files, f"skills.lock.json contains duplicate path: {path}") + require( + re.fullmatch(r"[0-9a-f]{64}", digest) is not None, + f"skills.lock.json has invalid SHA-256 for {path}", + ) + locked_files[path] = digest + +actual_files = { + path.relative_to(ROOT).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for name in EXPECTED_SKILLS + for path in (ROOT / "skills" / name).rglob("*") + if path.is_file() +} +require( + set(locked_files) == set(actual_files), + "skills.lock.json file list differs from the generated skills snapshot", +) +for path, digest in actual_files.items(): + require(locked_files.get(path) == digest, f"generated skill drift detected: {path}") + +skill_text: dict[str, str] = {} +for name in sorted(EXPECTED_SKILLS): + path = ROOT / "skills" / name / "SKILL.md" + if not path.exists(): + continue + text = path.read_text(encoding="utf-8") + skill_text[name] = text + require(text.startswith("---\n"), f"{name}: missing YAML frontmatter") + require(re.search(rf"(?m)^name:\s*{re.escape(name)}\s*$", text) is not None, f"{name}: frontmatter name mismatch") + require(re.search(r"(?m)^description:\s*\S", text) is not None, f"{name}: description is required") + +renter = skill_text.get("vastai", "") +host = skill_text.get("vastai-host", "") +support = skill_text.get("vastai-host-support", "") + +for stale in ("does not offer network/shared volumes", "rebinds the calling API key", "tombstone state"): + require(stale not in renter, f"vastai: stale guidance remains: {stale}") +require(re.search(r"(?m)^vastai create-team\b", renter) is None, "vastai: rejected create-team command remains") +require("vastai search network-volumes" in renter, "vastai: missing network-volume discovery") +require("vastai create network-volume" in renter, "vastai: missing network-volume creation") + +require(re.search(r"--price_min(?!_bid)", host) is None, "vastai-host: use --price_min_bid, not --price_min") +for stale in ("gpu-trends 'gpu_name=", "does NOT offer network/shared volumes"): + require(stale not in host, f"vastai-host: stale guidance remains: {stale}") +require(re.search(r"(?m)^vastai create-team\b", host) is None, "vastai-host: rejected create-team command remains") +require('vastai metrics gpu-trends "RTX 4090"' in host, "vastai-host: GPU trend filter must be positional") + +for command in ("vastai self-test machine", "vastai dump-logs", "--support-bundle-dir", "--include-local-host-artifacts"): + require(command in support, f"vastai-host-support: missing {command}") +require("actual Linux host" in support, "vastai-host-support: local artifact safety gate is required") +for text_name, text in (("vastai-host", host), ("vastai-host-support", support)): + require("--port-scan-timeout" in text, f"{text_name}: missing PR #458 port scan timeout guidance") + require("TCP and UDP" in text, f"{text_name}: missing PR #458 dual-protocol guidance") +require("four fixed" in support, "vastai-host-support: missing PR #458 fixed-mapping capacity guidance") + +for harness_specific in ("Claude transcript", "claude -p", "Codex transcript", "Cursor transcript"): + for name, text in skill_text.items(): + require(harness_specific not in text, f"{name}: harness-specific guidance remains: {harness_specific}") + +readme = (ROOT / "README.md").read_text(encoding="utf-8") +require("vastai-host-support" in readme, "README must document the host-support skill") +require("pytorch/pytorch:@vastai-automatic-tag" not in readme, "README contains an invalid automatic-tag image") + +marketplace = ROOT / ".claude-plugin" / "marketplace.json" +if marketplace.exists(): + data = json.loads(marketplace.read_text(encoding="utf-8")) + require(data.get("description") is not None, "Claude marketplace description is required") + require(data.get("owner", {}).get("url") == "https://vast.ai", "Claude marketplace owner URL must be canonical") + +if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + raise SystemExit(1) + +print( + f"Validated {ROOT.name}: {len(EXPECTED_SKILLS)} generated skills at {revision[:12]} " + f"and {manifest_path.relative_to(ROOT)}" +) diff --git a/skills.lock.json b/skills.lock.json new file mode 100644 index 0000000..5574324 --- /dev/null +++ b/skills.lock.json @@ -0,0 +1,34 @@ +{ + "schema_version": 1, + "bundle": "vastai", + "source": { + "repository": "https://github.com/vast-ai/skills", + "revision": "44ec627a44453e9f4860ed7075123f538d84c6cb" + }, + "files": [ + { + "path": "skills/vastai/LICENSE", + "sha256": "9c56237ce46085959623f16061a07a327805051c0024ced2558facb030398a01" + }, + { + "path": "skills/vastai/SKILL.md", + "sha256": "bbc3f21e1de9c86d4431faae79fcd08f1b01fe391333f7e60d0aae237669fd7e" + }, + { + "path": "skills/vastai-host/LICENSE", + "sha256": "9c56237ce46085959623f16061a07a327805051c0024ced2558facb030398a01" + }, + { + "path": "skills/vastai-host/SKILL.md", + "sha256": "46262a276cc67a01dbd5ab4963b3c361964cf84b405e1e797a8d2652ada08e83" + }, + { + "path": "skills/vastai-host-support/LICENSE", + "sha256": "9c56237ce46085959623f16061a07a327805051c0024ced2558facb030398a01" + }, + { + "path": "skills/vastai-host-support/SKILL.md", + "sha256": "3c5ab59bf52dc6485a349b359c0fbb671c860624108d8234cbb2ec9b95a6fb10" + } + ] +} diff --git a/skills/vastai-host-support/LICENSE b/skills/vastai-host-support/LICENSE new file mode 100644 index 0000000..82585c3 --- /dev/null +++ b/skills/vastai-host-support/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Vast.ai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/vastai-host-support/SKILL.md b/skills/vastai-host-support/SKILL.md new file mode 100644 index 0000000..d4086ac --- /dev/null +++ b/skills/vastai-host-support/SKILL.md @@ -0,0 +1,143 @@ +--- +name: vastai-host-support +description: Diagnose Vast.ai host self-test failures and collect safe support evidence with the Vast CLI. Covers self-test machine, automatic failure support bundles, dump-logs, instance/container/daemon logs, local host artifacts, redaction, bundle review, and cleanup auditing. Use when a host asks why verification failed, needs a support bundle, wants diagnostic logs, or mentions dump-logs, self-test failure codes, kaalia, Docker, NVIDIA, port mapping, or host troubleshooting. +allowed-tools: Bash(vastai:*) +metadata: + author: vast-ai + compatibility: Linux, macOS, Windows; local host artifact collection requires running on the actual Linux host +--- + +# vastai-host-support + +Diagnose host self-test failures and produce bounded, redacted evidence for Vast support. + +> Use `vastai-host` for listing, pricing, maintenance, and routine provider operations. +> Command is `vastai` (lowercase). Add `--raw` whenever output will be parsed. +> The support-bundle and `dump-logs` workflow requires Vast CLI 1.4.2 or newer. + +## Critical rules + +1. **A machine self-test is a paid, mutating operation.** It launches a temporary rental on the selected host. Confirm the exact machine ID and acceptance of the small charge before running it. If interrupted, audit the account for a leftover test instance. +2. **Do not disable failure bundles by default.** `self-test machine` creates a diagnostic tarball on failure. Use `--no-support-bundle` only when the user explicitly opts out or local policy forbids writing the artifact. +3. **Local host artifacts must come from the actual host.** Never pass `--include-local-host-artifacts` from a laptop, CI runner, or unrelated server; that would collect the wrong machine's OS, Docker, NVIDIA, network, and kaalia evidence. +4. **Review before sharing.** Bundles apply redaction and are created with owner-only permissions, but still inspect the file list and contents for tenant data, credentials, tokens, environment values, IPs, or other sensitive material before uploading it. +5. **Do not use a custom test image as a recovery guess.** `--test-image` is for explicitly testing self-test image changes. A custom image can invalidate comparison with the production verification path. +6. **`--ignore-requirements` does not qualify a host for verification.** It bypasses the requirements gate for diagnostics only; report that limitation clearly. +7. **Configured port-range scanning is a paired CLI/image feature.** CLI builds containing [vast-cli PR #458](https://github.com/vast-ai/vast-cli/pull/458) map and probe the configured range over TCP and UDP. Confirm `--port-scan-timeout` exists and use a production image that includes the matching TCP/UDP responders. Until the paired image is released, use a custom image only for an explicitly authorized development dogfood run, never as proof of production verification readiness. + +## Preflight + +```bash +vastai --version +vastai self-test machine --help +vastai show machine --raw +vastai show instances-v1 --raw --limit 25 +``` + +Confirm the machine is visible to the active API key. If the CLI reports missing `machine_read`, inspect the scoped key rather than rotating credentials blindly. + +If `self-test machine --help` does not list `--port-scan-timeout`, the installed CLI does not include configured TCP/UDP range scanning. Do not claim that it tested the full host range. + +## Run a self-test + +```bash +# Default: creates a support bundle in /tmp if the test fails +vastai self-test machine --raw + +# Put a failure bundle in a user-selected local directory +vastai self-test machine --support-bundle-dir --raw + +# Diagnostic-only bypass; a pass does not qualify the host for verification +vastai self-test machine --ignore-requirements --raw + +# PR #458+ only: increase the timeout for each mapped TCP/UDP probe +vastai self-test machine --port-scan-timeout 5 --raw +``` + +On failure, preserve the structured fields such as `stage`, `failure_code`, `reason`, `diagnostics`, and `instance_id`. Surface the server/CLI response as-is before proposing remediation. + +After an interrupted or failed run, audit for a leftover temporary instance: + +```bash +vastai show instances-v1 --raw --limit 25 +``` + +Do not destroy an unfamiliar instance. Match it to the self-test result and ask for confirmation before cleanup. + +## Diagnose configured TCP/UDP range failures + +PR #458-aware self-tests can report the configured range, range source, advertised direct-port capacity, missing mappings, and individual failed TCP/UDP probes. Preserve those structured fields exactly before proposing remediation. + +Check: + +1. The configured range is valid `start-end` syntax within ports `1024-65535`. +2. The machine offer advertises at least the range's port count plus four fixed self-test mappings. +3. Every configured container port has both a TCP and UDP mapping. +4. Host firewall, router/NAT, and upstream rules allow both protocols. +5. The selected self-test image contains the matching TCP listener and UDP echo responder. + +Do not describe a timeout as proof that the port is closed—the failure may also be a missing mapping, NAT hairpin behavior, firewall drop, stale offer metadata, or a mismatched test image. Report the exact public endpoint and protocol only in private diagnostic output, and redact them before sharing broadly. + +## Create a manual diagnostic bundle + +From a workstation or other machine that is not the Vast host: + +```bash +vastai dump-logs --output-dir --raw +vastai dump-logs --instance-id --output-dir --raw +``` + +`--instance-id` adds CLI-visible instance metadata plus container and daemon logs from the Vast API. + +Only when the command is running on the actual Linux host: + +```bash +vastai dump-logs --instance-id \ + --include-local-host-artifacts --output-dir --raw +``` + +Local collection may include bounded/redacted kaalia logs, Docker configuration and status, filtered kernel errors, NVIDIA state, network summaries, and relevant mount information. Collection errors are recorded in the bundle rather than hidden. + +## Inspect before sharing + +```bash +tar -tzf +``` + +Expected bundle entries can include: + +- `manifest.json` and `collection-errors.json` +- `self-test-result.json` and `self-test-output.log` +- `instance/show-instance.json`, `instance/container.log`, and `instance/daemon.log` +- `host/...` entries only when local host collection was explicitly enabled on the actual host + +Confirm the archive path reported by the CLI, inspect sensitive-looking entries in a private location, and share only the minimum bundle needed for the support case. + +## Related read-only evidence + +```bash +vastai reports --raw +vastai show machine --raw +vastai logs --tail 200 --raw +``` + +`reports` surfaces renter-submitted machine reports. `logs` is instance-scoped; do not substitute a machine ID for an instance ID. + +## Common failures + +| Symptom | Next evidence | +|---|---| +| Requirements/preflight failure | Keep the structured requirement result; use `--ignore-requirements` only for an explicitly diagnostic run | +| Test instance never becomes usable | Preserve `failure_code`, progress endpoint fields, mapped ports, and instance/daemon logs | +| Configured direct-port capacity failure | Compare range count plus four fixed mappings with the offer's `direct_port_count`; do not assume a fixed platform maximum | +| Missing or failed TCP/UDP range probes | Preserve missing mappings and per-protocol failures; verify both firewall protocols, NAT, offer metadata, and the paired responder image | +| Docker, CDI, or NVIDIA startup failure | Run `dump-logs` with the failed instance ID; add local artifacts only on the actual host | +| Support bundle write failure | Choose a writable `--support-bundle-dir` or `--output-dir`; report the exact filesystem error | +| Bundle has collection errors | Keep `collection-errors.json`; partial evidence is useful and should not be represented as complete | +| `401` / missing `machine_read` | Check active key precedence and permission scope; do not reset the key as a first response | + +## Environment variables + +- `VAST_API_KEY` — overrides the stored API key +- `VAST_SELF_TEST_IMAGE` — custom image override for explicit self-test image development +- `VAST_SELF_TEST_SUPPORT_BUNDLE=0|false|no|off` — disables automatic failure bundles; do not set by default diff --git a/skills/vastai-host/LICENSE b/skills/vastai-host/LICENSE new file mode 100644 index 0000000..82585c3 --- /dev/null +++ b/skills/vastai-host/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Vast.ai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/vastai-host/SKILL.md b/skills/vastai-host/SKILL.md index 450cca1..4216303 100644 --- a/skills/vastai-host/SKILL.md +++ b/skills/vastai-host/SKILL.md @@ -1,10 +1,10 @@ --- name: vastai-host -description: Vast.ai CLI for GPU hosts/providers — list and unlist machines on the marketplace, set pricing (min-bid, default GPU price), configure default jobs, schedule maintenance windows, run self-tests, view earnings, monitor marketplace metrics (gpu, gpu-trends, gpu-locations), manage network disks and clusters, defrag machines, clean up expired storage. Use this for any prompt about hosting on Vast, listing a machine, machine pricing, maintenance windows, host earnings, marketplace metrics, or any vastai command for GPU providers. +description: Vast.ai CLI for GPU hosts/providers — list and unlist machines on the marketplace, set pricing (min-bid, default GPU price), configure default jobs, schedule maintenance windows, run routine self-tests, view earnings, monitor marketplace metrics (gpu, gpu-trends, gpu-locations), manage network disks and clusters, defrag machines, and clean up expired storage. Use this for hosting, listing, pricing, maintenance, earnings, metrics, or routine provider operations. Use vastai-host-support for self-test failures, support bundles, and diagnostic log collection. allowed-tools: Bash(vastai:*) -compatibility: Linux, macOS metadata: author: vast-ai + compatibility: Linux, macOS --- # vastai-host @@ -29,13 +29,16 @@ These rules apply to every invocation. Do not skip them. 5. **Host actions are visible to renters and impact billing.** Listing, unlisting, price changes, maintenance windows, `defrag machines`, and `cleanup machine` all have side effects on live renters or on your earnings. Before running any of these, confirm with the user — quote the exact command back. Do not run them as a "let me just verify" probe. 6. **`schedule maint` evicts live renters at the start time.** Never schedule a maintenance window without explicit user confirmation of the start date and duration. Active renters on the machine will be terminated when the window opens. 7. **`defrag machines` is disruptive.** It reorganizes the named machines' GPU assignments to free up larger multi-GPU offers — running instances on those machines may be reshuffled or interrupted. The subcommand is `defrag machines` (its own `--help` lies — the usage line reads `vastai defragment machines IDs`, but `vastai defragment machines …` returns `invalid choice` at the parser; only `defrag machines` actually executes). Takes positional machine IDs (e.g. `vastai defrag machines 100 101`); confirm the exact ID list with the user before running. +8. **`self-test machine` launches a temporary paid instance.** Confirm the machine ID and that the user accepts the small rental charge before running it. The command normally cleans up its test instance, but audit instances afterward if the command is interrupted. Current development builds that include [vast-cli PR #458](https://github.com/vast-ai/vast-cli/pull/458) also map and probe every configured direct port over both TCP and UDP; use `vastai self-test machine --help` to confirm that `--port-scan-timeout` is available. Use the `vastai-host-support` skill for failure diagnostics and support bundles. -## Install +## CLI prerequisite ```bash -pip install vastai # PyPI (recommended) +vastai --version # 1.4.2 or newer ``` +If the installed CLI is older, report the version mismatch and ask the user whether they want upgrade instructions. Do not install or replace the `vastai` binary during a host operation. + ## Setup / first-time auth Create an API key at **** (not `cloud.vast.ai/account` — that URL does not exist). @@ -52,7 +55,7 @@ vastai show user # Verify auth > **Precedence trap.** Resolution: `--api-key` flag > `$VAST_API_KEY` > stored key. A `VAST_API_KEY` in the shell silently overrides whatever you just persisted with `vastai set api-key`. The CLI prints `⚠️ VAST_API_KEY is set in your environment and overrides the key you just saved` but it's easy to miss. For host work especially — where the persisted key may be a deliberately-scoped monitoring key — check `env | grep VAST_API_KEY` before authenticated calls and `unset` it if you don't want it active. -> **2FA.** If the host account has 2FA enabled, run `vastai tfa login --method-type {totp,sms,email} --code ` once per shell — the CLI writes a session key to `~/.config/vastai/vast_tfa_key` and uses it transparently. Without an active TFA session, `show machines`, `show earnings`, `show user`, `metrics gpu*`, etc. all return a `401` whose body says *"requires you to have logged in using Two Factor Authentication."* Recommend prefixing the login command with `!` in the Claude transcript so the 6-digit code does not enter conversation history. See the "Common errors" table for the exact pattern. +> **2FA.** If the host account has 2FA enabled, run `vastai tfa login --method-type {totp,sms,email} --code ` once per shell — the CLI writes a session key to `~/.config/vastai/vast_tfa_key` and uses it transparently. Without an active TFA session, `show machines`, `show earnings`, `show user`, `metrics gpu*`, etc. all return a `401` whose body says *"requires you to have logged in using Two Factor Authentication."* Run the login from a private terminal outside the agent transcript, or use the harness's non-recorded shell escape when available, so the 6-digit code does not enter conversation history. See the "Common errors" table for the exact pattern. Machine registration (one-time, on the host machine itself): @@ -105,11 +108,24 @@ vastai search templates 'name=pytorch recommended=true' ```bash vastai show machines # List all machines you host vastai show machine # Single machine details -vastai self-test machine # Run diagnostics (do this before listing) +vastai self-test machine # Paid temporary test; confirm first. Use vastai-host-support on failure +vastai self-test machine --port-scan-timeout 5 # PR #458+ only; per-port TCP/UDP probe timeout in seconds vastai reports # Renter-submitted reports for a machine vastai delete machine # Permanently remove from your account ``` +#### Configured direct-port scan + +CLI builds containing [PR #458](https://github.com/vast-ai/vast-cli/pull/458) discover the host-configured range, add TCP and UDP Docker mappings for every port, and probe the externally mapped ports during self-test. Before relying on this behavior, confirm the installed command exposes the flag: + +```bash +vastai self-test machine --help # Must show --port-scan-timeout +``` + +The range usually comes from `/var/lib/vastai_kaalia/host_port_range` when the CLI runs on the actual host, or from direct-port metadata on the temporary test instance. The machine offer's `direct_port_count` is the capacity source of truth: the configured range needs its full port count plus the self-test's four fixed mappings. There is no fixed platform-wide maximum. + +Do not increase `--port-scan-timeout` as a substitute for fixing missing mappings, TCP/UDP firewall rules, NAT behavior, or an undersized advertised direct-port capacity. A failed range scan belongs in `vastai-host-support`. + ### Listing & pricing ```bash @@ -127,7 +143,7 @@ vastai remove defjob # Remove default job - `--price_inetu <$/GB>` — internet upload (inbound) bandwidth - `--price_inetd <$/GB>` — internet download (outbound) bandwidth - `--price_disk <$/GB/month>` — storage (default: $0.10/GB/month) -- `--price_min_bid <$/hr>` — per-GPU minimum bid floor (alternative to `set min-bid`). NOT `--price_min`. +- `--price_min_bid <$/hr>` — per-GPU minimum bid floor (alternative to `set min-bid`). - `--discount_rate <0-1>` — max long-term prepay discount rate (default 0.4) - `--min_chunk ` — minimum GPUs per rental (default 1) - `--end_date ` / `--duration ` — contract offer expiration @@ -139,7 +155,7 @@ vastai remove defjob # Remove default job ```bash # --sdate is UNIX EPOCH SECONDS (not ISO 8601); --duration is HOURS as a float (not "4h"). -vastai schedule maint --sdate $(date -u -d '2026-06-01 02:00' +%s) --duration 4 --maintenance_category power +# Convert the user-confirmed UTC time with a platform-appropriate tool, then show the exact epoch before running. vastai schedule maint --sdate 1812031200 --duration 0.5 --maintenance_category gpu # 30-min GPU maintenance vastai cancel maint # Cancel a scheduled window vastai show maints # List all scheduled maintenance @@ -164,7 +180,12 @@ vastai add network-disk /mnt/disk1 # First time: positiona vastai add network-disk /mnt/disk1 -d 12345 # Subsequent attach: pass --disk_id (-d) of existing disk ``` -Network disks are a host-side storage primitive used internally by the marketplace. **Vast.ai does NOT offer network/shared volumes to renters** — `vastai create network-volume` is CLI plumbing for an unshipped product, not a feature. Don't tell renters they can use network-disks for shared storage. +Network disks are the host-side storage primitive. Current CLIs also expose an offer-based network-volume product: hosts can list network-disk capacity with `vastai list network-volume ` and renters can discover/create offers with `search network-volumes` and `create network-volume`. Do not promise a specific sharing or attachment topology without checking the selected offer and current product documentation. + +```bash +vastai list network-volume --price_disk 0.15 --size 500 # Creates a renter-visible offer +vastai unlist network-volume # Removes that offer +``` > The CLI has no `vastai remove network-disk` subcommand — detach/removal is currently console-only at . Do not call `remove network-disk` (it returns `invalid choice`). @@ -191,7 +212,7 @@ These require the `machine_read` permission group — available to hosts and adm vastai metrics gpu # Current GPU market state vastai metrics gpu --datacenter true --verified true # Filter vastai metrics gpu-trends # Historical trends -vastai metrics gpu-trends 'gpu_name=RTX_4090' # Filter trends +vastai metrics gpu-trends "RTX 4090" # GPU name is positional, not a query expression vastai metrics gpu-locations # Geographic distribution ``` @@ -221,8 +242,8 @@ vastai tfa activate CODE --secret SECRET -t totp # CODE is P ### Teams ```bash -vastai create-team --team-name "ops" # NOTE: hyphenated subcommand + --team-name. NOT `create team --name`. -vastai create-team --team-name "ops" --transfer-credit 50 # Optionally seed from personal credit +vastai create team --team-name "ops" # Spaced subcommand; creates a separate team account +vastai create team --team-name "ops" --transfer-credit 50 # Confirm the credit transfer amount first vastai destroy team vastai show members vastai invite member --email ops-engineer@example.com --role # --role, NOT --role-id @@ -234,16 +255,17 @@ vastai update team-role --permissions ./role.json # Same vastai remove team-role ``` -Use team-roles to give ops engineers scoped access to your host operations without sharing the main API key. +Team creation does not convert the personal account or rebind the calling key. It creates a separate account with independent billing and resources. Use team-roles to give ops engineers scoped access without sharing the main API key. ## Common errors | Error | Cause | Fix | |-------|-------|-----| -| `401` + `"...requires you to have logged in using Two Factor Authentication"` in body | Account has 2FA enabled but no current TFA session — affects almost every host read (`show machines`, `show earnings`, `metrics gpu*`, `show user`) | Run `vastai tfa login --method-type {totp,sms,email} --code ` once per shell. Recommend `!` prefix to keep the 6-digit code out of the transcript. Do NOT rotate the API key — that won't fix it. | +| `401` + `"...requires you to have logged in using Two Factor Authentication"` in body | Account has 2FA enabled but no current TFA session — affects almost every host read (`show machines`, `show earnings`, `metrics gpu*`, `show user`) | Run `vastai tfa login --method-type {totp,sms,email} --code ` once per shell from a private terminal or non-recorded shell escape. Keep the 6-digit code out of the transcript. Do NOT rotate the API key — that won't fix it. | | `401 Unauthorized` / `Invalid or expired API key` (no 2FA wording) | Invalid key OR scoped key lacks the permission set this command requires | First `env | grep VAST_API_KEY` — a shell env var may shadow the stored key. Then `vastai show api-keys --raw` to inspect scope; widen permissions or use your primary key. Only `vastai set api-key ` if the key itself is wrong. | | `Your key lacks the machine_read permission group` | Scoped API key missing host permissions | Use your primary key, or recreate the scoped key passing `--permission_file ./perms.json` to `create api-key`, where the JSON grants the `machine_read` group. (Inline JSON via `--permissions` does not work — the flag takes a file path.) | | `Machine not found` | Wrong machine ID or machine deleted | `vastai show machines --raw` to list current IDs | +| Direct-port capacity or TCP/UDP range scan failure | Configured range plus four fixed mappings exceeds the offer's `direct_port_count`, mappings are missing, or an external TCP/UDP probe cannot reach the host | Preserve the structured self-test result and use `vastai-host-support`; verify the configured range, both firewall protocols, mapped ports, and the matching self-test image before retrying | | `Cannot unlist machine with active rentals` | Existing renters on the machine | Wait for rentals to end, or contact support to evict | | `Maintenance window conflicts with active rental` | Renter has time on the requested window | Choose a later start, or accept that the renter will be evicted | | `defrag in progress` | Another `defrag machines` operation is already running on this account | Wait for it to finish; `vastai show machines --raw` shows defrag state | @@ -251,7 +273,7 @@ Use team-roles to give ops engineers scoped access to your host operations witho ## Pricing & strategy tips 1. **Run `self-test machine` before listing.** Failed self-tests cascade into bad reviews and low rentability. -2. **Set `--price_min`** (or `set min-bid`) so interruptible renters can't underpay your true cost. +2. **Set `--price_min_bid`** (or `set min-bid`) so interruptible renters can't underpay your true cost. 3. **Use `metrics gpu-locations`** to spot regions where your GPU is scarce — those command a premium. 4. **Don't undercut `metrics gpu` median by more than ~20%** unless you're seeding rentability; deep undercut races down the whole market. 5. **`set defjob`** lets your machine earn while idle by running a default job (e.g. distributed inference). Test the image works on your hardware first. diff --git a/skills/vastai/LICENSE b/skills/vastai/LICENSE new file mode 100644 index 0000000..82585c3 --- /dev/null +++ b/skills/vastai/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Vast.ai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/vastai/SKILL.md b/skills/vastai/SKILL.md index 2c73243..dfa11a4 100644 --- a/skills/vastai/SKILL.md +++ b/skills/vastai/SKILL.md @@ -2,9 +2,9 @@ name: vastai description: Vast.ai CLI for renters — search and launch GPU instances, SSH into them, copy files, run commands, manage volumes and serverless endpoints, manage user environment variables (HF_TOKEN, OPENAI_API_KEY, model config), check billing and balance, register SSH keys, destroy instances. Use this for any prompt about Vast.ai, vastai, GPU rental, or instance lifecycle. allowed-tools: Bash(vastai:*) -compatibility: Linux, macOS metadata: author: vast-ai + compatibility: Linux, macOS --- # vastai @@ -31,17 +31,18 @@ These rules apply to every invocation. Do not skip them. - If `vastai ` exits non-zero or produces output you don't understand, report the failure (exit code + stderr) to the user and ask what they want to do. Do not work around it. 8. **Always pass `--disk N`, `--ssh --direct`, and `--cancel-unavail` to `create instance` / `launch instance`.** Without `--disk` you get image-dependent defaults and surprise storage charges. Without `--ssh --direct` together you fall back to the slower proxy connection — `--ssh` alone is not enough. Without `--cancel-unavail`, an unavailable machine quietly produces a *stopped* instance that accrues disk charges while you poll forever for a `running` state it will never reach. 9. **Pass `--limit N` on `show instances-v1` and `show invoices-v1`** to short-circuit the interactive `Fetch next page? (y/N)` prompt that fires after the first page (it blocks `--raw` / non-interactive sessions even with `--raw` set). The two subcommands have **different flag sets** — do not assume what works on one works on the other. `show instances-v1 --limit` is capped at **1–25** (default 25) per `--help`; `show invoices-v1 --limit` has its own cap (read it from `--help`). When in doubt, run `vastai show --help`. (Example trap: `--latest-first` exists on `invoices-v1` but not on `instances-v1`.) -10. **Vast.ai does not offer network/shared volumes.** Only local (per-instance) volumes. If the user asks for "shared storage across instances" or "persistent shared state for serverless workers," do NOT reach for `create network-volume` (it's CLI plumbing for an unshipped product and will leave the user with a broken architecture). The correct answer is: replicate data per instance, or use external object storage (S3/GCS/etc.) via `vastai cloud copy`. +10. **Network volumes are offer-based and supported by current CLIs.** Discover them with `vastai search network-volumes`, then create one from an offer with `vastai create network-volume`. Do not assume every region has an offer or that one volume can be mounted by arbitrary instances simultaneously; inspect the selected offer and current product documentation before promising an attachment topology. For portable object data, `vastai cloud copy` remains the safer cross-region option. 11. **When SSH fails (`Permission denied (publickey)`, `Connection refused`, hangs), pull `vastai logs ` FIRST — before any other recovery action.** The container's sshd writes its rejection reason to host logs that surface in `vastai logs`, and that text usually pins down the cause in one read. Common rejections you only see in logs: `Authentication refused: bad ownership or modes for file /root/.ssh/authorized_keys` (image bug — destroy + retry on a different host, not the same one); `Failed publickey for root from ... ssh2: ` (the wrong local key is being offered — check `ssh -v` for the offered key vs `vastai show ssh-keys` for the registered ones); container exited / sshd not started (image issue — `vastai logs --tail 100` shows the startup failure). Do not loop on `attach ssh`, `reboot`, `destroy + relaunch` before reading logs — those are blind retries that burn minutes and money. Diagnostic discipline: `logs` then act, never act then guess. -12. **`vastai create team` rebinds the calling API key's account context — treat as destructive.** Running `vastai create team --team-name ` switches the API key from the user's personal account to the newly-created team account. The team starts empty: no credit, no instances, no env-vars, no SSH keys. The personal account's resources are untouched but are no longer reachable from this key. Deleting the team afterwards leaves the key bound to a tombstone with no CLI recovery path. Before running, the agent must confirm with the user that they have a *separate* API key bound to the personal account, or that this is a throwaway key. The CLI shows no warning and has no `-y`-style guard. +12. **`vastai create team` creates a separate account; it does not convert the personal account or rebind the calling key.** Team creation is still a billable account mutation. Confirm the exact `--team-name` and any `--transfer-credit` amount before running it. The creator becomes the owner, the team gets independent billing/resources, and a user may belong to multiple teams. -## Install +## CLI prerequisite ```bash -# PyPI (recommended) -pip install vastai +vastai --version # 1.4.2 or newer ``` +If the installed CLI is older, report the version mismatch and ask the user whether they want upgrade instructions. Do not install or replace the `vastai` binary during an operation. + ## Setup / first-time auth Create an API key at **** (not `cloud.vast.ai/account` — that URL does not exist). @@ -313,7 +314,8 @@ vastai search offers 'gpu_ram>=8 num_gpus=1 compute_cap>=' -o 'dph_to vastai search offers --type bid # Interruptible (spot) pricing vastai search offers --type reserved # Reserved pricing vastai search offers 'verified=any rentable=any gpu_name=H100_SXM' # Widen specific defaults — see "Hidden defaults" above -vastai search volumes # Volume offers (local only — Vast.ai does not offer network volumes) +vastai search volumes # Local volume offers +vastai search network-volumes 'verified=true' --storage 100 # Network-volume offers; availability varies vastai search templates 'name=pytorch' # Templates (structured query — NOT free-text. Fields: name, creator_id, count_created, hash_id, image, tag, recommended, use_ssh, jup_direct, ssh_direct, …). Pagination is server-side; this command does not accept --limit. Narrow the query (e.g. `recommended=true`) to control result size. vastai search templates 'count_created>100 recommended=true' vastai search benchmarks # Benchmark results @@ -390,7 +392,7 @@ vastai logs --daemon-logs # Host daemon logs (ins ### Volumes -Vast.ai offers **local (per-instance) volumes only**. There is no network/shared-volume product — see rule 10. If the user asks for shared storage across instances, recommend per-instance replication or external object storage via `vastai cloud copy`. +Vast.ai exposes both local volume offers and network-volume offers. Network-volume availability and attachment topology depend on the selected offer, so inspect the offer before designing shared storage around it. Use external object storage via `vastai cloud copy` when the workload needs portable cross-region object data. ```bash # Local volumes (per-machine) @@ -400,6 +402,11 @@ vastai create volume -s 500 -n my-data # Size in GB, name opti vastai clone volume -s 500 vastai delete volume +# Network volumes (offer-based; current CLI) +vastai search network-volumes 'verified=true disk_space>=100' --storage 100 --raw +vastai create network-volume -s 100 -n shared-data --raw +vastai show volumes --raw + # Container snapshots vastai take snapshot \ --repo myorg/myimage --container_registry docker.io \ @@ -506,19 +513,17 @@ vastai show deposit # Reserved instance dep `show invoices` (without `-v1`) is **deprecated** — use `show invoices-v1`. -> **Pagination gotcha:** `show invoices-v1` and `show instances-v1` print *"Fetch next page? (y/N)"* after the first page — even when `--raw` is set. This blocks `claude -p` and other non-interactive sessions. Always pass `--limit N` to short-circuit the prompt. The two commands have **separate flag sets** — check each with `vastai show --help` rather than assuming flags carry over (notably, `--latest-first` is invoices-v1 only). +> **Pagination gotcha:** `show invoices-v1` and `show instances-v1` print *"Fetch next page? (y/N)"* after the first page — even when `--raw` is set. This blocks headless and other non-interactive agent sessions. Always pass `--limit N` to short-circuit the prompt. The two commands have **separate flag sets** — check each with `vastai show --help` rather than assuming flags carry over (notably, `--latest-first` is invoices-v1 only). ### Teams -**`create team` is destructive: it rebinds your API key's account context.** See Critical Rule #12. The CLI shows no warning. Documenting the syntax for completeness — the agent must not run it without confirming per rule 12. - -**Subcommand-name version skew.** Primary form on current CLIs is `vastai create team` (space, with `--team-name`). Some CLIs expose `vastai create-team` (hyphenated). If the space form returns `invalid choice`, try hyphenated; if hyphenated returns `invalid choice`, try space. Either way the flag is `--team-name`, not `--name`. +`create team` creates a separate team account and does not convert the personal account. It still changes external state and can transfer credit, so confirm the exact name and transfer amount first. Current CLI syntax uses the spaced subcommand `vastai create team`; the hyphenated `vastai create-team` is not accepted even though the help usage line may render that spelling. ```bash -vastai show members --raw # Check membership FIRST — non-empty means you're already in a team; create team will fail with "Cannot create a team within a team". -vastai create team --team-name "myteam" # DESTRUCTIVE — rebinds API key context (rule 12). Primary form on current CLIs. If parser returns invalid choice, try `vastai create-team --team-name "myteam"`. -vastai create team --team-name "myteam" --transfer-credit 50 # Optionally seed from personal credit. Still destructive — rule 12 applies. -vastai destroy team # Note: deleting a team after `create team` leaves any key bound to that team in a tombstone state — see rule 12. +vastai show members --raw # Inspect the current account context before team administration +vastai create team --team-name "myteam" # Creates a separate team account; confirm the name first +vastai create team --team-name "myteam" --transfer-credit 50 # Confirm the credit transfer amount explicitly +vastai destroy team # Destructive: confirm the target team/account context first vastai show team-roles # ALWAYS run this first when inviting — roles are TEAM-DEFINED, not a fixed enum. "billing-admin", "viewer" etc. are NOT preset; using an unknown role name triggers a generic HTTP 500. vastai invite member --email user@example.com --role # Roles are team-defined; run `vastai show team-roles --raw` first. HTTP 500 on invite is seen both for unknown roles AND for valid roles returned by show team-roles. Surface the 500 to the user rather than retrying. vastai remove member @@ -558,7 +563,7 @@ vastai tfa delete --id-to-delete METHOD_ID --code CODE [-t METHOD] # Remo | Error | Cause | Fix | |-------|-------|-----| -| `401` + `"...requires you to have logged in using Two Factor Authentication"` in body | Account has 2FA enabled but no current TFA session key — affects almost all authenticated reads (`show user`, `show ssh-keys`, `show instances-v1`, `show invoices-v1`, `show env-vars`, etc.) | Run `vastai tfa login --method-type {totp,sms,email} --code ` once per shell. The CLI writes a session key to `~/.config/vastai/vast_tfa_key` and prefers it transparently on subsequent calls. **Tell the user to prefix the command with `!` in the Claude transcript so the 6-digit code does not enter conversation history.** Do NOT ask the user for a new API key — that won't fix it. | +| `401` + `"...requires you to have logged in using Two Factor Authentication"` in body | Account has 2FA enabled but no current TFA session key — affects almost all authenticated reads (`show user`, `show ssh-keys`, `show instances-v1`, `show invoices-v1`, `show env-vars`, etc.) | Run `vastai tfa login --method-type {totp,sms,email} --code ` once per shell. The CLI writes a session key to `~/.config/vastai/vast_tfa_key` and prefers it transparently on subsequent calls. **Tell the user to run it from a private terminal or non-recorded shell escape so the 6-digit code does not enter conversation history.** Do NOT ask the user for a new API key — that won't fix it. | | `401 Unauthorized` / `Invalid or expired API key` (no 2FA wording) | Invalid key OR scoped key lacks the permission set this command requires | Don't regenerate blindly. First check `env | grep VAST_API_KEY` — a shell env var may be shadowing the stored key. Then run `vastai show api-keys --raw` to inspect the key's scope; widen permissions or use your primary key. Only `vastai set api-key ` if the key itself is wrong. | | `Your key lacks the machine_read permission group` | Host/admin command (e.g. `metrics gpu`, `show machines`) on a renter account | Use the `vastai-host` skill — these commands are for GPU providers | | `Insufficient credits` | Account balance too low | Add credits at |