diff --git a/src/lib/onboard/hermes-dashboard.test.ts b/src/lib/onboard/hermes-dashboard.test.ts index 2bed3cfb7b..8c6749a3d4 100644 --- a/src/lib/onboard/hermes-dashboard.test.ts +++ b/src/lib/onboard/hermes-dashboard.test.ts @@ -36,8 +36,9 @@ describe("onboard Hermes dashboard helpers", () => { it("tracks registry drift for enabled dashboard settings", () => { const state = resolveHermesDashboardOnboardState({ + // 18789 = realistic resolved dashboard port; 8642 is the reserved API port (#4984). agentName: "hermes", - effectivePort: 8642, + effectivePort: 18789, env: { NEMOCLAW_HERMES_DASHBOARD: "1", NEMOCLAW_HERMES_DASHBOARD_PORT: "9120", @@ -58,6 +59,63 @@ describe("onboard Hermes dashboard helpers", () => { ).toBe(true); }); + it("rejects NEMOCLAW_DASHBOARD_PORT set to the reserved Hermes API port 8642 (#4984)", () => { + expect(() => + resolveHermesDashboardOnboardState({ + agentName: "hermes", + effectivePort: 18789, + env: { NEMOCLAW_DASHBOARD_PORT: "8642" }, + }), + ).toThrow("[SECURITY] Invalid Hermes dashboard port 8642 - reserved for the Hermes OpenAI-compatible API"); + }); + + it("routes the #4984 rejection through fail() so onboarding exits non-zero", () => { + const fail = vi.fn((message: string): never => { + throw new Error(message); + }); + expect(() => + resolveHermesDashboardOnboardState({ + agentName: "hermes", + effectivePort: 18789, + env: { NEMOCLAW_DASHBOARD_PORT: " 8642 " }, + fail, + }), + ).toThrow(/reserved for the Hermes OpenAI-compatible API/); + expect(fail).toHaveBeenCalledOnce(); + }); + + it("rejects a resolved dashboard port of 8642 from --control-ui-port / CHAT_UI_URL even when raw env is empty (#4984)", () => { + // --control-ui-port / CHAT_UI_URL / persisted port can resolve effectivePort to + // 8642 with the raw env unset; the host guard must still reject before build. + expect(() => + resolveHermesDashboardOnboardState({ + agentName: "hermes", + effectivePort: 8642, + env: {}, + }), + ).toThrow("[SECURITY] Invalid Hermes dashboard port 8642 - reserved for the Hermes OpenAI-compatible API"); + }); + + it("accepts a non-reserved NEMOCLAW_DASHBOARD_PORT for Hermes (#4984)", () => { + expect(() => + resolveHermesDashboardOnboardState({ + agentName: "hermes", + effectivePort: 18789, + env: { NEMOCLAW_DASHBOARD_PORT: "18790" }, + }), + ).not.toThrow(); + }); + + it("does not apply the #4984 reserved-port guard to non-Hermes agents", () => { + expect( + resolveHermesDashboardOnboardState({ + agentName: "openclaw", + effectivePort: 18789, + env: { NEMOCLAW_DASHBOARD_PORT: "8642" }, + }), + ).toEqual({ config: null, enabled: false }); + }); + it("rolls back and fails when an opted-in dashboard forward cannot start", () => { const rollback = vi.fn(); const fail = vi.fn((message: string): never => { @@ -65,8 +123,9 @@ describe("onboard Hermes dashboard helpers", () => { }); const ensure = createHermesDashboardForwardEnsurer({ state: resolveHermesDashboardOnboardState({ + // 18789 = realistic resolved dashboard port; 8642 is now reserved (#4984). agentName: "hermes", - effectivePort: 8642, + effectivePort: 18789, env: { NEMOCLAW_HERMES_DASHBOARD: "1" }, }), ensureForward: vi.fn(() => false), diff --git a/src/lib/onboard/hermes-dashboard.ts b/src/lib/onboard/hermes-dashboard.ts index d74dfa5b95..90911c2aad 100644 --- a/src/lib/onboard/hermes-dashboard.ts +++ b/src/lib/onboard/hermes-dashboard.ts @@ -11,6 +11,9 @@ import { } from "../hermes-dashboard"; import type { SandboxEntry } from "../state/registry"; +/** Hermes OpenAI-compatible API port (manifest `forward_ports[1]` / start.sh `PUBLIC_PORT`); reserved — never a dashboard port. (#4984) */ +const HERMES_OPENAI_API_PORT = 8642; + export interface HermesDashboardOnboardState { config: HermesDashboardConfig | null; enabled: boolean; @@ -31,6 +34,22 @@ export function resolveHermesDashboardOnboardState({ }): HermesDashboardOnboardState { if (agentName !== "hermes") return { config: null, enabled: false }; + // #4984 — reject the reserved Hermes API port as the dashboard port, host-side, + // before any sandbox is built. Check both the resolved effectivePort (covers + // --control-ui-port / CHAT_UI_URL / persisted port) and the raw env override, + // which the host otherwise silently drops so effectivePort never shows it. + // Message mirrors agents/hermes/start.sh:164. + const rawDashboardPort = env.NEMOCLAW_DASHBOARD_PORT?.trim(); + const requestedDashboardPort = rawDashboardPort ? Number(rawDashboardPort) : undefined; + if ( + effectivePort === HERMES_OPENAI_API_PORT || + requestedDashboardPort === HERMES_OPENAI_API_PORT + ) { + const message = `[SECURITY] Invalid Hermes dashboard port ${HERMES_OPENAI_API_PORT} - reserved for the Hermes OpenAI-compatible API`; + if (fail) return fail(message); + throw new Error(message); + } + let config: HermesDashboardConfig; try { config = readHermesDashboardConfig(env);