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.
Files changed (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. 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}