codetether 1.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- a2a_server/__init__.py +29 -0
- a2a_server/a2a_agent_card.py +365 -0
- a2a_server/a2a_errors.py +1133 -0
- a2a_server/a2a_executor.py +926 -0
- a2a_server/a2a_router.py +1033 -0
- a2a_server/a2a_types.py +344 -0
- a2a_server/agent_card.py +408 -0
- a2a_server/agents_server.py +271 -0
- a2a_server/auth_api.py +349 -0
- a2a_server/billing_api.py +638 -0
- a2a_server/billing_service.py +712 -0
- a2a_server/billing_webhooks.py +501 -0
- a2a_server/config.py +96 -0
- a2a_server/database.py +2165 -0
- a2a_server/email_inbound.py +398 -0
- a2a_server/email_notifications.py +486 -0
- a2a_server/enhanced_agents.py +919 -0
- a2a_server/enhanced_server.py +160 -0
- a2a_server/hosted_worker.py +1049 -0
- a2a_server/integrated_agents_server.py +347 -0
- a2a_server/keycloak_auth.py +750 -0
- a2a_server/livekit_bridge.py +439 -0
- a2a_server/marketing_tools.py +1364 -0
- a2a_server/mcp_client.py +196 -0
- a2a_server/mcp_http_server.py +2256 -0
- a2a_server/mcp_server.py +191 -0
- a2a_server/message_broker.py +725 -0
- a2a_server/mock_mcp.py +273 -0
- a2a_server/models.py +494 -0
- a2a_server/monitor_api.py +5904 -0
- a2a_server/opencode_bridge.py +1594 -0
- a2a_server/redis_task_manager.py +518 -0
- a2a_server/server.py +726 -0
- a2a_server/task_manager.py +668 -0
- a2a_server/task_queue.py +742 -0
- a2a_server/tenant_api.py +333 -0
- a2a_server/tenant_middleware.py +219 -0
- a2a_server/tenant_service.py +760 -0
- a2a_server/user_auth.py +721 -0
- a2a_server/vault_client.py +576 -0
- a2a_server/worker_sse.py +873 -0
- agent_worker/__init__.py +8 -0
- agent_worker/worker.py +4877 -0
- codetether/__init__.py +10 -0
- codetether/__main__.py +4 -0
- codetether/cli.py +112 -0
- codetether/worker_cli.py +57 -0
- codetether-1.2.2.dist-info/METADATA +570 -0
- codetether-1.2.2.dist-info/RECORD +66 -0
- codetether-1.2.2.dist-info/WHEEL +5 -0
- codetether-1.2.2.dist-info/entry_points.txt +4 -0
- codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
- codetether-1.2.2.dist-info/top_level.txt +5 -0
- codetether_voice_agent/__init__.py +6 -0
- codetether_voice_agent/agent.py +445 -0
- codetether_voice_agent/codetether_mcp.py +345 -0
- codetether_voice_agent/config.py +16 -0
- codetether_voice_agent/functiongemma_caller.py +380 -0
- codetether_voice_agent/session_playback.py +247 -0
- codetether_voice_agent/tools/__init__.py +21 -0
- codetether_voice_agent/tools/definitions.py +135 -0
- codetether_voice_agent/tools/handlers.py +380 -0
- run_server.py +314 -0
- ui/monitor-tailwind.html +1790 -0
- ui/monitor.html +1775 -0
- ui/monitor.js +2662 -0
a2a_server/auth_api.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication API endpoints for A2A Server.
|
|
3
|
+
|
|
4
|
+
Provides REST endpoints for:
|
|
5
|
+
- User login/logout
|
|
6
|
+
- Token refresh
|
|
7
|
+
- Session management
|
|
8
|
+
- User-codebase associations
|
|
9
|
+
- Cross-device sync
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional, Dict, Any, List
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, HTTPException, Depends, Query
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from .keycloak_auth import (
|
|
20
|
+
keycloak_auth,
|
|
21
|
+
get_current_user,
|
|
22
|
+
require_auth,
|
|
23
|
+
UserSession,
|
|
24
|
+
UserCodebaseAssociation,
|
|
25
|
+
UserAgentSession,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
router = APIRouter(prefix='/v1/auth', tags=['Authentication'])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Request/Response Models
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LoginRequest(BaseModel):
|
|
37
|
+
username: str
|
|
38
|
+
password: str
|
|
39
|
+
device_id: Optional[str] = None
|
|
40
|
+
device_name: Optional[str] = None
|
|
41
|
+
device_type: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LoginResponse(BaseModel):
|
|
45
|
+
access_token: str = Field(..., alias='accessToken')
|
|
46
|
+
refresh_token: Optional[str] = Field(None, alias='refreshToken')
|
|
47
|
+
expires_at: str = Field(..., alias='expiresAt')
|
|
48
|
+
session: Dict[str, Any]
|
|
49
|
+
|
|
50
|
+
class Config:
|
|
51
|
+
populate_by_name = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RefreshRequest(BaseModel):
|
|
55
|
+
refresh_token: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RefreshResponse(BaseModel):
|
|
59
|
+
access_token: str = Field(..., alias='accessToken')
|
|
60
|
+
refresh_token: Optional[str] = Field(None, alias='refreshToken')
|
|
61
|
+
expires_at: str = Field(..., alias='expiresAt')
|
|
62
|
+
session: Dict[str, Any]
|
|
63
|
+
|
|
64
|
+
class Config:
|
|
65
|
+
populate_by_name = True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class LogoutRequest(BaseModel):
|
|
69
|
+
session_id: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AuthStatusResponse(BaseModel):
|
|
73
|
+
available: bool
|
|
74
|
+
keycloak_url: str
|
|
75
|
+
realm: str
|
|
76
|
+
client_id: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CodebaseAssociationRequest(BaseModel):
|
|
80
|
+
codebase_id: str
|
|
81
|
+
role: str = 'owner'
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AgentSessionRequest(BaseModel):
|
|
85
|
+
codebase_id: str
|
|
86
|
+
agent_type: str = 'build'
|
|
87
|
+
device_id: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Endpoints
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.get('/status', response_model=AuthStatusResponse)
|
|
94
|
+
async def get_auth_status():
|
|
95
|
+
"""Check if authentication service is available."""
|
|
96
|
+
return AuthStatusResponse(
|
|
97
|
+
available=True,
|
|
98
|
+
keycloak_url=keycloak_auth.keycloak_url,
|
|
99
|
+
realm=keycloak_auth.realm,
|
|
100
|
+
client_id=keycloak_auth.client_id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.post('/login', response_model=LoginResponse)
|
|
105
|
+
async def login(request: LoginRequest):
|
|
106
|
+
"""Authenticate user with username and password."""
|
|
107
|
+
try:
|
|
108
|
+
device_info = {}
|
|
109
|
+
if request.device_id:
|
|
110
|
+
device_info['device_id'] = request.device_id
|
|
111
|
+
if request.device_name:
|
|
112
|
+
device_info['device_name'] = request.device_name
|
|
113
|
+
if request.device_type:
|
|
114
|
+
device_info['device_type'] = request.device_type
|
|
115
|
+
|
|
116
|
+
session = await keycloak_auth.authenticate_password(
|
|
117
|
+
username=request.username,
|
|
118
|
+
password=request.password,
|
|
119
|
+
device_info=device_info,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return LoginResponse(
|
|
123
|
+
accessToken=session.access_token,
|
|
124
|
+
refreshToken=session.refresh_token,
|
|
125
|
+
expiresAt=session.expires_at.isoformat(),
|
|
126
|
+
session={
|
|
127
|
+
'userId': session.user_id,
|
|
128
|
+
'sessionId': session.session_id,
|
|
129
|
+
'email': session.email,
|
|
130
|
+
'username': session.username,
|
|
131
|
+
'name': session.name,
|
|
132
|
+
'roles': session.roles,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
except HTTPException:
|
|
136
|
+
raise
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f'Login error: {e}')
|
|
139
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@router.post('/refresh', response_model=RefreshResponse)
|
|
143
|
+
async def refresh_token(request: RefreshRequest):
|
|
144
|
+
"""Refresh an access token using refresh token."""
|
|
145
|
+
try:
|
|
146
|
+
session = await keycloak_auth.refresh_session(request.refresh_token)
|
|
147
|
+
|
|
148
|
+
return RefreshResponse(
|
|
149
|
+
accessToken=session.access_token,
|
|
150
|
+
refreshToken=session.refresh_token,
|
|
151
|
+
expiresAt=session.expires_at.isoformat(),
|
|
152
|
+
session={
|
|
153
|
+
'userId': session.user_id,
|
|
154
|
+
'sessionId': session.session_id,
|
|
155
|
+
'email': session.email,
|
|
156
|
+
'username': session.username,
|
|
157
|
+
'name': session.name,
|
|
158
|
+
'roles': session.roles,
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
except HTTPException:
|
|
162
|
+
raise
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f'Token refresh error: {e}')
|
|
165
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@router.post('/logout')
|
|
169
|
+
async def logout(request: LogoutRequest):
|
|
170
|
+
"""Logout and invalidate session."""
|
|
171
|
+
await keycloak_auth.logout(request.session_id)
|
|
172
|
+
return {'success': True, 'message': 'Logged out successfully'}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@router.get('/me')
|
|
176
|
+
async def get_current_user_info(user: UserSession = Depends(require_auth)):
|
|
177
|
+
"""Get current authenticated user info."""
|
|
178
|
+
return {
|
|
179
|
+
'userId': user.user_id,
|
|
180
|
+
'sessionId': user.session_id,
|
|
181
|
+
'email': user.email,
|
|
182
|
+
'username': user.username,
|
|
183
|
+
'name': user.name,
|
|
184
|
+
'roles': user.roles,
|
|
185
|
+
'expiresAt': user.expires_at.isoformat(),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@router.get('/sync')
|
|
190
|
+
async def get_sync_state(
|
|
191
|
+
user_id: str = Query(..., description='User ID to sync'),
|
|
192
|
+
user: Optional[UserSession] = Depends(get_current_user),
|
|
193
|
+
):
|
|
194
|
+
"""Get synchronized state across all devices for a user."""
|
|
195
|
+
# Allow if authenticated user matches or no auth required for their own data
|
|
196
|
+
if user and user.user_id != user_id:
|
|
197
|
+
raise HTTPException(
|
|
198
|
+
status_code=403, detail="Cannot access other user's sync state"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return keycloak_auth.sync_session_state(user_id)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# User-Codebase Associations
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.get('/user/{user_id}/codebases')
|
|
208
|
+
async def get_user_codebases(
|
|
209
|
+
user_id: str,
|
|
210
|
+
user: Optional[UserSession] = Depends(get_current_user),
|
|
211
|
+
) -> List[Dict[str, Any]]:
|
|
212
|
+
"""Get all codebases associated with a user."""
|
|
213
|
+
if user and user.user_id != user_id:
|
|
214
|
+
raise HTTPException(
|
|
215
|
+
status_code=403, detail="Cannot access other user's codebases"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
associations = keycloak_auth.get_user_codebases(user_id)
|
|
219
|
+
return [a.to_dict() for a in associations]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@router.post('/user/{user_id}/codebases')
|
|
223
|
+
async def associate_codebase(
|
|
224
|
+
user_id: str,
|
|
225
|
+
request: CodebaseAssociationRequest,
|
|
226
|
+
user: UserSession = Depends(require_auth),
|
|
227
|
+
):
|
|
228
|
+
"""Associate a codebase with a user."""
|
|
229
|
+
if user.user_id != user_id:
|
|
230
|
+
raise HTTPException(
|
|
231
|
+
status_code=403, detail="Cannot modify other user's codebases"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Note: In a real implementation, we'd look up codebase details
|
|
235
|
+
association = keycloak_auth.associate_codebase(
|
|
236
|
+
user_id=user_id,
|
|
237
|
+
codebase_id=request.codebase_id,
|
|
238
|
+
codebase_name=request.codebase_id, # Would be fetched from codebase
|
|
239
|
+
codebase_path='', # Would be fetched from codebase
|
|
240
|
+
role=request.role,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return {'success': True, 'association': association.to_dict()}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@router.delete('/user/{user_id}/codebases/{codebase_id}')
|
|
247
|
+
async def remove_codebase_association(
|
|
248
|
+
user_id: str,
|
|
249
|
+
codebase_id: str,
|
|
250
|
+
user: UserSession = Depends(require_auth),
|
|
251
|
+
):
|
|
252
|
+
"""Remove a codebase association from a user."""
|
|
253
|
+
if user.user_id != user_id:
|
|
254
|
+
raise HTTPException(
|
|
255
|
+
status_code=403, detail="Cannot modify other user's codebases"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
removed = keycloak_auth.remove_codebase_association(user_id, codebase_id)
|
|
259
|
+
return {'success': removed}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# Agent Sessions
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@router.get('/user/{user_id}/agent-sessions')
|
|
266
|
+
async def get_user_agent_sessions(
|
|
267
|
+
user_id: str,
|
|
268
|
+
user: Optional[UserSession] = Depends(get_current_user),
|
|
269
|
+
) -> List[Dict[str, Any]]:
|
|
270
|
+
"""Get all agent sessions for a user."""
|
|
271
|
+
if user and user.user_id != user_id:
|
|
272
|
+
raise HTTPException(
|
|
273
|
+
status_code=403, detail="Cannot access other user's sessions"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
sessions = keycloak_auth.get_user_agent_sessions(user_id)
|
|
277
|
+
return [s.to_dict() for s in sessions]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@router.post('/user/{user_id}/agent-sessions')
|
|
281
|
+
async def create_agent_session(
|
|
282
|
+
user_id: str,
|
|
283
|
+
codebase_id: str = Query(...),
|
|
284
|
+
agent_type: str = Query('build'),
|
|
285
|
+
device_id: Optional[str] = Query(None),
|
|
286
|
+
user: UserSession = Depends(require_auth),
|
|
287
|
+
):
|
|
288
|
+
"""Create a new agent session for a user."""
|
|
289
|
+
if user.user_id != user_id:
|
|
290
|
+
raise HTTPException(
|
|
291
|
+
status_code=403, detail='Cannot create session for other user'
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
session = keycloak_auth.create_agent_session(
|
|
295
|
+
user_id=user_id,
|
|
296
|
+
codebase_id=codebase_id,
|
|
297
|
+
agent_type=agent_type,
|
|
298
|
+
device_id=device_id,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return {'success': True, 'session': session.to_dict()}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@router.delete('/user/{user_id}/agent-sessions/{session_id}')
|
|
305
|
+
async def close_agent_session(
|
|
306
|
+
user_id: str,
|
|
307
|
+
session_id: str,
|
|
308
|
+
user: UserSession = Depends(require_auth),
|
|
309
|
+
):
|
|
310
|
+
"""Close an agent session."""
|
|
311
|
+
if user.user_id != user_id:
|
|
312
|
+
raise HTTPException(
|
|
313
|
+
status_code=403, detail="Cannot close other user's session"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
keycloak_auth.close_agent_session(session_id)
|
|
317
|
+
return {'success': True}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Session management
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@router.get('/sessions')
|
|
324
|
+
async def get_active_sessions(user: UserSession = Depends(require_auth)):
|
|
325
|
+
"""Get all active sessions for current user."""
|
|
326
|
+
sessions = keycloak_auth.get_active_sessions_for_user(user.user_id)
|
|
327
|
+
return {
|
|
328
|
+
'sessions': [s.to_dict() for s in sessions],
|
|
329
|
+
'count': len(sessions),
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@router.delete('/sessions/{session_id}')
|
|
334
|
+
async def invalidate_session(
|
|
335
|
+
session_id: str,
|
|
336
|
+
user: UserSession = Depends(require_auth),
|
|
337
|
+
):
|
|
338
|
+
"""Invalidate a specific session."""
|
|
339
|
+
target_session = await keycloak_auth.get_session(session_id)
|
|
340
|
+
if not target_session:
|
|
341
|
+
raise HTTPException(status_code=404, detail='Session not found')
|
|
342
|
+
|
|
343
|
+
if target_session.user_id != user.user_id:
|
|
344
|
+
raise HTTPException(
|
|
345
|
+
status_code=403, detail="Cannot invalidate other user's session"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
await keycloak_auth.logout(session_id)
|
|
349
|
+
return {'success': True}
|