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:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.cache/
|
||||
.claude/
|
||||
*.log
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Executable
+108
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user