mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-02 07:11:49 +00:00
feat(dashboard-auth): Phase 7 — SPA AuthWidget + /api/status auth fields
Phase 7 surfaces the OAuth gate state to users.
web/src/components/AuthWidget.tsx (new):
Sidebar widget that fetches /api/auth/me on mount and renders a
compact 'Logged in as <user_id…> via <provider>' row with a logout
icon. Contract V1 (Nous Portal) emits no email/display_name claims,
so user_id is the display value (truncated to 14 chars + ellipsis);
display_name and email fallthroughs are forward-compat for OQ-C1.
Renders nothing on 401 from /api/auth/me — that's the signal the
gate isn't engaged (loopback mode), in which case the widget would
be confusing.
Logout POSTs /auth/logout (which clears cookies + redirects to
/login) then full-page-navigates to /login itself; the SPA's fetch
wrapper doesn't follow that redirect, so the navigation is explicit.
web/src/App.tsx: mounts <AuthWidget /> above <SidebarFooter />.
Component is self-hiding in loopback mode so there's no need for a
conditional mount.
web/src/lib/api.ts:
- getAuthMe() + logout() helpers
- AuthMeResponse type
- StatusResponse gets optional auth_required + auth_providers fields
so the existing StatusPage can render a gated/loopback badge.
hermes_cli/web_server.py: /api/status payload now includes
- auth_required: bool — whether app.state.auth_required is True
- auth_providers: list[str] — registered DashboardAuthProvider names
Lazy-imports list_providers so early-startup status calls don't
crash if the dashboard_auth module is still being set up.
tests/hermes_cli/test_dashboard_auth_status_endpoint.py: 3 new tests
covering the new status fields in both gated and loopback modes plus
a regression that no existing field got dropped from the payload.
The hermes status CLI is unchanged in this commit — that command
tracks model providers + OAuth credentials, not running-dashboard
state. The /api/status endpoint is the canonical place to query
dashboard auth-gate state, consumed by the React StatusPage already.
This commit is contained in:
parent
5e9308b5b8
commit
2fc4615fc4
5 changed files with 319 additions and 0 deletions
|
|
@ -153,6 +153,27 @@ export async function buildWsAuthParam(): Promise<[string, string]> {
|
|||
|
||||
export const api = {
|
||||
getStatus: () => fetchJSON<StatusResponse>("/api/status"),
|
||||
/**
|
||||
* Identity probe for the dashboard auth gate (Phase 7).
|
||||
*
|
||||
* Returns the verified Session as JSON when gated mode is active and a
|
||||
* valid cookie is attached. Loopback mode is unaffected — the endpoint
|
||||
* still exists but is never useful there (no Session, no cookie). The
|
||||
* AuthWidget component swallows 401s from this call: if the gate isn't
|
||||
* engaged, /api/auth/me returns 401 and the widget renders nothing.
|
||||
*/
|
||||
getAuthMe: () => fetchJSON<AuthMeResponse>("/api/auth/me"),
|
||||
logout: () =>
|
||||
fetch(`${BASE}/auth/logout`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}).then((r) => {
|
||||
// /auth/logout returns 302 → /login. Follow that with a full-page
|
||||
// navigation rather than letting fetch() opaquely consume the
|
||||
// redirect — the SPA needs to leave the protected area.
|
||||
window.location.assign("/login");
|
||||
return r;
|
||||
}),
|
||||
getSessions: (limit = 20, offset = 0) =>
|
||||
fetchJSON<PaginatedSessions>(`/api/sessions?limit=${limit}&offset=${offset}`),
|
||||
getSessionMessages: (id: string) =>
|
||||
|
|
@ -433,6 +454,23 @@ export const api = {
|
|||
}),
|
||||
};
|
||||
|
||||
/** Identity payload returned by ``GET /api/auth/me`` (Phase 7).
|
||||
*
|
||||
* Returned by the dashboard's gated middleware when a valid session cookie
|
||||
* is attached. ``email`` and ``display_name`` are empty strings under the
|
||||
* Nous Portal contract V1 (the access token has no email/name claims —
|
||||
* see Contract Anchor C4 in the plan). The AuthWidget surfaces a
|
||||
* truncated ``user_id`` instead.
|
||||
*/
|
||||
export interface AuthMeResponse {
|
||||
user_id: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
org_id: string;
|
||||
provider: string;
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
export interface ActionResponse {
|
||||
name: string;
|
||||
ok: boolean;
|
||||
|
|
@ -456,6 +494,14 @@ export interface PlatformStatus {
|
|||
|
||||
export interface StatusResponse {
|
||||
active_sessions: number;
|
||||
/** Phase 7: ``true`` when the dashboard's OAuth gate is engaged
|
||||
* (public bind, no ``--insecure``). Read alongside ``auth_providers``
|
||||
* to render a "gated / loopback" badge. */
|
||||
auth_required?: boolean;
|
||||
/** Phase 7: registered ``DashboardAuthProvider`` names (e.g. ``["nous"]``).
|
||||
* Empty in loopback mode; empty + ``auth_required=true`` is a
|
||||
* fail-closed state (the dashboard will refuse to bind). */
|
||||
auth_providers?: string[];
|
||||
config_path: string;
|
||||
config_version: number;
|
||||
env_path: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue