+
    „WÝj�K  ã                  ó’   € R t ^ RIHt ^ RIt^ RIHtHt ^ RIHt Rt	Rt
RtRR	R
R	/R R lltR R ltR
R	/R R lltR
R	/R R lltR# )a  Server-rendered /login page (no React, no SPA bundle, no injected token).

Providers come from the registry; an OAuth provider renders an anchor to
``/auth/login?provider=<name>``, a ``supports_password`` provider renders a
credential form wired by :data:`_PASSWORD_FORM_SCRIPT`. Styling mirrors the
``@nous-research/ui`` design system; fonts load from the SPA's ``/fonts/``
mount, which the gate allowlists pre-auth.

The ``class="provider-btn"`` anchor is test-stable: the suite extracts its
href to walk the OAuth flow.
)ÚannotationsN)ÚquoteÚ	urlencode)Úlist_session_providersuÂ  <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in â€” Hermes Agent</title>
<style>
  /* Brand fonts shipped by @nous-research/ui â€” same files the SPA loads. */
  @font-face {{
    font-family: 'Collapse';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/Collapse-Regular.woff2') format('woff2');
  }}
  @font-face {{
    font-family: 'Collapse';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/fonts/Collapse-Bold.woff2') format('woff2');
  }}
  @font-face {{
    font-family: 'Rules Compressed';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/RulesCompressed-Regular.woff2') format('woff2');
  }}
  @font-face {{
    font-family: 'Rules Compressed';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('/fonts/RulesCompressed-Medium.woff2') format('woff2');
  }}

  :root {{
    --background-base: #170d02;
    --background: #170d02;
    --midground: #ffac02;
    --foreground: #ffffff;
    --hairline: color-mix(in srgb, #ffac02 18%, transparent);
    --hairline-strong: color-mix(in srgb, #ffac02 35%, transparent);
  }}

  *, *::before, *::after {{ box-sizing: border-box; }}

  html, body {{
    margin: 0;
    padding: 0;
    min-height: 100%;
    background: var(--background-base);
    color: var(--foreground);
    font-family: 'Collapse', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    font-size: 16px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }}

  /* Subtle dot-grid backdrop â€” DS idiom (see `.dither` in globals.css). */
  body {{
    background-image:
      radial-gradient(
        ellipse at top,
        color-mix(in srgb, var(--midground) 6%, transparent) 0%,
        transparent 55%
      ),
      repeating-conic-gradient(
        color-mix(in srgb, var(--midground) 4%, transparent) 0% 25%,
        transparent 0% 50%
      );
    background-size: auto, 3px 3px;
    background-attachment: fixed;
  }}

  /* Layout: vertically center on tall screens, top-anchor on short. */
  body {{
    display: grid;
    place-items: center;
    padding: clamp(1.5rem, 6vh, 6rem) 1.25rem;
  }}

  main {{
    width: 100%;
    max-width: 26rem;
    position: relative;
    animation: slide-up 0.6s ease-out both;
  }}

  @keyframes slide-up {{
    from {{ opacity: 0; transform: translateY(6px); }}
    to   {{ opacity: 1; transform: translateY(0); }}
  }}

  @media (prefers-reduced-motion: reduce) {{
    main {{ animation: none; }}
  }}

  /* Brand wordmark above the card â€” same uppercase + wide-tracking
     idiom DS Buttons use. */
  .brand {{
    text-align: center;
    margin-bottom: 1.75rem;
    font-family: 'Rules Compressed', 'Collapse', sans-serif;
    font-weight: 600;
    font-size: 1.05rem;
    letter-spacing: 0.32em;
    text-transform: uppercase;
    color: var(--midground);
  }}
  .brand .dot {{
    display: inline-block;
    width: 6px;
    height: 6px;
    background: var(--midground);
    margin: 0 0.55em 0.18em;
    vertical-align: middle;
    border-radius: 1px;
  }}

  .card {{
    position: relative;
    padding: 2.25rem 2rem 2rem;
    background: color-mix(in srgb, #ffffff 2%, var(--background-base));
    border: 1px solid var(--hairline);
    /* Hairline highlight + bevel shadow â€” matches DS Button SHADOW_DEFAULT
       (`inset -1px -1px 0 #00000080, inset 1px 1px 0 #ffffff80`) at panel scale. */
    box-shadow:
      inset 1px 1px 0 0 color-mix(in srgb, #ffffff 5%, transparent),
      inset -1px -1px 0 0 rgba(0, 0, 0, 0.4),
      0 24px 60px -20px rgba(0, 0, 0, 0.6);
  }}

  h1 {{
    margin: 0 0 0.4rem;
    font-family: 'Rules Compressed', 'Collapse', sans-serif;
    font-weight: 600;
    font-size: 1.85rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--foreground);
  }}

  .subtitle {{
    margin: 0 0 1.75rem;
    color: color-mix(in srgb, var(--foreground) 65%, transparent);
    font-size: 0.95rem;
  }}

  .provider-list {{
    display: grid;
    gap: 0.75rem;
  }}

  /* Provider button â€” mirrors DS Button (default variant):
     amber surface, dark text, uppercase + wide tracking, inset bevel. */
  .provider-btn {{
    display: block;
    width: 100%;
    box-sizing: border-box;
    padding: 0.95rem 1rem;
    text-align: center;
    background: var(--midground);
    color: var(--background-base);
    font-family: 'Collapse', sans-serif;
    font-weight: 700;
    font-size: 0.78rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    text-decoration: none;
    border: 0;
    border-radius: 0;  /* DS Button is squared â€” no rounded corners. */
    cursor: pointer;
    box-shadow:
      inset 1px 1px 0 0 rgba(255, 255, 255, 0.5),
      inset -1px -1px 0 0 rgba(0, 0, 0, 0.5);
    transition: filter 0.12s ease-out;
  }}
  .provider-btn:hover {{
    filter: brightness(1.08);
  }}
  .provider-btn:active {{
    /* DS Button uses `active:invert` on the default surface. */
    filter: invert(1);
  }}
  .provider-btn:focus-visible {{
    outline: 2px solid var(--midground);
    outline-offset: 3px;
  }}

  /* Password provider form â€” same visual language as the OAuth buttons:
     squared inputs, hairline borders, amber focus ring. */
  .provider-form {{
    display: grid;
    gap: 0.75rem;
    text-align: left;
  }}
  .form-title {{
    font-family: 'Rules Compressed', 'Collapse', sans-serif;
    font-weight: 600;
    font-size: 0.72rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--foreground) 70%, transparent);
  }}
  .field {{
    display: grid;
    gap: 0.3rem;
  }}
  .field-label {{
    font-size: 0.72rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--foreground) 55%, transparent);
  }}
  .field-input {{
    width: 100%;
    box-sizing: border-box;
    padding: 0.7rem 0.8rem;
    background: color-mix(in srgb, #000000 25%, var(--background-base));
    color: var(--foreground);
    border: 1px solid var(--hairline-strong);
    border-radius: 0;
    font-family: 'Collapse', sans-serif;
    font-size: 0.95rem;
  }}
  .field-input:focus-visible {{
    outline: none;
    border-color: var(--midground);
    box-shadow: 0 0 0 1px var(--midground);
  }}
  .form-error {{
    color: #ff6b6b;
    font-size: 0.82rem;
    letter-spacing: 0.02em;
  }}
  .provider-form .provider-btn {{
    margin-top: 0.25rem;
  }}

  footer {{
    margin-top: 1.75rem;
    text-align: center;
    color: color-mix(in srgb, var(--foreground) 45%, transparent);
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    line-height: 1.7;
  }}
  footer .sep {{
    display: inline-block;
    width: 1.5rem;
    height: 1px;
    background: var(--hairline-strong);
    vertical-align: middle;
    margin: 0 0.6em 0.2em;
  }}

  /* Selection â€” DS uses midground bg + background text. */
  ::selection {{
    background: var(--midground);
    color: var(--background-base);
  }}
</style>
</head>
<body>
<main>
  <div class="brand">Nous<span class="dot"></span>Research</div>
  <div class="card">
    <h1>Sign in</h1>
    <p class="subtitle">Choose a sign-in method to continue to the Hermes Agent dashboard.</p>
    <div class="provider-list">
{provider_buttons}
    </div>
  </div>
  <footer>
    <span class="sep"></span>Public bind &middot; Auth required<span class="sep"></span>
  </footer>
</main>
{password_script}
</body>
</html>
uþ	  <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign-in unavailable â€” Hermes Agent</title>
<style>
  @font-face {
    font-family: 'Collapse';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/Collapse-Regular.woff2') format('woff2');
  }
  @font-face {
    font-family: 'Rules Compressed';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('/fonts/RulesCompressed-Medium.woff2') format('woff2');
  }
  :root {
    --background-base: #170d02;
    --midground: #ffac02;
    --foreground: #ffffff;
    --hairline: color-mix(in srgb, #ffac02 18%, transparent);
  }
  *, *::before, *::after { box-sizing: border-box; }
  html, body {
    margin: 0; padding: 0; min-height: 100%;
    background: var(--background-base);
    color: var(--foreground);
    font-family: 'Collapse', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    font-size: 16px; line-height: 1.5;
    -webkit-font-smoothing: antialiased;
  }
  body {
    display: grid; place-items: center;
    padding: clamp(1.5rem, 6vh, 6rem) 1.25rem;
  }
  main {
    width: 100%; max-width: 32rem;
    padding: 2.25rem 2rem;
    background: color-mix(in srgb, #ffffff 2%, var(--background-base));
    border: 1px solid var(--hairline);
    box-shadow:
      inset 1px 1px 0 0 color-mix(in srgb, #ffffff 5%, transparent),
      inset -1px -1px 0 0 rgba(0, 0, 0, 0.4),
      0 24px 60px -20px rgba(0, 0, 0, 0.6);
  }
  h1 {
    margin: 0 0 1rem;
    font-family: 'Rules Compressed', 'Collapse', sans-serif;
    font-weight: 600; font-size: 1.5rem;
    letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--midground);
  }
  p { margin: 0 0 1rem; }
  code {
    background: var(--midground);
    color: var(--background-base);
    padding: 0.1em 0.35em;
    font-family: 'Courier New', monospace;
    font-size: 0.9em;
  }
  a { color: var(--midground); }
</style>
</head>
<body>
<main>
<h1>Sign-in unavailable</h1>
<p>This dashboard is bound to a non-loopback host but no authentication
providers are available.</p>
<p>Configure the bundled username/password provider or an OAuth provider.
See the <a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/web-dashboard#authentication-gated-mode">dashboard
authentication documentation</a> for setup instructions.</p>
<p>For auth-free local use, bind to <code>127.0.0.1</code> and connect through
an SSH tunnel or Tailscale.</p>
</main>
</body>
</html>
aE  <script>
(function () {
  var mainEl = document.querySelector('main');
  var prefix = (mainEl && mainEl.getAttribute('data-prefix')) || '';
  function handle(form) {
    form.addEventListener('submit', function (ev) {
      ev.preventDefault();
      var err = form.querySelector('.form-error');
      var btn = form.querySelector('button[type=submit]');
      if (err) { err.hidden = true; err.textContent = ''; }
      if (btn) { btn.disabled = true; }
      var body = {
        provider: form.getAttribute('data-provider') || '',
        username: (form.querySelector('input[name=username]') || {}).value || '',
        password: (form.querySelector('input[name=password]') || {}).value || '',
        next: (form.querySelector('input[name=next]') || {}).value || ''
      };
      fetch(prefix + '/auth/password-login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        credentials: 'same-origin'
      }).then(function (resp) {
        if (resp.ok) {
          return resp.json().then(function (data) {
            window.location.assign((data && data.next) || (prefix + '/'));
          });
        }
        var msg = resp.status === 429
          ? 'Too many attempts. Please wait and try again.'
          : (resp.status === 401 ? 'Invalid username or password.'
                                 : 'Sign-in failed. Please try again.');
        if (err) { err.textContent = msg; err.hidden = false; }
        if (btn) { btn.disabled = false; }
      }).catch(function () {
        if (err) { err.textContent = 'Network error. Please try again.'; err.hidden = false; }
        if (btn) { btn.disabled = false; }
      });
    });
  }
  var forms = document.querySelectorAll('form.provider-form');
  for (var i = 0; i < forms.length; i++) { handle(forms[i]); }
})();
</script>
Ú	next_pathÚ Úprefixc               ó$   € V ^8„  d   QhRRRRRR/# ©é   r   Ústrr   Úreturn© )Úformats   "ÚXC:\Users\johnn\AppData\Local\hermes\hermes-agent\hermes_cli\dashboard_auth\login_page.pyÚ__annotate__r   Á  s!   € ÷ $1ñ $1 Cð $1°cð $1À3ñ $1ó    c                óx  € \        4       pV'       g   \        # V '       d'   R\        P                  ! \	        V RR7      RR7       2MRpV Uu. uFo  p\        VRR4      '       d   \        W@VR7      MKR	V R
\        P                  ! VP                  RR7       V R\        P                  ! VP                  4       R2NKq  	  pp\        ;QJ d    R V 4       F  '       g   K   RM	  RM! R V 4       4      p\        P                  RP                  V4      V'       d   \        MRR7      p\        Wq4      # u upi )u   Return the full HTML for ``GET /login``.

``next_path`` is threaded into each provider button/form so the OAuth round
trip carries it end-to-end. The caller validates it same-origin; it is
HTML-escaped here as defence in depth.

``prefix`` â€” when set, the path prefix from ``X-Forwarded-Prefix``
(e.g. ``"/hermes"``). Prepended to all auth URLs so the login page
works behind a reverse proxy that mounts the dashboard at a
sub-path. Empty string (default) means no prefix â€” bare-root
deploys are unaffected.
z&next=r   )ÚsafeT©r   Úsupports_passwordF)r   ú$      <a class="provider-btn" href="z/auth/login?provider=ú">Sign in with ú</a>c              3  ó<   "  € T F  p\        VR R4      x € K  	  R# 5i)r   FN)Úgetattr)Ú.0Úps   & r   Ú	<genexpr>Ú$render_login_html.<locals>.<genexpr>Ü  s   é € ÐZÑPYÈ1¤¨Ð+>À× FÐ FÓPYùs   ‚Ú
©Úprovider_buttonsÚpassword_script)r   Ú_EMPTY_HTMLÚhtmlÚescaper   r   Ú_render_password_formÚnameÚdisplay_nameÚanyÚ_LOGIN_HTML_TEMPLATEr   ÚjoinÚ_PASSWORD_FORM_SCRIPTÚ_apply_proxy_prefix)r   r   Ú	providersÚnext_qsr   ÚbuttonsÚneeds_password_scriptÚhtml_outs   $$      r   Úrender_login_htmlr4   Á  s&  € ô 'Ó(€IßÜÐ÷ PY�”t—{’{¤5¨¸Ô#<ÀDÔIÐJÑKÐ^`€Gñ óñ ˆAô	 �1Ð)¨5×1Ò1ô 	˜a°6Õ:ðØ�Ð-¬d¯kªk¸!¿&¹&ÈÔ.MÐ-NÈwÈið XÜŸš A§N¡NÓ3Ð4°Dð	:ò	:ñ
 ð ð ÷  ›CÑZÑPYÓZŸCŸCšCÑZÑPYÓZÓZÐÜ#×*Ñ*ØŸ™ 7Ó+ß1FÕ-ÈBð +ó €Hô ˜xÓ0Ð0ùò#s   ÁA5D7c               ó$   € V ^8„  d   QhRRRRRR/# )r   r3   r   r   r   r   )r   s   "r   r   r   è  s!   € ÷ ñ  #ð ¨sð °sñ r   c                ót   € V'       g   V # V P                  RRV R2^4      p V P                  RRV R24      p V # )a  Rewrite a rendered login-family page for a reverse-proxy sub-path.

