From 527a8447149cf7cbe0134fdb05b23251c92e2e39 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Thu, 18 Jun 2026 14:04:29 -0700 Subject: [PATCH] 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/ 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 --- playbooks/install-pi-nh3-extdev.yaml | 130 ++++++++++++++++++++++++++ servers/nh3-extdev/README.md | 49 ++++++++-- servers/nh3-extdev/system-details.txt | 17 ++-- 3 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 playbooks/install-pi-nh3-extdev.yaml diff --git a/playbooks/install-pi-nh3-extdev.yaml b/playbooks/install-pi-nh3-extdev.yaml new file mode 100644 index 0000000..bfa1803 --- /dev/null +++ b/playbooks/install-pi-nh3-extdev.yaml @@ -0,0 +1,130 @@ +# Install the `pi` coding agent (earendil-works) on nh3-extdev and wire every +# /opt/externs/ 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__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..{baseUrl, api, apiKey:"$ENV", models[]} +# - $DIR/settings.json : defaultProvider + defaultModel (bare id) +# The per-client GLM key lives in /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" < "$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" diff --git a/servers/nh3-extdev/README.md b/servers/nh3-extdev/README.md index 3a76336..73461be 100644 --- a/servers/nh3-extdev/README.md +++ b/servers/nh3-extdev/README.md @@ -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`. - +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//`; isolation is by directory + +credential, never a shared identity. + +### Per-client workspace layout (`/opt/externs//`) + +``` +/opt/externs// +├── AGENTS.md? (in repo/) # operating charter the agent loads every session +├── secrets.env # 600, gitignored — EXTERNS__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//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. diff --git a/servers/nh3-extdev/system-details.txt b/servers/nh3-extdev/system-details.txt index 2179ad2..52f030b 100644 --- a/servers/nh3-extdev/system-details.txt +++ b/servers/nh3-extdev/system-details.txt @@ -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