Skip to content

fix(dashboard): keep resumed chat alive on network binds - #26265

Closed
hehehe0803 wants to merge 7 commits into
NousResearch:mainfrom
hehehe0803:fix/dashboard-chat-resume-network-bind
Closed

hehehe0803 wants to merge 7 commits into
NousResearch:mainfrom
hehehe0803:fix/dashboard-chat-resume-network-bind

Conversation

@hehehe0803

@hehehe0803 hehehe0803 commented May 15, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes dashboard embedded chat resume when the dashboard is intentionally served on an explicit non-loopback network host, such as a private LAN/VPN address.

The existing merged WebSocket fix covers all-interface binds (0.0.0.0 / ::), but not the common explicit private-interface bind shape:

hermes dashboard --host <private-network-ip> --port <port> --insecure --tui --no-open

In that mode the HTTP dashboard can load, while /api/pty closes before accept because the WebSocket client-IP gate still behaves like the dashboard is loopback-only. This can present as Sessions → Resume in Chat opening a chat page that immediately shows [session ended], reconnect/disconnect UI, or a spawned TUI runtime error even though the underlying session is still valid.

This PR:

  • records the explicit --insecure operator opt-in on app.state.allow_public and uses it for dashboard PTY WebSocket admission;
  • keeps remote WebSocket clients rejected for default loopback-only dashboard binds;
  • adds a collision-resistant resumeNonce so repeated Sessions → Resume in Chat clicks for the same session can restart a stale PTY identity;
  • clarifies that resumeNonce is only folded into the opaque dashboard event channel id and is not interpreted as backend resume state;
  • treats unsupported Node runtimes as unusable even when HERMES_SKIP_NODE_BOOTSTRAP is set, surfacing a clear error instead of letting the embedded TUI crash later;
  • adds regression coverage using documentation/test IP addresses only.

Related context: #18633, #25072, #27801.

Related Issue

Fixes #26264

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/web_server.py
    • Persist allow_public from start_server() onto app.state.
    • Let _ws_client_is_allowed() accept non-loopback clients only when the dashboard was intentionally exposed with --insecure or bound to all interfaces.
  • tests/hermes_cli/test_web_server.py
    • Add regression coverage for explicit non-loopback dashboard binds with and without the --insecure opt-in.
    • Reset bound_host / allow_public in the PTY WebSocket test fixture so tests cannot pass because of leaked global app state.
  • web/src/pages/SessionsPage.tsx
    • Generate resumeNonce with crypto.randomUUID() when available, with a timestamp/random fallback.
  • web/src/pages/ChatPage.tsx
    • Update comments so they match the actual opaque channel-id behavior.
  • hermes_cli/main.py
    • Fail clearly when HERMES_SKIP_NODE_BOOTSTRAP is set but the available Node/npm runtime is unusable for the dashboard-spawned TUI.
  • tests/hermes_cli/test_tui_resume_flow.py
    • Cover the unsupported-Node + skip-bootstrap error path.

How to Test

Automated checks run on this branch:

  1. Python targeted tests:

    scripts/run_tests.sh tests/hermes_cli/test_web_server.py::TestPtyWebSocket tests/hermes_cli/test_tui_resume_flow.py -q

    Result: 40 passed.

  2. Python syntax check:

    python -m py_compile hermes_cli/main.py hermes_cli/web_server.py

    Result: passed.

  3. Web production build:

    cd web && npm run build

    Result: passed.

Manual/browser verification shape:

  1. Start the dashboard on a host machine with an explicit private-network bind:
    hermes dashboard --host <private-network-ip> --port <port> --insecure --tui --no-open
  2. From a separate browser/client on the same private network, open:
    http://<private-network-ip>:<port>/sessions
    
  3. Click Resume in Chat for an existing session.
  4. Verify the chat route loads a live embedded TUI for the selected session instead of [session ended], reconnect-only UI, or an unsupported Node runtime crash.

Additional check:

  • cd web && npm run lint was also run. It still fails on the current repository lint baseline in unrelated files/rules (for example OAuthProvidersCard.tsx, Toast.tsx, i18n files, and existing SessionsPage.tsx effect warnings). No new lint-specific issue was introduced by this change.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Linux / private-network dashboard bind

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

scripts/run_tests.sh tests/hermes_cli/test_web_server.py::TestPtyWebSocket tests/hermes_cli/test_tui_resume_flow.py -q
40 passed in 2.15s
python -m py_compile hermes_cli/main.py hermes_cli/web_server.py
# passed
cd web && npm run build
✓ built

