Skip to content

Commit 6cfff4c

Browse files
Merge pull request #3 from EvolutionAPI/develop
chore(changelog): add entry for API key sharing and flexible authentication in version 0.0.9
2 parents ef5e848 + 98c559e commit 6cfff4c

File tree

4 files changed

+165
-61
lines changed

4 files changed

+165
-61
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [0.0.9] - 2025-05-13
99

10+
### Added
11+
12+
- Add API key sharing and flexible authentication for chat routes
13+
1014
### Changed
1115

1216
- Enhance user authentication with detailed error handling

src/api/agent_routes.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,3 +560,64 @@ async def delete_agent(
560560
raise HTTPException(
561561
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
562562
)
563+
564+
565+
@router.post("/{agent_id}/share", response_model=Dict[str, str])
566+
async def share_agent(
567+
agent_id: uuid.UUID,
568+
x_client_id: uuid.UUID = Header(..., alias="x-client-id"),
569+
db: Session = Depends(get_db),
570+
payload: dict = Depends(get_jwt_token),
571+
):
572+
"""Returns the agent's API key for sharing"""
573+
await verify_user_client(payload, db, x_client_id)
574+
575+
# Verify if the agent exists
576+
agent = agent_service.get_agent(db, agent_id)
577+
if not agent:
578+
raise HTTPException(
579+
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
580+
)
581+
582+
# Verify if the agent belongs to the specified client
583+
if agent.client_id != x_client_id:
584+
raise HTTPException(
585+
status_code=status.HTTP_403_FORBIDDEN,
586+
detail="Agent does not belong to the specified client",
587+
)
588+
589+
# Verify if API key exists
590+
if not agent.config or not agent.config.get("api_key"):
591+
raise HTTPException(
592+
status_code=status.HTTP_400_BAD_REQUEST,
593+
detail="This agent does not have an API key",
594+
)
595+
596+
return {"api_key": agent.config["api_key"]}
597+
598+
599+
@router.get("/{agent_id}/shared", response_model=Agent)
600+
async def get_shared_agent(
601+
agent_id: uuid.UUID,
602+
api_key: str = Header(..., alias="x-api-key"),
603+
db: Session = Depends(get_db),
604+
):
605+
"""Get agent details using only API key authentication"""
606+
# Verify if the agent exists
607+
agent = agent_service.get_agent(db, agent_id)
608+
if not agent or not agent.config:
609+
raise HTTPException(
610+
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
611+
)
612+
613+
# Verify if the API key matches
614+
if not agent.config.get("api_key") or agent.config.get("api_key") != api_key:
615+
raise HTTPException(
616+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key"
617+
)
618+
619+
# Add agent card URL if not present
620+
if not agent.agent_card_url:
621+
agent.agent_card_url = agent.agent_card_url_property
622+
623+
return agent

src/api/chat_routes.py

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@
2727
└──────────────────────────────────────────────────────────────────────────────┘
2828
"""
2929

30+
import uuid
3031
from fastapi import (
3132
APIRouter,
3233
Depends,
3334
HTTPException,
3435
status,
3536
WebSocket,
3637
WebSocketDisconnect,
38+
Header,
3739
)
3840
from sqlalchemy.orm import Session
3941
from src.config.database import get_db
@@ -57,6 +59,7 @@
5759
from datetime import datetime
5860
import logging
5961
import json
62+
from typing import Optional, Dict
6063

6164
logger = logging.getLogger(__name__)
6265

@@ -67,6 +70,59 @@
6770
)
6871

6972

73+
async def get_agent_by_api_key(
74+
agent_id: str,
75+
api_key: Optional[str] = Header(None, alias="x-api-key"),
76+
authorization: Optional[str] = Header(None),
77+
db: Session = Depends(get_db),
78+
):
79+
"""Flexible authentication for chat routes, allowing JWT or API key"""
80+
if authorization:
81+
# Try to authenticate with JWT token first
82+
try:
83+
# Extract token from Authorization header if needed
84+
token = (
85+
authorization.replace("Bearer ", "")
86+
if authorization.startswith("Bearer ")
87+
else authorization
88+
)
89+
payload = await get_jwt_token(token)
90+
agent = agent_service.get_agent(db, agent_id)
91+
if not agent:
92+
raise HTTPException(
93+
status_code=status.HTTP_404_NOT_FOUND,
94+
detail="Agent not found",
95+
)
96+
97+
# Verify if the user has access to the agent's client
98+
await verify_user_client(payload, db, agent.client_id)
99+
return agent
100+
except Exception as e:
101+
logger.warning(f"JWT authentication failed: {str(e)}")
102+
# If JWT fails, continue to try with API key
103+
104+
# Try to authenticate with API key
105+
if not api_key:
106+
raise HTTPException(
107+
status_code=status.HTTP_401_UNAUTHORIZED,
108+
detail="Authentication required (JWT or API key)",
109+
)
110+
111+
agent = agent_service.get_agent(db, agent_id)
112+
if not agent or not agent.config:
113+
raise HTTPException(
114+
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
115+
)
116+
117+
# Verify if the API key matches
118+
if not agent.config.get("api_key") or agent.config.get("api_key") != api_key:
119+
raise HTTPException(
120+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key"
121+
)
122+
123+
return agent
124+
125+
70126
@router.websocket("/ws/{agent_id}/{external_id}")
71127
async def websocket_chat(
72128
websocket: WebSocket,
@@ -82,32 +138,49 @@ async def websocket_chat(
82138
# Wait for authentication message
83139
try:
84140
auth_data = await websocket.receive_json()
85-
logger.info(f"Received authentication data: {auth_data}")
141+
logger.info(f"Authentication data received: {auth_data}")
86142

87143
if not (
88-
auth_data.get("type") == "authorization" and auth_data.get("token")
144+
auth_data.get("type") == "authorization"
145+
and (auth_data.get("token") or auth_data.get("api_key"))
89146
):
90147
logger.warning("Invalid authentication message")
91148
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
92149
return
93150

94-
token = auth_data["token"]
95-
# Verify the token
96-
payload = await get_jwt_token_ws(token)
97-
if not payload:
98-
logger.warning("Invalid token")
99-
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
100-
return
101-
102-
# Verify if the agent belongs to the user's client
151+
# Verify if the agent exists
103152
agent = agent_service.get_agent(db, agent_id)
104153
if not agent:
105154
logger.warning(f"Agent {agent_id} not found")
106155
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
107156
return
108157

109-
# Verify if the user has access to the agent (via client)
110-
await verify_user_client(payload, db, agent.client_id)
158+
# Verify authentication
159+
is_authenticated = False
160+
161+
# Try with JWT token
162+
if auth_data.get("token"):
163+
try:
164+
payload = await get_jwt_token_ws(auth_data["token"])
165+
if payload:
166+
# Verify if the user has access to the agent
167+
await verify_user_client(payload, db, agent.client_id)
168+
is_authenticated = True
169+
except Exception as e:
170+
logger.warning(f"JWT authentication failed: {str(e)}")
171+
172+
# If JWT fails, try with API key
173+
if not is_authenticated and auth_data.get("api_key"):
174+
if agent.config and agent.config.get("api_key") == auth_data.get(
175+
"api_key"
176+
):
177+
is_authenticated = True
178+
else:
179+
logger.warning("Invalid API key")
180+
181+
if not is_authenticated:
182+
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
183+
return
111184

112185
logger.info(
113186
f"WebSocket connection established for agent {agent_id} and external_id {external_id}"
@@ -174,19 +247,9 @@ async def websocket_chat(
174247
)
175248
async def chat(
176249
request: ChatRequest,
250+
_=Depends(get_agent_by_api_key),
177251
db: Session = Depends(get_db),
178-
payload: dict = Depends(get_jwt_token),
179252
):
180-
# Verify if the agent belongs to the user's client
181-
agent = agent_service.get_agent(db, request.agent_id)
182-
if not agent:
183-
raise HTTPException(
184-
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
185-
)
186-
187-
# Verify if the user has access to the agent (via client)
188-
await verify_user_client(payload, db, agent.client_id)
189-
190253
try:
191254
final_response = await run_agent(
192255
request.agent_id,

src/services/a2a_task_manager.py

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -411,37 +411,14 @@ async def _stream_task_process(
411411
):
412412
try:
413413
chunk_data = json.loads(chunk)
414-
except Exception as e:
415-
logger.warning(f"Invalid chunk received: {chunk} - {e}")
416-
continue
417414

418-
if (
419-
isinstance(chunk_data, dict)
420-
and "type" in chunk_data
421-
and chunk_data["type"]
422-
in [
423-
"history",
424-
"history_update",
425-
"history_complete",
426-
]
427-
):
428-
continue
429-
430-
if isinstance(chunk_data, dict):
431-
if "type" not in chunk_data and "text" in chunk_data:
432-
chunk_data["type"] = "text"
415+
if isinstance(chunk_data, dict) and "content" in chunk_data:
416+
content = chunk_data.get("content", {})
417+
role = content.get("role", "agent")
418+
parts = content.get("parts", [])
433419

434-
if "type" in chunk_data:
435-
try:
436-
update_message = Message(role="agent", parts=[chunk_data])
437-
438-
await self.update_store(
439-
request.params.id,
440-
TaskStatus(
441-
state=TaskState.WORKING, message=update_message
442-
),
443-
update_history=False,
444-
)
420+
if parts:
421+
update_message = Message(role=role, parts=parts)
445422

446423
yield SendTaskStreamingResponse(
447424
id=request.id,
@@ -455,14 +432,13 @@ async def _stream_task_process(
455432
),
456433
)
457434

458-
if chunk_data.get("type") == "text":
459-
full_response += chunk_data.get("text", "")
460-
final_message = update_message
461-
462-
except Exception as e:
463-
logger.error(
464-
f"Error processing chunk: {e}, chunk: {chunk_data}"
465-
)
435+
for part in parts:
436+
if part.get("type") == "text":
437+
full_response += part.get("text", "")
438+
final_message = update_message
439+
except Exception as e:
440+
logger.error(f"Error processing chunk: {e}, chunk: {chunk}")
441+
continue
466442

467443
# Determine the final state of the task
468444
task_state = (

0 commit comments

Comments
 (0)