feat(new-setup): wrap node/docker installs and add generic set-env step

Adds three allowlist-friendly setup helpers so /new-setup and /new-setup-2
don't hit unmatchable commands during a fresh install:

- setup/install-node.sh — idempotent Node 22 install wrapper (macOS via brew,
  Linux via NodeSource + apt). Replaces the raw `curl | sudo -E bash -` flow
  whose stdin-consuming `bash -` segment can't be pre-approved.
- setup/install-docker.sh — same pattern for Docker (brew --cask on macOS,
  get.docker.com on Linux + usermod).
- setup/set-env.ts — generic `--step set-env` that writes KEY=VALUE to .env
  (and optionally syncs to data/env/env) so channel-install flows don't
  invent `grep && sed && rm` pipelines, which split at each && and can't be
  tightly allowlisted.

new-setup-2's Telegram path now uses set-env for TELEGRAM_BOT_TOKEN and
explicitly skips /add-telegram's Credentials section. new-setup step 1 and
step 2 now call the install wrappers; the raw curl/apt entries are gone from
the allowed-tools list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Koshkoshinsk
2026-04-20 15:18:35 +00:00
committed by exe.dev user
parent ccb676ae91
commit 712a0e1e01
6 changed files with 206 additions and 12 deletions
+14 -3
View File
@@ -1,7 +1,7 @@
---
name: new-setup-2
description: Follow-on to /new-setup. Captures the operator and agent names, wires a real messaging channel, and adds quality-of-life extras. Linear rollthrough; every step is skippable. Invoked when the user picks "continue setup" at the end of /new-setup.
allowed-tools: Bash(bash setup/probe.sh) Bash(bash setup/install-telegram.sh:*) Bash(pnpm exec tsx setup/index.ts:*) Bash(pnpm exec tsx scripts/init-first-agent.ts:*) Bash(tail:*) Bash(head:*) Bash(grep:*)
allowed-tools: Bash(bash setup/probe.sh) Bash(bash setup/install-telegram.sh) Bash(bash setup/install-telegram.sh:*) Bash(pnpm exec tsx setup/index.ts:*) Bash(pnpm exec tsx scripts/init-first-agent.ts:*) Bash(tail:*) Bash(head:*) Bash(grep:*)
---
# NanoClaw phase-2 setup
@@ -79,8 +79,19 @@ Print the list as a numbered plain-prose list (too many options for `AskUserQues
When the user picks one:
1. **Install the adapter.** For **Telegram**, run `bash setup/install-telegram.sh` directly — it bundles the preflight + fetch + copy + register + `pnpm install` + build from `/add-telegram` into one idempotent call, then continue with credentials and pairing (invoke `/add-telegram` afterwards and its preflight will skip straight to Credentials). For every other channel, invoke the matching `/add-<channel>` skill via the Skill tool; it copies the adapter source in from the `channels` branch, registers it, installs the pinned npm package, and handles credentials. Some channels also run a pairing step as part of their flow.
2. **Capture platform IDs.** After the `/add-<channel>` skill finishes, you need two values: the operator's user-id on that platform, and the chat/channel platform-id. Each channel surfaces these differently — consult the **Channel Info** section at the bottom of that skill's `SKILL.md` for the exact path. For Telegram, for example, the `pair-telegram` step emits `PLATFORM_ID` and `ADMIN_USER_ID` in a status block once the user sends the 4-digit code.
1. **Install the adapter.** For **Telegram**, run `bash setup/install-telegram.sh` directly — it bundles the preflight + fetch + copy + register + `pnpm install` + build from `/add-telegram` into one idempotent call. Then handle Telegram credentials inline (below) — **do not** invoke `/add-telegram` afterward; its Credentials section would generate an unapprovable `grep && sed && rm` to write `.env`. For every other channel, invoke the matching `/add-<channel>` skill via the Skill tool; it copies the adapter source in from the `channels` branch, registers it, installs the pinned npm package, and handles credentials. Some channels also run a pairing step as part of their flow.
**Telegram credentials (inline):**
- Walk the user through BotFather: `/newbot` → pick name + username ending in `bot` → copy the token.
- Remind them: in `@BotFather``/mybots` → their bot → Bot Settings → Group Privacy → **Turn off** (only needed if the bot will live in groups; DM-only can skip).
- Persist the token and sync it to the container mount with the generic setter:
```
pnpm exec tsx setup/index.ts --step set-env -- \
--key TELEGRAM_BOT_TOKEN --value "<token>" --sync-container
```
2. **Capture platform IDs.** After the `/add-<channel>` skill finishes (or after inline credentials for Telegram), you need two values: the operator's user-id on that platform, and the chat/channel platform-id. Each channel surfaces these differently — consult the **Channel Info** section at the bottom of that skill's `SKILL.md` for the exact path. For Telegram, run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent <main|wire-to:folder|new-agent:folder>` directly and follow its `PAIR_TELEGRAM_ISSUED`/`PAIR_TELEGRAM STATUS=success` blocks — `PLATFORM_ID` and `ADMIN_USER_ID` land in the success block.
3. **Wire the agent.** Run `init-first-agent.ts` in DM mode with `--no-cli-bonus` (this keeps the new agent off the CLI messaging group so the pre-existing throwaway agent still owns CLI routing cleanly):
```
+4 -9
View File
@@ -1,7 +1,7 @@
---
name: new-setup
description: Shortest path from zero to a working two-way agent chat, for any user regardless of technical background — ends at a running NanoClaw instance with at least one CLI-reachable agent.
allowed-tools: Bash(bash setup.sh) Bash(bash setup/probe.sh) Bash(pnpm exec tsx setup/index.ts:*) Bash(pnpm run chat:*) Bash(curl -fsSL https://get.docker.com | sh) Bash(curl -fsSL https://deb.nodesource.com/setup_22.x) Bash(sudo apt-get install -y nodejs) Bash(sudo usermod -aG docker:*) Bash(open -a Docker) Bash(sudo systemctl start docker) Bash(node --version) Bash(tail:*) Bash(head:*) Bash(grep:*)
allowed-tools: Bash(bash setup.sh) Bash(bash setup/probe.sh) Bash(bash setup/install-node.sh) Bash(bash setup/install-docker.sh) Bash(pnpm exec tsx setup/index.ts:*) Bash(pnpm run chat) Bash(pnpm run chat:*) Bash(open -a Docker) Bash(sudo systemctl start docker) Bash(node --version) Bash(tail:*) Bash(head:*) Bash(grep:*)
---
# NanoClaw bare-minimum setup
@@ -38,10 +38,7 @@ One permitted parallelism:
Check probe results and skip if `HOST_DEPS=ok` — Node, pnpm, `node_modules`, and `better-sqlite3`'s native binding are already in place.
If `HOST_DEPS=missing` and `node --version` fails (Node isn't installed at all), install Node 22 **before** running `bash setup.sh`, otherwise the first bootstrap run is guaranteed to fail:
- macOS: `brew install node@22`
- Linux / WSL: `curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs`
If `HOST_DEPS=missing` and `node --version` fails (Node isn't installed at all), run `bash setup/install-node.sh` **before** `bash setup.sh` — the script handles both macOS (via `brew`) and Linux/WSL (NodeSource + apt). It's idempotent and short-circuits when node is already on PATH.
Then run `bash setup.sh`. If Node is already present and only `HOST_DEPS=missing`, run `bash setup.sh` directly — deps just haven't been installed yet.
@@ -57,16 +54,14 @@ Parse the status block:
Check probe results and skip if `DOCKER=running` AND `IMAGE_PRESENT=true`.
**Runtime:**
- `DOCKER=not_found` → Docker itself is missing — install it so agent containers have an isolated place to run.
- macOS: `brew install --cask docker && open -a Docker`
- Linux: `curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER` (tell user they may need to log out/in for group membership)
- `DOCKER=not_found` → Docker is missing — install it so agent containers have an isolated place to run. Run `bash setup/install-docker.sh` (handles macOS via `brew --cask` and Linux via the official get.docker.com script, and adds the user to the `docker` group on Linux). On Linux the user may need to log out/in for group membership to take effect. On macOS, launch Docker afterwards with `open -a Docker`.
- `DOCKER=installed_not_running` → Docker is installed but the daemon is down — start it.
- macOS: `open -a Docker`
- Linux: `sudo systemctl start docker`
Wait ~15s after either, then proceed.
> **Loose commands:** Docker install/start. Justification: platform-specific package-manager invocations. Wrapping them in a `--step` would just move the same branching into TypeScript with no added value.
> **Loose commands:** `open -a Docker`, `sudo systemctl start docker`. Justification: daemon-start is a one-liner per platform, not worth wrapping. The actual install (which had the unmatchable `curl | sh` pattern) is now inside `setup/install-docker.sh`.
**Image (run if `IMAGE_PRESENT=false`):** build the agent container image — takes a few minutes the first time, one-off cost.
+1
View File
@@ -10,6 +10,7 @@ const STEPS: Record<
() => Promise<{ run: (args: string[]) => Promise<void> }>
> = {
timezone: () => import('./timezone.js'),
'set-env': () => import('./set-env.js'),
environment: () => import('./environment.js'),
container: () => import('./container.js'),
register: () => import('./register.js'),
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Setup helper: install-docker — bundles Docker install into one idempotent
# script so /new-setup can run it without needing `curl | sh` in the allowlist
# (pipelines split at matching time, and `sh` receiving stdin can't be
# pre-approved safely).
#
# The script itself is the allowlisted unit; the pipes and sudo live inside
# it. Starting the daemon (after install) stays separate — `open -a Docker`
# and `sudo systemctl start docker` are already in the allowlist.
set -euo pipefail
echo "=== NANOCLAW SETUP: INSTALL_DOCKER ==="
if command -v docker >/dev/null 2>&1; then
echo "STATUS: already-installed"
echo "DOCKER_VERSION: $(docker --version 2>/dev/null || echo unknown)"
echo "=== END ==="
exit 0
fi
case "$(uname -s)" in
Darwin)
echo "STEP: brew-install-docker"
if ! command -v brew >/dev/null 2>&1; then
echo "STATUS: failed"
echo "ERROR: Homebrew not installed. Install brew first (https://brew.sh) then re-run."
echo "=== END ==="
exit 1
fi
brew install --cask docker
;;
Linux)
echo "STEP: docker-get-script"
curl -fsSL https://get.docker.com | sh
echo "STEP: usermod-docker-group"
sudo usermod -aG docker "$USER"
echo "NOTE: you may need to log out and back in for docker group membership to take effect"
;;
*)
echo "STATUS: failed"
echo "ERROR: Unsupported platform: $(uname -s)"
echo "=== END ==="
exit 1
;;
esac
if ! command -v docker >/dev/null 2>&1; then
echo "STATUS: failed"
echo "ERROR: docker not found on PATH after install"
echo "=== END ==="
exit 1
fi
echo "STATUS: installed"
echo "DOCKER_VERSION: $(docker --version 2>/dev/null || echo unknown)"
echo "=== END ==="
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Setup helper: install-node — bundles Node 22 install into one idempotent
# script so /new-setup can run it without needing `curl | sudo -E bash -` in
# the allowlist (that pattern is inherently unmatchable — bash reads from
# stdin, so pre-approval can't inspect what's being executed).
#
# The script itself is the allowlisted unit; the pipes and sudo live inside
# it. Pure bash by design — runs before Node exists on the host.
set -euo pipefail
echo "=== NANOCLAW SETUP: INSTALL_NODE ==="
if command -v node >/dev/null 2>&1; then
echo "STATUS: already-installed"
echo "NODE_VERSION: $(node --version)"
echo "=== END ==="
exit 0
fi
case "$(uname -s)" in
Darwin)
echo "STEP: brew-install-node"
if ! command -v brew >/dev/null 2>&1; then
echo "STATUS: failed"
echo "ERROR: Homebrew not installed. Install brew first (https://brew.sh) then re-run."
echo "=== END ==="
exit 1
fi
brew install node@22
;;
Linux)
echo "STEP: nodesource-setup"
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
echo "STEP: apt-install-nodejs"
sudo apt-get install -y nodejs
;;
*)
echo "STATUS: failed"
echo "ERROR: Unsupported platform: $(uname -s)"
echo "=== END ==="
exit 1
;;
esac
if ! command -v node >/dev/null 2>&1; then
echo "STATUS: failed"
echo "ERROR: node not found on PATH after install"
echo "=== END ==="
exit 1
fi
echo "STATUS: installed"
echo "NODE_VERSION: $(node --version)"
echo "=== END ==="
+77
View File
@@ -0,0 +1,77 @@
/**
* Step: set-env — Write or update a KEY=VALUE in .env, with optional sync to
* data/env/env (the container-mounted copy).
*
* Usage:
* pnpm exec tsx setup/index.ts --step set-env -- \
* --key TELEGRAM_BOT_TOKEN --value "<token>" [--sync-container]
*
* Exists so channel-install flows don't have to invent grep/sed/rm pipelines
* (which can't be allowlisted tightly — sed can read any file, and each
* segment of an && chain is matched separately).
*
* Logs the key but never the value.
*/
import fs from 'fs';
import path from 'path';
import { log } from '../src/log.js';
import { emitStatus } from './status.js';
export async function run(args: string[]): Promise<void> {
const keyIdx = args.indexOf('--key');
const valueIdx = args.indexOf('--value');
const syncContainer = args.includes('--sync-container');
if (keyIdx === -1 || !args[keyIdx + 1]) {
throw new Error('--key <KEY> is required');
}
if (valueIdx === -1 || args[valueIdx + 1] === undefined) {
throw new Error('--value <VALUE> is required');
}
const key = args[keyIdx + 1];
const value = args[valueIdx + 1];
if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
throw new Error(`Invalid env key: ${key} (must be UPPER_SNAKE_CASE)`);
}
const projectRoot = process.cwd();
const envFile = path.join(projectRoot, '.env');
let content = '';
if (fs.existsSync(envFile)) {
content = fs.readFileSync(envFile, 'utf-8');
}
const lineRegex = new RegExp(`^${key}=.*$`, 'm');
const newLine = `${key}=${value}`;
const existed = lineRegex.test(content);
if (existed) {
content = content.replace(lineRegex, newLine);
} else {
const sep = content && !content.endsWith('\n') ? '\n' : '';
content = content + sep + newLine + '\n';
}
fs.writeFileSync(envFile, content);
log.info('Updated .env', { key, existed });
let synced = false;
if (syncContainer) {
const dataEnvDir = path.join(projectRoot, 'data', 'env');
fs.mkdirSync(dataEnvDir, { recursive: true });
fs.copyFileSync(envFile, path.join(dataEnvDir, 'env'));
synced = true;
log.info('Synced .env to container mount', { path: 'data/env/env' });
}
emitStatus('SET_ENV', {
KEY: key,
EXISTED: existed,
SYNCED_TO_CONTAINER: synced,
STATUS: 'success',
});
}