Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 112 additions & 5 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,37 @@ PROJECT_DIR="/home/clawbox/clawbox"
CLAWBOX_USER="clawbox"
CLAWBOX_HOME="/home/clawbox"

# ── Provisioning-status signal (read by the flash host) ──────────────────────
# A full install keeps some steps NON-FATAL on purpose — a half-provisioned box
# should still finish and come up reachable rather than abort mid-run. But
# "non-fatal" must never become "invisible": a step that reported errors has to
# reach the operator's summary AND the caller's exit status, or a flash host
# prints "Setup: 1/1 succeeded" over an install that told itself it was broken.
# So every non-fatal failure is recorded here, the summary lists them, install.sh
# exits non-zero, and a machine-readable marker is left for the flash host.
PROVISION_FAILURES=()
PROVISION_STATUS_FILE="${CLAWBOX_PROVISION_STATUS_FILE:-/etc/clawbox/provision-status}"

record_provision_failure() {
PROVISION_FAILURES+=("$1")
}

# Persist the final provisioning verdict where the flash host (or an operator,
# or the next update) can read it without re-parsing install.sh's stdout.
write_provision_status() {
local status="$1"; shift
local dir
dir="$(dirname "$PROVISION_STATUS_FILE")"
mkdir -p "$dir" 2>/dev/null || true
{
echo "# Written by install.sh at the end of a full install. Machine-readable."
echo "STATUS=$status"
echo "FAILED_STEPS=$*"
echo "TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
} > "$PROVISION_STATUS_FILE" 2>/dev/null || true
chmod 644 "$PROVISION_STATUS_FILE" 2>/dev/null || true
}
Comment on lines +81 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

A failed marker write leaves a stale STATUS=ok that contradicts the exit code.

mkdir -p, the redirect, and chmod all end in || true. If the redirect fails, for example on a read-only /etc or without write permission, the previous file content survives unchanged. A device that installed successfully once and then fails keeps STATUS=ok on disk while exit 1 and the [provision-status] INCOMPLETE sentinel report the failure. The three signals must agree, as stated on Lines 3383-3385.

Report the write failure so the operator sees that the marker is unreliable.

🐛 Proposed fix
   {
     echo "# Written by install.sh at the end of a full install. Machine-readable."
     echo "STATUS=$status"
     echo "FAILED_STEPS=$*"
     echo "TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
-  } > "$PROVISION_STATUS_FILE" 2>/dev/null || true
+  } > "$PROVISION_STATUS_FILE" 2>/dev/null || {
+    echo "  Warning: could not write $PROVISION_STATUS_FILE — any existing marker there is STALE and does not describe this run (STATUS=$status)."
+    return 0
+  }
   chmod 644 "$PROVISION_STATUS_FILE" 2>/dev/null || true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
write_provision_status() {
local status="$1"; shift
local dir
dir="$(dirname "$PROVISION_STATUS_FILE")"
mkdir -p "$dir" 2>/dev/null || true
{
echo "# Written by install.sh at the end of a full install. Machine-readable."
echo "STATUS=$status"
echo "FAILED_STEPS=$*"
echo "TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
} > "$PROVISION_STATUS_FILE" 2>/dev/null || true
chmod 644 "$PROVISION_STATUS_FILE" 2>/dev/null || true
}
write_provision_status() {
local status="$1"; shift
local dir
dir="$(dirname "$PROVISION_STATUS_FILE")"
mkdir -p "$dir" 2>/dev/null || true
{
echo "# Written by install.sh at the end of a full install. Machine-readable."
echo "STATUS=$status"
echo "FAILED_STEPS=$*"
echo "TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
} > "$PROVISION_STATUS_FILE" 2>/dev/null || {
echo " Warning: could not write $PROVISION_STATUS_FILE — any existing marker there is STALE and does not describe this run (STATUS=$status)."
return 0
}
chmod 644 "$PROVISION_STATUS_FILE" 2>/dev/null || true
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 81 - 93, Update write_provision_status so failures
creating the directory, writing PROVISION_STATUS_FILE, or applying chmod are no
longer silently ignored; report the marker-write failure to the operator while
preserving the install failure status and ensuring a stale STATUS=ok is not
treated as authoritative.


# ── Edition (single-harness lock) ────────────────────────────────────────────
# openclaw | hermes | dual. "openclaw" is the native product (single, locked),
# "hermes" is its own SKU, "dual" is premium (both harnesses + the runtime
Expand Down Expand Up @@ -3038,18 +3069,52 @@ step_validate_services() {
fi
# Probe 4 (hermes only): the dashboard auth proxy actually answers. The
# unit being "active" only means node started; without this, a proxy that
# crashed on its first request (or a dashboard with no auth provider)
# counted as a healthy install. Unauthenticated, so 401/403/3xx are the
# expected healthy answers — anything is fine except "no answer".
# crashed on its first request counted as a healthy install. An
# unauthenticated request (no clawbox_session) is answered by a healthy
# proxy with a 302 to /login, or a 403 from its origin/rebind guard — so
# 3xx/403 are healthy and "no answer" is not. 401 is NOT whitelisted:
# a browserless request never earns one from a healthy proxy, and the one
# place the proxy DOES emit 401 is when the SSO login desynced (see
# hermes-dashboard-proxy.js) — the exact failure this SKU must not hide.
local proxy_code
proxy_code=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
http://127.0.0.1:"${HERMES_DASH_PROXY_PORT:-8090}"/ 2>/dev/null) || proxy_code="000"
case "$proxy_code" in
2*|3*|401|403) ;;
2*|3*|403) ;;
*) failed_probe+=("Hermes: dashboard proxy on :${HERMES_DASH_PROXY_PORT:-8090} returned HTTP $proxy_code") ;;
esac
fi

