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.
This commit is contained in:
Oleks
2026-06-01 11:17:40 +03:00
commit cbcf122422
10 changed files with 728 additions and 0 deletions
+35
View File
@@ -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"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
.cache/
.claude/
*.log
+21
View File
@@ -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.
+59
View File
@@ -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 <cmd>` (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 '<cmd>' ['<cmd>' …]`** — drive the admin shell non-interactively (expect+PTY). Output cleaned of banner/prompt noise. The foundation for all settings work.
- **`nbapi <usage|summary|builds|raw>`** — read-only HTTP API client. Token from `$NIXBUILD_API_TOKEN` or `pass show infra/nixbuild/api-token`.
- **`nb-substituters <list|add-cache|add-key|remove|remove-key|reset>`** — 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 <SETTING>` 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 `/<hash>.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
+89
View File
@@ -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 <!-- BEGIN ROUTING TRIGGERS -->"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"<!-- END ROUTING TRIGGERS -->. 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 <host> shell`) — the ONLY place account settings,
SSH keys, tokens, and billing live. It is interactive-only and fussy:
- `ssh -T <host> shell` + piped stdin → silent (no output; needs a PTY).
- `ssh -tt <host> shell` → rejected: "pty not supported".
- `ssh <host> shell <cmd>` (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 <usage|summary|builds|raw>`** — 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 '<cmd>' ['<cmd>' …]`** — run admin-shell commands in one session,
output cleaned. Useful commands: `usage`, `settings <SETTING> --show`,
`settings substituters --add <url>`, `ssh-keys`, `tokens`, `builds`. (`exit`
is NOT valid — the session ends on its own.)
- **`nb-substituters <list|add-cache|add-key|remove|remove-key|reset>`** — 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 `/<hash>.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 <url> <key>` 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 <SETTING> --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.
+108
View File
@@ -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 `/<hash>.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 <url> [public-key] # add substituter (+ key if given)
# nb-substituters add-key <public-key> # add a trusted public key only
# nb-substituters remove <url> # remove a substituter
# nb-substituters remove-key <public-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 <<EOF
nb-substituters: '$url' is not an acceptable nixbuild substituter.
nixbuild accepts only host-root HTTPS (https://host, NO path), s3://bucket/prefix,
or cachix://name. A cache served at a sub-path (Attic, nix-serve) must be exposed
path-less — e.g. front it at a host root with a reverse-proxy rewrite of
/<hash>.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
Executable
+91
View File
@@ -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>`
# (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 <path> # 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
Executable
+112
View File
@@ -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 <host> shell`, and that shell is stubborn:
# - `ssh -T <host> shell` with piped stdin produces NO output (needs a PTY).
# - `ssh -tt <host> shell` is rejected: "pty not supported".
# - `ssh <host> shell <cmd>` (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
+119
View File
@@ -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 <SETTING>`. 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 <SETTING> --show`.
## Register a binary cache (the common task)
```bash
nb-substituters add-cache <url> <public-key>
```
`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 `/<hash>.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 <public-key>
nb-substituters remove <url>
nb-substituters remove-key <public-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 <SETTING> --add <value>' # generic add
nbshell 'settings <SETTING> --reset' # generic reset
```
Per-key overrides: most settings take `--ssh-key <SSH_KEY_ID>` 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.
+91
View File
@@ -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/<id>`; for a shareable web link: `nbapi raw /builds/<id>/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.