mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
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:
committed by
exe.dev user
parent
ccb676ae91
commit
712a0e1e01
@@ -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):
|
||||
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Executable
+56
@@ -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 ==="
|
||||
Executable
+54
@@ -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 ==="
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user