Skip to content

Commit 537875d

Browse files
committed
refactor(hermes): defer port recovery changes
Signed-off-by: Shannon Sands <shannon.sands.1979@gmail.com>
1 parent a11efe4 commit 537875d

9 files changed

Lines changed: 29 additions & 164 deletions

File tree

agents/hermes/plugin/__init__.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import os
1919
import subprocess
2020
import yaml
21-
from urllib.parse import urlparse
2221

2322

2423
def _load_nemoclaw_config():
@@ -48,22 +47,6 @@ def _load_hermes_config():
4847
return None
4948

5049

51-
def _get_gateway_port():
52-
"""Return the sandbox-exposed Hermes API port."""
53-
raw = os.environ.get("NEMOCLAW_DASHBOARD_PORT")
54-
if raw and raw.isdigit():
55-
return int(raw)
56-
chat_url = os.environ.get("CHAT_UI_URL", "")
57-
if chat_url:
58-
try:
59-
parsed = urlparse(chat_url)
60-
if parsed.port:
61-
return int(parsed.port)
62-
except Exception:
63-
pass
64-
return 8642
65-
66-
6750
def _get_sandbox_info():
6851
"""Gather sandbox status information."""
6952
hermes_cfg = _load_hermes_config()
@@ -84,11 +67,10 @@ def _get_sandbox_info():
8467
provider = nemoclaw_cfg.get("provider", provider)
8568

8669
# Check gateway health
87-
gateway_port = _get_gateway_port()
8870
gateway_ok = False
8971
try:
9072
result = subprocess.run(
91-
["curl", "-sf", f"http://localhost:{gateway_port}/health"],
73+
["curl", "-sf", "http://localhost:8642/health"],
9274
capture_output=True,
9375
text=True,
9476
timeout=5,
@@ -104,7 +86,7 @@ def _get_sandbox_info():
10486
"provider": provider,
10587
"base_url": base_url,
10688
"gateway": "running" if gateway_ok else "stopped",
107-
"port": gateway_port,
89+
"port": 8642,
10890
}
10991

11092

agents/hermes/start.sh

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -109,23 +109,7 @@ case "${1:-}" in
109109
esac
110110
NEMOCLAW_CMD=("$@")
111111
CHAT_UI_URL="${CHAT_UI_URL:-http://127.0.0.1:8642}"
112-
PUBLIC_PORT="${NEMOCLAW_DASHBOARD_PORT:-}"
113-
if [ -z "$PUBLIC_PORT" ]; then
114-
_chat_port="${CHAT_UI_URL##*:}"
115-
_chat_port="${_chat_port%%/*}"
116-
_chat_port="${_chat_port%%\?*}"
117-
_chat_port="${_chat_port%%#*}"
118-
case "$_chat_port" in
119-
'' | *[!0-9]*) PUBLIC_PORT=8642 ;;
120-
*) PUBLIC_PORT="$_chat_port" ;;
121-
esac
122-
fi
123-
case "$PUBLIC_PORT" in
124-
'' | *[!0-9]*) PUBLIC_PORT=8642 ;;
125-
esac
126-
export NEMOCLAW_DASHBOARD_PORT="$PUBLIC_PORT"
127-
CHAT_UI_URL="http://127.0.0.1:${PUBLIC_PORT}"
128-
export CHAT_UI_URL
112+
PUBLIC_PORT=8642
129113
# Hermes binds to 127.0.0.1 regardless of config (upstream bug).
130114
# Run it on an internal port and use socat to expose on PUBLIC_PORT.
131115
INTERNAL_PORT=18642

src/lib/actions/sandbox/process-recovery.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,20 @@ export function resolveSandboxDashboardPort(
5858
sandboxName: string,
5959
deps: SandboxPortDeps = {},
6060
): number {
61-
const getSandbox = deps.getSandbox ?? registry.getSandbox;
62-
const sandbox = getSandbox(sandboxName);
63-
if (isValidPort(sandbox?.dashboardPort)) {
64-
return sandbox.dashboardPort;
65-
}
66-
6761
const getSessionAgent = deps.getSessionAgent ?? agentRuntime.getSessionAgent;
6862
const agent = getSessionAgent(sandboxName);
69-
return agent && isValidPort(agent.forwardPort) ? agent.forwardPort : DASHBOARD_PORT;
63+
if (agent && isValidPort(agent.forwardPort)) {
64+
return agent.forwardPort;
65+
}
66+
67+
const getSandbox = deps.getSandbox ?? registry.getSandbox;
68+
const sandbox = getSandbox(sandboxName);
69+
return isValidPort(sandbox?.dashboardPort) ? sandbox.dashboardPort : DASHBOARD_PORT;
7070
}
7171