# Probe (hermes + dual): the dashboard auth PROVIDER is genuinely usable —
# the stored password actually verifies against the stored password_hash.
# This is the check that was missing when a Hermes provision printed
# "dashboard auth setup returned non-zero" and the validator, seconds later,
# reported every check healthy: the proxy liveness probe above returns 3xx
# whether or not the provider works, so it can see a dead node but never a
# desynced or absent auth provider. Runs the auth script's OWN classifier
# (`--check`), so there is a single source of truth for the invariant and no
# duplicated scrypt logic here. Runs as root, which can read the clawbox-owned
# 0600 config + password file. A self-healed box (the dashboard's
# ExecStartPre re-mints a coherent pair within this loop's retry window)
# passes honestly, because by then the invariant genuinely holds.
if has_hermes_harness; then
local auth_script="$PROJECT_DIR/scripts/setup-hermes-dashboard-auth.sh"
local auth_rc=0
if [ -f "$auth_script" ]; then
HERMES_CONFIG="$CLAWBOX_HOME/.hermes/config.yaml" CLAWBOX_ROOT="$PROJECT_DIR" \
bash "$auth_script" --check >/dev/null 2>&1 || auth_rc=$?
else
auth_rc=99
fi
case "$auth_rc" in
0) ;;
3) failed_probe+=("Hermes: dashboard auth is DESYNCED — the stored password does not verify against the stored password_hash (setup-hermes-dashboard-auth.sh --check == 3); the dashboard SSO will 401. Fix: sudo bash $PROJECT_DIR/install.sh --step hermes_edition") ;;
4) failed_probe+=("Hermes: no usable dashboard auth provider in $CLAWBOX_HOME/.hermes/config.yaml (--check == 4) — the dashboard refuses to start on its non-loopback bind without one. Fix: sudo bash $PROJECT_DIR/install.sh --step hermes_edition") ;;
5) failed_probe+=("Hermes: the dashboard password file is missing or empty (--check == 5). Fix: sudo bash $PROJECT_DIR/install.sh --step hermes_edition") ;;
*) failed_probe+=("Hermes: could not verify the dashboard auth provider (setup-hermes-dashboard-auth.sh --check == $auth_rc, environment error — not a confirmed-healthy state)") ;;
esac
fi

# Probe: no unit belonging to ANOTHER edition is running here.
#
# Every check above asks whether this edition's own units are UP, so a
Expand Down Expand Up @@ -3087,6 +3152,8 @@ step_validate_services() {
if is_test_mode; then probe_count=1; fi
# +3: gateway-inactive, gateway-port-silent, dashboard-proxy-answers.
if is_hermes_edition; then probe_count=$(( probe_count + 3 )); fi
# +1 (hermes AND dual): the dashboard auth provider actually verifies.
if has_hermes_harness; then probe_count=$(( probe_count + 1 )); fi
# One per foreign unit. Counted even when the unit is absent: "the other
# harness is not here" is a check that ran and passed, and folding it into the
# total is what stops the healthy line from being printable on a box that is
Expand Down Expand Up @@ -3276,11 +3343,18 @@ if has_hermes_harness; then
echo " # WARNING: Hermes provisioning FAILED."
echo " # Re-run: sudo bash $PROJECT_DIR/install.sh --step hermes_edition"
echo " ############################################################"
# Record it so the final summary + exit status + status marker report the
# failure even though the run continues. Without this the box could still
# print "Setup Complete", exit 0, and be shipped as healthy.
record_provision_failure hermes_edition
}
fi

