Skip to content

feat(dashboard): add durable service support - #37119

Open
LukasParke wants to merge 3 commits into
NousResearch:mainfrom
LukasParke:dashboard-service-support
Open

feat(dashboard): add durable service support#37119
LukasParke wants to merge 3 commits into
NousResearch:mainfrom
LukasParke:dashboard-service-support

Conversation

@LukasParke

@LukasParke LukasParke commented Jun 2, 2026

Copy link
Copy Markdown

What does this PR do?

Adds durable, installable service support for the Hermes web dashboard, matching the gateway's host-service workflow while keeping the dashboard loopback-bound by default.

This gives operators a first-class way to run the dashboard persistently under systemd, launchd, or a Windows Scheduled Task, plus secure remote-access helpers for Tailscale/headscale Serve and cloudflared. It also adds explicit dashboard Host-header allowlisting so loopback-bound dashboards can sit behind trusted local proxies/tunnels without disabling DNS-rebinding protection.

This builds on the new dashboard administration surface from #36704. Because the dashboard can now administer MCP, credentials, gateway lifecycle, memory, webhooks, and ops tasks, this PR keeps the default exposure model conservative: 127.0.0.1 by default, OAuth gate behavior preserved for public binds, and explicit allowlists for proxy hostnames.

Related Issue

Fixes #34390

Related follow-up/risk context:

Consolidated Related PR Content

#34390 was marked as part of the larger dashboard reverse-proxy Host-header allowlisting cluster. This PR consolidates the operator-facing Host-header / reverse-proxy portions of that cluster into the broader durable dashboard service/access feature, instead of landing another narrow allowlist-only variant.

Fully consolidated by this PR:

Related but not fully consolidated by this PR:

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

  • Added hermes_cli/dashboard_service.py:
    • systemd user/system unit generation and lifecycle
    • macOS launchd plist generation and lifecycle
    • Windows Scheduled Task wrapper generation and lifecycle
    • persisted dashboard service install options under $HERMES_HOME/dashboard-service/config.json
    • Tailscale/headscale Serve command generation/apply helper
    • cloudflared tunnel config generation and native service helper
  • Extended hermes dashboard CLI:
    • hermes dashboard service install|start|stop|restart|status|uninstall|unit
    • hermes dashboard access tailscale-serve
    • hermes dashboard access cloudflare-config
    • hermes dashboard access cloudflare-service ...
    • --allowed-hosts for direct dashboard runs
  • Added dashboard Host-header allowlisting:
    • CLI: --allowed-hosts
    • config: dashboard.allowed_hosts
    • env: HERMES_DASHBOARD_ALLOWED_HOSTS
    • applies to HTTP and WebSocket Host/Origin checks
  • Added authenticated dashboard admin endpoints:
    • dashboard service status/install/start/stop/restart/uninstall
    • Tailscale Serve apply
    • cloudflared config generation/service actions
  • Updated the System page with dashboard service and secure access controls.
  • Updated API types/client methods and web dashboard / CLI docs.
  • Added focused tests for service generation, access helpers, Host allowlisting, WebSocket allowlisting, and admin endpoints.

How to Test

  1. Generate a service definition:

    hermes dashboard service unit \
      --host 127.0.0.1 \
      --port 9119 \
      --allowed-hosts device.tailnet.ts.net \
      --public-url https://device.tailnet.ts.net
  2. Print secure-access helper commands/config:

    hermes dashboard access tailscale-serve --port 9119
    hermes dashboard access cloudflare-config \
      --tunnel t123 \
      --credentials-file /tmp/t.json \
      --hostname dash.example.com \
      --port 9119
  3. Run focused verification:

    python -m py_compile hermes_cli/dashboard_service.py hermes_cli/main.py hermes_cli/web_server.py
    uv run --frozen --with pytest --with pytest-timeout pytest tests/hermes_cli/test_dashboard_service.py
    uv run --frozen --with pytest --with pytest-timeout pytest tests/hermes_cli/test_web_server_host_header.py tests/hermes_cli/test_dashboard_admin_endpoints.py
    cd web && bun run build
    git diff --check

Observed results after resolving the latest origin/main merge and cleanup:

  • python -m py_compile hermes_cli/web_server.py tests/hermes_cli/test_web_server_host_header.py: passed
  • uv run --frozen --with pytest --with pytest-timeout pytest tests/hermes_cli/test_web_server_host_header.py tests/hermes_cli/test_dashboard_service.py tests/hermes_cli/test_dashboard_admin_endpoints.py: 62 passed, 1 existing audioop deprecation warning from discord/player.py
  • web production build passed, with the existing large chunk warning
  • git diff --check passed

Not run:

  • Full pytest tests/ -q. The focused dashboard/service/web tests above were run instead.
  • Native macOS launchd and Windows Scheduled Task execution. Those paths are covered by generation/unit tests and mirror the gateway service patterns, but were not executed on those platforms in this checkout.

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

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

No screenshots. This is primarily CLI/service/API wiring plus the System page controls.

@alt-glitch alt-glitch added type/feature New feature or request comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have labels Jun 2, 2026

@teknium1 teknium1 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.

Thanks for consolidating durable dashboard-service work with the real reverse-proxy/Tailscale Host-header problem documented in #34390.

Problems

  • The new loopback allowlist accepts an external Host at hermes_cli/web_server.py:251, but current main derives app.state.auth_required only from the bind address at hermes_cli/web_server.py:17015. An allowed proxy hostname on a loopback bind must force the auth gate, and that condition must also feed the interactive setup preflight.
  • HERMES_DASHBOARD_ALLOWED_HOSTS is a new non-secret behavioral setting. AGENTS.md:102-107 requires this policy to live in dashboard.allowed_hosts in config.yaml, not .env/environment configuration.
  • The new hermes_cli/dashboard_service.py embeds Tailscale and cloudflared management (build_tailscale_serve_command, _cloudflared_service_command), conflicting with the third-party integration boundary in AGENTS.md:126-136.
  • The branch is conflicting, and its parser edits target the old inline main.py implementation; current main's dashboard parser lives in hermes_cli/subcommands/dashboard.py:17-156.

Suggested changes

  • Share an effective-auth predicate between startup and preflight; add loopback-plus-allowlist HTTP/WS tests.
  • Keep the persistent allowlist in dashboard.allowed_hosts, remove the new env-var path, and separate vendor-specific tunnel management from the core service implementation.

This is an automated hermes-sweeper review.

Comment thread hermes_cli/web_server.py
@@ -226,7 +251,10 @@ def _is_accepted_host(host_header: str, bound_host: str) -> bool:
# Loopback bind: accept the loopback names

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.

Allowlisting an external Host on a loopback bind also needs to engage the dashboard auth gate. Current main derives app.state.auth_required only from the bind host (start_server), so this makes a proxy-reachable loopback dashboard accept the external origin while remaining ungated. Please share an effective-auth predicate with startup and the interactive preflight, and add a loopback-plus-allowlist regression test.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] dashboard: add --allowed-hosts flag for reverse-proxy and Tailscale access

4 participants