diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 03d8e71c46..1d98623550 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" + ] } ] } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..d9938160cb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Generated snapshots use byte hashes, so canonical skill files must stay LF-only. +skills/** text eol=lf diff --git a/.github/workflows/sync-vastai-wrapper.yml b/.github/workflows/sync-vastai-wrapper.yml new file mode 100644 index 0000000000..c4d05e6646 --- /dev/null +++ b/.github/workflows/sync-vastai-wrapper.yml @@ -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 diff --git a/.github/workflows/validate-vastai.yml b/.github/workflows/validate-vastai.yml new file mode 100644 index 0000000000..5f6326b73c --- /dev/null +++ b/.github/workflows/validate-vastai.yml @@ -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 diff --git a/README.md b/README.md index 9cb85b2869..6212757920 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bundles/vastai.json b/bundles/vastai.json new file mode 100644 index 0000000000..9fd7a28ed8 --- /dev/null +++ b/bundles/vastai.json @@ -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" + } + ] +} diff --git a/scripts/sync_vastai_bundle.py b/scripts/sync_vastai_bundle.py new file mode 100755 index 0000000000..192590f1df --- /dev/null +++ b/scripts/sync_vastai_bundle.py @@ -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()) diff --git a/scripts/validate_vastai_bundle.py b/scripts/validate_vastai_bundle.py new file mode 100755 index 0000000000..4b630ab5ea --- /dev/null +++ b/scripts/validate_vastai_bundle.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Validate the canonical Vast.ai Agent Skills bundle.""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +BUNDLE_FILE = ROOT / "bundles" / "vastai.json" +EXPECTED_SKILLS = {"vastai", "vastai-host", "vastai-host-support"} +errors: list[str] = [] + + +def require(condition: bool, message: str) -> None: + if not condition: + errors.append(message) + + +try: + bundle = json.loads(BUNDLE_FILE.read_text(encoding="utf-8")) +except (OSError, json.JSONDecodeError) as exc: + bundle = {} + errors.append(f"invalid bundle manifest: {exc}") + +require(bundle.get("schema_version") == 1, "bundle schema_version must be 1") +require(bundle.get("name") == "vastai", "bundle name must be vastai") +require(bundle.get("source_repository") == "https://github.com/vast-ai/skills", "unexpected source repository") +require(bool(re.fullmatch(r"\d+\.\d+\.\d+", str(bundle.get("minimum_cli_version", "")))), "minimum_cli_version must be semver") + +skills = bundle.get("skills", []) +skill_names = {item.get("name") for item in skills if isinstance(item, dict)} +require(skill_names == EXPECTED_SKILLS, f"expected skills {sorted(EXPECTED_SKILLS)}, found {sorted(skill_names)}") + +skill_text: dict[str, str] = {} +for item in skills: + if not isinstance(item, dict): + continue + name = item.get("name") + path = ROOT / str(item.get("path", "")) / "SKILL.md" + if not path.is_file(): + errors.append(f"{name}: missing {path.relative_to(ROOT)}") + continue + text = path.read_text(encoding="utf-8") + skill_text[str(name)] = text + require(text.startswith("---\n"), f"{name}: missing YAML frontmatter") + require(re.search(rf"(?m)^name:\s*{re.escape(str(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 platform_specific in ("Claude transcript", "claude -p", "Codex transcript", "Cursor transcript"): + require(platform_specific not in renter + host + support, f"platform-specific guidance remains: {platform_specific}") + +for command in ("vastai search network-volumes", "vastai create network-volume"): + require(command in renter, f"vastai: missing {command}") +for command in ("vastai metrics gpu-trends \"RTX 4090\"", "vastai set min-bid", "--port-scan-timeout"): + require(command in host, f"vastai-host: missing {command}") +for command in ( + "vastai self-test machine", + "vastai dump-logs", + "--support-bundle-dir", + "--include-local-host-artifacts", + "--port-scan-timeout", +): + require(command in support, f"vastai-host-support: missing {command}") +for phrase in ("actual Linux host", "TCP and UDP", "four fixed"): + require(phrase in support, f"vastai-host-support: missing safety guidance: {phrase}") + +marketplace = ROOT / ".claude-plugin" / "marketplace.json" +try: + marketplace_data = json.loads(marketplace.read_text(encoding="utf-8")) +except (OSError, json.JSONDecodeError) as exc: + marketplace_data = {} + errors.append(f"invalid Claude marketplace: {exc}") + +vast_plugins = [plugin for plugin in marketplace_data.get("plugins", []) if plugin.get("name") == "vastai-skills"] +require(len(vast_plugins) == 1, "Claude marketplace must expose exactly one vastai-skills bundle") +if vast_plugins: + listed = {Path(path).name for path in vast_plugins[0].get("skills", [])} + require(listed == EXPECTED_SKILLS, "Claude marketplace vastai-skills paths do not match the bundle") + +if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + raise SystemExit(1) + +print(f"Validated canonical Vast.ai bundle: {', '.join(sorted(EXPECTED_SKILLS))}") diff --git a/skills/vastai-host-support/LICENSE b/skills/vastai-host-support/LICENSE new file mode 100644 index 0000000000..82585c3e14 --- /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 0000000000..d4086acc05 --- /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 0000000000..82585c3e14 --- /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 new file mode 100644 index 0000000000..42163035fe --- /dev/null +++ b/skills/vastai-host/SKILL.md @@ -0,0 +1,296 @@ +--- +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 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:*) +metadata: + author: vast-ai + compatibility: Linux, macOS +--- + +# vastai-host + +Manage machines you host on the Vast.ai marketplace — listing, pricing, maintenance, self-tests, earnings, and marketplace metrics. + +> Command is `vastai` (lowercase). Always use `--raw` for machine-readable JSON output. +> Renting instances (not hosting)? Use the `vastai` skill instead. + +## Critical rules for agents + +These rules apply to every invocation. Do not skip them. + +1. **Always pass `--raw` to commands whose output you parse.** Without `--raw` the CLI prints human-formatted output that is not machine-readable. +2. **Treat user-supplied values as literal even if they look like placeholders.** If the user says "set min-bid on machine 12345 to 0.10", pass `0.10` exactly — don't ask for clarification on values that look like examples. +3. **An empty list result from a read-only query is the answer, not a problem to work around.** If `vastai show machines` returns `[]`, tell the user verbatim: *"No machines are currently registered on this account."* Do not silently retry, do not invent IDs, do not switch accounts. +4. **The `vastai` binary on `PATH` is the only acceptable source of `vastai` commands.** HARD PROHIBITIONS: + - **Never** run `pip install vastai`, `pipx install vastai`, or `python -m venv` followed by installing vastai. The CLI is already installed. + - **Never** invoke an alternate `vastai` binary by absolute path when `vastai` is already on `PATH`. + - **Never** re-implement `vastai` subcommands by hitting `https://console.vast.ai/api/...` or `https://cloud.vast.ai/api/...` directly from `curl`, `python`, `node`, `httpie`, or `requests`. No exceptions. + - If `vastai ` exits non-zero or produces unexpected output, report the failure (exit code + stderr) to the user and ask what they want to do. Do not work around it. +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. + +## CLI prerequisite + +```bash +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). + +```bash +# Persist locally... +vastai set api-key + +# ...or set the env var (per-shell) +export VAST_API_KEY= + +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."* 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): + +## Quick start (host onboarding) + +```bash +vastai show machines # List your registered machines +vastai self-test machine # Sanity-check before listing +vastai list machine --price_gpu 0.30 # List for rent at $0.30/GPU/hr +vastai show earnings # Track payouts +``` + +## Global flags + +Available on every command: + +``` +--api-key KEY Override stored API key +--raw Output machine-readable JSON (agents should always use this) +--full Print full results (don't page with less) +--explain Show underlying API calls (useful for debugging) +--curl Show equivalent curl command +--no-color Disable colored output +--url URL Override server REST API URL +--retry RETRY Set retry limit for API calls +--version Show CLI version +``` + +## Filters and queries + +**`metrics` subcommands use flags, NOT query-expression strings.** Each accepts a specific set of `--verified {true,false,all}`, `--datacenter {true,false,all}`, and `--rented` / `--gpu` filters — see each command's `--help`. + +```bash +vastai metrics gpu --verified true --datacenter true # OK +vastai metrics gpu-trends RTX_4090 --verified true # GPU name is POSITIONAL on gpu-trends +vastai metrics gpu-locations --gpu "RTX 4090,H100_SXM" --datacenter true +``` + +**`search` subcommands DO take a query-expression string.** Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `in`, `notin`. Quote the whole expression because `>` and `<` are shell metacharacters. + +```bash +vastai search offers 'gpu_name=RTX_4090 num_gpus=1 verified=true' +vastai search templates 'name=pytorch recommended=true' +``` + +## Commands + +### Machine inventory + +```bash +vastai show machines # List all machines you host +vastai show machine # Single machine details +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 +vastai list machine --price_gpu 0.30 # List at $0.30/GPU/hr on-demand +vastai list machine --price_gpu 0.30 --price_inetu 0.05 --price_inetd 0.05 --price_disk 0.10 +vastai unlist machine # Remove from marketplace (existing renters keep running) + +vastai set min-bid --price 0.10 # Floor price for interruptible/bid rentals +vastai set defjob --image vastai/pytorch:@vastai-automatic-tag --args "..." # Default job when idle +vastai remove defjob # Remove default job +``` + +**Pricing flags on `list machine`:** +- `--price_gpu <$/hr>` — on-demand GPU price (per GPU per hour) +- `--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`). +- `--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 +- `--vol_size ` / `--vol_price <$/GB/mo>` — volume contract offer alongside the machine offer + +> Price changes apply to **new** rentals only. Existing renters keep the price they signed up at unless they accept a price-increase. Use `vastai show machine --raw | jq .` to verify your current listed prices. + +### Maintenance + +```bash +# --sdate is UNIX EPOCH SECONDS (not ISO 8601); --duration is HOURS as a float (not "4h"). +# 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 +``` + +> `--maintenance_category` accepts `power | internet | disk | gpu | software | other`. Active renters on the machine are terminated when the maintenance window opens. Schedule outside peak hours and notify renters where possible. + +### Cleanup & defrag + +```bash +vastai cleanup machine # Remove expired storage from terminated instances on a single machine +vastai defrag machines ... # Defragment specific machines. POSITIONAL machine IDs. NOTE: the subcommand is `defrag machines` even though `--help` calls it `defragment machines` — that usage-line wording is misleading; only the abbreviated form executes. +``` + +> Both are disruptive. `cleanup` is per-machine and reclaims expired-instance disk. `defrag` takes a list of machine IDs and reshuffles GPU assignments on each to make multi-GPU offers possible — running instances may be interrupted. Confirm with the user before running, and always pass the exact ID list. + +### Network disks & clusters + +```bash +vastai show network-disks # List network disks across your machines +vastai add network-disk /mnt/disk1 # First time: positional MACHINES + MOUNT_PATH +vastai add network-disk /mnt/disk1 -d 12345 # Subsequent attach: pass --disk_id (-d) of existing disk +``` + +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`). + +### Earnings & billing + +```bash +vastai show earnings # Host earnings summary +vastai show earnings --start_date 2026-01-01 --end_date 2026-02-01 # UNDERSCORES — show earnings differs from show invoices-v1 (which uses hyphenated --start-date / --end-date). Verify each command's flag form with `vastai --help` before assuming. +vastai show invoices-v1 --limit --latest-first # Always pass --limit (see pagination gotcha below); cap is in `--help` +vastai show invoices-v1 --charges --limit --latest-first +vastai show invoices-v1 --invoices --limit --latest-first +vastai show deposit # Reserved-rental deposit info +``` + +`show invoices` (without `-v1`) is deprecated — use `show invoices-v1`. + +> **Pagination gotcha:** `show invoices-v1` prints *"Fetch next page? (y/N)"* after the first page — even with `--raw`. Pass `--limit N --latest-first` on `show invoices-v1` (both flags supported per `vastai show invoices-v1 --help`) to short-circuit the prompt. `show machines` is *not* paginated — `vastai show machines --help` lists no `--limit` flag, so pass none. + +### Marketplace metrics + +These require the `machine_read` permission group — available to hosts and admins only. They return `401` for pure renter accounts. + +```bash +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 "RTX 4090" # GPU name is positional, not a query expression +vastai metrics gpu-locations # Geographic distribution +``` + +Use these to spot underserved GPU/region combinations and price your machines competitively. + +### Account, API keys, 2FA + +```bash +vastai show user # Account info +vastai show api-keys # List API keys +vastai show api-key +vastai create api-key --name "host-monitoring" --permission_file ./perms.json # Scoped key. --permission_file takes a FILE PATH to JSON, NOT inline JSON. See https://vast.ai/docs/cli/roles-and-permissions +vastai delete api-key +vastai reset api-key # Rotate main key (get new from console) + +vastai show audit-logs # Account action history +vastai show ipaddrs # IP address history + +# 2FA — see full subcommand reference in the `vastai` (renter) skill +vastai tfa status # List configured methods +vastai tfa totp-setup # Start TOTP enrollment +vastai tfa activate CODE --secret SECRET -t totp # CODE is POSITIONAL; --secret is required; -t selects sms/totp +``` + +> Use scoped API keys for monitoring scripts — give them read-only permissions so a leaked key can't unlist machines or change prices. + +### Teams + +```bash +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 +vastai remove member +vastai create team-role --name "host-operator" --permissions ./role.json # --permissions takes FILE PATH to JSON, NOT inline +vastai show team-roles +vastai show team-role +vastai update team-role --permissions ./role.json # Same — file path +vastai remove team-role +``` + +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 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 | + +## Pricing & strategy tips + +1. **Run `self-test machine` before listing.** Failed self-tests cascade into bad reviews and low rentability. +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. +6. **Schedule maintenance** in low-utilization windows (check your own historic billing or `metrics gpu-trends` for the GPU in your region). +7. **Use scoped API keys** for monitoring scripts. Never embed your primary key in shell scripts. + +## URLs + +``` +https://vast.ai/console/host/ Host dashboard +https://vast.ai/console/host/setup/ Initial machine registration +https://vast.ai/console/host/billing/ Host payouts +https://console.vast.ai/manage-keys/ Create and manage API keys +https://docs.vast.ai/llms.txt Full docs index for LLMs +``` + +## Environment variables + +- `VAST_API_KEY` — API key (alternative to `vastai set api-key`) +- `VAST_URL` — API endpoint override (default `https://console.vast.ai`) diff --git a/skills/vastai/LICENSE b/skills/vastai/LICENSE new file mode 100644 index 0000000000..82585c3e14 --- /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 new file mode 100644 index 0000000000..dfa11a4275 --- /dev/null +++ b/skills/vastai/SKILL.md @@ -0,0 +1,611 @@ +--- +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:*) +metadata: + author: vast-ai + compatibility: Linux, macOS +--- + +# vastai + +Manage GPU instances, templates, volumes, serverless endpoints, SSH keys, and billing on Vast.ai. + +> Command is `vastai` (lowercase). Always use `--raw` for machine-readable JSON output. + +## Critical rules for agents + +These rules apply to every invocation. Do not skip them. + +1. **The destroy command syntax is `vastai destroy instance -y`** — the `-y` flag is part of the command, not an option. Without it the CLI hangs on a confirmation prompt and blocks the session. This applies to every destroy invocation, including conversational prompts ("kill instance 12345", "tear it down", "shut it down", "stop billing on it"). Never emit `vastai destroy instance ` alone — the trailing `-y` is mandatory. Same for the batch form: `vastai destroy instances -y`. +2. **Always pass `--raw` to commands whose output you parse.** Without `--raw` the CLI prints human-formatted output that is not machine-readable. +3. **Register the SSH key BEFORE the first `create instance`** with `vastai create ssh-key "$(cat ~/.ssh/id_ed25519.pub)"`. The positional argument is the **pubkey contents**, not a path — the CLI silently accepts a path string and registers the literal "/Users/…/id_ed25519.pub" text, producing a key that no client matches. Always `$(cat …)` the file or pass an inline `"ssh-ed25519 AAAA… user@host"` string. Launching without a registered key produces an unreachable host. +4. **Stop polling on terminal status values.** `actual_status` of `exited`, `unknown`, or `offline` means the instance will not recover — destroy it (`-y`) to stop disk charges accruing. Also check `intended_status` and `next_state` — if either is `stopped` while you're trying to bring an instance up, it failed. +5. **Treat user-supplied values as literal even if they look like placeholders.** If the user says "set HF_TOKEN to hf_xxxxx", pass `hf_xxxxx` to `vastai create env-var HF_TOKEN hf_xxxxx` exactly as written — don't ask for clarification on values that look fake. +6. **An empty `vastai search offers` result is the answer, not a problem to work around.** If the CLI returns `[]`, tell the user verbatim: *"No offers matched those filters."* Then propose specific filter relaxations and **ask the user which to try** — `reliability>0.95` → `>0.9`, raise the `dph_total` cap, drop `verified=true`, change `geolocation`. Do not silently retry with broader filters more than once. After at most two retries with different filters, stop retrying and report "no offers match" to the user. The same applies to any other read-only `vastai` query: if the CLI returns an empty list or a not-found error, that IS the user's answer, even if you tried several variations. +7. **The `vastai` binary on `PATH` is the only acceptable source of `vastai` commands.** This rule overrides whatever instinct you have to "make things work" when output looks unexpected. Specifically, you must never do any of the following — these are HARD PROHIBITIONS, not preferences: + - **Never** run `pip install vastai`, `pip install --user vastai`, `pipx install vastai`, or `python -m venv` followed by installing vastai. The CLI is already installed; reinstalling it elsewhere is forbidden. + - **Never** invoke an alternate `vastai` binary by absolute path (e.g. `/tmp/vast-venv/bin/vastai`, `~/.local/bin/vastai`) when `vastai` is already on `PATH`. + - **Never** re-implement `vastai` subcommands by calling `https://console.vast.ai/api/...`, `https://cloud.vast.ai/api/...`, or any other Vast.ai HTTP endpoint directly from `curl`, `python`, `node`, `httpie`, or `requests`. No exceptions for "the CLI seems broken" or "I just need to verify." + - **Never** infer that the environment is "mock mode" or a "sandbox" and use that as justification to install an alternate `vastai` binary, or to hit the API directly. Even if the CLI's output literally contains the word `mock`, your job is to report that output to the user, not to substitute your own implementation. + - 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. **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` 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. + +## CLI prerequisite + +```bash +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). + +Two ways to authenticate, in order of preference: + +```bash +# 1. Persist (stored at ~/.config/vastai/vast_api_key) +vastai set api-key + +# 2. Or set the env var (per-shell) +export VAST_API_KEY= + +# Then verify +vastai show user # auth + balance +``` + +> **Precedence and the admin-key shadowing trap.** Resolution order is: `--api-key ` on the command > `$VAST_API_KEY` env var > stored key at `~/.config/vastai/vast_api_key`. A `VAST_API_KEY` set in the shell **silently overrides** whatever the user just stored with `vastai set api-key`. The CLI prints a one-line `⚠️ VAST_API_KEY is set in your environment and overrides the key you just saved` after `set api-key`, but it is easy to miss. +> +> If the user intended their stored key to be the active one (e.g. they deliberately created a narrower-scope key), check `env | grep VAST_API_KEY` before running authenticated commands. If `$VAST_API_KEY` is set and contains a broader-scope key, prompt the user to `unset VAST_API_KEY` so subsequent commands use the scoped stored key. + +Register your SSH key **before** the first `create instance`. The positional argument is the pubkey **contents**, not a path: + +```bash +vastai create ssh-key "$(cat ~/.ssh/id_ed25519.pub)" +``` + +Passing the path directly (`vastai create ssh-key ~/.ssh/id_ed25519.pub`) silently registers the literal string "/path/to/file" — no error, but no client key will ever match it. + +**If the account has 2FA enabled,** authenticate once per shell with `vastai tfa login` — the CLI writes a session key to `~/.config/vastai/vast_tfa_key` and uses it transparently on subsequent calls: + +```bash +# Recommend the user run this themselves with `!` prefix so the 6-digit code stays out of the transcript: +!vastai tfa login --method-type totp --code 123456 +``` + +Without an active TFA session, **almost every authenticated read** (`show user`, `show ssh-keys`, `show instances-v1`, `show env-vars`, `show invoices-v1`, etc.) returns a `401` whose body contains *"requires you to have logged in using Two Factor Authentication."* Search (`vastai search offers`) and a few other read-only public endpoints still work. See the "Common errors" table for the exact remediation pattern. + +## Quick start + +```bash +vastai search offers 'gpu_name=RTX_4090 num_gpus=1 verified=true direct_port_count>=1 rentable=true' -o 'dlperf_usd-' +vastai create instance \ + --image vastai/pytorch:@vastai-automatic-tag \ + --disk 20 --ssh --direct --cancel-unavail +# Response: {"success": true, "new_contract": } +vastai show instance # Poll until actual_status == "running" +vastai ssh-url # Get SSH connection string (parse, don't $() — see SSH section) +vastai copy local:./data/ :/workspace/ # Upload files +vastai destroy instance -y # Clean up (stops all billing) +``` + +- `@vastai-automatic-tag` is server-resolved per machine. It only works on **Vast curated images** (`vastai/pytorch`, `vastai/vllm`, `vastai/comfy`, `vastai/base-image`, `vastai/linux-desktop`). Third-party images like `pytorch/pytorch` need a real tag (e.g. `pytorch/pytorch:2.4.0-cuda12.4-cudnn9-runtime`). + +## Global flags + +Available on every command: + +``` +--api-key KEY Override stored API key +--raw Output machine-readable JSON (agents should always use this) +--full Print full results (don't page with less) +--explain Show underlying API calls (useful for debugging) +--curl Show equivalent curl command +--no-color Disable colored output +--url URL Override server REST API URL +--retry RETRY Set retry limit for API calls +--version Show CLI version +``` + +## Query syntax + +Search commands accept filter expressions. Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `in`, `notin`. + +```bash +'gpu_name=RTX_4090 num_gpus=1' # Exact match + numeric +'gpu_ram>=48 reliability>0.95' # Greater-than filters +'geolocation=EU dph_total<=2.0' # Region + price cap +'gpu_name in ["RTX 4090","RTX 3090"] geolocation notin [CN,VN]' +``` + +Quote the whole expression — `>` and `<` are shell metacharacters. String values can use underscores (`gpu_name=RTX_4090`) or quoted spaces (`gpu_name='RTX 4090'`). + +**Common filter fields:** `num_gpus`, `gpu_name`, `gpu_ram`, `cpu_ram`, `disk_space`, `reliability`, `compute_cap`, `cuda_vers`, `inet_up`, `inet_down`, `dph_total`, `geolocation`, `direct_port_count`, `static_ip`, `verified`, `rentable`, `dlperf`, `dlperf_usd`, `total_flops`, `cpu_arch`, `gpu_arch`. + +**Common sort fields (`-o`):** +- `dlperf_usd-` — best DL-perf per dollar (recommended default for value) +- `dph_total` — cheapest first +- `score-` — overall value metric (CLI default) +- `dph_base` is the **legacy** field — prefer `dph_total`. + +**Sort direction:** plain field name is ascending; postfix `-` for descending (`-o 'dph_total-'`). + +**Hidden defaults on `search offers`:** Even with no `-n` flag, the CLI applies these implicit filters (per `vastai search offers --help` on current CLIs): + +``` +verified=true external=false rentable=true +``` + +To widen, **override the specific filter** (e.g. `verified=any` for unverified offers, `external=any` to include external hosts). **Do not pass `-n` / `--no-default`** to drop all three at once — that exposes the renter to unverified or external offers and produces unpredictable launches. + +## Recommended Vast images + +Vast curated images plus `@vastai-automatic-tag`: + +``` +vastai/base-image:@vastai-automatic-tag # Minimal Ubuntu base +vastai/pytorch:@vastai-automatic-tag # PyTorch + CUDA +vastai/vllm:@vastai-automatic-tag # vLLM (model via env) +vastai/comfy:@vastai-automatic-tag # ComfyUI (checkpoint via env) +vastai/linux-desktop:@vastai-automatic-tag # Linux desktop (VNC/RDP) +``` + +For vLLM/Comfy, set the model via `--env`, not `--args`: + +```bash +# vLLM +vastai create instance --image vastai/vllm:@vastai-automatic-tag --disk 40 --ssh --direct \ + --env '-e MODEL_NAME=Qwen/Qwen2.5-3B-Instruct -e HF_TOKEN=hf_xxx' + +# ComfyUI +vastai create instance --image vastai/comfy:@vastai-automatic-tag --disk 40 --ssh --direct \ + --env '-e CHECKPOINT_MODEL=black-forest-labs/FLUX.1-schnell -e HF_TOKEN=hf_xxx' +``` + +Browse pre-configured models at . + +> **vLLM gotcha:** vLLM requires a minimum CUDA compute capability. Vast's `compute_cap` field is **not** the raw `X.Y` decimal — it's an integer encoding. Read the current encoding from `vastai search offers --help` (the field's description line shows examples), pick the threshold for your target compute capability, and filter with that exact integer. If you guess based on the decimal version, the filter will silently let unsupported GPUs through and vLLM will fail at runtime with `no kernel image is available`. + +## Commands + +### Instances + +```bash +vastai show instances # Legacy list +vastai show instances-v1 # Paginated, filterable (recommended) +vastai show instances-v1 --status running loading # Filter by status +vastai show instances-v1 --gpu-name 'RTX 4090' # Filter by GPU +vastai show instances-v1 --label training # Filter by label +vastai show instances-v1 --order-by start_date desc # Sort +vastai show instances-v1 --cols id,status,gpu,dph # Custom columns +vastai show instances-v1 -a # Auto-fetch all pages +vastai show instance # Poll single instance + +vastai create instance --image vastai/pytorch:@vastai-automatic-tag --disk 20 --ssh --direct --cancel-unavail +# Response includes "new_contract": — that is your instance ID + +vastai launch instance -g RTX_4090 -n 1 -i vastai/pytorch:@vastai-automatic-tag -d 32 --ssh --direct --cancel-unavail +# Search + create top result in one call. Short flags: -g gpu-name, -n num-gpus, -i image, -d disk, -r region. + +vastai start instance # Start stopped instance +vastai stop instance # Stop (preserves disk, no GPU charges) +vastai reboot instance # Stop + start +vastai recycle instance # Destroy + recreate container — re-pulls image, keeps GPU priority +vastai update instance --image NEW_IMAGE # In-place update of template/image/args/env/onstart +vastai destroy instance -y # Permanent delete (-y required for non-interactive) +vastai destroy instances -y # Batch delete +vastai label instance "training-run-1" # Tag instance (label is POSITIONAL, not --label) +vastai prepay instance 100 # Deposit credits — amount POSITIONAL, not --amount +vastai change bid --price 0.20 # Change spot bid price (--price, NOT --bid / --bid_price) +vastai accept price-increase # Accept pending host price hike +``` + +**`recycle` vs `update`:** Use `recycle` to re-pull the image without losing GPU priority (e.g. after a `docker push`). Use `update --image NEW` to swap to a different image in place. + +**create instance flags:** +- `--image IMAGE` — Docker image +- `--disk DISK` — Local disk in GB (**always pass this**) +- `--ssh` / `--jupyter` — Connection type +- `--direct` — Faster direct connections (use with `--ssh`) +- `--cancel-unavail` — **Always pass this.** Fail if the chosen machine is unavailable instead of silently creating a stopped instance that bills for disk +- `--label LABEL` — Instance label +- `--env ENV` — Env vars and port mappings, e.g. `'-e TZ=UTC -p 8080:8080'` +- `--onstart FILE` — Path to a startup script file +- `--onstart-cmd CMD` — Inline startup script. The server enforces a length cap on `args`; payloads above the cap are rejected at `create instance` with `error 400/3471: Invalid args: len(args) > N` (`N` is whatever the server is currently configured to). If you hit it, **read the error for the current limit**, then either pass `--onstart FILE` (uploads the file, sidesteps the arg cap entirely) or gzip+base64 the script and decode inline — see "Long onstart scripts" below +- `--entrypoint` / `--args ...` — Override entrypoint and pass args (args must be last) +- `--bid_price PRICE` — Interruptible (spot) launches go through this same `create instance` command (there is no separate `vastai bid` subcommand); pass `--bid_price ` at or above the offer's `min_bid` field. Below the floor, the API rejects with `no_such_ask`. Check `min_bid` per offer in `search offers --type bid --raw` before deciding the bid. +- `--template_hash HASH` — Create from template +- `--create-volume --volume-size GB --mount-path /root/vol` — Attach new volume +- `--link-volume --mount-path /root/vol` — Attach existing volume +- `--login '-u USER -p PASS docker.io'` — Private registry credentials + +**Long onstart scripts (anything risking the server's arg-length cap — see `--onstart-cmd` note above):** + +```bash +SCRIPT_B64=$(gzip -c ./long_script.sh | base64 -w0) +vastai create instance \ + --image vastai/pytorch:@vastai-automatic-tag --disk 20 --ssh --direct --cancel-unavail \ + --onstart-cmd "echo $SCRIPT_B64 | base64 -d | gunzip | bash" +``` + +Or just use `--onstart ./long_script.sh` which uploads the file directly. + +**Instance status fields (read all four):** + +`show instance --raw` returns four status fields. Don't act on `actual_status` alone — diagnose with all four. + +| Field | What it tells you | +|-------|-------------------| +| `intended_status` | What the user/system asked for (`running`, `stopped`). The target. | +| `actual_status` | Current observed state — the polling target. See values table below. | +| `cur_state` | Provisioning sub-state (e.g. `scheduling`, `loading`, `running`). Distinguishes "still spinning up" from "stuck." | +| `next_state` | Scheduled transition queued by host or system (e.g. maintenance window about to evict). | + +**`actual_status` values:** + +| Value | Meaning | +|-------|---------| +| `null` | Provisioning | +| `created` | Created, not yet provisioned | +| `loading` | Image downloading / container starting | +| `running` | Active — GPU charges apply | +| `stopped` | Halted — disk charges only | +| `frozen` | Paused with memory — GPU charges apply | +| `exited` | Container process exited unexpectedly | +| `rebooting` | Restarting (transient) | +| `unknown` | No recent heartbeat from host | +| `offline` | Host disconnected from Vast servers | + +**Common combinations:** + +| Pattern | Meaning | Action | +|---------|---------|--------| +| `intended=running, actual=stopped` | **Spot eviction** — host outbid you | Raise the bid with `vastai change bid --price `, then `vastai start instance `. (`change bid` uses `--price`, not `--bid` — different from the `--bid_price` flag on `create instance`.) | +| `actual=created, cur_state=scheduling` | Still provisioning | Keep polling | +| `actual=loading` (long) | Image pulling (vLLM ~15 GB takes 5–10 min) | Keep polling; tail `vastai logs ` | +| `actual=running, intended=stopped` | Stop is queued | Transient; will become `stopped` | +| `actual=stopped, intended=stopped` | Cleanly stopped by user | `vastai start instance ` to resume | +| `actual in [exited, unknown, offline]` | **Terminal failure** | Destroy with `-y` and retry on a different offer | +| `next_state=stopped` while bringing up | Host has queued an eviction | Treat as fatal; try another offer | + +> **Poll loop warning:** Terminal `actual_status` values (`exited`, `unknown`, `offline`) never recover. Always add a timeout — your script otherwise loops forever while disk charges accrue. + +> **Charges:** Storage charges begin at creation (or earlier if `--cancel-unavail` is omitted on an unavailable machine). GPU charges begin when status reaches `running`. + +### Polling pattern (with timeout) + +```bash +INST= +deadline=$((SECONDS + 600)) +while [ $SECONDS -lt $deadline ]; do + STATUS=$(vastai show instance $INST --raw | jq -r '.actual_status // "null"') + case "$STATUS" in + running) echo "ready"; break ;; + exited|unknown|offline) echo "fatal: $STATUS"; vastai destroy instance $INST -y; exit 1 ;; + *) echo "status=$STATUS"; sleep 10 ;; + esac +done +[ $SECONDS -ge $deadline ] && { echo "timed out"; vastai destroy instance $INST -y; exit 1; } +``` + +### Container port ≠ host port + +`-p 8000:8000` in `--env` is NOT the host port — Vast remaps to a random host port. Always read the actual mapping: + +```bash +IP=$(vastai show instance --raw | jq -r '.public_ipaddr') +PORT=$(vastai show instance --raw | jq -r '.ports."8000/tcp"[0].HostPort') +echo "http://$IP:$PORT" +``` + +### Search + +```bash +vastai search offers # Default: verified, on-demand, score-sorted +vastai search offers 'gpu_name=RTX_4090 num_gpus=1 verified=true direct_port_count>=1' -o 'dlperf_usd-' +vastai search offers 'num_gpus>=4 reliability>0.99' -o 'num_gpus-' +vastai search offers 'gpu_ram>=8 num_gpus=1 compute_cap>=' -o 'dph_total' --limit 10 # Derive from `vastai search offers --help` (compute_cap encoding) for your target CUDA capability — see the vLLM gotcha above +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 # 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 +vastai search invoices 'amount_cents>3000' # Invoice history (structured query, same syntax) +``` + +**search offers flags:** `--type on-demand|reserved|bid`, `--order/-o FIELD[-]`, `--limit`, `--storage GB` (storage budget for pricing). + +### SSH & file transfer + +```bash +vastai ssh-url # ssh:// connection URL (does NOT open a session) +vastai scp-url # scp:// URL +vastai attach ssh "ssh-ed25519 AAAA..." # Per-instance SSH key attach +vastai detach ssh # Per-instance SSH key detach. Note: `vastai detach ssh --help` prints `vastai detach ` (no `ssh`) — the help text is misleading; the actual subcommand IS `detach ssh`. Pass numeric IDs only; passing the public-key string crashes server-side. +vastai show ssh-keys # List account SSH keys +vastai create ssh-key "$(cat ~/.ssh/id_ed25519.pub)" # Add key — positional takes CONTENTS, not a path. Bare path strings are silently accepted and stored verbatim. On a team-context API key this returns HTTP 400 "team SSH keys are not supported" — SSH keys must be registered against the personal account (the team admin's, or have the user switch context). +vastai create ssh-key # Generate new key if needed +vastai create ssh-key "ssh-ed25519 AAAA..." # Add SSH key inline +vastai delete ssh-key # Remove SSH key from account +vastai update ssh-key "ssh-ed25519 AAAA..." # Update SSH key value +``` + +**`ssh-url` does NOT open a session, and its output is NOT directly usable by `ssh`.** Both with and without `--raw` it returns the **same plain string** in the form `ssh://root@:` (e.g. `ssh://root@ssh5.vast.ai:38266`). The `--raw` flag is accepted but does NOT yield JSON — `ssh-url` has no structured output. `ssh` rejects the `ssh://...` form directly. Parse it, or — the most reliable path — read `ssh_host` and `ssh_port` from `show instance --raw` (which IS real JSON): + +```bash +# Preferred — JSON from show instance is reliable structured output +HOST=$(vastai show instance --raw | jq -r '.ssh_host') +PORT=$(vastai show instance --raw | jq -r '.ssh_port') +ssh -p "$PORT" "root@$HOST" + +# Fallback — parse the plain ssh:// string from ssh-url --raw +URL=$(vastai ssh-url --raw ) # returns: ssh://root@ssh5.vast.ai:38266 (a string) +HOST=$(echo "$URL" | awk -F'[@:/]' '{print $5}') +PORT=$(echo "$URL" | awk -F'[@:/]' '{print $NF}') +ssh -p "$PORT" "root@$HOST" +``` + +### File copy + +```bash +vastai copy local:./data/ :/workspace/data/ # Local → instance (preferred form) +vastai copy :/workspace/ local:./pulled/ # Instance → local +vastai copy :/workspace/ :/workspace/ # Instance → instance +vastai copy 12345:./data ./local-data # Legacy form still works +vastai cancel copy # Cancel a running copy. can be a bare instance id (`12371`) or the full `instance_id:/path` form to disambiguate multiple copies into the same instance. + +# Cloud storage sync — needs a configured connection (set up in console at https://cloud.vast.ai/cloud-integrations/) +vastai show connections +vastai cloud copy --src ./data --dst s3://bucket/path \ + --instance 12345 --connection \ + --transfer "Instance To Cloud" + +# Recurring sync +vastai cloud copy --src ./logs --dst s3://bucket/logs/ \ + --instance 12345 --connection 7 --transfer "Instance To Cloud" \ + --schedule DAILY --hour 4 +``` + +### Logs + +```bash +vastai logs # Container logs (last 1000 lines) +vastai logs --tail 100 # Last 100 lines +vastai logs --filter "error" # Grep filter +vastai logs --daemon-logs # Host daemon logs (instead of container) +``` + +> Logs are stored in S3 and may take 30–60 s to appear after start. Initial fetches return "waiting on logs" — keep retrying. +> +> **There is NO working way to inspect or run commands on a STOPPED instance.** Verified 2026-06-02: `vastai execute` crashes (`AttributeError: 'str' object has no attribute 'get'`) on valid input, and `vastai copy :/path ./local` against a stopped instance fails with `rsync: Unknown module ''` because the in-instance rsync daemon isn't running. To inspect a stopped instance, `vastai start instance `, wait for `actual_status == running`, then SSH in or `vastai copy`. Don't suggest `execute` — it appears in `vastai --help` but doesn't work. +> +> For arbitrary commands on a **running** instance: SSH in. Read `ssh_host` / `ssh_port` from `show instance --raw` and `ssh -p $PORT root@$HOST ''`. + +### Volumes + +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) +vastai search volumes +vastai show volumes +vastai create volume -s 500 -n my-data # Size in GB, name optional +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 \ + --docker_login_user $DOCKER_USER --docker_login_pass $DOCKER_TOKEN \ + --pause true # Pause during commit (safer, slower) +``` + +### Serverless & deployments + +3-tier model: **endpoints** route requests to **worker groups** which back **deployments**. + +```bash +vastai show endpoints +vastai create endpoint --endpoint_name "qwen25-3b" \ + --min_load 10 --target_util 0.9 --cold_mult 2.5 \ + --cold_workers 1 --max_workers 20 \ + --max_queue_time 30 --target_queue_time 10 --inactivity_timeout 600 +vastai update endpoint --max_workers 50 +vastai delete endpoint +vastai get endpt-logs + +vastai show workergroups +vastai create workergroup --template_hash --endpoint_name "qwen25-3b" --cold_workers 1 # No --name flag — identity is template + endpoint. Use --template_hash (or --template_id) + --endpoint_name (or --endpoint_id). Pass --search_params if not inheriting from template. Requires an admin-scope API key AND a positive deposited balance on the team account (separate from regular credit; fund via prepay or auto-recharge). Endpoint operations return 403 without both. +vastai update workergroup --endpoint_id [options] # --endpoint_id is REQUIRED per the usage line (vastai update workergroup --help) +vastai update workers # Trigger rolling worker update +vastai update workers --cancel # Cancel an in-progress rollout +vastai delete workergroup +vastai get wrkgrp-logs +vastai get endpt-workers # List workers with status + measured_perf + +vastai show deployments +vastai show deployment +vastai show deployment-versions +vastai delete deployment + +vastai show scheduled-jobs +vastai delete scheduled-job +``` + +### Templates + +```bash +vastai search templates 'name=pytorch' # Structured query — fields: name, creator_id, count_created, hash_id, image, tag, recommended, use_ssh, jup_direct, ssh_direct, recent_create_date, recommended_disk_space +vastai create template --name "Qwen vLLM" --image vastai/vllm:@vastai-automatic-tag \ + --env '-p 8000:8000 -e MODEL_NAME=Qwen/Qwen2.5-3B-Instruct' --disk_space 40 # --disk_space, NOT --disk +vastai update template --disk_space 100 # POSITIONAL is the template hash_id (string), not a numeric id. Flag is --disk_space. +vastai delete template --template-id # NUMERIC `id` field only. --hash-id is advertised in --help but returns 400 on current CLIs. List-style discovery is through `vastai search templates '' --raw` — read the `id` field from the result. There is no `vastai show templates` subcommand. +vastai run benchmarks --template_hash --gpus RTX_4090 -y # --template_hash (or --template_id); --gpus comma-separated (e.g. "RTX_4090,RTX_3090"), supports inline Nx prefix ("2x RTX_4090"); --num_gpus N sets GPUs per instance when no Nx prefix; -y skips the cost confirmation. See https://docs.vast.ai/cli/reference/run-benchmarks for examples. +``` + +> If `run benchmarks` returns `400 endpoint_name: Value error, contains disallowed shell characters`, the auto-generated endpoint name on the user's CLI version isn't passing the API's `[a-z0-9_-]+` check. Pass the response back to the user verbatim and suggest they upgrade `vastai` (`pip install -U vastai`) or check the latest CLI release; don't retry or rewrite the command from this side. + +### Account & API keys + +```bash +vastai set api-key # Save API key locally +vastai show api-key # Show a specific key +vastai show api-keys # List all your API keys +vastai create api-key --name "ci" --permission_file ./perms.json # Create restricted key. --permission_file takes a FILE PATH to valid JSON (NOT inline JSON). Two distinct HTTP 400 cases — same status code, different cause: (a) malformed/empty JSON → validate with `jq . perms.json` first; (b) the CALLING key isn't admin-scope → no JSON change will help, the user needs an admin key. If a `jq`-valid file still 400s, surface this as a scope issue to the user, don't retry with permission tweaks. See https://vast.ai/docs/cli/roles-and-permissions +vastai delete api-key +vastai reset api-key # Reset main key (get new from console) +vastai show user # Account info + credit balance +vastai show audit-logs # Account action history +vastai show connections # Cloud storage connections +vastai show ipaddrs # IP address history +vastai transfer credit --recipient EMAIL --amount 10 +vastai show subaccounts +vastai create subaccount --email sub@example.com --username sub --password '' --type host # --type is `host` or `client`. All four flags are required. Requires an admin-scope API key — scoped/read-only keys return HTTP 400 with no useful body. +``` + +### Environment variables + +User-scoped environment variables injected into instances at launch — for API tokens (HuggingFace, OpenAI, etc.) and model config. + +```bash +vastai show env-vars --raw # List names only — values are ALWAYS masked as '********', even with -s/--show-values (verified CLI 1.0.13). There is no working way to retrieve env-var values via the CLI; treat them as write-only. +vastai create env-var HF_TOKEN hf_abc123 --raw # Create (value passed literally) +vastai update env-var HF_TOKEN hf_new456 --raw +vastai delete env-var HF_TOKEN --raw +``` + +> **Account env-vars vs `--env`:** Account env-vars are stored server-side and auto-injected into *every* instance you create. `--env` on `create instance` is per-instance only and doesn't persist. Use account env-vars for secrets you reuse (HF_TOKEN, OPENAI_API_KEY) and `--env` for per-launch config (port mappings, MODEL_NAME, TZ). + +Common prompts and the calls they map to: + +- *"set HF_TOKEN to hf_xxxxx"* → `vastai create env-var HF_TOKEN hf_xxxxx --raw` +- *"add a HuggingFace token"* → ask for the token value, then `vastai create env-var HF_TOKEN --raw` +- *"list my env vars"* → `vastai show env-vars --raw` +- *"unset OPENAI_API_KEY"* → `vastai delete env-var OPENAI_API_KEY --raw` + +### Billing + +**One of `-c` / `--charges` or `-i` / `--invoices` is REQUIRED.** Bare `vastai show invoices-v1 --raw --limit --latest-first` (without either) fails — the server needs to know which ledger to return. Always pass `--limit` too to avoid the `(y/N)` pagination prompt (cap is in `--help`). + +```bash +vastai show invoices-v1 -c --limit --latest-first # Charges (most common) +vastai show invoices-v1 -i --limit --latest-first # Paid invoices +vastai show invoices-v1 -c --charge-type i v s --limit --latest-first # i=instance v=volume s=serverless +vastai show invoices-v1 -c --start-date --end-date --limit +vastai show invoices-v1 -c --format tree --verbose --limit --latest-first # Full details (tree only) +vastai show invoices-v1 -c --next-token # Resume pagination explicitly +vastai show deposit # Reserved instance deposit info +``` + +`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 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` 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 # 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 +vastai create team-role --name "viewer" --permissions ./role.json # --permissions takes a FILE PATH to JSON, NOT inline +vastai show team-role +vastai update team-role --permissions ./role.json # Same — file path +vastai remove team-role +``` + +### 2FA + +**All `tfa *` subcommands require a personal-context API key**, not a team-context key. A team-context key returns `403 "2FA actions are available in a Team Context. Please switch to your personal context and try again."` (the wording is misleading — it means tfa actions are NOT available in team context). Before any `tfa` call, confirm the user is operating with their personal-account key (`vastai show members --raw` returns `[]`). + +```bash +# Setup +vastai tfa status # List configured 2FA methods + IDs +vastai tfa totp-setup # Start TOTP enrollment (returns secret to scan) +vastai tfa activate CODE --secret SECRET -t {sms,totp} # Finish enrollment. CODE is positional. +vastai tfa send-sms # Send an SMS code to the registered phone +vastai tfa send-email # Send an email code +vastai tfa resend-sms --secret SECRET [--phone-number +1XXX] # Re-send during a login flow + +# Login / auth flows +vastai tfa login --code CODE [-t {sms,totp,email}] [--secret SECRET] [--backup-code CODE] [--method-id ID] +vastai tfa auth-new --code CODE --secret SECRET # Authenticate a new device session +vastai tfa regen-codes --code CODE [-t METHOD] [--secret SECRET] # Regenerate one-time backup codes + +# Manage methods +vastai tfa update METHOD_ID [--label NAME] [--set-primary true] +vastai tfa delete --id-to-delete METHOD_ID --code CODE [-t METHOD] # Remove a 2FA method +``` +> `--method-type` (`-t`) accepts `sms`, `totp`, or `email` (some subcommands only support a subset — check `--help`). `--backup-code` is accepted as an alternative to `--code` on `login`, `delete`, `regen-codes`, `auth-new`. + +> Hosting a machine on Vast? Those commands (`show machines`, `list machine`, `set min-bid`, `schedule maint`, `metrics gpu`, `show earnings`, …) live in the separate `vastai-host` skill. + +## 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 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 | +| `No offers found` | Filters too restrictive (often blocked by hidden defaults) | Override the specific default (`verified=any`, `rentable=any`) — see "Hidden defaults" in Query syntax. Do NOT pass `-n` to drop all defaults. | +| `Permission denied` (SSH) | No SSH key attached | `vastai create ssh-key` BEFORE `create instance` | +| `Connection refused` | Instance not yet running | Poll `show instance ` until `actual_status == "running"` | +| `no kernel image is available` (vLLM or similar) | The launched GPU's CUDA compute capability is below what the workload requires | Filter `compute_cap` in the search query. `compute_cap` is an encoded integer, not the decimal version — read its description in `vastai search offers --help` to pick the right threshold. | +| Hangs on `destroy instance` | Confirmation prompt | Add `-y`: `vastai destroy instance -y` | +| Wrong host port | Reading `-p 8000:8000` literally | Read `.ports."8000/tcp"[0].HostPort` from `show instance --raw` | + +## Troubleshooting + +### Instance failed silently +Don't trust `actual_status` alone — check `intended_status` and `next_state`. If either is `stopped` while bringing an instance up, it failed. + +```bash +vastai show instance --raw | python3 -c " +import json, sys +d = json.load(sys.stdin) +for k in ('actual_status', 'intended_status', 'next_state', 'status_msg'): + print(f'{k}: {d.get(k)}') +" +``` + +Common causes: GPU hardware error (try different offer), missing/private image, insufficient disk, CUDA incompat. + +### Long startup +Large images (vLLM ~15 GB) can take 5–10 min to pull. Status shows `loading` while pulling, `running` when up. Tail with `vastai logs --tail 50`. + +## URLs + +``` +https://console.vast.ai/instances/ Your instances +https://console.vast.ai/create/ Search GPU offers +https://console.vast.ai/manage-keys/ Create and manage API keys +https://cloud.vast.ai/billing/ Billing +https://cloud.vast.ai/cloud-integrations/ Cloud storage connections +https://vast.ai/model-library Pre-configured models +https://docs.vast.ai/llms.txt Full docs index for LLMs +``` + +## Environment variables + +- `VAST_API_KEY` — API key (alternative to `vastai set api-key`) +- `VAST_URL` — API endpoint override (default `https://console.vast.ai`)