diff --git a/README.md b/README.md index 7954c39..e3ded90 100644 --- a/README.md +++ b/README.md @@ -7,49 +7,58 @@ One-liner + skill conversacional para deixar o **Hermes Agent** pronto no client - Personalidade em `SOUL.md` - Setup guiado por LLM (Codex ou Hermes) -## One-liner (produção) — entrar na VPS + instalar +## One-liner (produção) — VPS compartilhada multi-tenant -O bootstrap roda **dentro da VPS Linux**, não no Mac do operador. +**Uma VPS DomHubs, vários clientes.** Isolamento = **Hermes profile** por cliente (não VM nova, não Docker por padrão). + +| Cliente | Home | Gateway unit | +|---------|------|----------------| +| `flavia` | `~/.hermes/profiles/flavia/` | `hermes-gateway-flavia` | +| `acme` | `~/.hermes/profiles/acme/` | `hermes-gateway-acme` | + +Cada um: bot token, allowlist, SOUL, `state.db` próprios. **Nunca** reutilizar token entre profiles. ### 1) Entrar na VPS -```bash -ssh root@IP_DA_VPS -``` - -DomHubs ops (alias no Mac, após chave `~/.ssh/domhubs_vps`): - ```bash ssh domhubs-vps # → root@169.58.116.28 ``` -### 2) One-liner (já dentro da VPS) +### 2) One-liner **com slug do cliente** (obrigatório) ```bash -curl -fsSL https://setup.domhubs.com.br/hermes | bash +curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client flavia ``` -Instala/atualiza Hermes + skill + launcher e **abre o onboarding** (condutor Hermes por padrão). +Isso: instala/atualiza Hermes + skill → provisiona profile isolado → abre onboarding **só nesse profile**. -Variantes (ainda na VPS): +Variantes: ```bash -# só instalar, sem abrir agente -curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --no-launch +# provisionar sem abrir o agente +curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client acme --no-launch -# perguntar Codex vs Hermes -curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --ask-conductor +# reabrir onboarding de um cliente +hermes-client-onboarding --client flavia + +# só criar/atualizar a instância +hermes-client-provision --client acme ``` -### 3) Voltar depois +### 3) Ops diário ```bash -ssh root@IP_DA_VPS # ou: ssh domhubs-vps -hermes gateway status -hermes-client-onboarding # se precisar retomar o onboarding +ssh domhubs-vps +hermes profile list +hermes --profile flavia gateway status +journalctl --user -u hermes-gateway-flavia -n 50 --no-pager ``` +### Por que não container (ainda) + +Profiles + `HERMES_HOME` + multi-gateway já isolam secrets/histórico/polling do Telegram com ~150 MB RAM/cliente. Docker entra depois se precisar sandbox de shell/tools por cliente. + Espelho GitHub (fallback): ```bash diff --git a/install.sh b/install.sh index 872c276..008b4e9 100755 --- a/install.sh +++ b/install.sh @@ -7,20 +7,23 @@ # ssh root@IP_DA_VPS # # DomHubs ops alias: ssh domhubs-vps (169.58.116.28, key ~/.ssh/domhubs_vps) # -# Then one-liner (inside the VPS): -# curl -fsSL https://setup.domhubs.com.br/hermes | bash +# Then one-liner (inside the VPS) — SHARED host multi-tenant: +# curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client flavia # -# Local checkout: ./install.sh [--conductor codex|hermes|skip] [--no-launch] +# Each --client creates an isolated Hermes profile + gateway unit. +# Local checkout: ./install.sh --client SLUG [--conductor hermes|skip] [--no-launch] set -euo pipefail SKILL_NAME="hermes-client-onboarding" DEFAULT_BASE="${HERMES_ONBOARD_BASE:-https://setup.domhubs.com.br/hermes}" HERMES_INSTALL_URL="${HERMES_INSTALL_URL:-https://hermes-agent.nousresearch.com/install.sh}" -KICKOFF_MSG="${HERMES_ONBOARD_KICKOFF:-Inicie o onboarding agora. Skill hermes-client-onboarding. Pre-flight silencioso e Phase 1 (voce fala primeiro).}" +KICKOFF_MSG="${HERMES_ONBOARD_KICKOFF:-}" CONDUCTOR="${HERMES_ONBOARD_CONDUCTOR:-hermes}" +CLIENT_SLUG="${HERMES_CLIENT_SLUG:-}" NO_LAUNCH=0 NONINTERACTIVE=0 +REQUIRE_CLIENT="${HERMES_REQUIRE_CLIENT:-1}" log() { printf '==> %s\n' "$*"; } warn() { printf 'warn: %s\n' "$*" >&2; } @@ -30,40 +33,44 @@ usage() { cat <<'EOF' Usage: install.sh [options] - Run this ON the Linux VPS after SSH: - ssh root@IP_DA_VPS - curl -fsSL https://setup.domhubs.com.br/hermes | bash + Shared DomHubs VPS — one host, many isolated Hermes clients (profiles): - DomHubs ops (Mac alias): ssh domhubs-vps + curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client flavia - --conductor codex|hermes|skip Who runs onboarding (default: hermes; use skip for install-only) - --no-launch Install only; do not start the conductor + --client SLUG Required. Isolated profile name (a-z0-9-) + --conductor codex|hermes|skip Who runs onboarding (default: hermes) + --no-launch Install + provision only; do not start conductor --base URL Asset base for skill files (or HERMES_ONBOARD_BASE) --non-interactive No prompts --ask-conductor Prompt for conductor even when default is hermes + --allow-default Allow missing --client (single-tenant / laptop only) -h, --help Show help Env: - HERMES_ONBOARD_BASE, HERMES_ONBOARD_CONDUCTOR, HERMES_ONBOARD_KICKOFF - HERMES_INSTALL_URL, HERMES_ONBOARD_NO_LAUNCH=1 + HERMES_CLIENT_SLUG, HERMES_ONBOARD_BASE, HERMES_ONBOARD_CONDUCTOR + HERMES_ONBOARD_KICKOFF, HERMES_INSTALL_URL, HERMES_ONBOARD_NO_LAUNCH=1 + HERMES_REQUIRE_CLIENT=0 Same as --allow-default EOF } ASK_CONDUCTOR=0 while [[ $# -gt 0 ]]; do case "$1" in + --client) CLIENT_SLUG="${2:-}"; shift 2 ;; --conductor) CONDUCTOR="${2:-}"; shift 2 ;; --no-launch) NO_LAUNCH=1; shift ;; --base) DEFAULT_BASE="${2:-}"; shift 2 ;; --non-interactive) NONINTERACTIVE=1; shift ;; --ask-conductor) ASK_CONDUCTOR=1; shift ;; + --allow-default) REQUIRE_CLIENT=0; shift ;; -h|--help) usage; exit 0 ;; *) die "unknown arg: $1" ;; esac done [[ "${HERMES_ONBOARD_NO_LAUNCH:-0}" == "1" ]] && NO_LAUNCH=1 +[[ "${HERMES_REQUIRE_CLIENT:-1}" == "0" ]] && REQUIRE_CLIENT=0 # --------------------------------------------------------------------------- # Resolve skill source: local checkout vs remote base URL @@ -123,6 +130,9 @@ copy_tree() { if [[ -f "$dest/scripts/apply-core-config.sh" ]]; then chmod +x "$dest/scripts/apply-core-config.sh" fi + if [[ -f "$dest/scripts/provision-client-instance.sh" ]]; then + chmod +x "$dest/scripts/provision-client-instance.sh" + fi if [[ -f "$dest/scripts/start-onboarding.sh" ]]; then chmod +x "$dest/scripts/start-onboarding.sh" fi @@ -140,9 +150,11 @@ fetch_skill_to() { curl -fsSL "${base}/skill/${SKILL_NAME}/SKILL.md" -o "$dest/SKILL.md" curl -fsSL "${base}/skill/${SKILL_NAME}/references/troubleshooting.md" -o "$dest/references/troubleshooting.md" curl -fsSL "${base}/skill/${SKILL_NAME}/scripts/apply-core-config.sh" -o "$dest/scripts/apply-core-config.sh" + curl -fsSL "${base}/skill/${SKILL_NAME}/scripts/provision-client-instance.sh" -o "$dest/scripts/provision-client-instance.sh" || true curl -fsSL "${base}/skill/${SKILL_NAME}/scripts/start-onboarding.sh" -o "$dest/scripts/start-onboarding.sh" || true curl -fsSL "${base}/skill/${SKILL_NAME}/scripts/auto_kickoff_cli.py" -o "$dest/scripts/auto_kickoff_cli.py" || true chmod +x "$dest/scripts/apply-core-config.sh" + [[ -f "$dest/scripts/provision-client-instance.sh" ]] && chmod +x "$dest/scripts/provision-client-instance.sh" [[ -f "$dest/scripts/start-onboarding.sh" ]] && chmod +x "$dest/scripts/start-onboarding.sh" [[ -f "$dest/scripts/auto_kickoff_cli.py" ]] && chmod +x "$dest/scripts/auto_kickoff_cli.py" [[ -s "$dest/SKILL.md" ]] || die "failed to download SKILL.md from $base" @@ -166,12 +178,17 @@ install_skill() { copy_tree "$staging" "$hermes_dest" log "Skill installed for Hermes → $hermes_dest" - # Launcher: auto-starts Phase 1 (agent speaks first via CLI PTY inject) + # Launcher + provisioner mkdir -p "${HOME}/.local/bin" "${HOME}/.local/share/hermes-client-onboarding" if [[ -f "$staging/scripts/start-onboarding.sh" ]]; then cp -f "$staging/scripts/start-onboarding.sh" "${HOME}/.local/bin/hermes-client-onboarding" chmod +x "${HOME}/.local/bin/hermes-client-onboarding" - log "Launcher → ~/.local/bin/hermes-client-onboarding (agent fala primeiro)" + log "Launcher → ~/.local/bin/hermes-client-onboarding [--client SLUG]" + fi + if [[ -f "$staging/scripts/provision-client-instance.sh" ]]; then + cp -f "$staging/scripts/provision-client-instance.sh" "${HOME}/.local/bin/hermes-client-provision" + chmod +x "${HOME}/.local/bin/hermes-client-provision" + log "Provisioner → ~/.local/bin/hermes-client-provision --client SLUG" fi if [[ -f "$staging/scripts/auto_kickoff_cli.py" ]]; then cp -f "$staging/scripts/auto_kickoff_cli.py" "${HOME}/.local/share/hermes-client-onboarding/auto_kickoff_cli.py" @@ -239,15 +256,26 @@ pick_conductor() { } launch_hermes_onboarding() { + if [[ -n "$CLIENT_SLUG" ]]; then + export HERMES_CLIENT_SLUG="$CLIENT_SLUG" + export HERMES_HOME="${HOME}/.hermes/profiles/${CLIENT_SLUG}" + if [[ -z "$KICKOFF_MSG" ]]; then + KICKOFF_MSG="Inicie onboarding do cliente ${CLIENT_SLUG}. Skill hermes-client-onboarding. Multi-tenant profile ${CLIENT_SLUG} only. Pre-flight e Phase 1." + fi + elif [[ -z "$KICKOFF_MSG" ]]; then + KICKOFF_MSG="Inicie o onboarding agora. Skill hermes-client-onboarding. Pre-flight silencioso e Phase 1 (voce fala primeiro)." + fi export HERMES_ONBOARD_KICKOFF="$KICKOFF_MSG" export HERMES_ONBOARD_SKILL="$SKILL_NAME" export HERMES_TUI_QUERY="$KICKOFF_MSG" export HERMES_TUI_SKILLS="$SKILL_NAME" if [[ -x "${HOME}/.local/bin/hermes-client-onboarding" ]]; then + local launch_args=() + [[ -n "$CLIENT_SLUG" ]] && launch_args=(--client "$CLIENT_SLUG") if [[ -r /dev/tty ]]; then - exec "${HOME}/.local/bin/hermes-client-onboarding" /dev/tty 2>/dev/tty + exec "${HOME}/.local/bin/hermes-client-onboarding" "${launch_args[@]}" /dev/tty 2>/dev/tty else - exec "${HOME}/.local/bin/hermes-client-onboarding" + exec "${HOME}/.local/bin/hermes-client-onboarding" "${launch_args[@]}" fi fi # Fallback: classic CLI + PTY inject script if present @@ -258,7 +286,7 @@ launch_hermes_onboarding() { fi exec python3 "$auto_py" fi - die "launcher missing — re-run install or: hermes chat --cli -s ${SKILL_NAME}" + die "launcher missing — re-run install or: hermes --profile ${CLIENT_SLUG:-default} chat --cli -s ${SKILL_NAME}" } launch_conductor() { @@ -292,22 +320,44 @@ launch_conductor() { } # --------------------------------------------------------------------------- +provision_client_if_needed() { + if [[ -z "$CLIENT_SLUG" ]]; then + if [[ "$REQUIRE_CLIENT" == "1" ]]; then + die "missing --client SLUG (shared VPS multi-tenant). Example: bash -s -- --client flavia | or HERMES_REQUIRE_CLIENT=0 for laptop" + fi + warn "No --client: using default ~/.hermes (not multi-tenant safe on shared host)" + return 0 + fi + # normalize slug early + CLIENT_SLUG="$(printf '%s' "$CLIENT_SLUG" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed -E 's/-+/-/g; s/^-|-$//g')" + export HERMES_CLIENT_SLUG="$CLIENT_SLUG" + local prov="${HOME}/.local/bin/hermes-client-provision" + [[ -x "$prov" ]] || prov="${HOME}/.hermes/skills/${SKILL_NAME}/scripts/provision-client-instance.sh" + [[ -x "$prov" ]] || die "provision script missing after skill install" + log "Provisioning isolated instance: ${CLIENT_SLUG}" + "$prov" --client "$CLIENT_SLUG" --no-start +} + main() { - log "DomHubs Hermes Client Onboarding" - log "Target: this machine (run via SSH on the client VPS)" + log "DomHubs Hermes Client Onboarding (shared VPS multi-tenant)" + log "Target: this machine (run via SSH on the DomHubs VPS)" if [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]]; then - warn "You appear to be on macOS. Production onboarding expects Ubuntu/Debian VPS:" - warn " ssh root@IP_DA_VPS" - warn " curl -fsSL https://setup.domhubs.com.br/hermes | bash" + warn "You appear to be on macOS. Production onboarding expects the DomHubs VPS:" + warn " ssh domhubs-vps" + warn " curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client SLUG" fi ensure_hermes install_skill + provision_client_if_needed if [[ "$NO_LAUNCH" -eq 1 ]]; then - log "Done (--no-launch). Re-enter VPS later with: ssh root@IP_DA_VPS (or ssh domhubs-vps)" - log "Start with (agent speaks first):" - echo " hermes-client-onboarding" - echo " # ou: hermes chat --tui -s ${SKILL_NAME} -q \"…\"" + log "Done (--no-launch). Re-enter: ssh domhubs-vps" + if [[ -n "$CLIENT_SLUG" ]]; then + echo " hermes-client-onboarding --client ${CLIENT_SLUG}" + echo " hermes --profile ${CLIENT_SLUG} gateway status" + else + echo " hermes-client-onboarding" + fi exit 0 fi diff --git a/skill/hermes-client-onboarding/SKILL.md b/skill/hermes-client-onboarding/SKILL.md index c376c86..9810d65 100644 --- a/skill/hermes-client-onboarding/SKILL.md +++ b/skill/hermes-client-onboarding/SKILL.md @@ -1,13 +1,13 @@ --- name: hermes-client-onboarding description: Use when setting up Hermes for a client, install Hermes + Telegram + DeepSeek, run a demo setup, or launch client onboarding. Conducts guided conversational onboarding on a clean Linux VM (deepseek-v4-flash, Telegram gateway, systemd, SOUL.md). -version: 1.3.0 +version: 1.4.0 author: DomHubs license: MIT platforms: [linux, macos] metadata: hermes: - tags: [onboarding, client, telegram, deepseek, gateway, demo, vps] + tags: [onboarding, client, telegram, deepseek, gateway, demo, vps, multi-tenant] related_skills: [] --- @@ -15,9 +15,15 @@ metadata: ## Overview -You are conducting a professional, step-by-step onboarding of Hermes Agent on a clean Ubuntu/Debian **VPS** so a client can start using it immediately (primarily via Telegram). The goal is a working agent in minutes, with **native DeepSeek** (`DEEPSEEK_API_KEY`, provider `deepseek`) and model **`deepseek-v4-flash`** (V4 Flash 0731 family) as the default, Telegram as the primary channel, and the gateway running as a persistent service. +You are conducting a professional, step-by-step onboarding of Hermes Agent on the **shared DomHubs Ubuntu/Debian VPS** so a client can start using it immediately (primarily via Telegram). DomHubs does **not** give each client a new VM — many clients share one host. -This skill is designed for live demos in front of the client and for commercial handoff. Be clear, structured, and efficient. Always confirm critical values before applying them. +**Isolation model (required):** one client = one Hermes **profile** (`~/.hermes/profiles//`) + its own gateway unit (`hermes-gateway-.service`) + its own bot token, allowlist, SOUL, and `state.db`. Never put a client bot on the default `~/.hermes` home. + +Containers are optional later (OS sandbox for untrusted tools). Profiles already prevent secret/history/gateway conflicts for Telegram bots. + +Goal: working agent in minutes, native DeepSeek (`DEEPSEEK_API_KEY`, provider `deepseek`), model **`deepseek-v4-flash`**, Telegram primary, gateway as systemd user service **for that profile only**. + +Be clear, structured, and efficient. Always confirm critical values before applying them. ## When to Use @@ -32,63 +38,73 @@ Don't use for: day-to-day Hermes coding tasks after onboarding is done; multi-te The onboarding is complete only when all of the following are true: -- Operator can **SSH into the VPS** and re-enter when needed -- Hermes is installed and `hermes` command works -- Model is set to `deepseek-v4-flash` with provider `deepseek` -- `DEEPSEEK_API_KEY` is configured -- Telegram bot token and at least one allowed user ID are set -- Gateway is installed as a systemd service and is running -- A test message sent to the Telegram bot receives a coherent reply -- `hermes doctor` reports no critical errors -- SOUL.md has been personalized (or the user explicitly skipped it) +- Operator can **SSH into the shared DomHubs VPS** +- Client has a dedicated **profile slug** (not default `~/.hermes`) +- Hermes is installed; `hermes --profile …` works +- Model is set to `deepseek-v4-flash` with provider `deepseek` **inside that profile** +- `DEEPSEEK_API_KEY` is configured **in the profile `.env`** +- Telegram bot token (unique to this client) and allowlist are set **in the profile** +- Gateway unit `hermes-gateway-` is installed and running +- No other profile uses the same Telegram bot token +- A test message to the bot receives a coherent reply +- `hermes --profile doctor` has no critical errors +- SOUL.md personalized under the profile home (or user skipped) -## Phase 0 — Entrar na VPS (antes de tudo) +## Phase 0 — Entrar na VPS + escolher instância (antes de tudo) -Onboarding runs **inside** the Linux VPS, not on the operator’s laptop. First step is always SSH. +Onboarding runs **inside** the shared DomHubs VPS. First step is always SSH; second is a **client slug**. -**Generic (any client VM):** - -```bash -ssh root@IP_DA_VPS -# or: ssh USER@IP_DA_VPS -``` - -**DomHubs ops alias (operator Mac, after key is set):** +**DomHubs ops (operator Mac):** ```bash ssh domhubs-vps # Host: 169.58.116.28 User: root Key: ~/.ssh/domhubs_vps ``` -**Then, already on the VPS, bootstrap + this skill (one-liner):** +**On the VPS — provision + onboarding for ONE client (required):** ```bash -curl -fsSL https://setup.domhubs.com.br/hermes | bash +# slug = stable id (flavia, acme, joao-silva). Never reuse across clients. +curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client SLUG ``` -Variants on the VPS: +Examples: ```bash -# install only (no auto-launch) -curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --no-launch - -# re-open onboarding later -hermes-client-onboarding +curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client flavia +curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client acme --no-launch ``` -If the session drops mid-onboarding, reconnect with the same `ssh` and reattach tmux if used: +This creates/reuses: + +| Piece | Path / unit | +|-------|-------------| +| Home | `~/.hermes/profiles//` | +| Secrets | `…/profiles//.env` | +| Soul | `…/profiles//SOUL.md` | +| History | `…/profiles//state.db` | +| Gateway | `hermes-gateway-.service` | + +**Re-enter later:** ```bash -ssh root@IP_DA_VPS # or: ssh domhubs-vps -tmux ls -tmux attach -t hermes-onboard- # if the launcher created one -# or restart onboarding: -hermes-client-onboarding +ssh domhubs-vps +hermes --profile SLUG gateway status +hermes-client-onboarding --client SLUG +# tmux: tmux ls && tmux attach -t hermes-onboard-SLUG-… ``` -In Phase 6 handover, **always** leave the client/operator with the exact SSH command for *their* IP (do not invent IPs). +**Hard isolation rules (never break these):** -**Done when:** shell is on the target Linux VPS (hostname/IP known) and you can run commands as the deploy user (usually `root`). +1. One Telegram bot token → exactly one profile/gateway. +2. All `hermes config set` / doctor / gateway for a client use `--profile SLUG` (or `HERMES_HOME=~/.hermes/profiles/SLUG`). +3. Do not start client bots on the default `hermes-gateway.service` (host default is for tooling/setup only). +4. Do not enable heavy shared MCP servers on client profiles (leaks tokens + RAM). +5. `kanban.dispatch_in_gateway: false` on client profiles (shared host). + +If the kickoff mentions a profile slug, **stay inside that profile for the entire onboarding**. + +**Done when:** shell is on DomHubs VPS, client slug known, profile home exists (or will be created immediately). ## Pre-flight Checks (do these first) @@ -227,67 +243,74 @@ Write the final content to `~/.hermes/SOUL.md`. Confirm before overwriting if th **Done when:** SOUL.md written or user explicitly skipped personalization. -### Phase 5 — Gateway & Persistence +### Phase 5 — Gateway & Persistence (per profile) -1. Install the gateway as a system service: +Assume client slug is `$SLUG` (from Phase 0 / kickoff). + +1. Prefer the provisioner if the unit is missing: ```bash -hermes gateway install +hermes-client-provision --client "$SLUG" +# or: hermes --profile "$SLUG" gateway install ``` -2. Start / restart it: +2. Start / restart **only this profile’s** gateway: ```bash -hermes gateway start +hermes --profile "$SLUG" gateway start # or -hermes gateway restart +hermes --profile "$SLUG" gateway restart ``` 3. Check status: ```bash -hermes gateway status +hermes --profile "$SLUG" gateway status +systemctl --user is-active "hermes-gateway-${SLUG}.service" ``` -4. If the service fails, inspect logs (`hermes gateway logs` or `journalctl -u hermes* -n 50` / `launchctl` on macOS) and fix common issues (PATH, missing env, permissions). See `references/troubleshooting.md`. +4. If the service fails, inspect logs: -**Done when:** gateway status shows running and service is installed for reboot persistence. +```bash +journalctl --user -u "hermes-gateway-${SLUG}" -n 50 --no-pager +``` + +Never restart the default `hermes-gateway.service` for a client bot unless you intentionally want the default home (you should not). + +**Done when:** `hermes-gateway-` is active and Telegram connected for that profile only. ### Phase 6 — Validation & Handover -Run the full validation sequence: +Run the full validation sequence (replace `$SLUG`): ```bash -hermes doctor -hermes gateway status +hermes --profile "$SLUG" doctor +hermes --profile "$SLUG" gateway status ``` Then instruct the user to send a test message to the Telegram bot (“oi” ou “teste”). Confirm that a coherent reply arrives. -Final checklist to present to the user: +Final checklist: -- [ ] Hermes installed and in PATH -- [ ] Model = deepseek-v4-flash via provider deepseek (native API) -- [ ] Telegram bot responding -- [ ] Gateway running as service (survives reboot) -- [ ] SOUL.md personalized -- [ ] `hermes doctor` clean +- [ ] Profile `~/.hermes/profiles//` isolated +- [ ] Model = deepseek-v4-flash via provider deepseek (in profile) +- [ ] Unique Telegram bot responding +- [ ] Unit `hermes-gateway-` running (survives reboot + linger) +- [ ] SOUL.md personalized under profile +- [ ] `hermes --profile doctor` clean +- [ ] No token collision with other profiles -Give the user the useful commands for later: +Ops commands for later: ```bash -# Re-enter the VPS (fill real IP / use DomHubs alias) -ssh root@IP_DA_VPS -# ssh domhubs-vps - -hermes gateway status -hermes gateway logs -hermes doctor -hermes config get model.default -hermes update +ssh domhubs-vps +hermes --profile SLUG gateway status +journalctl --user -u hermes-gateway-SLUG -n 50 --no-pager +hermes --profile SLUG doctor +hermes profile list ``` -**Done when:** checklist walked, test Telegram reply confirmed, SSH re-entry command + useful commands delivered. +**Done when:** checklist walked, test Telegram reply confirmed, slug + SSH re-entry delivered. ## Error Handling Guidelines @@ -321,35 +344,34 @@ hermes update ## Reference Commands (quick lookup) ```bash -# Enter VPS first -ssh root@IP_DA_VPS -# DomHubs ops: ssh domhubs-vps +# Enter shared VPS +ssh domhubs-vps -# DomHubs one-liner (on the VPS) -curl -fsSL https://setup.domhubs.com.br/hermes | bash +# New / resume client instance (on VPS) +curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client SLUG +hermes-client-provision --client SLUG +hermes-client-onboarding --client SLUG -# Install Hermes only -curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --skip-browser +# Config inside profile (HERMES_HOME or --profile) +export HERMES_HOME=~/.hermes/profiles/SLUG +# or: hermes --profile SLUG config set … +hermes --profile SLUG config set DEEPSEEK_API_KEY "sk-..." +hermes --profile SLUG config set model.provider deepseek +hermes --profile SLUG config set model.default deepseek-v4-flash +hermes --profile SLUG config set model.base_url "https://api.deepseek.com/v1" +hermes --profile SLUG config set TELEGRAM_BOT_TOKEN "..." +hermes --profile SLUG config set TELEGRAM_ALLOWED_USERS "123456789" +grep -E '^TELEGRAM_ALLOWED_USERS=' ~/.hermes/profiles/SLUG/.env -# Core config (or use the helper script) -hermes config set DEEPSEEK_API_KEY "sk-..." -hermes config set model.provider deepseek -hermes config set model.default deepseek-v4-flash -hermes config set model.base_url "https://api.deepseek.com/v1" -hermes config set TELEGRAM_BOT_TOKEN "..." -hermes config set TELEGRAM_ALLOWED_USERS "123456789" -grep -E '^TELEGRAM_ALLOWED_USERS=' ~/.hermes/.env # must match IDs above +# Gateway for this client only +hermes --profile SLUG gateway install +hermes --profile SLUG gateway restart +hermes --profile SLUG gateway status +journalctl --user -u hermes-gateway-SLUG -n 50 --no-pager -# Gateway -hermes gateway install -hermes gateway restart -hermes gateway status -# Linux: journalctl --user -u hermes-gateway -n 50 -# macOS: tail -f ~/.hermes/logs/gateway.log - -# Validation -hermes doctor -hermes --version +# Fleet +hermes profile list +systemctl --user list-units 'hermes-gateway*' --all ``` When the user says the onboarding is finished or the bot is responding correctly, summarize what was configured and congratulate them. Offer to make any final adjustments. diff --git a/skill/hermes-client-onboarding/scripts/provision-client-instance.sh b/skill/hermes-client-onboarding/scripts/provision-client-instance.sh new file mode 100755 index 0000000..b17e41e --- /dev/null +++ b/skill/hermes-client-onboarding/scripts/provision-client-instance.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# DomHubs — provision an isolated Hermes client instance on a SHARED VPS. +# +# Model: one Linux host, many clients = many Hermes *profiles* +# ~/.hermes/profiles// → own .env, SOUL, state.db, gateway unit +# systemd: hermes-gateway-.service +# Telegram: one bot token per profile (never share tokens) +# +# Why not Docker first: Hermes already isolates via HERMES_HOME + multi-gateway. +# Containers later only if you need OS-level sandbox for untrusted tool use. +# +# Usage (on the VPS as root/deploy user): +# provision-client-instance.sh --client flavia +# provision-client-instance.sh --client acme --description "Acme assistant" +# provision-client-instance.sh --client flavia --clone-from edwiges +# +set -euo pipefail + +SLUG="" +DESCRIPTION="" +CLONE_FROM="" +START_GATEWAY=1 +INSTALL_GATEWAY=1 +LEAN_MCP=1 + +log() { printf '==> %s\n' "$*"; } +warn() { printf 'warn: %s\n' "$*" >&2; } +die() { printf 'error: %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Usage: provision-client-instance.sh --client SLUG [options] + + --client SLUG Required. lowercase [a-z0-9-] (e.g. flavia, acme-corp) + --description TEXT Stored on the profile + --clone-from PROFILE Clone config skeleton from another profile (not secrets by default uses --clone) + --no-gateway-install Create profile only + --no-start Install unit but do not start + --keep-mcp Do not force mcp_servers: {} + -h, --help + +Env: + HERMES_CLIENT_SLUG Same as --client +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --client) SLUG="${2:-}"; shift 2 ;; + --description) DESCRIPTION="${2:-}"; shift 2 ;; + --clone-from) CLONE_FROM="${2:-}"; shift 2 ;; + --no-gateway-install) INSTALL_GATEWAY=0; shift ;; + --no-start) START_GATEWAY=0; shift ;; + --keep-mcp) LEAN_MCP=0; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown arg: $1" ;; + esac +done + +SLUG="${SLUG:-${HERMES_CLIENT_SLUG:-}}" +[[ -n "$SLUG" ]] || die "--client SLUG is required (shared VPS multi-tenant)" + +# normalize: lowercase, allow a-z0-9- +SLUG="$(printf '%s' "$SLUG" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')" +SLUG="$(printf '%s' "$SLUG" | sed -E 's/-+/-/g; s/^-|-$//g')" +[[ "$SLUG" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || die "invalid slug after normalize: $SLUG" +[[ "$SLUG" != "default" && "$SLUG" != "root" && "$SLUG" != "main" ]] || die "reserved slug: $SLUG" + +command -v hermes >/dev/null 2>&1 || die "hermes not on PATH" +export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}" + +PROFILE_HOME="${HERMES_ROOT:-${HOME}/.hermes}/profiles/${SLUG}" +DESCRIPTION="${DESCRIPTION:-DomHubs client instance: ${SLUG}}" + +log "Provisioning isolated client instance: ${SLUG}" +log "Profile home: ${PROFILE_HOME}" + +if [[ -d "$PROFILE_HOME" && -f "$PROFILE_HOME/config.yaml" ]]; then + log "Profile already exists — reusing ${SLUG}" +else + create_args=(profile create "$SLUG" --description "$DESCRIPTION" --no-skills) + if [[ -n "$CLONE_FROM" ]]; then + create_args+=(--clone-from "$CLONE_FROM") + fi + hermes "${create_args[@]}" +fi + +[[ -d "$PROFILE_HOME" ]] || die "profile home missing after create: $PROFILE_HOME" +chmod 700 "$PROFILE_HOME" 2>/dev/null || true + +# Lean production defaults for multi-tenant bots +python3 - "$PROFILE_HOME" "$LEAN_MCP" <<'PY' +import sys, re +from pathlib import Path +home = Path(sys.argv[1]) +lean = sys.argv[2] == "1" +cfg = home / "config.yaml" +if not cfg.exists(): + sys.exit(0) +text = cfg.read_text() +# kanban: only default should dispatch on shared host +if re.search(r"^kanban:\s*$", text, re.M): + if "dispatch_in_gateway" not in text: + text = re.sub(r"^kanban:\s*$", "kanban:\n dispatch_in_gateway: false", text, count=1, flags=re.M) +elif "dispatch_in_gateway" not in text: + text = text.rstrip() + "\n\nkanban:\n dispatch_in_gateway: false\n" +if lean: + if re.search(r"^mcp_servers:\s*$", text, re.M): + text = re.sub(r"^mcp_servers:\n(?: .*\n)*", "mcp_servers: {}\n", text, count=1, flags=re.M) + elif re.search(r"^mcp_servers:\s*\{\s*\}\s*$", text, re.M): + pass + elif re.search(r"^mcp_servers:", text, re.M): + text = re.sub(r"^mcp_servers:\n(?: .*\n)*", "mcp_servers: {}\n", text, count=1, flags=re.M) + else: + text = text.rstrip() + "\nmcp_servers: {}\n" +cfg.write_text(text) +print("config hardened (kanban dispatch off, lean mcp)" if lean else "config hardened (kanban dispatch off)") +PY + +# Ensure empty secrets file exists with safe perms +touch "${PROFILE_HOME}/.env" +chmod 600 "${PROFILE_HOME}/.env" + +if [[ "$INSTALL_GATEWAY" -eq 1 ]]; then + log "Installing systemd user unit: hermes-gateway-${SLUG}" + hermes --profile "$SLUG" gateway install + if [[ "$START_GATEWAY" -eq 1 ]]; then + # Do not start if no bot token yet — gateway can still run but wasteful + if grep -qE '^TELEGRAM_BOT_TOKEN=.+' "${PROFILE_HOME}/.env" 2>/dev/null; then + hermes --profile "$SLUG" gateway restart || hermes --profile "$SLUG" gateway start + log "Gateway started for ${SLUG}" + else + warn "No TELEGRAM_BOT_TOKEN in profile .env yet — unit installed, not started" + warn "After onboarding secrets: hermes --profile ${SLUG} gateway start" + fi + fi +fi + +# Mark instance metadata (no secrets) +mkdir -p "${PROFILE_HOME}/domhubs" +cat > "${PROFILE_HOME}/domhubs/instance.json" </dev/null 2>&1; then echo "error: hermes not on PATH" >&2 exit 1 fi +HERMES_BIN=(hermes) +if [[ -n "$CLIENT_SLUG" ]]; then + HERMES_BIN=(hermes --profile "$CLIENT_SLUG") + export HERMES_HOME="${HOME}/.hermes/profiles/${CLIENT_SLUG}" + echo "==> Profile/client: ${CLIENT_SLUG} (HERMES_HOME=${HERMES_HOME})" +fi + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AUTO_PY="" for candidate in \ @@ -36,19 +60,20 @@ done use_tui="${HERMES_ONBOARD_USE_TUI:-0}" if [[ "$use_tui" == "1" ]]; then if [[ -r /dev/tty ]]; then - exec hermes chat --tui -s "$SKILL_NAME" --query "$KICKOFF" /dev/null 2>&1 && [[ -t 0 && -t 1 ]]; then - SESSION="hermes-onboard-$$" + SESSION="hermes-onboard-${CLIENT_SLUG:-default}-$$" # Kill leftover same-name (shouldn't happen with $$) tmux has-session -t "$SESSION" 2>/dev/null && tmux kill-session -t "$SESSION" 2>/dev/null || true + HERMES_CMD="${HERMES_BIN[*]} chat --cli -s ${SKILL_NAME}" tmux new-session -d -s "$SESSION" -x "$(tput cols 2>/dev/null || echo 120)" -y "$(tput lines 2>/dev/null || echo 40)" \ - "export PATH=\"${PATH}\"; hermes chat --cli -s ${SKILL_NAME}; exec bash" + "export PATH=\"${PATH}\"; export HERMES_CLIENT_SLUG=\"${CLIENT_SLUG:-}\"; export HERMES_HOME=\"${HERMES_HOME:-}\"; ${HERMES_CMD}; exec bash" # Wait until Hermes is up, then type kickoff + Enter for i in $(seq 1 40); do