Multi-tenant shared VPS: one client = one Hermes profile

Require --client SLUG on the one-liner; add hermes-client-provision for
isolated HERMES_HOME + hermes-gateway-<slug>. Document profile isolation
over per-VM/Docker for DomHubs fleet on a single host.
This commit is contained in:
domfelipe 2026-08-04 20:40:23 -03:00
parent 4ea1708cdd
commit ff725338f3
5 changed files with 422 additions and 145 deletions

View file

@ -7,49 +7,58 @@ One-liner + skill conversacional para deixar o **Hermes Agent** pronto no client
- Personalidade em `SOUL.md` - Personalidade em `SOUL.md`
- Setup guiado por LLM (Codex ou Hermes) - 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 ### 1) Entrar na VPS
```bash
ssh root@IP_DA_VPS
```
DomHubs ops (alias no Mac, após chave `~/.ssh/domhubs_vps`):
```bash ```bash
ssh domhubs-vps ssh domhubs-vps
# → root@169.58.116.28 # → root@169.58.116.28
``` ```
### 2) One-liner (já dentro da VPS) ### 2) One-liner **com slug do cliente** (obrigatório)
```bash ```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 ```bash
# só instalar, sem abrir agente # provisionar sem abrir o agente
curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --no-launch curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client acme --no-launch
# perguntar Codex vs Hermes # reabrir onboarding de um cliente
curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --ask-conductor hermes-client-onboarding --client flavia
# só criar/atualizar a instância
hermes-client-provision --client acme
``` ```
### 3) Voltar depois ### 3) Ops diário
```bash ```bash
ssh root@IP_DA_VPS # ou: ssh domhubs-vps ssh domhubs-vps
hermes gateway status hermes profile list
hermes-client-onboarding # se precisar retomar o onboarding 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 ~150MB RAM/cliente. Docker entra depois se precisar sandbox de shell/tools por cliente.
Espelho GitHub (fallback): Espelho GitHub (fallback):
```bash ```bash

View file

@ -7,20 +7,23 @@
# ssh root@IP_DA_VPS # ssh root@IP_DA_VPS
# # DomHubs ops alias: ssh domhubs-vps (169.58.116.28, key ~/.ssh/domhubs_vps) # # DomHubs ops alias: ssh domhubs-vps (169.58.116.28, key ~/.ssh/domhubs_vps)
# #
# Then one-liner (inside the VPS): # Then one-liner (inside the VPS) — SHARED host multi-tenant:
# curl -fsSL https://setup.domhubs.com.br/hermes | bash # 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 set -euo pipefail
SKILL_NAME="hermes-client-onboarding" SKILL_NAME="hermes-client-onboarding"
DEFAULT_BASE="${HERMES_ONBOARD_BASE:-https://setup.domhubs.com.br/hermes}" DEFAULT_BASE="${HERMES_ONBOARD_BASE:-https://setup.domhubs.com.br/hermes}"
HERMES_INSTALL_URL="${HERMES_INSTALL_URL:-https://hermes-agent.nousresearch.com/install.sh}" 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}" CONDUCTOR="${HERMES_ONBOARD_CONDUCTOR:-hermes}"
CLIENT_SLUG="${HERMES_CLIENT_SLUG:-}"
NO_LAUNCH=0 NO_LAUNCH=0
NONINTERACTIVE=0 NONINTERACTIVE=0
REQUIRE_CLIENT="${HERMES_REQUIRE_CLIENT:-1}"
log() { printf '==> %s\n' "$*"; } log() { printf '==> %s\n' "$*"; }
warn() { printf 'warn: %s\n' "$*" >&2; } warn() { printf 'warn: %s\n' "$*" >&2; }
@ -30,40 +33,44 @@ usage() {
cat <<'EOF' cat <<'EOF'
Usage: install.sh [options] Usage: install.sh [options]
Run this ON the Linux VPS after SSH: Shared DomHubs VPS — one host, many isolated Hermes clients (profiles):
ssh root@IP_DA_VPS
curl -fsSL https://setup.domhubs.com.br/hermes | bash
DomHubs ops (Mac alias):
ssh domhubs-vps 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) --client SLUG Required. Isolated profile name (a-z0-9-)
--no-launch Install only; do not start the conductor --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) --base URL Asset base for skill files (or HERMES_ONBOARD_BASE)
--non-interactive No prompts --non-interactive No prompts
--ask-conductor Prompt for conductor even when default is hermes --ask-conductor Prompt for conductor even when default is hermes
--allow-default Allow missing --client (single-tenant / laptop only)
-h, --help Show help -h, --help Show help
Env: Env:
HERMES_ONBOARD_BASE, HERMES_ONBOARD_CONDUCTOR, HERMES_ONBOARD_KICKOFF HERMES_CLIENT_SLUG, HERMES_ONBOARD_BASE, HERMES_ONBOARD_CONDUCTOR
HERMES_INSTALL_URL, HERMES_ONBOARD_NO_LAUNCH=1 HERMES_ONBOARD_KICKOFF, HERMES_INSTALL_URL, HERMES_ONBOARD_NO_LAUNCH=1
HERMES_REQUIRE_CLIENT=0 Same as --allow-default
EOF EOF
} }
ASK_CONDUCTOR=0 ASK_CONDUCTOR=0
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--client) CLIENT_SLUG="${2:-}"; shift 2 ;;
--conductor) CONDUCTOR="${2:-}"; shift 2 ;; --conductor) CONDUCTOR="${2:-}"; shift 2 ;;
--no-launch) NO_LAUNCH=1; shift ;; --no-launch) NO_LAUNCH=1; shift ;;
--base) DEFAULT_BASE="${2:-}"; shift 2 ;; --base) DEFAULT_BASE="${2:-}"; shift 2 ;;
--non-interactive) NONINTERACTIVE=1; shift ;; --non-interactive) NONINTERACTIVE=1; shift ;;
--ask-conductor) ASK_CONDUCTOR=1; shift ;; --ask-conductor) ASK_CONDUCTOR=1; shift ;;
--allow-default) REQUIRE_CLIENT=0; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) die "unknown arg: $1" ;; *) die "unknown arg: $1" ;;
esac esac
done done
[[ "${HERMES_ONBOARD_NO_LAUNCH:-0}" == "1" ]] && NO_LAUNCH=1 [[ "${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 # Resolve skill source: local checkout vs remote base URL
@ -123,6 +130,9 @@ copy_tree() {
if [[ -f "$dest/scripts/apply-core-config.sh" ]]; then if [[ -f "$dest/scripts/apply-core-config.sh" ]]; then
chmod +x "$dest/scripts/apply-core-config.sh" chmod +x "$dest/scripts/apply-core-config.sh"
fi 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 if [[ -f "$dest/scripts/start-onboarding.sh" ]]; then
chmod +x "$dest/scripts/start-onboarding.sh" chmod +x "$dest/scripts/start-onboarding.sh"
fi 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}/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}/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/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/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 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" 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/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" [[ -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" [[ -s "$dest/SKILL.md" ]] || die "failed to download SKILL.md from $base"
@ -166,12 +178,17 @@ install_skill() {
copy_tree "$staging" "$hermes_dest" copy_tree "$staging" "$hermes_dest"
log "Skill installed for Hermes → $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" mkdir -p "${HOME}/.local/bin" "${HOME}/.local/share/hermes-client-onboarding"
if [[ -f "$staging/scripts/start-onboarding.sh" ]]; then if [[ -f "$staging/scripts/start-onboarding.sh" ]]; then
cp -f "$staging/scripts/start-onboarding.sh" "${HOME}/.local/bin/hermes-client-onboarding" cp -f "$staging/scripts/start-onboarding.sh" "${HOME}/.local/bin/hermes-client-onboarding"
chmod +x "${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 fi
if [[ -f "$staging/scripts/auto_kickoff_cli.py" ]]; then 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" 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() { 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_KICKOFF="$KICKOFF_MSG"
export HERMES_ONBOARD_SKILL="$SKILL_NAME" export HERMES_ONBOARD_SKILL="$SKILL_NAME"
export HERMES_TUI_QUERY="$KICKOFF_MSG" export HERMES_TUI_QUERY="$KICKOFF_MSG"
export HERMES_TUI_SKILLS="$SKILL_NAME" export HERMES_TUI_SKILLS="$SKILL_NAME"
if [[ -x "${HOME}/.local/bin/hermes-client-onboarding" ]]; then 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 if [[ -r /dev/tty ]]; then
exec "${HOME}/.local/bin/hermes-client-onboarding" </dev/tty >/dev/tty 2>/dev/tty exec "${HOME}/.local/bin/hermes-client-onboarding" "${launch_args[@]}" </dev/tty >/dev/tty 2>/dev/tty
else else
exec "${HOME}/.local/bin/hermes-client-onboarding" exec "${HOME}/.local/bin/hermes-client-onboarding" "${launch_args[@]}"
fi fi
fi fi
# Fallback: classic CLI + PTY inject script if present # Fallback: classic CLI + PTY inject script if present
@ -258,7 +286,7 @@ launch_hermes_onboarding() {
fi fi
exec python3 "$auto_py" exec python3 "$auto_py"
fi 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() { 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() { main() {
log "DomHubs Hermes Client Onboarding" log "DomHubs Hermes Client Onboarding (shared VPS multi-tenant)"
log "Target: this machine (run via SSH on the client VPS)" log "Target: this machine (run via SSH on the DomHubs VPS)"
if [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]]; then if [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]]; then
warn "You appear to be on macOS. Production onboarding expects Ubuntu/Debian VPS:" warn "You appear to be on macOS. Production onboarding expects the DomHubs VPS:"
warn " ssh root@IP_DA_VPS" warn " ssh domhubs-vps"
warn " curl -fsSL https://setup.domhubs.com.br/hermes | bash" warn " curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client SLUG"
fi fi
ensure_hermes ensure_hermes
install_skill install_skill
provision_client_if_needed
if [[ "$NO_LAUNCH" -eq 1 ]]; then 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 "Done (--no-launch). Re-enter: ssh domhubs-vps"
log "Start with (agent speaks first):" if [[ -n "$CLIENT_SLUG" ]]; then
echo " hermes-client-onboarding --client ${CLIENT_SLUG}"
echo " hermes --profile ${CLIENT_SLUG} gateway status"
else
echo " hermes-client-onboarding" echo " hermes-client-onboarding"
echo " # ou: hermes chat --tui -s ${SKILL_NAME} -q \"…\"" fi
exit 0 exit 0
fi fi

View file

@ -1,13 +1,13 @@
--- ---
name: hermes-client-onboarding 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). 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 author: DomHubs
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
metadata: metadata:
hermes: hermes:
tags: [onboarding, client, telegram, deepseek, gateway, demo, vps] tags: [onboarding, client, telegram, deepseek, gateway, demo, vps, multi-tenant]
related_skills: [] related_skills: []
--- ---
@ -15,9 +15,15 @@ metadata:
## Overview ## 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/<slug>/`) + its own gateway unit (`hermes-gateway-<slug>.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 ## 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: The onboarding is complete only when all of the following are true:
- Operator can **SSH into the VPS** and re-enter when needed - Operator can **SSH into the shared DomHubs VPS**
- Hermes is installed and `hermes` command works - Client has a dedicated **profile slug** (not default `~/.hermes`)
- Model is set to `deepseek-v4-flash` with provider `deepseek` - Hermes is installed; `hermes --profile <slug> …` works
- `DEEPSEEK_API_KEY` is configured - Model is set to `deepseek-v4-flash` with provider `deepseek` **inside that profile**
- Telegram bot token and at least one allowed user ID are set - `DEEPSEEK_API_KEY` is configured **in the profile `.env`**
- Gateway is installed as a systemd service and is running - Telegram bot token (unique to this client) and allowlist are set **in the profile**
- A test message sent to the Telegram bot receives a coherent reply - Gateway unit `hermes-gateway-<slug>` is installed and running
- `hermes doctor` reports no critical errors - No other profile uses the same Telegram bot token
- SOUL.md has been personalized (or the user explicitly skipped it) - A test message to the bot receives a coherent reply
- `hermes --profile <slug> 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 operators 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):** **DomHubs ops (operator Mac):**
```bash
ssh root@IP_DA_VPS
# or: ssh USER@IP_DA_VPS
```
**DomHubs ops alias (operator Mac, after key is set):**
```bash ```bash
ssh domhubs-vps ssh domhubs-vps
# Host: 169.58.116.28 User: root Key: ~/.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 ```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 ```bash
# install only (no auto-launch) curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client flavia
curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --no-launch curl -fsSL https://setup.domhubs.com.br/hermes | bash -s -- --client acme --no-launch
# re-open onboarding later
hermes-client-onboarding
``` ```
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/<slug>/` |
| Secrets | `…/profiles/<slug>/.env` |
| Soul | `…/profiles/<slug>/SOUL.md` |
| History | `…/profiles/<slug>/state.db` |
| Gateway | `hermes-gateway-<slug>.service` |
**Re-enter later:**
```bash ```bash
ssh root@IP_DA_VPS # or: ssh domhubs-vps ssh domhubs-vps
tmux ls hermes --profile SLUG gateway status
tmux attach -t hermes-onboard-<pid> # if the launcher created one hermes-client-onboarding --client SLUG
# or restart onboarding: # tmux: tmux ls && tmux attach -t hermes-onboard-SLUG-…
hermes-client-onboarding
``` ```
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) ## 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. **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 ```bash
hermes gateway install hermes-client-provision --client "$SLUG"
# or: hermes --profile "$SLUG" gateway install
``` ```
2. Start / restart it: 2. Start / restart **only this profiles** gateway:
```bash ```bash
hermes gateway start hermes --profile "$SLUG" gateway start
# or # or
hermes gateway restart hermes --profile "$SLUG" gateway restart
``` ```
3. Check status: 3. Check status:
```bash ```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-<slug>` is active and Telegram connected for that profile only.
### Phase 6 — Validation & Handover ### Phase 6 — Validation & Handover
Run the full validation sequence: Run the full validation sequence (replace `$SLUG`):
```bash ```bash
hermes doctor hermes --profile "$SLUG" doctor
hermes gateway status 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. 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 - [ ] Profile `~/.hermes/profiles/<slug>/` isolated
- [ ] Model = deepseek-v4-flash via provider deepseek (native API) - [ ] Model = deepseek-v4-flash via provider deepseek (in profile)
- [ ] Telegram bot responding - [ ] Unique Telegram bot responding
- [ ] Gateway running as service (survives reboot) - [ ] Unit `hermes-gateway-<slug>` running (survives reboot + linger)
- [ ] SOUL.md personalized - [ ] SOUL.md personalized under profile
- [ ] `hermes doctor` clean - [ ] `hermes --profile <slug> doctor` clean
- [ ] No token collision with other profiles
Give the user the useful commands for later: Ops commands for later:
```bash ```bash
# Re-enter the VPS (fill real IP / use DomHubs alias) ssh domhubs-vps
ssh root@IP_DA_VPS hermes --profile SLUG gateway status
# ssh domhubs-vps journalctl --user -u hermes-gateway-SLUG -n 50 --no-pager
hermes --profile SLUG doctor
hermes gateway status hermes profile list
hermes gateway logs
hermes doctor
hermes config get model.default
hermes update
``` ```
**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 ## Error Handling Guidelines
@ -321,35 +344,34 @@ hermes update
## Reference Commands (quick lookup) ## Reference Commands (quick lookup)
```bash ```bash
# Enter VPS first # Enter shared VPS
ssh root@IP_DA_VPS ssh domhubs-vps
# DomHubs ops: ssh domhubs-vps
# DomHubs one-liner (on the VPS) # New / resume client instance (on VPS)
curl -fsSL https://setup.domhubs.com.br/hermes | bash 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 # Config inside profile (HERMES_HOME or --profile)
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --skip-browser 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) # Gateway for this client only
hermes config set DEEPSEEK_API_KEY "sk-..." hermes --profile SLUG gateway install
hermes config set model.provider deepseek hermes --profile SLUG gateway restart
hermes config set model.default deepseek-v4-flash hermes --profile SLUG gateway status
hermes config set model.base_url "https://api.deepseek.com/v1" journalctl --user -u hermes-gateway-SLUG -n 50 --no-pager
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 # Fleet
hermes gateway install hermes profile list
hermes gateway restart systemctl --user list-units 'hermes-gateway*' --all
hermes gateway status
# Linux: journalctl --user -u hermes-gateway -n 50
# macOS: tail -f ~/.hermes/logs/gateway.log
# Validation
hermes doctor
hermes --version
``` ```
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. 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.

View file

@ -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/<slug>/ → own .env, SOUL, state.db, gateway unit
# systemd: hermes-gateway-<slug>.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" <<EOF
{
"slug": "${SLUG}",
"profile_home": "${PROFILE_HOME}",
"gateway_unit": "hermes-gateway-${SLUG}.service",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"isolation": "hermes-profile"
}
EOF
chmod 644 "${PROFILE_HOME}/domhubs/instance.json"
cat <<EOF
✓ Instance ready: ${SLUG}
Home: ${PROFILE_HOME}
Unit: hermes-gateway-${SLUG}.service
Commands:
hermes --profile ${SLUG} doctor
hermes --profile ${SLUG} gateway status
hermes --profile ${SLUG} chat --cli -s hermes-client-onboarding
journalctl --user -u hermes-gateway-${SLUG} -n 50 --no-pager
Isolation rules:
• One Telegram bot token per profile (never reuse)
• TELEGRAM_ALLOWED_USERS only for that client
• Do not run gateway on default ~/.hermes for client bots
• Default host profile: setup/tooling only
Next: run onboarding against this profile (apply secrets, SOUL, start gateway).
EOF

View file

@ -9,20 +9,44 @@ set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}" export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}"
SKILL_NAME="${HERMES_ONBOARD_SKILL:-hermes-client-onboarding}" SKILL_NAME="${HERMES_ONBOARD_SKILL:-hermes-client-onboarding}"
# Shared VPS multi-tenant: isolate into Hermes profile
CLIENT_SLUG="${HERMES_CLIENT_SLUG:-${HERMES_PROFILE:-}}"
if [[ -n "${1:-}" && "$1" == "--client" && -n "${2:-}" ]]; then
CLIENT_SLUG="$2"
shift 2
fi
if [[ -n "${1:-}" && "$1" == "--profile" && -n "${2:-}" ]]; then
CLIENT_SLUG="$2"
shift 2
fi
# Short: long text trips Hermes paste-collapse # Short: long text trips Hermes paste-collapse
KICKOFF="${HERMES_ONBOARD_KICKOFF:-Inicie o onboarding agora. Skill hermes-client-onboarding. Pre-flight silencioso e Phase 1 (voce fala primeiro).}" if [[ -n "$CLIENT_SLUG" ]]; then
KICKOFF_DEFAULT="Inicie onboarding do cliente ${CLIENT_SLUG}. Skill hermes-client-onboarding. Multi-tenant profile ${CLIENT_SLUG} only. Pre-flight e Phase 1."
else
KICKOFF_DEFAULT="Inicie o onboarding agora. Skill hermes-client-onboarding. Pre-flight silencioso e Phase 1 (voce fala primeiro)."
fi
KICKOFF="${HERMES_ONBOARD_KICKOFF:-$KICKOFF_DEFAULT}"
KICKOFF="$(printf '%s' "$KICKOFF" | tr '\n' ' ' | sed 's/ */ /g')" KICKOFF="$(printf '%s' "$KICKOFF" | tr '\n' ' ' | sed 's/ */ /g')"
export HERMES_ONBOARD_SKILL="$SKILL_NAME" export HERMES_ONBOARD_SKILL="$SKILL_NAME"
export HERMES_ONBOARD_KICKOFF="$KICKOFF" export HERMES_ONBOARD_KICKOFF="$KICKOFF"
export HERMES_TUI_SKILLS="$SKILL_NAME" export HERMES_TUI_SKILLS="$SKILL_NAME"
export HERMES_TUI_QUERY="$KICKOFF" export HERMES_TUI_QUERY="$KICKOFF"
export HERMES_CLIENT_SLUG="${CLIENT_SLUG:-}"
if ! command -v hermes >/dev/null 2>&1; then if ! command -v hermes >/dev/null 2>&1; then
echo "error: hermes not on PATH" >&2 echo "error: hermes not on PATH" >&2
exit 1 exit 1
fi 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)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AUTO_PY="" AUTO_PY=""
for candidate in \ for candidate in \
@ -36,19 +60,20 @@ done
use_tui="${HERMES_ONBOARD_USE_TUI:-0}" use_tui="${HERMES_ONBOARD_USE_TUI:-0}"
if [[ "$use_tui" == "1" ]]; then if [[ "$use_tui" == "1" ]]; then
if [[ -r /dev/tty ]]; then if [[ -r /dev/tty ]]; then
exec hermes chat --tui -s "$SKILL_NAME" --query "$KICKOFF" </dev/tty exec "${HERMES_BIN[@]}" chat --tui -s "$SKILL_NAME" --query "$KICKOFF" </dev/tty
fi fi
exec hermes chat --tui -s "$SKILL_NAME" --query "$KICKOFF" exec "${HERMES_BIN[@]}" chat --tui -s "$SKILL_NAME" --query "$KICKOFF"
fi fi
# --- Preferred: tmux (no frozen PTY wrapper) --- # --- Preferred: tmux (no frozen PTY wrapper) ---
if command -v tmux >/dev/null 2>&1 && [[ -t 0 && -t 1 ]]; then if command -v tmux >/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 $$) # 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 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)" \ 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 # Wait until Hermes is up, then type kickoff + Enter
for i in $(seq 1 40); do for i in $(seq 1 40); do