7272
function getSandboxHealthProbeUrl(sandboxName: string): string {
7373
const agent = agentRuntime.getSessionAgent(sandboxName);
74-
const port = resolveSandboxDashboardPort(sandboxName);
75-
if (agent) return agentRuntime.getHealthProbeUrlForPort(agent, port);
74+
if (agent) return agentRuntime.getHealthProbeUrl(agent);
7675
return `http://127.0.0.1:${resolveSandboxDashboardPort(sandboxName)}/health`;
7776
}
7877

@@ -302,10 +301,6 @@ function ensureSandboxPortForward(sandboxName: string): boolean {
302301
return isSandboxForwardHealthy(sandboxName) === true;
303302
}
304303

305-
function getPortForwardLabel(agent: unknown): string {
306-
return agent ? `${agentRuntime.getAgentDisplayName(agent)} port forward` : "Dashboard port forward";
307-
}
308-
309304
/**
310305
* Probe `openshell forward list` for the sandbox's dashboard forward.
311306
* Returns true when an entry exists for the expected sandbox+port pair
@@ -336,7 +331,7 @@ export function classifySandboxForwardHealth(
336331
const match = entries.find((entry) => entry.port === port);
337332
if (!match) return false;
338333
if (match.sandboxName !== sandboxName) return "occupied";
339-
return match.status.includes("running") || match.status.includes("active");
334+
return match.status === "running";
340335
}
341336

342337
/**
@@ -356,7 +351,6 @@ export function checkAndRecoverSandboxProcesses(
356351
}
357352
const recoveryAgent = agentRuntime.getSessionAgent(sandboxName);
358353
const recoveryPort = resolveSandboxDashboardPort(sandboxName);
359-
const forwardLabel = getPortForwardLabel(recoveryAgent);
360354
if (running) {
361355
// Gateway is alive but the host-side forward can still be dead or
362356
// owned by another sandbox. Probe and re-establish only when
@@ -365,15 +359,15 @@ export function checkAndRecoverSandboxProcesses(
365359
if (forwardHealthy === false) {
366360
if (!quiet) {
367361
console.log("");
368-
console.log(` ${forwardLabel} to '${sandboxName}' is missing or dead.`);
362+
console.log(` Dashboard port forward to '${sandboxName}' is missing or dead.`);
369363
console.log(" Re-establishing...");
370364
}
371365
const forwardRecovered = ensureSandboxPortForward(sandboxName);
372366
if (!quiet) {
373367
if (forwardRecovered) {
374-
console.log(` ${G}${R} ${forwardLabel} re-established.`);
368+
console.log(` ${G}${R} Dashboard port forward re-established.`);
375369
} else {
376-
console.error(` Failed to re-establish the ${forwardLabel.toLowerCase()}.`);
370+
console.error(" Failed to re-establish the dashboard port forward.");
377371
console.error(
378372
` Run \`openshell forward start --background <port> ${sandboxName}\` manually.`,
379373
);
@@ -422,9 +416,9 @@ export function checkAndRecoverSandboxProcesses(
422416
` ${G}${R} ${agentRuntime.getAgentDisplayName(recoveryAgent)} gateway restarted inside sandbox.`,
423417
);
424418
if (forwardRecovered) {
425-
console.log(` ${G}${R} ${forwardLabel} re-established.`);
419+
console.log(` ${G}${R} Dashboard port forward re-established.`);
426420
} else {
427-
console.error(` Failed to re-establish the ${forwardLabel.toLowerCase()}.`);
421+
console.error(" Failed to re-establish the dashboard port forward.");
428422
console.error(
429423
` Run \`openshell forward start --background <port> ${sandboxName}\` manually.`,
430424
);

src/lib/agent/runtime.test.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
buildManualRecoveryCommand,
88
buildOpenClawRecoveryScript,
99
buildRecoveryScript,
10-
getHealthProbeUrlForPort,
1110
} from "../../../dist/lib/agent/runtime";
1211
import type { AgentDefinition } from "./defs";
1312

@@ -94,21 +93,14 @@ describe("buildRecoveryScript", () => {
9493
});
9594

9695
it("omits --port for Hermes so config.yaml controls the internal listen port (#2426)", () => {
97-
const script = buildRecoveryScript(hermesAgent, 18790);
96+
const script = buildRecoveryScript(hermesAgent, 8642);
9897
expect(script).toContain("export HERMES_HOME=/sandbox/.hermes");
9998
expect(script).toContain("HERMES_HOME=/sandbox/.hermes");
100-
expect(script).toContain("NEMOCLAW_DASHBOARD_PORT=18790");
101-
expect(script).toContain("CHAT_UI_URL=http://127.0.0.1:18790");
10299
expect(script).toContain("HTTPS_PROXY=http://127.0.0.1:3129");
103-
expect(script).toContain("http://localhost:18790/health");
104100
expect(script).toContain("nemoclaw-decode-proxy");
105101
expect(script).toContain('"$AGENT_BIN" gateway run');
106-
expect(script).not.toContain('"$AGENT_BIN" gateway run --port 18790');
107-
expect(script).not.toContain("hermes gateway run --port 18790");
108-
});
109-
110-
it("rewrites health probe URLs to the active forwarded port", () => {
111-
expect(getHealthProbeUrlForPort(hermesAgent, 18790)).toBe("http://localhost:18790/health");
102+
expect(script).not.toContain('"$AGENT_BIN" gateway run --port 8642');
103+
expect(script).not.toContain("hermes gateway run --port 8642");
112104
});
113105

114106
it("falls back to openclaw gateway run when gateway_command is absent", () => {
@@ -322,14 +314,12 @@ describe("buildManualRecoveryCommand (#2426)", () => {
322314
});
323315

324316
it("omits --port for Hermes and uses the current Hermes home", () => {
325-
const cmd = buildManualRecoveryCommand(hermesAgent, 18790);
317+
const cmd = buildManualRecoveryCommand(hermesAgent, 8642);
326318
expect(cmd).toContain("HERMES_HOME=/sandbox/.hermes");
327-
expect(cmd).toContain("NEMOCLAW_DASHBOARD_PORT=18790");
328-
expect(cmd).toContain("CHAT_UI_URL=http://127.0.0.1:18790");
329319
expect(cmd).toContain("HTTPS_PROXY=http://127.0.0.1:3129");
330320
expect(cmd).toContain("nemoclaw-decode-proxy");
331321
expect(cmd).toContain("nohup hermes gateway run");
332-
expect(cmd).not.toContain("--port 18790");
322+
expect(cmd).not.toContain("--port 8642");
333323
expect(cmd).not.toContain("/sandbox/.hermes-data");
334324
});
335325

src/lib/agent/runtime.ts

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,6 @@ export function getHealthProbeUrl(agent: AgentDefinition | null): string {
5252
return agent.healthProbe?.url || `http://127.0.0.1:${DASHBOARD_PORT}/health`;
5353
}
5454

55-
export function getHealthProbeUrlForPort(agent: AgentDefinition | null, port: number): string {
56-
const probeUrl = getHealthProbeUrl(agent);
57-
if (!Number.isInteger(port) || port < 1 || port > 65535) return probeUrl;
58-
try {
59-
const parsed = new URL(probeUrl);
60-
parsed.port = String(port);
61-
return parsed.toString();
62-
} catch {
63-
return probeUrl.replace(/:(\d+)(?=\/|$)/, `:${String(port)}`);
64-
}
65-
}
66-
6755
function escapeEre(value: string): string {
6856
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6957
}
@@ -154,20 +142,15 @@ function gatewayLaunchCommand(command: string, runAsUser?: string): string {
154142
return `${logSelection} if [ "$(id -u)" = "0" ] && command -v gosu >/dev/null 2>&1 && id ${shellQuote(runAsUser)} >/dev/null 2>&1; then nohup gosu ${shellQuote(runAsUser)} ${command} >> "$_GATEWAY_LOG" 2>&1 & else ${userLaunch} fi;`;
155143
}
156144

157-
function hermesGatewayEnvPrefix(port?: number): string {
145+
function hermesGatewayEnvPrefix(): string {
158146
const decodeProxy = "http://127.0.0.1:3129";
159-
const envVars = [
147+
return [
160148
"HERMES_HOME=/sandbox/.hermes",
161149
`HTTPS_PROXY=${decodeProxy}`,
162150
`HTTP_PROXY=${decodeProxy}`,
163151
`https_proxy=${decodeProxy}`,
164152
`http_proxy=${decodeProxy}`,
165-
];
166-
if (typeof port === "number" && Number.isInteger(port) && port >= 1 && port <= 65535) {
167-
envVars.push(`NEMOCLAW_DASHBOARD_PORT=${String(port)}`);
168-
envVars.push(`CHAT_UI_URL=http://127.0.0.1:${String(port)}`);
169-
}
170-
return envVars.join(" ");
153+
].join(" ");
171154
}
172155

173156
function hermesDecodeProxyRecoveryCommand(): string {
@@ -207,7 +190,7 @@ export function buildOpenClawRecoveryScript(port: number): string {
207190
export function buildRecoveryScript(agent: AgentDefinition | null, port: number): string | null {
208191
if (!agent) return null;
209192

210-
const probeUrl = getHealthProbeUrlForPort(agent, port);
193+
const probeUrl = getHealthProbeUrl(agent);
211194
const binaryPath = agent.binary_path || "/usr/local/bin/openclaw";
212195
const binaryName = binaryPath.split("/").pop() ?? "openclaw";
213196
const defaultGatewayCommand = `${binaryName} gateway run`;
@@ -231,7 +214,7 @@ export function buildRecoveryScript(agent: AgentDefinition | null, port: number)
231214
// that's about to crash on a missing guard. (#2478)
232215
const isHermes = agent.name === "hermes";
233216
const hermesHome = isHermes ? "export HERMES_HOME=/sandbox/.hermes; " : "";
234-
const hermesLaunchEnv = isHermes ? `env ${hermesGatewayEnvPrefix(port)} ` : "";
217+
const hermesLaunchEnv = isHermes ? `env ${hermesGatewayEnvPrefix()} ` : "";
235218
const launchCommand = usesValidatedBinary
236219
? gatewayLaunchCommand(`${hermesLaunchEnv}"$AGENT_BIN" gateway run${isHermes ? "" : ` --port ${port}`}`)
237220
: gatewayLaunchCommand(
@@ -279,10 +262,6 @@ export function getGatewayCommand(agent: AgentDefinition | null): string {
279262
return agent?.gateway_command || "openclaw gateway run";
280263
}
281264

282-
export function getAgentForwardPort(agent: AgentDefinition | null): number {
283-
return agent?.forwardPort ?? DASHBOARD_PORT;
284-
}
285-
286265
/**
287266
* Build a single copy-pasteable command for the user to run when automatic
288267
* gateway recovery fails. Unlike the raw gateway command, this keeps the
@@ -293,7 +272,7 @@ export function buildManualRecoveryCommand(agent: AgentDefinition | null, port:
293272
const defaultGatewayCommand = `${shellQuote(binaryPath)} gateway run`;
294273
const gatewayCmd = agent?.gateway_command?.trim() || defaultGatewayCommand;
295274
const isHermes = agent?.name === "hermes";
296-
const envPrefix = isHermes ? `${hermesGatewayEnvPrefix(port)} ` : "";
275+
const envPrefix = isHermes ? `${hermesGatewayEnvPrefix()} ` : "";
297276
const portFlag = isHermes ? "" : ` --port ${port}`;
298277
const decodeProxySetup = isHermes ? `${hermesDecodeProxyRecoveryCommand()} ` : "";
299278
return `${buildGatewayLogSelection()} ${decodeProxySetup}${envPrefix}nohup ${gatewayCmd}${portFlag} >> "$_GATEWAY_LOG" 2>&1 &`;

test/hermes-plugin-handlers.test.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,6 @@ function runPython(script: string): string {
1515
}
1616

1717
describe("Hermes NemoClaw plugin handlers", () => {
18-
it("reads the active Hermes API port from the sandbox environment", () => {
19-
const output = runPython(`
20-
import importlib.util
21-
import os
22-
import pathlib
23-
import sys
24-
import types
25-
26-
plugin_path = pathlib.Path(sys.argv[1])
27-
yaml_stub = types.ModuleType("yaml")
28-
yaml_stub.safe_load = lambda *_args, **_kwargs: {}
29-
sys.modules.setdefault("yaml", yaml_stub)
30-
spec = importlib.util.spec_from_file_location("hermes_plugin", plugin_path)
31-
module = importlib.util.module_from_spec(spec)
32-
spec.loader.exec_module(module)
33-
34-
os.environ["NEMOCLAW_DASHBOARD_PORT"] = "18790"
35-
print(module._get_gateway_port())
36-
`);
37-
38-
expect(output.trim()).toBe("18790");
39-
});
40-
4118
it("accepts Hermes dispatch kwargs for status, info, and reload handlers", () => {
4219
const output = runPython(`
4320
import importlib.util

test/process-recovery.test.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,12 @@ describe("resolveSandboxDashboardPort", () => {
2727
).toBe(18789);
2828
});
2929

30-
it("uses the recorded dashboard port for non-OpenClaw agents when present", () => {
30+
it("keeps non-OpenClaw agents on their declared forward port", () => {
3131
expect(
3232
resolveSandboxDashboardPort("hermes-box", {
3333
getSessionAgent: () => ({ forwardPort: 8642 }),
3434
getSandbox: () => ({ name: "hermes-box", dashboardPort: 18790 }),
3535
}),
36-
).toBe(18790);
37-
});
38-
39-
it("falls back to a non-OpenClaw agent's declared forward port without registry metadata", () => {
40-
expect(
41-
resolveSandboxDashboardPort("hermes-box", {
42-
getSessionAgent: () => ({ forwardPort: 8642 }),
43-
getSandbox: () => null,
44-
}),
4536
).toBe(8642);
4637
});
4738

test/recover-port-forward.test.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ function setupFixture(opts: {
3131
* reporting the original dead/missing state — models a failed restart. */
3232
forwardStartHeals?: boolean;
3333
port?: string;
34-
agentName?: string;
3534
}): Fixture {
3635
const sandboxName = opts.sandboxName;
3736
const port = opts.port ?? "18789";
@@ -56,7 +55,6 @@ function setupFixture(opts: {
5655
provider: "nvidia-prod",
5756
gpuEnabled: false,
5857
policies: [],
59-
agent: opts.agentName,
6058
dashboardPort: Number(port),
6159
},
6260
},
@@ -242,26 +240,4 @@ describe("nemoclaw <name> recover", () => {
242240
expect(calls.some((l) => l.startsWith("forward start "))).toBe(false);
243241
},
244242
);
245-
246-
it(
247-
"uses the registry dashboard port for Hermes recovery instead of the manifest default",
248-
testTimeoutOptions(20_000),
249-
() => {
250-
const fixture = setupFixture({
251-
sandboxName: "hermes-sandbox",
252-
gatewayProbe: "RUNNING",
253-
forwardListStatus: "dead",
254-
port: "18790",
255-
agentName: "hermes",
256-
});
257-
const result = runRecover(fixture);
258-
expect(result.status).toBe(0);
259-
260-
const calls = fs.readFileSync(fixture.invocationLog, "utf-8").split("\n");
261-
expect(calls.some((l) => l.startsWith("forward stop 18790"))).toBe(true);
262-
expect(calls.some((l) => l.startsWith("forward start --background 18790"))).toBe(true);
263-
expect(calls.some((l) => l.startsWith("forward stop 8642"))).toBe(false);
264-
expect(calls.some((l) => l.startsWith("forward start --background 8642"))).toBe(false);
265-
},
266-
);
267243
});

0 commit comments

Comments
 (0)