@hehehe0803
hehehe0803 force-pushed the fix/dashboard-chat-resume-network-bind branch from b408a82 to 6d47206 Compare May 15, 2026 09:45
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/cli CLI entry point, hermes_cli/, setup wizard labels May 15, 2026
NishantEC

This comment was marked as outdated.

@hehehe0803
hehehe0803 force-pushed the fix/dashboard-chat-resume-network-bind branch from 6d47206 to 9fca80e Compare May 18, 2026 10:11
@hehehe0803

Copy link
Copy Markdown
Contributor Author

Rebased/retested this fix against current main and pushed an updated branch.

What changed in the update:

  • Kept the original explicit non-loopback dashboard bind fix for /api/pty.
  • Tightened the Node bootstrap predicate to match the documented Vite/runtime floor: Node >=20.19, >=22.12, or >22.
  • Strengthened the regression coverage so Starlette TestClient's loopback-like peer name cannot mask the real non-loopback WebSocket guard behavior.
  • Kept the test data anonymized with RFC documentation/test addresses only.

Verification run locally on current main plus this branch:

python -m py_compile hermes_cli/main.py hermes_cli/web_server.py tests/hermes_cli/test_web_server.py
scripts/run_tests.sh tests/hermes_cli/test_web_server.py::TestPtyWebSocket -q
  14 passed
scripts/run_tests.sh tests/hermes_cli/test_web_server.py -q
  147 passed
cd web && npm run build
  built successfully

I also checked the diff for private/local addresses and did not include any real LAN/VPN IPs, hostnames, local user paths, or credentials.

One unrelated note from the broader retest: tests/hermes_cli currently has a baseline failure on current main around auxiliary.session_search config/test drift. That is separate from this dashboard PR; I am filing/fixing it separately rather than mixing it into this branch.

@austinpickett

Copy link
Copy Markdown
Collaborator

Please use PULL_REQUEST_TEMPLATE.md

@austinpickett
austinpickett requested a review from Copilot May 18, 2026 14:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes the dashboard’s “Sessions → Resume in Chat” behavior when the dashboard is intentionally bound to a specific non-loopback host (LAN/VPN), ensuring the embedded PTY WebSocket can stay connected and that repeat resume attempts can reliably restart a stale terminal.

Changes:

  • Allow /api/pty WebSocket clients when the dashboard is bound to an explicit non-loopback host (still guarded by the session token).
  • Add a resumeNonce query param so repeated “Resume in Chat” clicks for the same session force a fresh PTY identity in the still-mounted ChatPage.
  • Validate that the Node binary used for dashboard-spawned TUI is new enough, so old system Node versions don’t get treated as “good enough”.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/src/pages/SessionsPage.tsx Adds resumeNonce to chat navigation query to force a fresh resume attempt.