Two rewrites, both no-ops when *prefix* is empty (bare-root deploys are
byte-for-byte unaffected):

* ``<main>`` gains ``data-prefix`` so the password-login JS (a plain
  string, never ``str.format``-aware) can prepend the prefix to its
  fetch target and post-login landing.
* ``@font-face`` ``url('/fonts/...')`` becomes ``url('<prefix>/fonts/...')``
  so the fonts load from the mount instead of the origin root (404
  behind a prefix-stripping proxy).
z<main>z<main data-prefix="z">zurl('/fonts/zurl('z/fonts/)Úreplace)r3   r   s   &&r   r.   r.   è  sJ   € ÷ ØˆØ×Ñ Ð,?À¸xÀrÐ*JÈAÓN€HØ×Ñ °%¸°x¸wÐ0GÓH€HØ€Or   c               ó4   € V ^8„  d   QhRRRRRRRRRRRRRR/# )	r   Úauthorize_pathr   Úcode_challengeÚcode_challenge_methodÚredirect_uriÚstater   r   r   )r   s   "r   r   r   ü  sH   € ÷ ñ Ø&)ðØ;>ðà"ðà25ðà>Aðð ðð !ñr   c           
     ó€  € RVRVRVRV/p. pV  Fo  p	\         P                  ! V R\        / VCRV	P                  /C4       2RR7      p
