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
4 changes: 2 additions & 2 deletions cmd/obol/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func agentWalletCommand(cfg *config.Config) *cli.Command {
},
{
Name: "backup",
Usage: "Back up wallet keys for an OpenClaw agent instance",
Usage: "Back up wallet keys for an agent instance",
ArgsUsage: "[instance-name]",
Flags: []cli.Flag{
agentRuntimeFlag(""),
Expand Down Expand Up @@ -247,7 +247,7 @@ func agentWalletCommand(cfg *config.Config) *cli.Command {
},
{
Name: "restore",
Usage: "Restore wallet keys for an OpenClaw agent instance",
Usage: "Restore wallet keys for an agent instance",
ArgsUsage: "[instance-name]",
Flags: []cli.Flag{
agentRuntimeFlag(""),
Expand Down
17 changes: 17 additions & 0 deletions cmd/obol/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ func TestAgentWalletCommand_Structure(t *testing.T) {
}
}

func TestAgentWalletCommand_UsageIsRuntimeNeutral(t *testing.T) {
cfg := newTestConfig(t)
wallet := findSubcommand(t, agentCommand(cfg), "wallet")

for _, name := range []string{"backup", "restore"} {
t.Run(name, func(t *testing.T) {
sub := findSubcommand(t, wallet, name)
if strings.Contains(sub.Usage, "OpenClaw") {
t.Fatalf("%s usage still says OpenClaw-only: %q", name, sub.Usage)
}
if !strings.Contains(sub.Usage, "agent instance") {
t.Fatalf("%s usage = %q, want generic agent instance wording", name, sub.Usage)
}
})
}
}

func TestResolveAgentTarget(t *testing.T) {
tests := []struct {
name string
Expand Down
69 changes: 12 additions & 57 deletions internal/hermes/hermes.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ const (
// renovate: datasource=helm depName=raw registryUrl=https://bedag.github.io/helm-charts/
rawChartVersion = "2.0.2"

defaultImage = "nousresearch/hermes-agent:v2026.4.23"
hermesInstallDir = "/data/.hermes/hermes-agent"
hermesRepoURL = "https://github.com/NousResearch/hermes-agent.git"
hermesBinary = hermesInstallDir + "/venv/bin/hermes"
defaultImage = "nousresearch/hermes-agent:v2026.4.30"
// Use the upstream image venv instead of cloning Hermes into the PVC on
// every cold start. The init container below validates the required extras
// are present so image regressions fail before the gateway starts.
hermesBinary = "/opt/hermes/.venv/bin/hermes"

containerUID = 10000
containerGID = 10000
Expand Down Expand Up @@ -799,67 +800,23 @@ func generateValues(namespace, hostname, dashboardHostname, agentBaseURL, token,
fsGroup: %d
initContainers:
- name: init-hermes-data
image: busybox:1.36
command:
- sh
- -c
- mkdir -p /data/.hermes && chown -R %d:%d /data/.hermes
securityContext:
runAsUser: 0
volumeMounts:
- name: data
mountPath: /data
- name: bootstrap-hermes-install
image: %s
imagePullPolicy: IfNotPresent
command:
- sh
- -ec
- |
install_dir=%s
repo_url=%s
mkdir -p /data/.hermes/home /data/.hermes/workspace
lock_dir="${install_dir}.lock"
got_lock=0
for _ in $(seq 1 120); do
if mkdir "$lock_dir" 2>/dev/null; then
got_lock=1
break
fi
sleep 1
done
if [ "$got_lock" != 1 ]; then
echo "Timed out waiting for Hermes install lock: $lock_dir" >&2
if [ ! -x /opt/hermes/.venv/bin/hermes ]; then
echo "Hermes binary missing from image: /opt/hermes/.venv/bin/hermes" >&2
exit 1
fi
cleanup_lock() {
rmdir "$lock_dir" 2>/dev/null || true
}
trap cleanup_lock EXIT

if [ ! -d "$install_dir/.git" ] || { [ ! -f "$install_dir/pyproject.toml" ] && [ ! -f "$install_dir/setup.py" ]; }; then
rm -rf "${install_dir}.tmp"
if [ -e "$install_dir" ]; then
mv "$install_dir" "${install_dir}.backup.$(date +%%s)"
fi
git clone --depth 1 "$repo_url" "${install_dir}.tmp"
mv "${install_dir}.tmp" "$install_dir"
fi
cd "$install_dir"
# Reinstall when the venv is missing the hermes binary OR
# any selected extra is absent. The upstream image installs
# ".[all]"; we re-create the venv from a fresh clone, so the
# extras must be re-requested explicitly. The import check
# picks one module per extra so existing PVCs trigger a
# rebuild when we add a new extra to the install line.
if [ ! -x "$install_dir/venv/bin/hermes" ] || \
! "$install_dir/venv/bin/python3" -c "import fastapi, uvicorn, telegram, mcp, ptyprocess, simple_term_menu, googleapiclient" >/dev/null 2>&1; then
rm -rf "$install_dir/venv"
uv venv --python python3 --system-site-packages venv
VIRTUAL_ENV="$install_dir/venv" uv pip install -e ".[web,messaging,mcp,pty,cli,acp,google]"
if ! /opt/hermes/.venv/bin/python3 -c "import fastapi, uvicorn, telegram, mcp, ptyprocess, simple_term_menu, googleapiclient" >/dev/null 2>&1; then
echo "Hermes image is missing required extras: web,messaging,mcp,pty,cli,acp,google" >&2
exit 1
fi
if [ -f /data/.hermes/state.db ]; then
if ! python3 - <<'PY'
if ! /opt/hermes/.venv/bin/python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('/data/.hermes/state.db')
row = conn.execute('PRAGMA quick_check').fetchone()
Expand All @@ -875,8 +832,6 @@ func generateValues(namespace, hostname, dashboardHostname, agentBaseURL, token,
echo "Backed up malformed Hermes state DB to $backup_dir"
fi
fi
cleanup_lock
trap - EXIT
volumeMounts:
- name: data
mountPath: /data
Expand Down Expand Up @@ -917,7 +872,7 @@ func generateValues(namespace, hostname, dashboardHostname, agentBaseURL, token,
value: %s
- name: OBOL_SKILLS_DIR
value: /data/.hermes/%s
`, desc.DataPVCName, namespace, desc.ServiceName, desc.ServiceName, namespace, desc.ServiceName, desc.ServiceName, desc.ServiceName, desc.ServiceName, containerUID, containerGID, containerGID, containerUID, containerGID, quoteYAML(image()), quoteYAML(hermesInstallDir), quoteYAML(hermesRepoURL), desc.ServiceName, quoteYAML(image()), quoteYAML(hermesBinary), desc.DefaultPort, desc.DefaultPort, quoteYAML(primary), quoteYAML(namespace), obolSkillsDirName)
`, desc.DataPVCName, namespace, desc.ServiceName, desc.ServiceName, namespace, desc.ServiceName, desc.ServiceName, desc.ServiceName, desc.ServiceName, containerUID, containerGID, containerGID, quoteYAML(image()), desc.ServiceName, quoteYAML(image()), quoteYAML(hermesBinary), desc.DefaultPort, desc.DefaultPort, quoteYAML(primary), quoteYAML(namespace), obolSkillsDirName)

if agentBaseURL != "" {
fmt.Fprintf(&b, " - name: AGENT_BASE_URL\n value: %s\n", quoteYAML(agentBaseURL))
Expand Down
25 changes: 15 additions & 10 deletions internal/hermes/hermes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,12 @@ func TestGenerateValues_UsesHermesNativeNames(t *testing.T) {
"containerPort: 8642",
"containerPort: 9119",
"init-hermes-data",
"bootstrap-hermes-install",
`install_dir="/data/.hermes/hermes-agent"`,
`repo_url="https://github.com/NousResearch/hermes-agent.git"`,
`lock_dir="${install_dir}.lock"`,
`Timed out waiting for Hermes install lock`,
`git clone --depth 1 "$repo_url" "${install_dir}.tmp"`,
"uv venv --python python3 --system-site-packages venv",
`uv pip install -e ".[web,messaging,mcp,pty,cli,acp,google]"`,
`Hermes binary missing from image: /opt/hermes/.venv/bin/hermes`,
`Hermes image is missing required extras: web,messaging,mcp,pty,cli,acp,google`,
`import fastapi, uvicorn, telegram, mcp, ptyprocess, simple_term_menu, googleapiclient`,
`PRAGMA quick_check`,
`state-db-corrupt-$ts`,
`- "/data/.hermes/hermes-agent/venv/bin/hermes"`,
`- "/opt/hermes/.venv/bin/hermes"`,
`- "hermes-obol-agent.obol.stack"`,
`- "obol-agent.obol.stack"`,
"name: hermes-dashboard",
Expand All @@ -165,6 +159,17 @@ func TestGenerateValues_UsesHermesNativeNames(t *testing.T) {
}
}

for _, banned := range []string{
"bootstrap-hermes-install",
"git clone",
"uv pip install",
"/data/.hermes/hermes-agent",
} {
if strings.Contains(values, banned) {
t.Fatalf("generateValues() should not rebuild Hermes inside the PVC, found %q:\n%s", banned, values)
}
}

var parsed any
if err := yaml.Unmarshal([]byte(values), &parsed); err != nil {
t.Fatalf("generateValues() produced invalid YAML: %v\n%s", err, values)
Expand Down Expand Up @@ -203,7 +208,7 @@ func TestHermesExecArgs_UsesNativeHermesBinary(t *testing.T) {
"-n", "hermes-obol-agent",
"deploy/hermes",
"--",
"/data/.hermes/hermes-agent/venv/bin/hermes",
"/opt/hermes/.venv/bin/hermes",
"skills",
"audit",
}
Expand Down
1 change: 1 addition & 0 deletions internal/hermes/wallet_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ func RestoreWalletCmd(cfg *config.Config, id string, opts RestoreWalletOptions,
}

if opts.ApplyCluster {
applyWalletMetadataConfigMapFn(cfg, id, deployDir)
applyHermesKeystorePasswordSecret(cfg, id, w.KeystorePassword, u)
restartHermesRemoteSignerFn(cfg, id, u)
}
Expand Down
Loading
Loading