mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: dashboard OAuth provider management
Add OAuth provider management to the Hermes dashboard with full
lifecycle support for Anthropic (PKCE), Nous and OpenAI Codex
(device-code) flows.
## Backend (hermes_cli/web_server.py)
- 6 new API endpoints:
GET /api/providers/oauth — list providers with connection status
POST /api/providers/oauth/{id}/start — initiate PKCE or device-code
POST /api/providers/oauth/{id}/submit — exchange PKCE auth code
GET /api/providers/oauth/{id}/poll/{session} — poll device-code
DELETE /api/providers/oauth/{id} — disconnect provider
DELETE /api/providers/oauth/sessions/{id} — cancel pending session
- OAuth constants imported from anthropic_adapter (no duplication)
- Blocking I/O wrapped in run_in_executor for async safety
- In-memory session store with 15-minute TTL and automatic GC
- Auth token required on all mutating endpoints
## Frontend
- OAuthLoginModal — PKCE (paste auth code) and device-code (poll) flows
- OAuthProvidersCard — status, token preview, connect/disconnect actions
- Toast fix: createPortal to document.body for correct z-index
- App.tsx: skip animation key bump on initial mount (prevent double-mount)
- Integrated into the Env/Keys page
This commit is contained in:
parent
2773b18b56
commit
247929b0dd
11 changed files with 1789 additions and 96 deletions
|
|
@ -22,7 +22,8 @@ async function getSessionToken(): Promise<string> {
|
|||
|
||||
export const api = {
|
||||
getStatus: () => fetchJSON<StatusResponse>("/api/status"),
|
||||
getSessions: () => fetchJSON<SessionInfo[]>("/api/sessions"),
|
||||
getSessions: (limit = 20, offset = 0) =>
|
||||
fetchJSON<PaginatedSessions>(`/api/sessions?limit=${limit}&offset=${offset}`),
|
||||
getSessionMessages: (id: string) =>
|
||||
fetchJSON<SessionMessagesResponse>(`/api/sessions/${encodeURIComponent(id)}/messages`),
|
||||
deleteSession: (id: string) =>
|
||||
|
|
@ -110,6 +111,62 @@ export const api = {
|
|||
// Session search (FTS5)
|
||||
searchSessions: (q: string) =>
|
||||
fetchJSON<SessionSearchResponse>(`/api/sessions/search?q=${encodeURIComponent(q)}`),
|
||||
|
||||
// OAuth provider management
|
||||
getOAuthProviders: () =>
|
||||
fetchJSON<OAuthProvidersResponse>("/api/providers/oauth"),
|
||||
disconnectOAuthProvider: async (providerId: string) => {
|
||||
const token = await getSessionToken();
|
||||
return fetchJSON<{ ok: boolean; provider: string }>(
|
||||
`/api/providers/oauth/${encodeURIComponent(providerId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
},
|
||||
startOAuthLogin: async (providerId: string) => {
|
||||
const token = await getSessionToken();
|
||||
return fetchJSON<OAuthStartResponse>(
|
||||
`/api/providers/oauth/${encodeURIComponent(providerId)}/start`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: "{}",
|
||||
},
|
||||
);
|
||||
},
|
||||
submitOAuthCode: async (providerId: string, sessionId: string, code: string) => {
|
||||
const token = await getSessionToken();
|
||||
return fetchJSON<OAuthSubmitResponse>(
|
||||
`/api/providers/oauth/${encodeURIComponent(providerId)}/submit`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ session_id: sessionId, code }),
|
||||
},
|
||||
);
|
||||
},
|
||||
pollOAuthSession: (providerId: string, sessionId: string) =>
|
||||
fetchJSON<OAuthPollResponse>(
|
||||
`/api/providers/oauth/${encodeURIComponent(providerId)}/poll/${encodeURIComponent(sessionId)}`,
|
||||
),
|
||||
cancelOAuthSession: async (sessionId: string) => {
|
||||
const token = await getSessionToken();
|
||||
return fetchJSON<{ ok: boolean }>(
|
||||
`/api/providers/oauth/sessions/${encodeURIComponent(sessionId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export interface PlatformStatus {
|
||||
|
|
@ -152,6 +209,13 @@ export interface SessionInfo {
|
|||
preview: string | null;
|
||||
}
|
||||
|
||||
export interface PaginatedSessions {
|
||||
sessions: SessionInfo[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface EnvVarInfo {
|
||||
is_set: boolean;
|
||||
redacted_value: string | null;
|
||||
|
|
@ -260,3 +324,61 @@ export interface SessionSearchResult {
|
|||
export interface SessionSearchResponse {
|
||||
results: SessionSearchResult[];
|
||||
}
|
||||
|
||||
// ── OAuth provider types ────────────────────────────────────────────────
|
||||
|
||||
export interface OAuthProviderStatus {
|
||||
logged_in: boolean;
|
||||
source?: string | null;
|
||||
source_label?: string | null;
|
||||
token_preview?: string | null;
|
||||
expires_at?: string | null;
|
||||
has_refresh_token?: boolean;
|
||||
last_refresh?: string | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface OAuthProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
/** "pkce" (browser redirect + paste code), "device_code" (show code + URL),
|
||||
* or "external" (delegated to a separate CLI like Claude Code or Qwen). */
|
||||
flow: "pkce" | "device_code" | "external";
|
||||
cli_command: string;
|
||||
docs_url: string;
|
||||
status: OAuthProviderStatus;
|
||||
}
|
||||
|
||||
export interface OAuthProvidersResponse {
|
||||
providers: OAuthProvider[];
|
||||
}
|
||||
|
||||
/** Discriminated union — the shape of /start depends on the flow. */
|
||||
export type OAuthStartResponse =
|
||||
| {
|
||||
session_id: string;
|
||||
flow: "pkce";
|
||||
auth_url: string;
|
||||
expires_in: number;
|
||||
}
|
||||
| {
|
||||
session_id: string;
|
||||
flow: "device_code";
|
||||
user_code: string;
|
||||
verification_url: string;
|
||||
expires_in: number;
|
||||
poll_interval: number;
|
||||
};
|
||||
|
||||
export interface OAuthSubmitResponse {
|
||||
ok: boolean;
|
||||
status: "approved" | "error";
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface OAuthPollResponse {
|
||||
session_id: string;
|
||||
status: "pending" | "approved" | "denied" | "expired" | "error";
|
||||
error_message?: string | null;
|
||||
expires_at?: number | null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue