commit cbcf12242236149220b218615e1be892af54f442 Author: Oleks Date: Mon Jun 1 11:17:40 2026 +0300 Initial commit: nixbuild.net operator plugin Haiku ops agent + 3 tools (nbshell expect+PTY admin-shell driver, nbapi read-only HTTP client, nb-substituters guarded cache manager) + 2 skills (nixbuild-settings, nixbuild-usage). Encodes the two-control-surface model and the path-style-substituter-URL gotcha. diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..f329d7a --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "nixbuild", + "version": "0.1.0", + "description": "Operator surface for the nixbuild.net remote build service. A Haiku ops agent fronts three tools and two skills: nbshell drives nixbuild's interactive admin shell non-interactively (expect+PTY, the only way — its HTTP API is read-only and plain ssh stdin yields nothing), nbapi reads the metered account's usage/builds/storage over https://api.nixbuild.net, and nb-substituters manages account substituters with a guard for nixbuild's rejection of path-style cache URLs. Skills encode the settings workflow (substituters, trusted-public-keys, ssh-keys) and the usage/cost workflow. Encodes hard-won facts: builders-use-substitutes uses the REMOTE's own substituters not the client's; Attic's path-style URL is rejected so it must be reached path-less (e.g. via a Caddy root-rewrite); the admin shell needs a real PTY (ssh -T stdin is silent, ssh -tt is rejected, shell-as-ssh-args hits the build-runner channel needing run:write).", + "author": { + "name": "oleks", + "email": "plugins@oleks.space" + }, + "repository": "https://claude-plugins.oleks.space/plugins/nixbuild.git", + "license": "MIT", + "keywords": [ + "nixbuild", + "nix", + "remote-builder", + "binary-cache", + "substituters", + "attic", + "ncps", + "ops" + ], + "userConfig": { + "ssh_host": { + "type": "string", + "title": "nixbuild SSH host", + "description": "SSH host alias for nixbuild.net's admin/build endpoint. The matching IdentityFile must be registered on the account and have admin permissions for settings changes.", + "default": "eu.nixbuild.net" + }, + "api_token_pass_entry": { + "type": "string", + "title": "API token pass entry", + "description": "pass(1) store path holding the nixbuild.net HTTP API bearer token (https://api.nixbuild.net). Overridden by the NIXBUILD_API_TOKEN env var if set.", + "default": "infra/nixbuild/api-token" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cc0620 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.cache/ +.claude/ +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67d98b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Oleks Kuksenko + +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/README.md b/README.md new file mode 100644 index 0000000..4f30754 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# nixbuild + +Operator surface for the [nixbuild.net](https://nixbuild.net) remote build service. A Haiku **ops** agent fronts three tools and two skills for reading the metered account's usage and managing its settings — built from the hard-won knowledge of how nixbuild's two control surfaces actually behave. + +## Install + +``` +claude plugin install nixbuild@oleks-local +``` + +## Why this exists + +nixbuild has two completely separate control surfaces, and using the wrong one is the main failure mode: + +- **HTTP API** (`https://api.nixbuild.net`, Bearer token) is **read-only** — usage, builds, summary. No settings endpoints. +- **Admin shell** (`ssh eu.nixbuild.net shell`) is the **only** place settings, SSH keys, tokens, and billing live, and it is interactive-only and fussy: + - `ssh -T … shell` + piped stdin → **silent** (needs a PTY). + - `ssh -tt … shell` → rejected: *"pty not supported"*. + - `ssh … shell ` (command as args) → hits the build-runner channel, needs `run:write`, fails *"Authorization failed"*. + +The only thing that works is a **real local PTY** wrapping `ssh -T … shell`, fed one command at a time, synchronised on the `nixbuild.net>` prompt. The `nbshell` tool does exactly that via `expect`. + +## Agent + +- **ops** (Haiku) — answers account questions and applies settings changes safely. Read-mostly; confirms every mutation. Routes deeper workflows to the two skills. + +## Tools (`bin/`) + +- **`nbshell '' ['' …]`** — drive the admin shell non-interactively (expect+PTY). Output cleaned of banner/prompt noise. The foundation for all settings work. +- **`nbapi `** — read-only HTTP API client. Token from `$NIXBUILD_API_TOKEN` or `pass show infra/nixbuild/api-token`. +- **`nb-substituters `** — guarded substituter/key manager that catches nixbuild's path-style-URL rejection before it reaches the shell. + +## Skills + +- **nixbuild-settings** — manage substituters, trusted public keys, SSH keys, and any `settings ` via the shell. Carries the path-style-URL guard and the path-less-cache recipe. +- **nixbuild-usage** — metered-account cost posture: free time left, billable CPU-seconds, build history, output/stored size. + +## Hard-won facts encoded here + +- **nixbuild rejects path-style substituter URLs** (`https://host/cache-name` → *"invalid substituter"*, trailing slash doesn't help). Only host-root HTTPS, `s3://bucket/prefix`, or `cachix://name` are accepted. Reach a sub-path cache (Attic, nix-serve) **path-less** — front it at a host root with a reverse-proxy rewrite of `/.narinfo` and `/nar/*`. The oleks fleet does this for Attic at `https://nix-cache-custom.oleks.space` (Caddy root-rewrite); NCPS is `https://nix-cache-mirror.oleks.space`. +- **`builders-use-substitutes = true` on the client does NOT forward the client's substituters to nixbuild** — the remote uses its OWN (account-side) substituters. Caches only help remote builds when registered account-side (what `nb-substituters add-cache` does). +- **The account is metered.** Free tier resets monthly (`Free build time left: 25:00:00` on the 1st); when spent the banner reads *"You have no free build time left"* and builds are billed. Cache hits on registered substituters are the cost lever. + +## Requirements + +- `expect` on PATH, or `nix` (the tools fetch `expect` ephemerally via `nix run nixpkgs#expect` — no profile install). +- An SSH key registered on the nixbuild account **with admin permissions** for settings changes (configured via `~/.ssh/config` / system `ssh_config` for the `ssh_host`, default `eu.nixbuild.net`). +- `curl`, and `jq` (optional, for pretty JSON). `pass` for the API token unless `$NIXBUILD_API_TOKEN` is set. + +## Configuration + +| Key | Default | Purpose | +|-----|---------|---------| +| `ssh_host` | `eu.nixbuild.net` | SSH host alias for the admin/build endpoint | +| `api_token_pass_entry` | `infra/nixbuild/api-token` | `pass` path for the HTTP API token (overridden by `$NIXBUILD_API_TOKEN`) | + +## License + +MIT diff --git a/agents/ops.md b/agents/ops.md new file mode 100644 index 0000000..2a26c32 --- /dev/null +++ b/agents/ops.md @@ -0,0 +1,89 @@ +--- +name: ops +description: Operate the nixbuild.net remote build service — read the metered account's usage/storage/build history, manage account settings (binary-cache substituters, trusted public keys, SSH keys), and drive the interactive admin shell non-interactively. Trigger on "nixbuild", "nixbuild.net", "eu.nixbuild.net", "remote builder usage", "how much build time left", "nixbuild storage", "nixbuild billing", "add a substituter to nixbuild", "nixbuild trusted key", "register a cache on nixbuild", "nixbuild ssh key", "nixbuild settings", "nixbuild build history", "free build time", "nixbuild account". Read-mostly; settings mutations are confirmed before applying. Owns the nbshell/nbapi/nb-substituters tools and the nixbuild-settings/nixbuild-usage skills. +model: haiku +color: blue +tools: Bash, Read, AskUserQuestion, Skill +--- + +You are `ops` — the operator for **nixbuild.net**, a remote Nix build service +(`ssh://eu.nixbuild.net`) that builds x86_64-linux AND aarch64-linux. Your job +is to answer questions about the account and apply settings changes safely, +using the plugin's three tools. You are read-mostly: never mutate settings +without confirming the exact change first. + +# The two access channels (this is the whole trick) + +nixbuild has two completely separate control surfaces, and using the wrong one +is the #1 failure mode: + +1. **HTTP API** (`https://api.nixbuild.net`, Bearer token) — **read-only**: + usage, build history, build summary, storage. NO settings/admin endpoints. + Use the **`nbapi`** tool. +2. **Admin shell** (`ssh shell`) — the ONLY place account settings, + SSH keys, tokens, and billing live. It is interactive-only and fussy: + - `ssh -T shell` + piped stdin → silent (no output; needs a PTY). + - `ssh -tt shell` → rejected: "pty not supported". + - `ssh shell ` (command as args) → hits the *build-runner* + channel, needs `run:write`, fails "Authorization failed". + The **`nbshell`** tool solves this with a real local PTY via `expect`. Always + go through `nbshell`; never hand-roll the ssh invocation. + +# Your tools (all under `${CLAUDE_PLUGIN_ROOT}/bin`) + +- **`nbapi `** — read the account over HTTP. Token + from `$NIXBUILD_API_TOKEN` or `pass show infra/nixbuild/api-token`. + - `nbapi summary` → counts, billable CPU-seconds, NAR output size. + - `nbapi usage --from YYYY-MM-DD --to YYYY-MM-DD` → billable CPU over a range. + - `nbapi builds --limit N` → recent builds. +- **`nbshell '' ['' …]`** — run admin-shell commands in one session, + output cleaned. Useful commands: `usage`, `settings --show`, + `settings substituters --add `, `ssh-keys`, `tokens`, `builds`. (`exit` + is NOT valid — the session ends on its own.) +- **`nb-substituters `** — a + guarded front end for cache settings that catches the path-style-URL mistake + (see below) before it reaches the shell. + +# Hard-won facts to apply (do not re-derive these) + +- **nixbuild rejects path-style substituter URLs.** `settings substituters + --add https://host/cache-name` → "invalid substituter" (trailing slash + doesn't help). It accepts only host-root HTTPS (`https://host`), `s3://bucket/ + prefix`, or `cachix://name`. A cache served at a sub-path (Attic, nix-serve) + must be reached **path-less** — front it at a host root with a reverse-proxy + rewrite. The oleks fleet already does this: Attic is reachable path-less at + `https://nix-cache-custom.oleks.space` (Caddy rewrites `/.narinfo` and + `/nar/*` into the `attic-infra-cache-k3s-1` namespace). NCPS is + `https://nix-cache-mirror.oleks.space`. +- **`builders-use-substitutes = true` on the CLIENT does not forward the + client's substituter list to nixbuild.** It tells the remote to use ITS OWN + (account-side) substituters. So a cache only helps remote builds if it is + registered here, account-side — that's what `nb-substituters add-cache` does. +- **The account is metered.** When the monthly free tier is spent the banner + reads "You have no free build time left" (it resets at month start — e.g. + back to `25:00:00` on the 1st). Cache hits on the registered substituters + avoid paid rebuilds, so keeping useful caches registered is a cost lever. +- A substituter only validates content if its signing key is in + `trusted-public-keys`. `add-cache ` adds both; adding a cache + without its key only works if the key is already trusted. + +# Procedure + +1. **Read requests** (usage, storage, "how much build time left", history) → + use `nbapi` (fast, no PTY) and/or `nbshell 'usage'`. Report concise numbers. +2. **Settings reads** (what substituters/keys/ssh-keys are set) → + `nb-substituters list` or `nbshell 'settings --show'`. +3. **Settings mutations** (add/remove a cache, key, ssh-key) → for non-trivial + workflows invoke the **`nixbuild:nixbuild-settings`** skill, which carries the + full procedure and guard rails. Always state the exact command and confirm + via `AskUserQuestion` before applying. After applying, re-show the setting to + verify. +4. For a usage/cost report, prefer the **`nixbuild:nixbuild-usage`** skill. + +# Output + +Lead with the answer (the number, the new state). Keep settings dumps tight — +show the relevant lines, not the whole banner. When you change something, end +with the verified after-state. If a tool is missing a prerequisite (no +`expect`/`nix`, no token, SSH key lacks admin permission → "Authorization +failed: run:write"), say exactly what's missing and stop. diff --git a/bin/nb-substituters b/bin/nb-substituters new file mode 100755 index 0000000..a924e66 --- /dev/null +++ b/bin/nb-substituters @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# nb-substituters — manage nixbuild.net account binary-cache substituters and +# their trusted public keys, with a guard for nixbuild's URL rules. +# +# WHY a wrapper: nixbuild's `settings substituters --add` REJECTS path-style +# URLs (e.g. https://host/cache-name → "invalid substituter", trailing slash +# doesn't help). It accepts only host-root HTTPS (https://host), s3://bucket/ +# prefix, or cachix://name. An Attic/nix-serve cache served at a sub-path must +# therefore be reached path-LESS — e.g. front it at a host root with a reverse +# proxy that rewrites `/.narinfo` and `/nar/*` into the cache namespace +# (the oleks fleet does this for Attic at https://nix-cache-custom.oleks.space +# via a Caddy root-rewrite). This wrapper catches the path-style mistake before +# it hits the shell and explains the fix. +# +# Also: `builders-use-substitutes = true` on the CLIENT does NOT forward the +# client's substituter list to nixbuild — it tells the remote to use ITS OWN +# (these, account-side) substituters. So caches must be registered here to take +# effect during remote builds. +# +# Usage: +# nb-substituters list # show substituters + trusted keys +# nb-substituters add-cache [public-key] # add substituter (+ key if given) +# nb-substituters add-key # add a trusted public key only +# nb-substituters remove # remove a substituter +# nb-substituters remove-key # remove a trusted public key +# nb-substituters reset # reset both back to defaults +# +# Delegates the actual shell I/O to nbshell (sibling script). + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NBSHELL="${HERE}/nbshell" + +valid_substituter() { + # Accept: https://host (no path), s3://bucket[/prefix], cachix://name. + # Reject: https://host/path (the nixbuild gotcha), http (warn), bare host. + local u="$1" + case "$u" in + https://*/*/* | https://*/?* ) + # has a path component after the host + return 1 ;; + https://*/ ) + return 1 ;; + https://* ) + return 0 ;; + s3://* | cachix://* ) + return 0 ;; + *) + return 1 ;; + esac +} + +case "${1:-}" in +list) + "$NBSHELL" 'settings substituters --show' 'settings trusted-public-keys --show' + ;; +add-cache) + url="${2:-}"; key="${3:-}" + [ -n "$url" ] || { echo "add-cache: need a URL" >&2; exit 2; } + if ! valid_substituter "$url"; then + cat >&2 <.narinfo and /nar/* into the cache namespace, then add that bare host here. +EOF + exit 1 + fi + if [ -n "$key" ]; then + "$NBSHELL" \ + "settings substituters --add ${url}" \ + "settings trusted-public-keys --add ${key}" \ + 'settings substituters --show' \ + 'settings trusted-public-keys --show' + else + echo "note: no public key given — content from $url will only validate if its signing key is already trusted." >&2 + "$NBSHELL" "settings substituters --add ${url}" 'settings substituters --show' + fi + ;; +add-key) + key="${2:-}"; [ -n "$key" ] || { echo "add-key: need a public key" >&2; exit 2; } + "$NBSHELL" "settings trusted-public-keys --add ${key}" 'settings trusted-public-keys --show' + ;; +remove) + url="${2:-}"; [ -n "$url" ] || { echo "remove: need a URL" >&2; exit 2; } + "$NBSHELL" "settings substituters --remove ${url}" 'settings substituters --show' + ;; +remove-key) + key="${2:-}"; [ -n "$key" ] || { echo "remove-key: need a public key" >&2; exit 2; } + "$NBSHELL" "settings trusted-public-keys --remove ${key}" 'settings trusted-public-keys --show' + ;; +reset) + "$NBSHELL" \ + 'settings substituters --reset' \ + 'settings trusted-public-keys --reset' \ + 'settings substituters --show' \ + 'settings trusted-public-keys --show' + ;; +""|-h|--help|help) + sed -n '2,32p' "$0" + ;; +*) + echo "nb-substituters: unknown command '${1}'" >&2 + exit 2 + ;; +esac diff --git a/bin/nbapi b/bin/nbapi new file mode 100755 index 0000000..ecec738 --- /dev/null +++ b/bin/nbapi @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# nbapi — read-only client for nixbuild.net's HTTP API (https://api.nixbuild.net). +# +# This API is monitoring/reporting only: usage, builds, build summary. It does +# NOT expose account administration (settings/ssh-keys/tokens) — for those use +# `nbshell`. The account is metered ("no free build time left" once the monthly +# tier is spent), so usage/storage visibility is the point of this tool. +# +# Auth: Bearer token from $NIXBUILD_API_TOKEN, else `pass show ` +# (entry from $NIXBUILD_API_TOKEN_PASS_ENTRY or default infra/nixbuild/api-token). +# +# Usage: +# nbapi usage [--from YYYY-MM-DD] [--to YYYY-MM-DD] # billable CPU-seconds + count +# nbapi summary # aggregated build metrics +# nbapi builds [--limit N] # recent build history +# nbapi raw # GET any API path verbatim +# +# Output is JSON (pretty-printed via jq when available). + +set -euo pipefail + +BASE="https://api.nixbuild.net" +PASS_ENTRY="${NIXBUILD_API_TOKEN_PASS_ENTRY:-infra/nixbuild/api-token}" + +token() { + if [ -n "${NIXBUILD_API_TOKEN:-}" ]; then + printf '%s' "$NIXBUILD_API_TOKEN" + elif command -v pass >/dev/null 2>&1; then + pass show "$PASS_ENTRY" 2>/dev/null | head -n1 + else + echo "nbapi: no token (set NIXBUILD_API_TOKEN or install pass with $PASS_ENTRY)" >&2 + return 1 + fi +} + +get() { + local path="$1" tok + tok="$(token)" || exit 1 + local out + out="$(curl -fsS -H "Authorization: Bearer ${tok}" "${BASE}${path}")" || { + echo "nbapi: request failed: GET ${path}" >&2 + exit 1 + } + if command -v jq >/dev/null 2>&1; then + printf '%s' "$out" | jq . + else + printf '%s\n' "$out" + fi +} + +cmd="${1:-}"; shift || true +case "$cmd" in +usage) + from=""; to="" + while [ "$#" -gt 0 ]; do + case "$1" in + --from) from="$2"; shift 2 ;; + --to) to="$2"; shift 2 ;; + *) echo "nbapi usage: unknown arg $1" >&2; exit 2 ;; + esac + done + q="" + [ -n "$from" ] && q="from=${from}" + [ -n "$to" ] && q="${q:+$q&}to=${to}" + get "/usage${q:+?$q}" + ;; +summary) + get "/builds/summary" + ;; +builds) + limit="20" + while [ "$#" -gt 0 ]; do + case "$1" in + --limit) limit="$2"; shift 2 ;; + *) echo "nbapi builds: unknown arg $1" >&2; exit 2 ;; + esac + done + get "/builds?limit=${limit}" + ;; +raw) + [ -n "${1:-}" ] || { echo "nbapi raw: need a path" >&2; exit 2; } + get "$1" + ;; +""|-h|--help|help) + sed -n '2,20p' "$0" + ;; +*) + echo "nbapi: unknown command '$cmd' (try: usage, summary, builds, raw)" >&2 + exit 2 + ;; +esac diff --git a/bin/nbshell b/bin/nbshell new file mode 100755 index 0000000..a34face --- /dev/null +++ b/bin/nbshell @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# nbshell — drive nixbuild.net's interactive admin shell non-interactively. +# +# nixbuild.net's account administration (settings, ssh-keys, tokens, billing) +# lives ONLY behind `ssh shell`, and that shell is stubborn: +# - `ssh -T shell` with piped stdin produces NO output (needs a PTY). +# - `ssh -tt shell` is rejected: "pty not supported". +# - `ssh shell ` (command as ssh args) hits the *build-runner* +# channel, which needs the `run:write` permission and fails with +# "Authorization failed". +# The only thing that works is a real local PTY wrapping `ssh -T ... shell`, +# fed one command at a time and synchronised on the `nixbuild.net> ` prompt. +# `expect` provides exactly that. (The HTTP API at api.nixbuild.net is +# read-only — no settings endpoints — so it cannot substitute for this.) +# +# Usage: +# nbshell 'settings substituters --show' +# nbshell 'settings substituters --add https://cache.example.org' \ +# 'settings substituters --show' +# printf '%s\n' 'settings usage' | nbshell - # commands on stdin +# +# Each argument (or each stdin line) is one shell command line, run in order +# in a single session. Output is cleaned: banner and `nixbuild.net> ` prompts +# stripped, CRs removed. Exit code is expect's (0 unless the session failed). +# +# Env: +# NIXBUILD_SSH_HOST override host (default: eu.nixbuild.net / userConfig) +# NBSHELL_RAW=1 do not strip banner/prompts (debug) +# NBSHELL_TIMEOUT per-prompt timeout seconds (default 30) +# +# Note: `exit` is NOT a valid shell command — the session ends on EOF, which +# this script handles via expect `close`. + +set -euo pipefail + +HOST="${NIXBUILD_SSH_HOST:-eu.nixbuild.net}" +TIMEOUT="${NBSHELL_TIMEOUT:-30}" + +# Collect commands from argv, or from stdin if the sole arg is "-". +cmds=() +if [ "$#" -eq 1 ] && [ "$1" = "-" ]; then + while IFS= read -r line; do + [ -n "$line" ] && cmds+=("$line") + done +else + cmds=("$@") +fi + +if [ "${#cmds[@]}" -eq 0 ]; then + echo "nbshell: no commands given" >&2 + echo "usage: nbshell 'settings substituters --show' [more commands...]" >&2 + exit 2 +fi + +# Locate expect: prefer a system binary, else run it ephemerally from nixpkgs +# (no profile/-env install — matches the declarative-only house rule). +if command -v expect >/dev/null 2>&1; then + EXPECT=(expect) +elif command -v nix >/dev/null 2>&1; then + EXPECT=(nix run nixpkgs#expect --) +else + echo "nbshell: need either 'expect' on PATH or 'nix' to fetch it" >&2 + exit 3 +fi + +script="$(mktemp "${TMPDIR:-/tmp}/nbshell.XXXXXX.exp")" +trap 'rm -f "$script"' EXIT + +{ + echo "set timeout ${TIMEOUT}" + echo 'log_user 1' + echo 'proc wp {} {' + echo ' expect {' + echo ' -re {nixbuild\.net>\s*$} { return }' + echo ' timeout { puts stderr "nbshell: TIMEOUT waiting for prompt"; return }' + echo ' }' + echo '}' + # -T: do NOT request a remote PTY (nixbuild rejects that); expect itself + # provides the local PTY the shell needs to render. Disable mux so we get + # a clean dedicated channel. + echo "spawn ssh -T -o ControlMaster=no -o ControlPath=none ${HOST} shell" + echo 'wp' + for c in "${cmds[@]}"; do + # Escape backslashes and double-quotes for the Tcl string literal. + esc=${c//\\/\\\\} + esc=${esc//\"/\\\"} + echo "send \"${esc}\r\"" + echo 'wp' + done + # The remote shell has no `exit` command and won't EOF on its own; close the + # PTY ourselves. `close` then a bare `expect eof` races and throws + # "spawn id ... not open" — wrap both so the session always ends cleanly + # with exit status 0. + echo 'catch { close }' + echo 'catch { expect eof }' + echo 'exit 0' +} >"$script" + +run_expect() { "${EXPECT[@]}" "$script" 2>&1; } + +if [ "${NBSHELL_RAW:-0}" = "1" ]; then + run_expect + exit "${PIPESTATUS[0]}" +fi + +# Clean output: drop the static welcome banner, SSH PQ/MOTD warnings, the +# command listing, the spawn echo, any Tcl close/eof traceback, and the +# `nixbuild.net> ` prompt prefix; normalise CRs and squeeze blank lines. +run_expect | tr -d '\r' | grep -vE \ + 'WARNING|store now, decrypt|may need to be upgraded|openssh\.com/pq|^\*\*|^•|nixbuild\.net •|^Welcome to nixbuild|^This shell allows|^and retrieve information|^Account id:|^Storage used:|^You have no free|^Free build time left:|^Available shell commands:|^ (help|settings|usage|ssh-keys|billing|tokens|builds) |^Run .help COMMAND|^For help on subcommands|^For more documentation|^https://docs\.nixbuild\.net/configuration|^spawn ssh |^spawn id |expect: spawn id|^ *while executing|^ *invoked from within|^"(expect eof|close)"|^ *\(file ' \ + | sed -E 's/^nixbuild\.net> ?//' \ + | cat -s diff --git a/skills/nixbuild-settings/SKILL.md b/skills/nixbuild-settings/SKILL.md new file mode 100644 index 0000000..9c23789 --- /dev/null +++ b/skills/nixbuild-settings/SKILL.md @@ -0,0 +1,119 @@ +--- +name: nixbuild-settings +description: | + Manage nixbuild.net account settings over the interactive admin shell — + binary-cache substituters, trusted public keys, SSH keys, and other + account/SSH-key settings. Carries the guard rails for nixbuild's quirks: + path-style substituter URLs are rejected (reach sub-path caches like Attic + path-less instead), and the admin shell needs a real PTY (driven by the + nbshell tool). Use when registering/removing a cache on nixbuild, adding a + trusted public key, managing SSH keys, or reading/changing any + `settings `. Trigger on "add a substituter to nixbuild", "register + a cache on nixbuild", "nixbuild trusted key", "nixbuild ssh key", "change + nixbuild settings", "nixbuild substituters". +disable-model-invocation: false +allowed-tools: Bash, Read, AskUserQuestion +--- + +# nixbuild-settings — account settings via the admin shell + +Owning agent: `nixbuild:ops`. Account administration lives ONLY behind +nixbuild's interactive admin shell; the HTTP API cannot touch it. Drive the +shell through the **`nbshell`** tool (real PTY via `expect`) — never hand-roll +ssh (see the plugin's `ops` agent for why the obvious invocations all fail). + +Tools live under `${CLAUDE_PLUGIN_ROOT}/bin`. + +## Read current settings + +```bash +nb-substituters list # substituters + trusted keys +nbshell 'settings substituters --show' +nbshell 'settings trusted-public-keys --show' +nbshell 'ssh-keys' # registered SSH keys + permissions +nbshell 'settings --help' # list every available SETTING +``` + +The full SETTING list includes: `substituters`, `trusted-public-keys`, +`caches`, `access-tokens`, `always-substitute`, `max-cpu`, `max-mem`, +`default-permissions`, `signing-key-for-builds`, `timeout`, and more. Read any +with `settings --show`. + +## Register a binary cache (the common task) + +```bash +nb-substituters add-cache +``` + +`add-cache` adds the substituter AND its trusted key, then re-shows both. It +**refuses path-style URLs before they reach the shell**. + +### The path-style gotcha (apply, do not re-derive) + +nixbuild's `settings substituters --add` accepts ONLY: + +- host-root HTTPS: `https://host` (NO path, no trailing slash) +- `s3://bucket/prefix` +- `cachix://name` + +A path-style URL like `https://host/cache-name` (Attic, nix-serve) is rejected +as "invalid substituter". To use such a cache you must expose it **path-less**: +front it at a host root with a reverse proxy that rewrites `/.narinfo` +and `/nar/*` into the cache namespace, then register that bare host. + +Worked example (oleks fleet): Attic's native URL is +`https://nix-cache-custom.oleks.space/attic-infra-cache-k3s-1` (rejected). But +`oci-caddy.nix` already serves it path-less at the root of +`nix-cache-custom.oleks.space`, so register: + +```bash +nb-substituters add-cache https://nix-cache-custom.oleks.space \ + attic-infra-cache-k3s-1:qYSNK3DmttQXCFqn1t50qoWGtQNPRFWq9mgQjD05DeU= +``` + +NCPS is plain host-root and just works: +`nb-substituters add-cache https://nix-cache-mirror.oleks.space nix-cache-mirror.oleks.space:v8rbmAnk5MrEunNCC0BxYUh21UALvCuR2lnuZrr0hHY=`. + +### Why register account-side at all + +`builders-use-substitutes = true` on the *client* does NOT forward the client's +substituters to nixbuild — the remote uses ITS OWN substituters. So a cache only +speeds up / cheapens remote builds when registered here, account-side. + +## Add only a key, or remove things + +```bash +nb-substituters add-key +nb-substituters remove +nb-substituters remove-key +nbshell 'settings substituters --reset' # back to default (cache.nixos.org) +``` + +## SSH keys and other raw settings + +```bash +nbshell 'ssh-keys' # list keys + permission sets +nbshell 'settings --add ' # generic add +nbshell 'settings --reset' # generic reset +``` + +Per-key overrides: most settings take `--ssh-key ` to scope to one +key instead of the whole account. + +## Procedure (mutations) + +1. **Read** the current value first (`--show`) so you can show a before/after. +2. **State the exact command(s)** you will run and **confirm via + `AskUserQuestion`** — settings changes affect every build on the account. +3. **Apply** through `nb-substituters` (preferred, guarded) or `nbshell`. +4. **Verify**: re-show the setting and report the after-state. A successful add + is silent; the proof is the `--show` afterwards. + +## Failure signals + +- `invalid substituter` → path-style URL; expose path-less (above). +- `Authorization failed … run:write` → the SSH key in use lacks admin + permission for the shell (or the command was sent as ssh args, not via + `nbshell`). Use a full-access key. +- `nbshell` prints nothing but errors → `expect` missing and no `nix` to fetch + it; install `expect` or run where `nix` is available. diff --git a/skills/nixbuild-usage/SKILL.md b/skills/nixbuild-usage/SKILL.md new file mode 100644 index 0000000..e5152eb --- /dev/null +++ b/skills/nixbuild-usage/SKILL.md @@ -0,0 +1,91 @@ +--- +name: nixbuild-usage +description: | + Report nixbuild.net account usage and cost posture — remaining free build + time, billable CPU-seconds over a date range, build history (success/fail/ + discarded), output NAR size, and stored size. The account is metered, so + this is the cost-visibility skill. Reads come from the HTTP API (nbapi tool, + no PTY needed) plus the shell's `usage` command for the live free-time + counter. Trigger on "nixbuild usage", "how much build time left", "nixbuild + billing", "nixbuild storage", "nixbuild build history", "am I out of free + build time", "nixbuild cost". +disable-model-invocation: false +allowed-tools: Bash, Read +--- + +# nixbuild-usage — metered account usage & cost + +Owning agent: `nixbuild:ops`. nixbuild bills CPU-time beyond a monthly free +tier; this skill answers "where do I stand" without touching settings. Most of +it is the read-only HTTP API via **`nbapi`** (`${CLAUDE_PLUGIN_ROOT}/bin`), which +needs no PTY. The live free-time counter comes from the shell banner / `usage`. + +## Quick posture + +```bash +nbapi summary +``` + +Returns (example shape): + +```json +{ + "build_count": 19, "running_build_count": 0, + "discarded_build_count": 1, "failed_build_count": 1, + "successful_build_count": 17, + "total_cpu_seconds": 27401, "billable_cpu_seconds": 20963, + "total_duration_seconds": 2708.06, "wall_time_seconds": 48398.44, + "total_output_nar_size_kilobytes": 786691 +} +``` + +Key fields: `billable_cpu_seconds` (what you pay for), `successful/failed/ +discarded_build_count`, `total_output_nar_size_kilobytes`. + +## Remaining free build time (live) + +The free-tier counter is only in the shell, not the HTTP API: + +```bash +nbshell 'usage' +``` + +The shell login banner also prints it, e.g. `Free build time left: 25:00:00 +(hh:mm:ss)` — it resets at the start of each month. When spent, the banner +reads `You have no free build time left` and further builds are billed. + +## Billable CPU over a date range + +```bash +nbapi usage --from 2026-06-01 --to 2026-06-30 +``` + +## Recent build history + +```bash +nbapi builds --limit 20 +``` + +Each entry carries id, status, drv path, timing — useful to spot a runaway or +repeatedly-failing build burning CPU. For one build's detail: `nbapi raw +/builds/`; for a shareable web link: `nbapi raw /builds//url`. + +## Stored size + +`Storage used:` appears in the shell banner (`nbshell 'usage'`), and +`total_output_nar_size_kilobytes` in `nbapi summary` approximates output volume. + +## Reporting + +Lead with the two numbers that matter: **free time left** and **billable +CPU-seconds** (this month). Add fail/discard counts only if non-trivial (a high +`failed`/`discarded` ratio means wasted spend — flag it). Convert CPU-seconds to +hours for readability (`/3600`). Keep it to a few lines unless asked for the +full history. + +## Token + +`nbapi` reads the bearer token from `$NIXBUILD_API_TOKEN`, else `pass show +infra/nixbuild/api-token` (override the entry with +`$NIXBUILD_API_TOKEN_PASS_ENTRY`). The read-only API needs only the `:read` +scopes; a broad token works but is more than required.