log "Validating services..."
step_validate_services
# Capture rather than let set -e abort here: we still want to print the summary
# AND fold this into the single honest exit status at the very end.
VALIDATE_RC=0
step_validate_services || VALIDATE_RC=$?

# ── Done ─────────────────────────────────────────────────────────────────────

Expand All @@ -3301,3 +3375,36 @@ echo " systemctl status clawbox-ap"
echo " systemctl status clawbox-setup"
echo " systemctl status clawbox-gateway"
echo ""

# ── Honest final status ──────────────────────────────────────────────────────
# One exit code that reflects the WHOLE run: a non-fatal provisioning step that
# reported errors, or a failed service validation, must not be reportable as
# success by the caller (the flash host's "Setup: N/N succeeded"). The marker
# file carries the same verdict for a caller that reads a file instead of the
# exit code, and the sentinel line ([provision-status] ...) for one that greps
# stdout. Keep all three in agreement.
FINAL_RC=0
if [ "${#PROVISION_FAILURES[@]}" -gt 0 ] || [ "${VALIDATE_RC:-0}" -ne 0 ]; then
FINAL_RC=1
fi

if [ "$FINAL_RC" -ne 0 ]; then
echo " ############################################################"
echo " # PROVISIONING INCOMPLETE — the box came up but the install"
echo " # reported errors. Do NOT ship this box as healthy."
if [ "${#PROVISION_FAILURES[@]}" -gt 0 ]; then
echo " # Steps that failed: ${PROVISION_FAILURES[*]}"
echo " # Re-run: sudo bash $PROJECT_DIR/install.sh --step ${PROVISION_FAILURES[0]}"
fi
if [ "${VALIDATE_RC:-0}" -ne 0 ]; then
echo " # Service validation FAILED (see the checks listed above)."
fi
echo " ############################################################"
write_provision_status incomplete "${PROVISION_FAILURES[*]:-}"
echo "[provision-status] INCOMPLETE${PROVISION_FAILURES[*]:+ (${PROVISION_FAILURES[*]})}"
else
write_provision_status ok ""
echo "[provision-status] OK"
fi

exit "$FINAL_RC"
36 changes: 36 additions & 0 deletions scripts/register-mcp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,40 @@ MCP_ENTRY="$PROJECT_DIR/mcp/clawbox-mcp.ts"
MCP_TOKEN_FILE="$PROJECT_DIR/data/.mcp-token"
EDITION_FILE="${CLAWBOX_EDITION_FILE:-/etc/clawbox/edition.env}"
API_BASE="${CLAWBOX_API_BASE:-http://127.0.0.1:80}"
# Shared with setup-hermes-dashboard-auth.sh: BOTH scripts read-modify-write
# ~/.hermes/config.yaml, and at install time they run seconds apart
# (production-server.js fire-and-forgets this script on the clawbox-setup
# restart in step_start_services; setup-hermes-edition.sh runs the auth script
# right after). Without a shared lock, whichever one snapshotted the file first
# and wrote last silently erased the other's block — the auth script's dashboard
# block vanished and its verify failed, blaming credentials that were correct.
# Same path derivation on both sides (HERMES_CONFIG + ".lock") so they collide.
CONFIG_LOCK="${HERMES_CONFIG}.lock"
Comment on lines +47 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare HERMES_CONFIG resolution across the two cooperating writers and the unit that spawns register-mcp.sh.
set -euo pipefail

rg -n -C 3 'HERMES_CONFIG' --glob 'scripts/*.sh' --glob 'install.sh' || true

# How is register-mcp.sh spawned, and with which user/HOME?
rg -n -C 6 'register-mcp' --glob '!**/node_modules/**' || true

# Service user and environment of the units involved.
fd -e service . --exec rg -n -e '^User=' -e '^Environment=' -e '^WorkingDirectory=' {} +

Repository: ID-Robots/clawbox

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant files ---'
git ls-files | rg '(^|/)(register-mcp\.sh|setup-hermes-dashboard-auth\.sh|production-server\.js|install\.sh|.*\.service)$' || true

