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
63 changes: 61 additions & 2 deletions src/lib/onboard/hermes-dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -58,15 +59,73 @@ 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 => {
throw new Error(message);
});
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),
Expand Down
19 changes: 19 additions & 0 deletions src/lib/onboard/hermes-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down