VP	                  R	V
 R
\         P                  ! V	P
                  4       R24       Kq  	  V'       g   \        # \        \        P                  RP                  V4      RR7      V4      # )aÁ  Provider picker for a native authorize request with more than one interactive provider.

Every link re-enters ``/auth/native/authorize`` with the SAME desktop PKCE inputs plus an
explicit ``provider``, so the choice never leaves the validated native flow. ``prefix``
(from ``X-Forwarded-Prefix``) rewrites the template's font URLs so the page works behind
a reverse-proxy sub-path; the buttons' ``authorize_path`` is already prefixed by the caller.
r:   r;   r<   r=   Ú?ÚproviderTr   r   r   r   r    r   r!   )r%   r&   r   r(   Úappendr)   r$   r.   r+   r   r,   )r/   r9   r:   r;   r<   r=   r   Úcommonr1   r   Úhrefs   $$$$$$$    r   Ú"render_native_provider_choice_htmlrD   ü  sÙ   € ð  Ð0GÐI^Ø˜l¨G°Uð<€Fà€GÛˆÜ�{Š{˜nÐ-¨Q¬yÐ9W¸FÐ9WÀJÐPQ×PVÑPVÑ9WÓ/XÐ.YÐZØ!%ô'ˆà�‰Ð=¸d¸Vð D'Ü'+§{¢{°1·>±>Ó'BÐ&CÀ4ðIö 	Jñ ÷
 ÜÐÜÜ×#Ñ#Ø!ŸY™Y wÓ/Àð 	$ó 	Eàóð r   c               ó$   € V ^8„  d   QhRRRRRR/# r
   r   )r   s   "r   r   r     s!   € ÷ !ñ !¨sð !¸sð !ÈCñ !r   c               óâ   € \         P                  ! V P                  RR7      p\         P                  ! V P                  4      pV'       d   \         P                  ! VRR7      MRpRV RV RV R2# )u¬  Username/password form for a ``supports_password`` provider.

