diff --git a/README.md b/README.md index 8bcf0d91..a231773c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,31 @@ By default, Agent UI connects to `http://localhost:7777`. You can easily change > **Warning**: Make sure your AgentOS is actually running on the specified endpoint before attempting to connect. -### 3. Test the Connection +### 3. Configure Authentication (Optional) + +If your AgentOS instance requires authentication, you can configure it in two ways: + +#### Option 1: Environment Variable (Recommended) + +Set the `OS_SECURITY_KEY` environment variable: + +```bash +# In your .env.local file or shell environment +NEXT_PUBLIC_OS_SECURITY_KEY=your_auth_token_here +``` + +> **Note**: This uses the same environment variable as AgentOS, so if you're running both on the same machine, you only need to set it once. The token will be automatically loaded when the application starts. + +#### Option 2: UI Configuration + +1. In the left sidebar, locate the "Auth Token" section +2. Click on the token field to edit it +3. Enter your authentication token +4. The token will be securely stored and included in all API requests + +> **Security Note**: Authentication tokens are stored locally in global store and are included as Bearer tokens in API requests to your AgentOS instance. + +### 4. Test the Connection Once you've configured the endpoint: @@ -89,8 +113,6 @@ Once you've configured the endpoint: 2. If successful, you'll see your agents available in the chat interface 3. If there are connection issues, check that your AgentOS is running and accessible. Check out the troubleshooting guide [here](https://docs.agno.com/faq/agentos-connection) - - ## Contributing Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines. diff --git a/src/api/os.ts b/src/api/os.ts index c475932b..12aba26b 100644 --- a/src/api/os.ts +++ b/src/api/os.ts @@ -4,12 +4,29 @@ import { APIRoutes } from './routes' import { AgentDetails, Sessions, TeamDetails } from '@/types/os' +// Helper function to create headers with optional auth token +const createHeaders = (authToken?: string): HeadersInit => { + const headers: HeadersInit = { + 'Content-Type': 'application/json' + } + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}` + } + + return headers +} + export const getAgentsAPI = async ( - endpoint: string + endpoint: string, + authToken?: string ): Promise => { const url = APIRoutes.GetAgents(endpoint) try { - const response = await fetch(url, { method: 'GET' }) + const response = await fetch(url, { + method: 'GET', + headers: createHeaders(authToken) + }) if (!response.ok) { toast.error(`Failed to fetch agents: ${response.statusText}`) return [] @@ -22,9 +39,13 @@ export const getAgentsAPI = async ( } } -export const getStatusAPI = async (base: string): Promise => { +export const getStatusAPI = async ( + base: string, + authToken?: string +): Promise => { const response = await fetch(APIRoutes.Status(base), { - method: 'GET' + method: 'GET', + headers: createHeaders(authToken) }) return response.status } @@ -33,7 +54,8 @@ export const getAllSessionsAPI = async ( base: string, type: 'agent' | 'team', componentId: string, - dbId: string + dbId: string, + authToken?: string ): Promise => { try { const url = new URL(APIRoutes.GetSessions(base)) @@ -42,7 +64,8 @@ export const getAllSessionsAPI = async ( url.searchParams.set('db_id', dbId) const response = await fetch(url.toString(), { - method: 'GET' + method: 'GET', + headers: createHeaders(authToken) }) if (!response.ok) { @@ -61,7 +84,8 @@ export const getSessionAPI = async ( base: string, type: 'agent' | 'team', sessionId: string, - dbId?: string + dbId?: string, + authToken?: string ) => { // build query string const queryParams = new URLSearchParams({ type }) @@ -70,7 +94,8 @@ export const getSessionAPI = async ( const response = await fetch( `${APIRoutes.GetSession(base, sessionId)}?${queryParams.toString()}`, { - method: 'GET' + method: 'GET', + headers: createHeaders(authToken) } ) @@ -84,23 +109,31 @@ export const getSessionAPI = async ( export const deleteSessionAPI = async ( base: string, dbId: string, - sessionId: string + sessionId: string, + authToken?: string ) => { const queryParams = new URLSearchParams() if (dbId) queryParams.append('db_id', dbId) const response = await fetch( `${APIRoutes.DeleteSession(base, sessionId)}?${queryParams.toString()}`, { - method: 'DELETE' + method: 'DELETE', + headers: createHeaders(authToken) } ) return response } -export const getTeamsAPI = async (endpoint: string): Promise => { +export const getTeamsAPI = async ( + endpoint: string, + authToken?: string +): Promise => { const url = APIRoutes.GetTeams(endpoint) try { - const response = await fetch(url, { method: 'GET' }) + const response = await fetch(url, { + method: 'GET', + headers: createHeaders(authToken) + }) if (!response.ok) { toast.error(`Failed to fetch teams: ${response.statusText}`) return [] @@ -117,12 +150,14 @@ export const getTeamsAPI = async (endpoint: string): Promise => { export const deleteTeamSessionAPI = async ( base: string, teamId: string, - sessionId: string + sessionId: string, + authToken?: string ) => { const response = await fetch( APIRoutes.DeleteTeamSession(base, teamId, sessionId), { - method: 'DELETE' + method: 'DELETE', + headers: createHeaders(authToken) } ) diff --git a/src/app/page.tsx b/src/app/page.tsx index fdf149cf..715e052a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,10 +4,13 @@ import { ChatArea } from '@/components/chat/ChatArea' import { Suspense } from 'react' export default function Home() { + // Check if OS_SECURITY_KEY is defined on server-side + const hasEnvToken = !!process.env.NEXT_PUBLIC_OS_SECURITY_KEY + const envToken = process.env.NEXT_PUBLIC_OS_SECURITY_KEY || '' return ( Loading...}>
- +
diff --git a/src/components/chat/Sidebar/AuthToken.tsx b/src/components/chat/Sidebar/AuthToken.tsx new file mode 100644 index 00000000..79152b14 --- /dev/null +++ b/src/components/chat/Sidebar/AuthToken.tsx @@ -0,0 +1,143 @@ +'use client' +import { Button } from '@/components/ui/button' +import { useStore } from '@/store' +import { motion, AnimatePresence } from 'framer-motion' +import { useState, useEffect } from 'react' +import Icon from '@/components/ui/icon' + +const AuthToken = ({ + hasEnvToken, + envToken +}: { + hasEnvToken?: boolean + envToken?: string +}) => { + const { authToken, setAuthToken } = useStore() + const [isEditing, setIsEditing] = useState(false) + const [tokenValue, setTokenValue] = useState('') + const [isMounted, setIsMounted] = useState(false) + const [isHovering, setIsHovering] = useState(false) + + useEffect(() => { + // Initialize with environment variable if available and no token is set + if (hasEnvToken && envToken && !authToken) { + setAuthToken(envToken) + setTokenValue(envToken) + } else { + setTokenValue(authToken) + } + setIsMounted(true) + }, [authToken, setAuthToken, hasEnvToken, envToken]) + + const handleSave = () => { + const cleanToken = tokenValue.trim() + setAuthToken(cleanToken) + setIsEditing(false) + setIsHovering(false) + } + + const handleCancel = () => { + setTokenValue(authToken) + setIsEditing(false) + setIsHovering(false) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSave() + } else if (e.key === 'Escape') { + handleCancel() + } + } + + const handleClear = () => { + setAuthToken('') + setTokenValue('') + } + + const displayValue = authToken + ? `${'*'.repeat(Math.min(authToken.length, 20))}${authToken.length > 20 ? '...' : ''}` + : 'NO TOKEN SET' + + return ( +
+
+ Auth Token +
+ {isEditing ? ( +
+ setTokenValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter authentication token..." + className="flex h-9 w-full items-center text-ellipsis rounded-xl border border-primary/15 bg-accent p-3 text-xs font-medium text-muted placeholder:text-muted/50" + autoFocus + /> + +
+ ) : ( +
+ setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + onClick={() => setIsEditing(true)} + transition={{ type: 'spring', stiffness: 400, damping: 10 }} + > + + {isHovering ? ( + +

+ EDIT TOKEN +

+
+ ) : ( + +

+ {isMounted ? displayValue : 'NO TOKEN SET'} +

+
+ )} +
+
+ {authToken && ( + + )} +
+ )} +
+ ) +} + +export default AuthToken diff --git a/src/components/chat/Sidebar/Sessions/SessionItem.tsx b/src/components/chat/Sidebar/Sessions/SessionItem.tsx index d2eb4c42..d64989ef 100644 --- a/src/components/chat/Sidebar/Sessions/SessionItem.tsx +++ b/src/components/chat/Sidebar/Sessions/SessionItem.tsx @@ -27,6 +27,7 @@ const SessionItem = ({ const [teamId] = useQueryState('team') const [dbId] = useQueryState('db_id') const [, setSessionId] = useQueryState('session') + const authToken = useStore((state) => state.authToken) const { getSession } = useSessionLoader() const { selectedEndpoint, sessionsData, setSessionsData, mode } = useStore() const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) @@ -56,7 +57,8 @@ const SessionItem = ({ const response = await deleteSessionAPI( selectedEndpoint, dbId ?? '', - session_id + session_id, + authToken ) if (response?.ok && sessionsData) { diff --git a/src/components/chat/Sidebar/Sessions/Sessions.tsx b/src/components/chat/Sidebar/Sessions/Sessions.tsx index f2ba6494..88421f76 100644 --- a/src/components/chat/Sidebar/Sessions/Sessions.tsx +++ b/src/components/chat/Sidebar/Sessions/Sessions.tsx @@ -33,7 +33,7 @@ const SkeletonList: FC = ({ skeletonCount }) => { const Sessions = () => { const [agentId] = useQueryState('agent', { - parse: (v) => v || undefined, + parse: (v: string | null) => v || undefined, history: 'push' }) const [teamId] = useQueryState('team') @@ -51,8 +51,6 @@ const Sessions = () => { isSessionsLoading } = useStore() - console.log({ sessionsData }) - const [isScrolling, setIsScrolling] = useState(false) const [selectedSessionId, setSelectedSessionId] = useState( null diff --git a/src/components/chat/Sidebar/Sidebar.tsx b/src/components/chat/Sidebar/Sidebar.tsx index cd6895cf..461651a2 100644 --- a/src/components/chat/Sidebar/Sidebar.tsx +++ b/src/components/chat/Sidebar/Sidebar.tsx @@ -9,6 +9,7 @@ import { useState, useEffect } from 'react' import Icon from '@/components/ui/icon' import { getProviderIcon } from '@/lib/modelProvider' import Sessions from './Sessions' +import AuthToken from './AuthToken' import { isValidUrl } from '@/lib/utils' import { toast } from 'sonner' import { useQueryState } from 'nuqs' @@ -200,7 +201,13 @@ const Endpoint = () => { ) } -const Sidebar = () => { +const Sidebar = ({ + hasEnvToken, + envToken +}: { + hasEnvToken?: boolean + envToken?: string +}) => { const [isCollapsed, setIsCollapsed] = useState(false) const { clearChat, focusChatInput, initialize } = useChatActions() const { @@ -264,6 +271,7 @@ const Sidebar = () => { {isMounted && ( <> + {isEndpointActive && ( <> { const [teamId] = useQueryState('team') const [sessionId, setSessionId] = useQueryState('session') const selectedEndpoint = useStore((state) => state.selectedEndpoint) + const authToken = useStore((state) => state.authToken) const mode = useStore((state) => state.mode) const setStreamingErrorMessage = useStore( (state) => state.setStreamingErrorMessage @@ -162,8 +163,15 @@ const useAIChatStreamHandler = () => { formData.append('stream', 'true') formData.append('session_id', sessionId ?? '') + // Create headers with auth token if available + const headers: Record = {} + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}` + } + await streamResponse({ apiUrl: RunUrl, + headers, requestBody: formData, onChunk: (chunk: RunResponse) => { if ( @@ -424,6 +432,7 @@ const useAIChatStreamHandler = () => { addMessage, updateMessagesWithErrorState, selectedEndpoint, + authToken, streamResponse, agentId, teamId, diff --git a/src/hooks/useChatActions.ts b/src/hooks/useChatActions.ts index f5209477..ae7d8a82 100644 --- a/src/hooks/useChatActions.ts +++ b/src/hooks/useChatActions.ts @@ -10,6 +10,7 @@ import { useQueryState } from 'nuqs' const useChatActions = () => { const { chatInputRef } = useStore() const selectedEndpoint = useStore((state) => state.selectedEndpoint) + const authToken = useStore((state) => state.authToken) const [, setSessionId] = useQueryState('session') const setMessages = useStore((state) => state.setMessages) const setIsEndpointActive = useStore((state) => state.setIsEndpointActive) @@ -24,32 +25,32 @@ const useChatActions = () => { const getStatus = useCallback(async () => { try { - const status = await getStatusAPI(selectedEndpoint) + const status = await getStatusAPI(selectedEndpoint, authToken) return status } catch { return 503 } - }, [selectedEndpoint]) + }, [selectedEndpoint, authToken]) const getAgents = useCallback(async () => { try { - const agents = await getAgentsAPI(selectedEndpoint) + const agents = await getAgentsAPI(selectedEndpoint, authToken) return agents } catch { toast.error('Error fetching agents') return [] } - }, [selectedEndpoint]) + }, [selectedEndpoint, authToken]) const getTeams = useCallback(async () => { try { - const teams = await getTeamsAPI(selectedEndpoint) + const teams = await getTeamsAPI(selectedEndpoint, authToken) return teams } catch { toast.error('Error fetching teams') return [] } - }, [selectedEndpoint]) + }, [selectedEndpoint, authToken]) const clearChat = useCallback(() => { setMessages([]) @@ -81,11 +82,9 @@ const useChatActions = () => { setIsEndpointActive(true) teams = await getTeams() agents = await getAgents() - console.log(' is active', teams, agents) if (!agentId && !teamId) { const currentMode = useStore.getState().mode - console.log('Current mode:', currentMode) if (currentMode === 'team' && teams.length > 0) { const firstTeam = teams[0] diff --git a/src/hooks/useSessionLoader.tsx b/src/hooks/useSessionLoader.tsx index da9f1a1c..947eaf0e 100644 --- a/src/hooks/useSessionLoader.tsx +++ b/src/hooks/useSessionLoader.tsx @@ -27,6 +27,7 @@ interface LoaderArgs { const useSessionLoader = () => { const setMessages = useStore((state) => state.setMessages) const selectedEndpoint = useStore((state) => state.selectedEndpoint) + const authToken = useStore((state) => state.authToken) const setIsSessionsLoading = useStore((state) => state.setIsSessionsLoading) const setSessionsData = useStore((state) => state.setSessionsData) @@ -42,9 +43,9 @@ const useSessionLoader = () => { selectedEndpoint, entityType, selectedId, - dbId + dbId, + authToken ) - console.log('Fetched sessions:', sessions) setSessionsData(sessions.data ?? []) } catch { toast.error('Error loading sessions') @@ -53,7 +54,7 @@ const useSessionLoader = () => { setIsSessionsLoading(false) } }, - [selectedEndpoint, setSessionsData, setIsSessionsLoading] + [selectedEndpoint, authToken, setSessionsData, setIsSessionsLoading] ) const getSession = useCallback( @@ -70,16 +71,15 @@ const useSessionLoader = () => { !dbId ) return - console.log(entityType) try { const response: SessionResponse = await getSessionAPI( selectedEndpoint, entityType, sessionId, - dbId + dbId, + authToken ) - console.log('Fetched session:', response) if (response) { if (Array.isArray(response)) { const messagesFor = response.flatMap((run) => { @@ -163,7 +163,7 @@ const useSessionLoader = () => { return null } }, - [selectedEndpoint, setMessages] + [selectedEndpoint, authToken, setMessages] ) return { getSession, getSessions } diff --git a/src/store.ts b/src/store.ts index 24cf6d87..4056a11f 100644 --- a/src/store.ts +++ b/src/store.ts @@ -36,6 +36,8 @@ interface Store { chatInputRef: React.RefObject selectedEndpoint: string setSelectedEndpoint: (selectedEndpoint: string) => void + authToken: string + setAuthToken: (authToken: string) => void agents: AgentDetails[] setAgents: (agents: AgentDetails[]) => void teams: TeamDetails[] @@ -82,6 +84,8 @@ export const useStore = create()( selectedEndpoint: 'http://localhost:7777', setSelectedEndpoint: (selectedEndpoint) => set(() => ({ selectedEndpoint })), + authToken: '', + setAuthToken: (authToken) => set(() => ({ authToken })), agents: [], setAgents: (agents) => set({ agents }), teams: [],