printf '%s\n' '--- HERMES_CONFIG references ---'
rg -n -C 8 'HERMES_CONFIG|register-mcp|setup-hermes-dashboard-auth' --glob 'scripts/*.sh' --glob 'install.sh' --glob '*.js' --glob '*.service' . || true

printf '%s\n' '--- service identity and environment ---'
fd -e service . --exec sh -c 'printf "\n--- %s ---\n" "$1"; rg -n -e "^(User|Group|Environment|WorkingDirectory|ExecStart)=" "$1" || true' sh {} +

Repository: ID-Robots/clawbox

Length of output: 49287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- install-time home and auth invocation ---'
sed -n '1,90p' install.sh
sed -n '3070,3120p' install.sh

printf '%s\n' '--- Hermes edition variables and cooperating invocations ---'
sed -n '1,75p' scripts/setup-hermes-edition.sh
sed -n '155,195p' scripts/setup-hermes-edition.sh

printf '%s\n' '--- setup service environment and install flow references ---'
sed -n '1,45p' config/clawbox-setup.service
rg -n -C 5 'step_start_services|setup-hermes-edition|CLAWBOX_HOME|HOME=' install.sh scripts production-server.js config

printf '%s\n' '--- shell expansion probe for the exact assignments ---'
python3 - <<'PY'
from pathlib import Path
import re

register = Path("scripts/register-mcp.sh").read_text()
auth = Path("scripts/setup-hermes-dashboard-auth.sh").read_text()
service = Path("config/clawbox-setup.service").read_text()

for name, text, patterns in [
    ("register-mcp.sh", register, [
        r'^HOME_DIR=.*$',
        r'^HERMES_CONFIG=.*$',
        r'^CONFIG_LOCK=.*$',
    ]),
    ("setup-hermes-dashboard-auth.sh", auth, [
        r'^HERMES_CONFIG=.*$',
        r'^CONFIG_LOCK=.*$',
    ]),
    ("clawbox-setup.service", service, [
        r'^User=.*$',
        r'^Environment=HOME=.*$',
        r'^ExecStart=.*$',
    ]),
]:
    print(f"--- {name} ---")
    for pattern in patterns:
        for line in text.splitlines():
            if re.match(pattern, line):
                print(line)
PY

Repository: ID-Robots/clawbox

Length of output: 50373


🌐 Web query:

systemd.exec HOME environment variable User= system services documentation

💡 Result:

