feat(nh3-extdev): install pi (earendil-works) + wire /opt/externs client agents to GLM 5.2

- user-level Node v22.23.0 LTS (static tarball, checksum-verified) + pi 0.79.7
  installed -g into the user prefix (box is sudo-less: no root/apt/docker)
- every /opt/externs/<client> wired to GLM 5.2 via the litellm gateway with an
  isolated PI_CODING_AGENT_DIR + scoped per-client key (models.json/settings.json
  + run-pi.sh launcher); replaces the scaffold's incorrect config.example guess
- playbooks/install-pi-nh3-extdev.yaml: idempotent reproduce / add-client / upgrade
  (validated clean: 4 steps skipped, live gbcnc->GLM 5.2 round-trip OK)
- README: settled role + per-client workspace layout; refresh system-details
This commit is contained in:
2026-06-18 14:04:29 -07:00
parent a67d4950d0
commit 527a844714
3 changed files with 180 additions and 16 deletions
+130
View File
@@ -0,0 +1,130 @@
# Install the `pi` coding agent (earendil-works) on nh3-extdev and wire every
# /opt/externs/<client> workspace to GLM 5.2 via the litellm gateway.
#
# Context: nh3-extdev is SUDO-LESS (no root, no apt, no docker). So Node is
# installed user-level from the official static tarball (checksum-verified),
# pi is installed `-g` into that user-space prefix, and each client gets an
# ISOLATED pi config dir via PI_CODING_AGENT_DIR (set by its run-pi.sh launcher).
#
# Idempotent: a second run shows mostly skip/ok. Rerunnable to add a client —
# append its name to `clients` (its workspace dir + secrets.env with an
# EXTERNS_<NAME>_GLM_KEY must already exist; workspace scaffolding is separate).
#
# scripts/elway nh3-extdev --playbook playbooks/install-pi-nh3-extdev.yaml
#
# pi config layout (authoritative, from the installed package):
# - PI_CODING_AGENT_DIR overrides the agent dir (default ~/.pi/agent)
# - $DIR/models.json : providers.<name>.{baseUrl, api, apiKey:"$ENV", models[]}
# - $DIR/settings.json : defaultProvider + defaultModel (bare id)
# The per-client GLM key lives in <workspace>/secrets.env (600, gitignored),
# referenced indirectly so the key never lands in models.json.
vars:
node_ver: v22.23.0 # latest v22 LTS "Jod"; matches pi engine floor >=22.19.0
node_arch: linux-x64
node_root: /home/infra-ops/.local # absolute (not $HOME — elway doesn't shell-expand creates:); identity is always infra-ops
gateway: http://10.250.50.70:4000/v1
externs: /opt/externs
clients: gbcnc surefire svsconstruction
steps:
- name: Download + verify + extract user-level Node
shell: |
set -euo pipefail
DEST="{{ node_root }}"; DIR="$DEST/node-{{ node_ver }}-{{ node_arch }}"
mkdir -p "$DEST"; cd /tmp
curl -fsSLO "https://nodejs.org/dist/{{ node_ver }}/node-{{ node_ver }}-{{ node_arch }}.tar.xz"
curl -fsSL "https://nodejs.org/dist/{{ node_ver }}/SHASUMS256.txt" -o SHASUMS256.txt
grep " node-{{ node_ver }}-{{ node_arch }}.tar.xz$" SHASUMS256.txt | sha256sum -c -
tar -xJf "node-{{ node_ver }}-{{ node_arch }}.tar.xz" -C "$DEST"
rm -f "node-{{ node_ver }}-{{ node_arch }}.tar.xz" SHASUMS256.txt
# Tier-1 idempotency: skip the whole download if the node binary is already there.
creates: "{{ node_root }}/node-{{ node_ver }}-{{ node_arch }}/bin/node"
- name: Wire node/pi onto PATH for login + interactive shells
shell: |
set -euo pipefail
LINE='export PATH="$HOME/.local/node-{{ node_ver }}-{{ node_arch }}/bin:$PATH"'
for RC in "$HOME/.profile" "$HOME/.bashrc"; do
grep -qF "$LINE" "$RC" 2>/dev/null || {
printf '\n# >>> pi/node user-level PATH >>>\n%s\n# <<< pi/node user-level PATH <<<\n' "$LINE" >> "$RC"
}
done
when: "! grep -qF 'pi/node user-level PATH' $HOME/.bashrc 2>/dev/null"
- name: Install the latest pi coding agent into the user prefix
shell: |
set -euo pipefail
export PATH="{{ node_root }}/node-{{ node_ver }}-{{ node_arch }}/bin:$PATH"
npm install -g @earendil-works/pi-coding-agent
creates: "{{ node_root }}/node-{{ node_ver }}-{{ node_arch }}/bin/pi"
- name: Wire each client workspace to GLM 5.2 (isolated config + scoped key)
shell: |
set -euo pipefail
for c in {{ clients }}; do
W="{{ externs }}/$c"; PI="$W/.pi"
[ -d "$PI" ] || { echo "!! $c: missing $PI (scaffold first)"; exit 1; }
KEYVAR=$(grep -oE '^EXTERNS_[A-Z0-9_]+_GLM_KEY' "$W/secrets.env" | head -1)
[ -n "$KEYVAR" ] || { echo "!! $c: no EXTERNS_*_GLM_KEY in secrets.env"; exit 1; }
cat > "$PI/models.json" <<JSON
{
"providers": {
"litellm-glm": {
"baseUrl": "{{ gateway }}",
"api": "openai-completions",
"apiKey": "\$$KEYVAR",
"models": [
{ "id": "glm-5.2", "name": "GLM 5.2 (litellm/z.ai)" },
{ "id": "glm-5.2-reasoning", "name": "GLM 5.2 reasoning (litellm/z.ai)" }
]
}
}
}
JSON
cat > "$PI/settings.json" <<'JSON'
{
"defaultProvider": "litellm-glm",
"defaultModel": "glm-5.2"
}
JSON
cat > "$W/run-pi.sh" <<'SH'
#!/usr/bin/env bash
# Launch pi for this client: isolated config dir + scoped GLM key + repo cwd.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
set -a; . "$HERE/secrets.env"; set +a
export PI_CODING_AGENT_DIR="$HERE/.pi"
cd "$HERE/repo"
exec pi "$@"
SH
chmod 600 "$PI/models.json" "$PI/settings.json"
chmod 700 "$W/run-pi.sh"
rm -f "$PI/config.example"
done
# Re-write is deterministic; skip when gbcnc is already wired to the gateway
# AND its launcher exists (proxy for "all three wired").
when: "! ( grep -qF '{{ gateway }}' {{ externs }}/gbcnc/.pi/models.json 2>/dev/null && test -x {{ externs }}/gbcnc/run-pi.sh )"
verify:
- name: pi binary reports a version
shell: |
export PATH="{{ node_root }}/node-{{ node_ver }}-{{ node_arch }}/bin:$PATH"
pi --version
changed_when: "false"
- name: each client has models.json + settings.json + run-pi.sh
shell: |
for c in {{ clients }}; do
W="{{ externs }}/$c"
test -s "$W/.pi/models.json" && test -s "$W/.pi/settings.json" && test -x "$W/run-pi.sh" \
|| { echo "$c incomplete"; exit 1; }
done
changed_when: "false"
- name: gbcnc resolves glm-5.2 through the gateway (live round-trip)
shell: |
export PATH="{{ node_root }}/node-{{ node_ver }}-{{ node_arch }}/bin:$PATH"
{{ externs }}/gbcnc/run-pi.sh --no-tools --no-session --approve -p "Reply with exactly: PI_GLM_OK" \
| grep -qF PI_GLM_OK
changed_when: "false"
+41 -8
View File
@@ -19,16 +19,49 @@ is root-equivalent).
## Purpose
NH3 manager / external-dev box; role being established (2026-06-17). Successor to
the retired `nh3-ansible`.
<!-- Refine once the workload is settled — e.g. if this becomes the pi/GLM
client-agent control node, document the per-client workspace layout here. -->
NH3 **client-agent control node** (successor to the retired `nh3-ansible`): runs
the **`pi` coding agent** (earendil-works) on **GLM 5.2**, one isolated agent per
external client, to manage that client's infrastructure. Each client gets a
walled workspace under `/opt/externs/<client>/`; isolation is by directory +
credential, never a shared identity.
### Per-client workspace layout (`/opt/externs/<client>/`)
```
/opt/externs/<client>/
├── AGENTS.md? (in repo/) # operating charter the agent loads every session
├── secrets.env # 600, gitignored — EXTERNS_<CLIENT>_GLM_KEY (scoped litellm key)
├── run-pi.sh # 700 launcher: sources secrets.env, sets PI_CODING_AGENT_DIR,
│ # cd repo/, exec pi (per-client isolated config + key)
├── .pi/ # this client's pi agent dir (PI_CODING_AGENT_DIR points here)
│ ├── models.json # provider litellm-glm -> gateway, models glm-5.2[/-reasoning]
│ ├── settings.json # defaultProvider litellm-glm, defaultModel glm-5.2
│ └── (sessions/auth) # pi-managed at runtime
├── repo/ # client infra workspace (own .git; AGENTS.md/scripts/servers/…)
└── .ssh/ # per-client scoped deploy key (reaches THIS client's hosts only)
```
Launch an agent: `/opt/externs/<client>/run-pi.sh` (add pi flags as needed, e.g.
`-p "…"` non-interactive). `pi` itself reaches GLM 5.2 through the litellm gateway
(`http://10.250.50.70:4000/v1`, reachable cross-site from NH3 — verified).
### pi / Node install (user-level, no root)
Node and pi are installed **user-level** (the box is sudo-less): Node v22.23.0 LTS
from the official static tarball (checksum-verified) under
`~/.local/node-v22.23.0-linux-x64/`, with `pi` installed `-g` into that prefix
(`pi --version` → 0.79.7). PATH is wired in `~/.profile` + `~/.bashrc`.
Reproduce / add a client / upgrade: **`playbooks/install-pi-nh3-extdev.yaml`**
(idempotent — `scripts/elway nh3-extdev --playbook …`). It installs Node+pi and
wires every workspace in its `clients` var to GLM 5.2; the workspace *scaffold*
(dirs, `secrets.env`, deploy keys, `repo/`) is provisioned separately.
## Notes
- Debian **13** (trixie) — newer than the fleet's Debian-12 baseline; watch for
package/behaviour drift vs other hosts.
- `sudo` warns `unable to resolve host nh3-extdev` (hostname not in `/etc/hosts`)
— cosmetic; add `127.0.1.1 nh3-extdev` to `/etc/hosts` (needs root) to silence.
- Stood up 2026-06-17; first `system-details.txt` captured the same day (sudo-less,
so docker/root-only sections are necessarily blank).
- `/etc/hosts` now carries `nh3-extdev` (the old `unable to resolve host` sudo
warning is silenced).
- Stood up 2026-06-17; pi-on-GLM-5.2 client agents wired 2026-06-18. `system-details.txt`
is sudo-less, so docker/root-only sections are necessarily blank.
+9 -8
View File
@@ -2,8 +2,8 @@
===== HOST =====
Hostname: nh3-extdev
Date: 2026-06-17T14:55:16-07:00
Uptime: up 16 minutes
Date: 2026-06-18T14:04:13-07:00
Uptime: up 23 hours, 25 minutes
OS: Debian GNU/Linux 13 (trixie)
Kernel: 6.12.90+deb13.1-amd64
Arch: x86_64
@@ -13,7 +13,7 @@ Arch: x86_64
CPU cores: 8
CPU model: QEMU Virtual CPU version 2.5+
MemTotal: 7.8 GB
MemAvailable: 7.4 GB
MemAvailable: 7.3 GB
===== GPUS =====
@@ -22,7 +22,7 @@ nvidia-smi not present (no NVIDIA GPUs or driver not installed)
===== FILESYSTEMS (df) =====
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 250G 3.2G 235G 2% /
/dev/sda1 250G 3.6G 234G 2% /
===== PERSISTENT MOUNTS (/etc/fstab, non-comment) =====
@@ -32,10 +32,11 @@ UUID=bf3fd6d0-4537-4626-bdcc-551c5603ab98 none swap sw
===== TARGETED DATA PATHS =====
/opt (total: 4.0K)
total 8
drwxr-xr-x 2 root root 4096 2024-05-30 19:51 .
drwxr-xr-x 18 root root 4096 2026-06-17 14:37 ..
/opt (total: 1.5M)
total 12
drwxr-xr-x 3 root root 4096 2026-06-17 15:03 .
drwxr-xr-x 18 root root 4096 2026-06-17 14:37 ..
drwxrws---+ 5 infra-ops externs 4096 2026-06-17 15:13 externs
/srv (total: 4.0K)
total 8