``next_path`` rides in a hidden field (already validated by the caller,
HTML-escaped here). The provider name is a ``data-`` attribute so the
script does not depend on field ordering.

``prefix`` â€” reverse-proxy path prefix (e.g. ``"/hermes"``); kept for
interface parity with the other renderers (the submit handler reads
the prefix from ``<main data-prefix>``).
Tr   r   z1      <form class="provider-form" data-provider="zB" autocomplete="on">
        <div class="form-title">Sign in with z7</div>
        <input type="hidden" name="next" value="ab  ">
        <label class="field">
          <span class="field-label">Username</span>
          <input class="field-input" type="text" name="username" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" required>
        </label>
        <label class="field">
          <span class="field-label">Password</span>
          <input class="field-input" type="password" name="password" autocomplete="current-password" required>
        </label>
        <div class="form-error" role="alert" hidden></div>
        <button class="provider-btn" type="submit">Sign in</button>
      </form>)r%   r&   r(   r)   )r@   r   r   ÚpnameÚplabelÚ	safe_nexts   &&$   r   r'   r'     sm   € ô �KŠK˜Ÿ™¨TÔ2€EÜ�[Š[˜×.Ñ.Ó/€Fß6?”—’˜I¨TÕ2ÀR€Ià
;¸E¸7ð C8à8>°xð @;Ø;D¸+ð Fð	ðr   )Ú__doc__Ú
__future__r   r%   Úurllib.parser   r   Úhermes_cli.dashboard_authr   r+   r$   r-   r4   r.   rD   r'   r   r   r   Ú<module>rN      sm   ðñ
õ #ã ß )å <ð]Ð ð~R€ð|-Ð ð`$1¨"ð $1¸B÷ $1õNð(ð ÷ð6!ÀR÷ !ñ !r   