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