In systemd services, the HOME environment variable is controlled by the SetLoginEnvironment= directive within the unit file's [Service] section [1][2]. Key behaviors regarding the HOME environment variable: 1. Automatic Setting: By default, systemd automatically sets the $HOME, $LOGNAME, and $SHELL environment variables when User=, DynamicUser=, or PAMName= are configured for a system service [1][3][2]. 2. Configuration via SetLoginEnvironment=: You can explicitly control this behavior using the SetLoginEnvironment= boolean option [1][2]: - If set to true, $HOME, $LOGNAME, and $SHELL are set regardless of whether User=, DynamicUser=, or PAMName= are used (this includes the root user) [1][3]. - If set to false, these variables are not set by the service manager, even if User=, DynamicUser=, or PAMName= are specified [1][2]. 3. Per-User Service Manager: The SetLoginEnvironment= option generally has no effect on services run by the per-user service manager (systemd --user), as these variables are typically inherited from the user manager's own environment [1][2]. 4. User Home Directory Path: While systemd can set the $HOME environment variable to the user's home directory (as defined in the system's user database), it does not provide a direct specifier (like %h) that resolves to the home directory of the user specified by User= for use in other configuration lines (such as Environment=) [4]. The %h specifier in system unit files resolves to the home directory of the user running the service manager (typically root), not the user specified in the User= directive [4]. For services needing to store data in a user's home directory, it is often recommended to use systemd's built-in directory management directives—such as StateDirectory=, CacheDirectory=, or ConfigurationDirectory=—which are specifically designed to provide predictable paths for service data [3][5][6]. These directories are automatically created and associated with environment variables (e.g., $STATE_DIRECTORY) that point to the correct locations [3][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- environment-file loading and HERMES_CONFIG overrides ---'
rg -n -C 4 'source .*\.env|EnvironmentFile|dotenv|HERMES_CONFIG|HOME=' install.sh production-server.js config/clawbox-setup.service scripts/setup-hermes-edition.sh scripts/setup-hermes-dashboard-auth.sh scripts/register-mcp.sh

printf '%s\n' '--- tracked environment templates ---'
git ls-files | rg '(^|/)(\.env|.*\.env.*|environment.*)$' || true

printf '%s\n' '--- concise source facts ---'
python3 - <<'PY'
from pathlib import Path
for file, ranges in {
    "config/clawbox-setup.service": [(1, 45)],
    "scripts/register-mcp.sh": [(32, 58)],
    "scripts/setup-hermes-edition.sh": [(10, 22), (160, 188)],
    "scripts/setup-hermes-dashboard-auth.sh": [(48, 61)],
    "install.sh": [(280, 305), (1318, 1331)],
}.items():
    print(f"--- {file} ---")
    lines = Path(file).read_text().splitlines()
    for start, end in ranges:
        for i in range(start, min(end, len(lines)) + 1):
            print(f"{i}:{lines[i-1]}")
PY

printf '%s\n' '--- deterministic environment model ---'
python3 - <<'PY'
# Model the relevant precedence without executing repository code.
def register_path(env):
    home_dir = env.get("HOME", "/home/clawbox")
    return env.get("HERMES_CONFIG", f"{home_dir}/.hermes/config.yaml")

def auth_path(env):
    return env.get("HERMES_CONFIG", f"{env.get('HOME', '')}/.hermes/config.yaml")

cases = [
    ("systemd default HOME", {"HOME": "/home/clawbox"}),
    ("custom HOME from .env", {"HOME": "/tmp/other"}),
    ("custom HERMES_CONFIG from .env", {
        "HOME": "/home/clawbox",
        "HERMES_CONFIG": "/tmp/other/config.yaml",
    }),
]
for label, env in cases:
    print(label)
    print("  register:", register_path(env))
    print("  auth:", auth_path(env))
    print("  same:", register_path(env) == auth_path(env))
PY

Repository: ID-Robots/clawbox

Length of output: 36254


Set HERMES_CONFIG explicitly after the EnvironmentFile= entries. clawbox-setup.service imports the writable .env, and register-mcp.sh honors HOME and HERMES_CONFIG from that environment. setup-hermes-edition.sh explicitly uses /home/clawbox/.hermes/config.yaml, so an override can make the boot-time writer use a different config and lock path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/register-mcp.sh` around lines 47 - 55, Set HERMES_CONFIG explicitly
to /home/clawbox/.hermes/config.yaml after the EnvironmentFile entries in
clawbox-setup.service, ensuring register-mcp.sh uses the same configuration and
lock path as setup-hermes-edition.sh regardless of imported environment
overrides.


log() { echo "[register-mcp] $*"; }

# Take the exclusive config lock for the rest of the run (fd 9, released on
# exit). Covers BOTH the PyYAML reconcile below AND the `hermes tools disable`
# call — the Hermes CLI does its own wide load→save_config on the same file, so
# it has to be inside the same critical section. Best-effort: proceed without
# the lock rather than skip registering the device's tools if flock is missing.
acquire_config_lock() {
command -v flock >/dev/null 2>&1 || {
log "flock unavailable — proceeding without the config lock"
return 0
}
mkdir -p "$(dirname "$CONFIG_LOCK")" 2>/dev/null || true
# Probe writability in a scoped subshell before opening fd 9; keep the `exec`
# redirect CLEAN (a `2>/dev/null` on it would silence the whole script,
# because redirections on exec are permanent).
if ! ( : > "$CONFIG_LOCK" ) 2>/dev/null; then
log "could not create $CONFIG_LOCK — proceeding without the config lock"
return 0
fi
exec 9>"$CONFIG_LOCK"
flock -w 120 9 || log "could not acquire $CONFIG_LOCK within 120s — proceeding without it"
}

# ── 1. Which edition is this? ───────────────────────────────────────────────
# Root-owned lock first, environment second, "openclaw" last — the same order
# and the same reasons as src/lib/edition-source.ts. Reading the file rather
Expand Down Expand Up @@ -112,6 +143,11 @@ chmod 600 "$MCP_TOKEN_FILE" 2>/dev/null || true
# NOT via `hermes mcp add`: that command performs live tool discovery and
# rewrites the whole config through Hermes' own save_config(), which is a much
# wider blast radius for a boot-time provisioning step, and it is slow.
#
# Everything from here to the end of the script touches config.yaml, so take the
# shared lock now and hold it until exit.
acquire_config_lock

export CLAWBOX_MCP_HERMES_CONFIG="$HERMES_CONFIG"
export CLAWBOX_MCP_BUN_BIN="$BUN_BIN"
export CLAWBOX_MCP_ENTRY="$MCP_ENTRY"
Expand Down
Loading
Loading