web/src/pages/ChatPage.tsx Incorporates resumeNonce into the PTY “identity” so the WS/terminal is rebuilt without unmounting the page.
tests/hermes_cli/test_web_server.py Adds regression coverage for explicit network-host binds and the WS client allow-guard behavior.
hermes_cli/web_server.py Broadens the “public bind” determination to include any non-loopback bind (not only 0.0.0.0/::).
hermes_cli/main.py Adds Node version probing so unsupported system Node is treated as unusable and triggers bootstrap.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread web/src/pages/SessionsPage.tsx Outdated
Comment on lines +367 to +369
const qs = new URLSearchParams({
resume: session.id,
resumeNonce: String(Date.now()),
Comment on lines +159 to +170
// SessionsPage may intentionally navigate to the *same* resume target again
// after a previous dashboard PTY has ended. React keeps ChatPage mounted
// persistently so ordinary tab switches preserve the live PTY, which also
// means a same-URL resume click would otherwise leave the stale terminal on
// screen forever. `resumeNonce` is a route-level "start a fresh PTY for this
// resume click" signal; it is not forwarded to the backend.
const resumeNonce = searchParams.get("resumeNonce");
const channel = useMemo(
() =>
["chat", resumeParam ?? "new", resumeNonce ?? "0", generateChannelId()].join(
"_",
),
Comment thread hermes_cli/main.py Outdated
Comment on lines 1009 to 1012
if _node_is_usable(shutil.which("node")) and shutil.which("npm"):
return
if os.environ.get("HERMES_SKIP_NODE_BOOTSTRAP"):
return
@hehehe0803
hehehe0803 force-pushed the fix/dashboard-chat-resume-network-bind branch from 9fca80e to 6c1415e Compare May 20, 2026 05:14
@hehehe0803

Copy link
Copy Markdown
Contributor Author

Updated this PR against current main and addressed the review feedback:

  • Rebased onto current origin/main.
  • Replaced Date.now() resume nonce with crypto.randomUUID() plus fallback.
  • Corrected the resumeNonce comment: it is included only in the opaque dashboard event channel id, not interpreted as backend resume state.
  • Tightened the WebSocket public-bind gate to use the explicit allow_public startup opt-in instead of treating any non-loopback bound host as public by implication.
  • Added tests for explicit non-loopback bind allowed with --insecure and rejected without the opt-in.
  • Made HERMES_SKIP_NODE_BOOTSTRAP fail clearly when the available Node/npm runtime is unusable.
  • Updated the PR body to the repo template.

Verification run locally:

scripts/run_tests.sh tests/hermes_cli/test_web_server.py::TestPtyWebSocket tests/hermes_cli/test_tui_resume_flow.py -q
40 passed
python -m py_compile hermes_cli/main.py hermes_cli/web_server.py
passed
cd web && npm run build
passed

I also ran cd web && npm run lint; it still fails on the existing repo lint baseline in unrelated files/rules, so I did not include lint as a passing gate for this PR.

@hehehe0803

hehehe0803 commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up pushed to address the failing CI test job on commit 2b9d2b2c1.

Root causes from the failed test log:

  • tests/plugins/web/test_web_search_provider_plugins.py still asserted an exact seven-provider web plugin list, but the bundled web-xai provider is now discovered too. I converted that to an invariant-style core-provider subset check and added xAI capability / availability coverage instead of keeping a brittle count snapshot.
  • tests/hermes_cli/test_update_hangup_protection.py could compare against a stale _UpdateOutputStream class object after another test reloads hermes_cli.main; the assertion now imports the current class at assertion time.

Local verification:

  • scripts/run_tests.sh tests/plugins/web/test_web_search_provider_plugins.py tests/hermes_cli/test_update_hangup_protection.py -q → 65 passed

CI has restarted; most checks are already green, with the long test and arm build jobs still running at the time of this comment.

@hehehe0803
hehehe0803 force-pushed the fix/dashboard-chat-resume-network-bind branch from 2b9d2b2 to 856135b Compare May 20, 2026 11:33
@hehehe0803

Copy link
Copy Markdown
Contributor Author

Follow-up after the latest CI failure:

  • Fixed the order-sensitive stdout/stderr mirror assertion in test_update_hangup_protection.py by asserting the stable _UpdateOutputStream wrapper protocol instead of Python class identity. This avoids failures when other CLI tests reload hermes_cli.main in the same worker.
  • Fixed the Vercel sandbox snapshot persistence test to scope its snapshot store with set_hermes_home_override(...), matching code paths where a context-local Hermes home can override HERMES_HOME.
  • Verified locally with:
    • scripts/run_tests.sh tests/hermes_cli/test_update_hangup_protection.py tests/tools/test_vercel_sandbox_environment.py tests/hermes_cli/test_web_server.py::TestPtyWebSocket tests/hermes_cli/test_tui_resume_flow.py -q --tb=short
    • Result: 72 passed
  • GitHub Actions is now green on head 856135b80; the previously failing test job passed in 12m26s.

All checks are green; PR merge state is CLEAN.

@hehehe0803
hehehe0803 force-pushed the fix/dashboard-chat-resume-network-bind branch 2 times, most recently from 079be7c to b0e4d8a Compare May 24, 2026 06:15
@hehehe0803
hehehe0803 force-pushed the fix/dashboard-chat-resume-network-bind branch from b0e4d8a to 77c45a2 Compare May 25, 2026 07:43
@alt-glitch alt-glitch added comp/dashboard Web dashboard / control panel UI (dashboard/, landing) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jun 26, 2026
@alt-glitch alt-glitch removed the comp/tui Terminal UI (ui-tui/ + tui_gateway/) label Jun 26, 2026
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for documenting the explicit private-interface reproduction and adding targeted regression coverage. This is already implemented on current main.

  • Automated hermes-sweeper review verified commit 234ac009376daba225525195afca96be8a82634c (fix(dashboard): allow insecure WS peers on explicit non-loopback binds (#35386)), which fixes the same explicit LAN/Tailscale bind case.
  • hermes_cli/web_server.py:14265-14281 now permits peers for explicit non-loopback binds while preserving the loopback-only restriction for loopback binds.
  • tests/hermes_cli/test_dashboard_auth_ws_auth.py:361-386 covers the explicit non-loopback peer path and retains the Host-header rejection guard.
  • The subsequent dashboard hardening in hermes_cli/web_server.py:16920-16938 makes every non-loopback bind authenticated and treats --insecure as a no-op, so this branch's allow_public-based admission model is superseded.

@teknium1 teknium1 closed this Jul 13, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard Resume in Chat shows session ended on explicit network binds

6 participants