* feat(dashboard-auth): extend RFC 8252 native sign-in to password providers The desktop app runs password sign-in for gated gateways in an embedded Electron BrowserWindow, where OS password managers (macOS Passwords / iCloud Keychain autofill) cannot reach the form — Chromium-in-Electron has no bridge to them, so users retype credentials by hand even though the /login form already carries the right autocomplete attributes. The existing RFC 8252 native flow (system browser + loopback + PKCE) solves exactly this for OAuth providers, but was explicitly disabled for password providers on the grounds that they have "no IDP round trip to broker". The brokering is still worth having: it moves the credential form into the system browser, where password-manager autofill just works. Gateway-only change; the desktop needs no changes (runNativeLogin is already page-agnostic), and older desktop builds pick the capability up automatically once the gateway advertises it: * /auth/native/authorize now accepts a supports_password provider: register the pending broker authorization as usual, then 302 the system browser to the interactive /login form with the opaque broker_state in the gateway's PKCE cookie (the same server-controlled channel the OAuth branch uses) instead of an IDP redirect. * /auth/password-login: when the server-set PKCE cookie carries a broker handle, a successful credential check completes the pending authorization exactly like the /auth/callback native branch — mint the one-time loopback code, return the loopback redirect (validated loopback-only at authorize time) as `next`, clear the PKCE cookie, and set NO session cookies. A lapsed broker is a clean 400 telling the user to restart sign-in; a failed credential attempt leaves the pending entry intact so the user can retype. * /api/status now advertises "native_pkce" whenever any interactive session provider is registered (previously only for non-password providers), so the desktop selects the system-browser strategy for password-only gateways. Security posture is unchanged from the existing flow: loopback-literal redirect_uri enforcement, PKCE S256 binding, single-use short-TTL codes, constant-time comparison, and the same rate limiter on password attempts. Tests: full authorize → /login → password-login → loopback → token → bearer round trip, wrong-password keeps the pending entry, lapsed broker → 400, no-broker browser login keeps minting cookies, and the /api/status advertisement for password-only gateways. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dashboard-auth): bind native password completion to the authorize-time provider Review follow-ups for #75808: * /auth/password-login now enforces that body.provider matches the provider recorded in the server-set PKCE cookie by /auth/native/authorize before completing a pending native authorization. /login renders a form for every session provider, so without this a native flow started for provider A could be completed with provider B's credentials, binding B's session into A's pending entry. The mismatch is rejected BEFORE credential verification (no session minted, no oracle) and preserves both the pending entry and the cookie, so the user can still submit the correct provider's form. Covered by a two-password-provider E2E regression test. * Update the two docs spots that still said password-only providers do not advertise native_pkce (website desktop-native-signin guide and the auth_flows type comment in web/src/lib/api.ts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: map contributor email for #75808 (buffpesos) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
Hermes Agent — Web UI
Browser-based dashboard for managing Hermes Agent configuration, API keys, and monitoring active sessions.
Stack
- Vite + React 19 + TypeScript
- Tailwind CSS v4 with custom dark theme
- shadcn/ui-style components (hand-rolled, no CLI dependency)
Development
# Start the backend API server
cd ../
python -m hermes_cli.main web --no-open
# In another terminal, start the Vite dev server (with HMR + API proxy)
cd web/
npm install
npm run dev
Open the Vite URL printed in the terminal (usually http://localhost:5173). That is the live-reload UI.
hermes dashboard on port 9119 serves the built bundle from hermes_cli/web_dist/, not the Vite dev server — changes in web/src/ will not appear there until you run npm run build and restart the dashboard (or use web --no-open + Vite as above).
The Vite dev server proxies /api requests to http://127.0.0.1:9119 (the FastAPI backend).
Build
npm run build
This outputs to ../hermes_cli/web_dist/, which the FastAPI server serves as a static SPA. The built assets are included in the Python package via pyproject.toml package-data.
Structure
src/
├── components/ui/ # Reusable UI primitives (Card, Badge, Button, Input, etc.)
├── lib/
│ ├── api.ts # API client — typed fetch wrappers for all backend endpoints
│ └── utils.ts # cn() helper for Tailwind class merging
├── pages/
│ ├── StatusPage # Agent status, active/recent sessions
│ ├── ConfigPage # Dynamic config editor (reads schema from backend)
│ └── EnvPage # API key management with save/clear
├── App.tsx # Main layout and navigation
├── main.tsx # React entry point
└── index.css # Tailwind imports and theme variables
Typography & contrast rules
Read before adding or editing UI styles. These rules keep the dashboard legible across all built-in themes and stop drift back into the patterns the design system was just refactored out of.
Text size floor
- Minimum body size:
text-xs(12px / 0.75rem). Do not use arbitrarytext-[0.6rem],text-[0.65rem],text-[9px],text-[10px], ortext-[11px]on copy, hints, labels, counts, or badges. Use the standard scale:text-xs,text-sm,text-base. - Smaller sizes are only acceptable on decorative overlays (chart stripes, empty-state icons) — never on text the user is meant to read.
Opacity floor on text
- Never apply opacity below 0.7 to text. No
opacity-30,opacity-50,opacity-60on<span>s,<p>s, labels, etc. - Do not stack opacity tokens. Patterns like
text-muted-foreground/60,text-midground/70,text-foreground/50create unpredictable WCAG failures because the parent token already has alpha. - Use the semantic text tokens from
@nous-research/ui'sglobals.css:text-text-primary— default body text.text-text-secondary— subtitles, meta, inactive nav.text-text-tertiary— small chrome labels, counts, footnotes.text-text-disabled— disabled states.text-text-on-accent— text on filled accent surfaces.
Brand uppercase via text-display, not raw uppercase
- The dashboard preserves the Nous brand uppercase aesthetic, but it is opt-in per element, not global.
- Apply uppercase via the DS utility
text-displayon brand chrome only — page titles, nav section headings, badges, brand wordmark. DS components (Button,Badge,Tabs,Segmented, etc.) already self-applytext-display. - Do not introduce new
uppercase(the literal Tailwind class) inhermes-agent/web/src. Prefertext-displayfor new brand chrome. Legacyuppercasecall sites (e.g.components/ui/label.tsx,card.tsx) remain until migrated. - The app shell no longer forces uppercase globally, so blanket
normal-caseopt-outs are unnecessary. Usenormal-caseonly where a DS component appliestext-displaybut the label should stay sentence case — e.g. dynamic user content (model slugs, theme names) or fixed UI copy that is not brand chrome (EnvPage “not configured” toggle, sidebar “New chat”).
Fonts
Typography is opt-in per surface, not global on layout shells — the app shell and page header keep their original theme/expanded fonts; Mondwest applies only where explicitly set.
| Tier | Classes | Use for |
|---|---|---|
| Brand chrome | font-mondwest text-display (or themedChrome) |
Sidebar nav, card section headers (CardTitle), Segmented filter buttons, filter panel headings |
| Themed body | font-mondwest normal-case (or themedBody) |
Card content (Card, CardDescription), session/platform rows, analytics tables — scoped to the component |
| Page chrome | font-expanded |
Page header h1 (PageHeaderProvider) — sentence case, not text-display |
| Wordmark | Typography + size/tracking only |
Sidebar/mobile “Hermes Agent” — mixed case, no Mondwest, no text-display |
| Technical | font-mono-ui / font-mono / font-courier |
Model slugs, env keys, schedules, YAML, repo URLs |
- Do not put
themedBodyorthemedFonton<main>,App, or other layout wrappers — it overrides component-scoped styles. CardappliesthemedBody;CardTitleusestext-display(uppercase chrome);CardDescriptionusesthemedBody.NouiTypographydefaults tofont-sansunless a font prop is passed.- Do not use raw
font-sansorfont-display(theme sans variable) on new dashboard UI — prefer Mondwest tiers above where brand-appropriate.
Color tokens
- Prefer semantic tokens (
text-text-*,bg-card,border-border,text-foreground,text-destructive,text-success,text-warning) over raw layer references (text-midground,text-foreground). text-muted-foregroundis now wired to--color-text-secondary, so existing call sites stay correct, but new code should prefer the semantic name.- When you genuinely need a non-token color (icon de-emphasis on a chart, terminal foreground via inline style), keep alpha at
≥ 0.7for any text.