Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,29 @@ 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
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 your browser and are included as Bearer tokens in API requests to your AgentOS instance.

### 4. Test the Connection

Once you've configured the endpoint:

Expand Down
63 changes: 49 additions & 14 deletions src/api/os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentDetails[]> => {
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 []
Expand All @@ -22,9 +39,13 @@ export const getAgentsAPI = async (
}
}

export const getStatusAPI = async (base: string): Promise<number> => {
export const getStatusAPI = async (
base: string,
authToken?: string
): Promise<number> => {
const response = await fetch(APIRoutes.Status(base), {
method: 'GET'
method: 'GET',
headers: createHeaders(authToken)
})
return response.status
}
Expand All @@ -33,7 +54,8 @@ export const getAllSessionsAPI = async (
base: string,
type: 'agent' | 'team',
componentId: string,
dbId: string
dbId: string,
authToken?: string
): Promise<Sessions | { data: [] }> => {
try {
const url = new URL(APIRoutes.GetSessions(base))
Expand All @@ -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) {
Expand All @@ -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 })
Expand All @@ -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)
}
)

Expand All @@ -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<TeamDetails[]> => {
export const getTeamsAPI = async (
endpoint: string,
authToken?: string
): Promise<TeamDetails[]> => {
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 []
Expand All @@ -117,12 +150,14 @@ export const getTeamsAPI = async (endpoint: string): Promise<TeamDetails[]> => {
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)
}
)

Expand Down
6 changes: 5 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ 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.OS_SECURITY_KEY
const envToken = process.env.OS_SECURITY_KEY || ''

return (
<Suspense fallback={<div>Loading...</div>}>
<div className="flex h-screen bg-background/80">
<Sidebar />
<Sidebar hasEnvToken={hasEnvToken} envToken={envToken} />
<ChatArea />
</div>
</Suspense>
Expand Down
146 changes: 146 additions & 0 deletions src/components/chat/Sidebar/AuthToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'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<HTMLInputElement>) => {
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 (
<div className="flex flex-col items-start gap-2">
<div className="text-xs font-medium uppercase text-primary">
Auth Token
</div>
{isEditing ? (
<div className="flex w-full items-center gap-1">
<input
type="password"
value={tokenValue}
onChange={(e) => 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
/>
<Button
variant="ghost"
size="icon"
onClick={handleSave}
className="hover:cursor-pointer hover:bg-transparent"
>
<Icon type="save" size="xs" />
</Button>
</div>
) : (
<div className="flex w-full items-center gap-1">
<motion.div
className="relative flex h-9 w-full cursor-pointer items-center justify-between rounded-xl border border-primary/15 bg-accent p-3 uppercase"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onClick={() => setIsEditing(true)}
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
>
<AnimatePresence mode="wait">
{isHovering ? (
<motion.div
key="token-display-hover"
className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<p className="flex items-center gap-2 whitespace-nowrap text-xs font-medium text-primary">
<Icon type="edit" size="xxs" /> EDIT TOKEN
</p>
</motion.div>
) : (
<motion.div
key="token-display"
className="absolute inset-0 flex items-center justify-between px-3"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<p className="text-xs font-medium text-muted">
{isMounted ? displayValue : 'NO TOKEN SET'}
</p>
{authToken && (
<div className="size-2 shrink-0 rounded-full bg-positive" />
)}
</motion.div>
)}
</AnimatePresence>
</motion.div>
{authToken && (
<Button
variant="ghost"
size="icon"
onClick={handleClear}
className="hover:cursor-pointer hover:bg-transparent"
title="Clear token"
>
<Icon type="x" size="xs" />
</Button>
)}
</div>
)}
</div>
)
}

export default AuthToken
10 changes: 9 additions & 1 deletion src/components/chat/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -264,6 +271,7 @@ const Sidebar = () => {
{isMounted && (
<>
<Endpoint />
<AuthToken hasEnvToken={hasEnvToken} envToken={envToken} />
{isEndpointActive && (
<>
<motion.div
Expand Down
9 changes: 9 additions & 0 deletions src/hooks/useAIStreamHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const useAIChatStreamHandler = () => {
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
Expand Down Expand Up @@ -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<string, string> = {}
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`
}

await streamResponse({
apiUrl: RunUrl,
headers,
requestBody: formData,
onChunk: (chunk: RunResponse) => {
if (
Expand Down Expand Up @@ -424,6 +432,7 @@ const useAIChatStreamHandler = () => {
addMessage,
updateMessagesWithErrorState,
selectedEndpoint,
authToken,
streamResponse,
agentId,
teamId,
Expand Down
Loading