django-agent-studio 0.3.0__py3-none-any.whl → 0.3.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.
@@ -24,6 +24,10 @@ from django_agent_runtime.models import (
24
24
  AgentSystemMember,
25
25
  AgentSystemVersion,
26
26
  AgentSystemSnapshot,
27
+ # Collaborator models
28
+ CollaboratorRole,
29
+ AgentCollaborator,
30
+ SystemCollaborator,
27
31
  )
28
32
  from django_agent_studio.api.serializers import (
29
33
  AgentDefinitionListSerializer,
@@ -36,6 +40,11 @@ from django_agent_studio.api.serializers import (
36
40
  DynamicToolExecutionSerializer,
37
41
  ProjectScanRequestSerializer,
38
42
  GenerateToolsRequestSerializer,
43
+ # Collaborator serializers
44
+ AgentCollaboratorSerializer,
45
+ SystemCollaboratorSerializer,
46
+ AddCollaboratorSerializer,
47
+ UpdateCollaboratorRoleSerializer,
39
48
  )
40
49
 
41
50
 
@@ -43,16 +52,49 @@ from django_agent_studio.api.serializers import (
43
52
  # Helper functions for consistent access control
44
53
  # =============================================================================
45
54
 
55
+ from django.db.models import Q
56
+
57
+
46
58
  def get_agent_for_user(user, agent_id):
47
59
  """
48
60
  Get an agent if the user has access.
49
61
 
50
62
  - Superusers can access any agent
51
- - Regular users can only access their own agents
63
+ - Owners can access their own agents
64
+ - Collaborators can access agents they've been granted access to
65
+ - Users with system access can access agents in that system
52
66
  """
53
67
  if user.is_superuser:
54
68
  return get_object_or_404(AgentDefinition, id=agent_id)
55
- return get_object_or_404(AgentDefinition, id=agent_id, owner=user)
69
+
70
+ # Try to get the agent
71
+ agent = get_object_or_404(AgentDefinition, id=agent_id)
72
+
73
+ # Check if owner
74
+ if agent.owner == user:
75
+ return agent
76
+
77
+ # Check if direct collaborator on agent
78
+ if AgentCollaborator.objects.filter(agent=agent, user=user).exists():
79
+ return agent
80
+
81
+ # Check if user has access via a system that contains this agent
82
+ # Get all systems this agent belongs to
83
+ agent_system_ids = AgentSystemMember.objects.filter(
84
+ agent=agent
85
+ ).values_list('system_id', flat=True)
86
+
87
+ if agent_system_ids:
88
+ # Check if user owns or is a collaborator on any of these systems
89
+ has_system_access = AgentSystem.objects.filter(
90
+ Q(id__in=agent_system_ids) &
91
+ (Q(owner=user) | Q(collaborators__user=user))
92
+ ).exists()
93
+ if has_system_access:
94
+ return agent
95
+
96
+ # No access
97
+ raise Http404("Agent not found")
56
98
 
57
99
 
58
100
  def get_agent_queryset_for_user(user):
@@ -60,11 +102,29 @@ def get_agent_queryset_for_user(user):
60
102
  Get queryset of agents accessible to the user.
61
103
 
62
104
  - Superusers can see all agents
63
- - Regular users see only their own agents
105
+ - Regular users see:
106
+ - Owned agents
107
+ - Agents where they are direct collaborators
108
+ - Agents in systems they own or are collaborators on
64
109
  """
65
110
  if user.is_superuser:
66
111
  return AgentDefinition.objects.all()
67
- return AgentDefinition.objects.filter(owner=user)
112
+
113
+ # Get system IDs the user has access to (owner or collaborator)
114
+ accessible_system_ids = AgentSystem.objects.filter(
115
+ Q(owner=user) | Q(collaborators__user=user)
116
+ ).values_list('id', flat=True)
117
+
118
+ # Get agent IDs that are members of accessible systems
119
+ agents_via_systems = AgentSystemMember.objects.filter(
120
+ system_id__in=accessible_system_ids
121
+ ).values_list('agent_id', flat=True)
122
+
123
+ return AgentDefinition.objects.filter(
124
+ Q(owner=user) |
125
+ Q(collaborators__user=user) |
126
+ Q(id__in=agents_via_systems)
127
+ ).distinct()
68
128
 
69
129
 
70
130
  def get_system_for_user(user, system_id):
@@ -72,11 +132,27 @@ def get_system_for_user(user, system_id):
72
132
  Get a system if the user has access.
73
133
 
74
134
  - Superusers can access any system
75
- - Regular users can only access their own systems
135
+ - Owners can access their own systems
136
+ - Collaborators can access systems they've been granted access to
76
137
  """
138
+ from django_agent_runtime.models import SystemCollaborator
139
+
77
140
  if user.is_superuser:
78
141
  return get_object_or_404(AgentSystem, id=system_id)
79
- return get_object_or_404(AgentSystem, id=system_id, owner=user)
142
+
143
+ # Try to get the system
144
+ system = get_object_or_404(AgentSystem, id=system_id)
145
+
146
+ # Check if owner
147
+ if system.owner == user:
148
+ return system
149
+
150
+ # Check if collaborator
151
+ if SystemCollaborator.objects.filter(system=system, user=user).exists():
152
+ return system
153
+
154
+ # No access
155
+ raise Http404("System not found")
80
156
 
81
157
 
82
158
  def get_system_queryset_for_user(user):
@@ -84,11 +160,13 @@ def get_system_queryset_for_user(user):
84
160
  Get queryset of systems accessible to the user.
85
161
 
86
162
  - Superusers can see all systems
87
- - Regular users see only their own systems
163
+ - Regular users see owned systems and systems where they are collaborators
88
164
  """
89
165
  if user.is_superuser:
90
166
  return AgentSystem.objects.all()
91
- return AgentSystem.objects.filter(owner=user)
167
+ return AgentSystem.objects.filter(
168
+ Q(owner=user) | Q(collaborators__user=user)
169
+ ).distinct()
92
170
 
93
171
 
94
172
  class AgentDefinitionListCreateView(generics.ListCreateAPIView):
@@ -2068,3 +2146,387 @@ class AgentSpecDocumentView(APIView):
2068
2146
  'created': created,
2069
2147
  'message': f"Spec {'created' if created else 'updated'} for {agent.name}",
2070
2148
  })
2149
+
2150
+
2151
+ # =============================================================================
2152
+ # Collaborator Management Views
2153
+ # =============================================================================
2154
+
2155
+ from django_agent_runtime.models import (
2156
+ AgentCollaborator,
2157
+ SystemCollaborator,
2158
+ CollaboratorRole,
2159
+ )
2160
+ from django_agent_studio.api.serializers import (
2161
+ AgentCollaboratorSerializer,
2162
+ SystemCollaboratorSerializer,
2163
+ AddCollaboratorSerializer,
2164
+ UpdateCollaboratorRoleSerializer,
2165
+ )
2166
+
2167
+
2168
+ class AgentCollaboratorListCreateView(generics.ListCreateAPIView):
2169
+ """
2170
+ List and add collaborators to an agent.
2171
+
2172
+ GET /api/agents/{id}/collaborators/
2173
+ POST /api/agents/{id}/collaborators/
2174
+
2175
+ Only owners and admins can manage collaborators.
2176
+ """
2177
+
2178
+ permission_classes = [IsAuthenticated]
2179
+
2180
+ def get_serializer_class(self):
2181
+ if self.request.method == 'POST':
2182
+ return AddCollaboratorSerializer
2183
+ return AgentCollaboratorSerializer
2184
+
2185
+ def get_queryset(self):
2186
+ agent = get_agent_for_user(self.request.user, self.kwargs['agent_id'])
2187
+ return agent.collaborators.select_related('user', 'added_by').all()
2188
+
2189
+ def create(self, request, *args, **kwargs):
2190
+ agent = get_agent_for_user(request.user, self.kwargs['agent_id'])
2191
+
2192
+ # Check if user can manage collaborators (owner or admin)
2193
+ if not self._can_admin(request.user, agent):
2194
+ return Response(
2195
+ {'error': 'Only owners and admins can manage collaborators'},
2196
+ status=status.HTTP_403_FORBIDDEN,
2197
+ )
2198
+
2199
+ serializer = self.get_serializer(data=request.data)
2200
+ serializer.is_valid(raise_exception=True)
2201
+
2202
+ # Find user by email or username
2203
+ from django.contrib.auth import get_user_model
2204
+ User = get_user_model()
2205
+ identifier = serializer.validated_data['email']
2206
+
2207
+ # Try to find user by email first, then username
2208
+ user = None
2209
+ user_fields = [f.name for f in User._meta.get_fields()]
2210
+
2211
+ if 'email' in user_fields:
2212
+ user = User.objects.filter(email=identifier).first()
2213
+ if not user and 'username' in user_fields:
2214
+ user = User.objects.filter(username=identifier).first()
2215
+
2216
+ if not user:
2217
+ return Response(
2218
+ {'error': f"User '{identifier}' not found"},
2219
+ status=status.HTTP_404_NOT_FOUND,
2220
+ )
2221
+
2222
+ # Can't add owner as collaborator
2223
+ if user == agent.owner:
2224
+ return Response(
2225
+ {'error': 'Cannot add the owner as a collaborator'},
2226
+ status=status.HTTP_400_BAD_REQUEST,
2227
+ )
2228
+
2229
+ # Check if already a collaborator
2230
+ if AgentCollaborator.objects.filter(agent=agent, user=user).exists():
2231
+ return Response(
2232
+ {'error': 'User is already a collaborator on this agent'},
2233
+ status=status.HTTP_400_BAD_REQUEST,
2234
+ )
2235
+
2236
+ # Create collaborator
2237
+ collaborator = AgentCollaborator.objects.create(
2238
+ agent=agent,
2239
+ user=user,
2240
+ role=serializer.validated_data.get('role', CollaboratorRole.VIEWER),
2241
+ added_by=request.user,
2242
+ )
2243
+
2244
+ return Response(
2245
+ AgentCollaboratorSerializer(collaborator).data,
2246
+ status=status.HTTP_201_CREATED,
2247
+ )
2248
+
2249
+ def _can_admin(self, user, agent):
2250
+ """Check if user can manage collaborators."""
2251
+ if user.is_superuser or agent.owner == user:
2252
+ return True
2253
+ try:
2254
+ collab = AgentCollaborator.objects.get(agent=agent, user=user)
2255
+ return collab.can_admin
2256
+ except AgentCollaborator.DoesNotExist:
2257
+ return False
2258
+
2259
+
2260
+ class AgentCollaboratorDetailView(generics.RetrieveUpdateDestroyAPIView):
2261
+ """
2262
+ Retrieve, update, or delete an agent collaborator.
2263
+
2264
+ GET/PUT/PATCH/DELETE /api/agents/{id}/collaborators/{collaborator_id}/
2265
+
2266
+ Only owners and admins can manage collaborators.
2267
+ """
2268
+
2269
+ permission_classes = [IsAuthenticated]
2270
+ serializer_class = AgentCollaboratorSerializer
2271
+
2272
+ def get_queryset(self):
2273
+ agent = get_agent_for_user(self.request.user, self.kwargs['agent_id'])
2274
+ return agent.collaborators.select_related('user', 'added_by').all()
2275
+
2276
+ def update(self, request, *args, **kwargs):
2277
+ agent = get_agent_for_user(request.user, self.kwargs['agent_id'])
2278
+
2279
+ if not self._can_admin(request.user, agent):
2280
+ return Response(
2281
+ {'error': 'Only owners and admins can manage collaborators'},
2282
+ status=status.HTTP_403_FORBIDDEN,
2283
+ )
2284
+
2285
+ # Use the role update serializer for validation
2286
+ role_serializer = UpdateCollaboratorRoleSerializer(data=request.data)
2287
+ role_serializer.is_valid(raise_exception=True)
2288
+
2289
+ collaborator = self.get_object()
2290
+ collaborator.role = role_serializer.validated_data['role']
2291
+ collaborator.save()
2292
+
2293
+ return Response(AgentCollaboratorSerializer(collaborator).data)
2294
+
2295
+ def destroy(self, request, *args, **kwargs):
2296
+ agent = get_agent_for_user(request.user, self.kwargs['agent_id'])
2297
+
2298
+ if not self._can_admin(request.user, agent):
2299
+ return Response(
2300
+ {'error': 'Only owners and admins can manage collaborators'},
2301
+ status=status.HTTP_403_FORBIDDEN,
2302
+ )
2303
+
2304
+ return super().destroy(request, *args, **kwargs)
2305
+
2306
+ def _can_admin(self, user, agent):
2307
+ """Check if user can manage collaborators."""
2308
+ if user.is_superuser or agent.owner == user:
2309
+ return True
2310
+ try:
2311
+ collab = AgentCollaborator.objects.get(agent=agent, user=user)
2312
+ return collab.can_admin
2313
+ except AgentCollaborator.DoesNotExist:
2314
+ return False
2315
+
2316
+
2317
+ class SystemCollaboratorListCreateView(generics.ListCreateAPIView):
2318
+ """
2319
+ List and add collaborators to a system.
2320
+
2321
+ GET /api/systems/{id}/collaborators/
2322
+ POST /api/systems/{id}/collaborators/
2323
+
2324
+ Only owners and admins can manage collaborators.
2325
+ """
2326
+
2327
+ permission_classes = [IsAuthenticated]
2328
+
2329
+ def get_serializer_class(self):
2330
+ if self.request.method == 'POST':
2331
+ return AddCollaboratorSerializer
2332
+ return SystemCollaboratorSerializer
2333
+
2334
+ def get_queryset(self):
2335
+ system = get_system_for_user(self.request.user, self.kwargs['system_id'])
2336
+ return system.collaborators.select_related('user', 'added_by').all()
2337
+
2338
+ def create(self, request, *args, **kwargs):
2339
+ system = get_system_for_user(request.user, self.kwargs['system_id'])
2340
+
2341
+ # Check if user can manage collaborators (owner or admin)
2342
+ if not self._can_admin(request.user, system):
2343
+ return Response(
2344
+ {'error': 'Only owners and admins can manage collaborators'},
2345
+ status=status.HTTP_403_FORBIDDEN,
2346
+ )
2347
+
2348
+ serializer = self.get_serializer(data=request.data)
2349
+ serializer.is_valid(raise_exception=True)
2350
+
2351
+ # Find user by email or username
2352
+ from django.contrib.auth import get_user_model
2353
+ User = get_user_model()
2354
+ identifier = serializer.validated_data['email']
2355
+
2356
+ # Try to find user by email first, then username
2357
+ user = None
2358
+ user_fields = [f.name for f in User._meta.get_fields()]
2359
+
2360
+ if 'email' in user_fields:
2361
+ user = User.objects.filter(email=identifier).first()
2362
+ if not user and 'username' in user_fields:
2363
+ user = User.objects.filter(username=identifier).first()
2364
+
2365
+ if not user:
2366
+ return Response(
2367
+ {'error': f"User '{identifier}' not found"},
2368
+ status=status.HTTP_404_NOT_FOUND,
2369
+ )
2370
+
2371
+ # Can't add owner as collaborator
2372
+ if user == system.owner:
2373
+ return Response(
2374
+ {'error': 'Cannot add the owner as a collaborator'},
2375
+ status=status.HTTP_400_BAD_REQUEST,
2376
+ )
2377
+
2378
+ # Check if already a collaborator
2379
+ if SystemCollaborator.objects.filter(system=system, user=user).exists():
2380
+ return Response(
2381
+ {'error': 'User is already a collaborator on this system'},
2382
+ status=status.HTTP_400_BAD_REQUEST,
2383
+ )
2384
+
2385
+ # Create collaborator
2386
+ collaborator = SystemCollaborator.objects.create(
2387
+ system=system,
2388
+ user=user,
2389
+ role=serializer.validated_data.get('role', CollaboratorRole.VIEWER),
2390
+ added_by=request.user,
2391
+ )
2392
+
2393
+ return Response(
2394
+ SystemCollaboratorSerializer(collaborator).data,
2395
+ status=status.HTTP_201_CREATED,
2396
+ )
2397
+
2398
+ def _can_admin(self, user, system):
2399
+ """Check if user can manage collaborators."""
2400
+ if user.is_superuser or system.owner == user:
2401
+ return True
2402
+ try:
2403
+ collab = SystemCollaborator.objects.get(system=system, user=user)
2404
+ return collab.can_admin
2405
+ except SystemCollaborator.DoesNotExist:
2406
+ return False
2407
+
2408
+
2409
+ class SystemCollaboratorDetailView(generics.RetrieveUpdateDestroyAPIView):
2410
+ """
2411
+ Retrieve, update, or delete a system collaborator.
2412
+
2413
+ GET/PUT/PATCH/DELETE /api/systems/{id}/collaborators/{collaborator_id}/
2414
+
2415
+ Only owners and admins can manage collaborators.
2416
+ """
2417
+
2418
+ permission_classes = [IsAuthenticated]
2419
+ serializer_class = SystemCollaboratorSerializer
2420
+
2421
+ def get_queryset(self):
2422
+ system = get_system_for_user(self.request.user, self.kwargs['system_id'])
2423
+ return system.collaborators.select_related('user', 'added_by').all()
2424
+
2425
+ def update(self, request, *args, **kwargs):
2426
+ system = get_system_for_user(request.user, self.kwargs['system_id'])
2427
+
2428
+ if not self._can_admin(request.user, system):
2429
+ return Response(
2430
+ {'error': 'Only owners and admins can manage collaborators'},
2431
+ status=status.HTTP_403_FORBIDDEN,
2432
+ )
2433
+
2434
+ # Use the role update serializer for validation
2435
+ role_serializer = UpdateCollaboratorRoleSerializer(data=request.data)
2436
+ role_serializer.is_valid(raise_exception=True)
2437
+
2438
+ collaborator = self.get_object()
2439
+ collaborator.role = role_serializer.validated_data['role']
2440
+ collaborator.save()
2441
+
2442
+ return Response(SystemCollaboratorSerializer(collaborator).data)
2443
+
2444
+ def destroy(self, request, *args, **kwargs):
2445
+ system = get_system_for_user(request.user, self.kwargs['system_id'])
2446
+
2447
+ if not self._can_admin(request.user, system):
2448
+ return Response(
2449
+ {'error': 'Only owners and admins can manage collaborators'},
2450
+ status=status.HTTP_403_FORBIDDEN,
2451
+ )
2452
+
2453
+ return super().destroy(request, *args, **kwargs)
2454
+
2455
+ def _can_admin(self, user, system):
2456
+ """Check if user can manage collaborators."""
2457
+ if user.is_superuser or system.owner == user:
2458
+ return True
2459
+ try:
2460
+ collab = SystemCollaborator.objects.get(system=system, user=user)
2461
+ return collab.can_admin
2462
+ except SystemCollaborator.DoesNotExist:
2463
+ return False
2464
+
2465
+
2466
+ class UserSearchView(APIView):
2467
+ """
2468
+ Search for users by email, username, or name for collaborator autocomplete.
2469
+
2470
+ GET /api/users/search/?q=<query>
2471
+
2472
+ Returns up to 10 matching users.
2473
+ Handles both email-based and username-based User models.
2474
+ """
2475
+
2476
+ permission_classes = [IsAuthenticated]
2477
+
2478
+ def get(self, request):
2479
+ from django.contrib.auth import get_user_model
2480
+ from django.db.models import Q
2481
+
2482
+ User = get_user_model()
2483
+ query = request.query_params.get('q', '').strip()
2484
+
2485
+ if len(query) < 2:
2486
+ return Response([])
2487
+
2488
+ # Build query filters based on available fields
2489
+ # Check which fields exist on the User model
2490
+ user_fields = [f.name for f in User._meta.get_fields()]
2491
+
2492
+ filters = Q()
2493
+ if 'email' in user_fields:
2494
+ filters |= Q(email__icontains=query)
2495
+ if 'username' in user_fields:
2496
+ filters |= Q(username__icontains=query)
2497
+ if 'first_name' in user_fields:
2498
+ filters |= Q(first_name__icontains=query)
2499
+ if 'last_name' in user_fields:
2500
+ filters |= Q(last_name__icontains=query)
2501
+ if 'full_name' in user_fields:
2502
+ filters |= Q(full_name__icontains=query)
2503
+
2504
+ users = User.objects.filter(filters).exclude(
2505
+ id=request.user.id # Exclude current user
2506
+ )[:10]
2507
+
2508
+ results = []
2509
+ for user in users:
2510
+ # Get identifier (email or username)
2511
+ identifier = getattr(user, 'email', None) or getattr(user, 'username', None) or str(user)
2512
+
2513
+ # Get display name
2514
+ full_name = getattr(user, 'full_name', None) or ''
2515
+ first_name = getattr(user, 'first_name', None) or ''
2516
+ last_name = getattr(user, 'last_name', None) or ''
2517
+ name = full_name or f"{first_name} {last_name}".strip() or identifier
2518
+
2519
+ # Build display string
2520
+ if name != identifier:
2521
+ display = f"{name} ({identifier})"
2522
+ else:
2523
+ display = identifier
2524
+
2525
+ results.append({
2526
+ 'id': str(user.id),
2527
+ 'email': identifier, # Keep as 'email' for backward compatibility
2528
+ 'name': name,
2529
+ 'display': display,
2530
+ })
2531
+
2532
+ return Response(results)
@@ -58,11 +58,18 @@
58
58
  </div>
59
59
  </div>
60
60
  <div class="border-t border-gray-100 px-4 py-3 bg-gray-50 flex items-center justify-between">
61
- <a href="{% url 'agent_studio:agent_edit' agent.id %}"
62
- class="text-primary-600 hover:text-primary-700 text-sm font-medium">
63
- Edit
64
- </a>
65
- <a href="{% url 'agent_studio:agent_test' agent.id %}"
61
+ <div class="flex items-center space-x-3">
62
+ <a href="{% url 'agent_studio:agent_edit' agent.id %}"
63
+ class="text-primary-600 hover:text-primary-700 text-sm font-medium">
64
+ Edit
65
+ </a>
66
+ <a href="{% url 'agent_studio:agent_collaborators' agent.id %}"
67
+ class="text-gray-500 hover:text-gray-700 text-sm"
68
+ title="Manage collaborators">
69
+ <i class="pi pi-users"></i>
70
+ </a>
71
+ </div>
72
+ <a href="{% url 'agent_studio:agent_test' agent.id %}"
66
73
  class="text-gray-600 hover:text-gray-700 text-sm">
67
74
  Test →
68
75
  </a>
@@ -25,9 +25,9 @@
25
25
  <script>
26
26
  // Configuration passed from Django
27
27
  window.STUDIO_CONFIG = {
28
- agentId: {% if agent %}"{{ agent.id }}"{% else %}null{% endif %},
28
+ agentId: {% if agent %}"{{ agent.id|escapejs }}"{% else %}null{% endif %},
29
29
  csrfToken: "{{ csrf_token }}",
30
- builderAgentKey: "{{ builder_agent_key }}",
30
+ builderAgentKey: "{{ builder_agent_key|escapejs }}",
31
31
  };
32
32
  </script>
33
33