@cryptotaxi247 / CoPilot / commits / 4590aea4

fix: agent delete proceeds even when upstream services error (#867)

* fix: agent delete proceeds even when upstream services error Two stacked issues caused DELETE /api/agents/{id}/delete to 500 and leave agents stuck in CoPilot's DB: 1. ensure_client_deleted (post-delete verification step in delete_agent_velociraptor) used the artifact "Server.Information.Clients", which doesn't exist on Velociraptor 0.75.6. The query returned `None`, then `.get("flow_id")` on `None` crashed: 'NoneType' object has no attribute 'get' The actual delete had already succeeded — the verification was redundant (delete_client raises on failure on its own) and Velociraptor's eventual consistency made the poll flaky regardless. Drop ensure_client_deleted and the now-unused check_client_in_results. 2. The delete route ran Wazuh → Velociraptor → CoPilot DB sequentially with no error isolation. Any upstream failure (Velociraptor down, client already gone, server-side bug) blocked the CoPilot bookkeeping delete. Wrap each upstream step in try/except, log the failure, append to upstream_errors, and always proceed to the DB delete. Successful responses surface upstream issues in the message but don't fail the request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: remove unnecessary blank lines in delete_client function --------- Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

taylorcopilot committed May 9, 2026 at 11:51 UTC 4590aea4491c90051e6eb1c32f9ea843fa42f305
2 files changed +23 -67
backend/app/agents/routes/agents.py
+23 -5
@@ -1277,12 +1277,30 @@ async def delete_agent(
1277 if not agent:
1278 raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found or access denied")
1279
1280 - await delete_agent_wazuh(agent_id)
1281 - client_id = await fetch_velociraptor_id(db=session, agent_id=agent_id)
1282 - logger.info(f"Client ID: {client_id}")
1283 - if client_id != "Unknown":
1284 - await delete_agent_velociraptor(client_id)
1280 + upstream_errors: list[str] = []
1281 +
1282 + try:
1283 + await delete_agent_wazuh(agent_id)
1284 + except Exception as e:
1285 + logger.error(f"Wazuh delete failed for agent {agent_id}: {e}")
1286 + upstream_errors.append(f"Wazuh: {e}")
1287 +
1288 + try:
1289 + client_id = await fetch_velociraptor_id(db=session, agent_id=agent_id)
1290 + logger.info(f"Client ID: {client_id}")
1291 + if client_id != "Unknown":
1292 + await delete_agent_velociraptor(client_id)
1293 + except Exception as e:
1294 + logger.error(f"Velociraptor delete failed for agent {agent_id}: {e}")
1295 + upstream_errors.append(f"Velociraptor: {e}")
1296 +
1297 await delete_agent_from_database(db=session, agent_id=agent_id)
1298 +
1299 + if upstream_errors:
1300 + return AgentModifyResponse(
1301 + success=True,
1302 + message=f"Agent {agent_id} deleted from CoPilot; upstream services reported: {'; '.join(upstream_errors)}",
1303 + )
1304 return AgentModifyResponse(
1305 success=True,
1306 message=f"Agent {agent_id} deleted successfully",
backend/app/agents/velociraptor/services/agents.py
-62
@@ -214,33 +214,6 @@ def check_flow_success(flow: dict, client_id: str) -> dict:
214 )
215
216
217 -def check_client_in_results(results: dict, client_id: str) -> dict:
218 - """
219 - Checks if a client is present in the results dictionary.
220 -
221 - Args:
222 - results (dict): The dictionary containing the results.
223 - client_id (str): The ID of the client to check.
224 -
225 - Returns:
226 - dict: A dictionary with a success message if the client is found, otherwise an error message.
227 - """
228 - if results["results"] == []:
229 - logger.info(f"Successfully deleted velociraptor client {client_id}")
230 - return {
231 - "message": f"Successfully deleted velociraptor client {client_id}",
232 - "success": True,
233 - }
234 -
235 - for result in results["results"]:
236 - if result["client_id"] == client_id:
237 - logger.error(f"Failed to delete velociraptor client {client_id}")
238 - return handle_exception(
239 - e="Failed to delete velociraptor client",
240 - client_id=client_id,
241 - )
242 -
243 -
217 def handle_exception(e: Exception, client_id: str) -> dict:
218 """
219 Handles exceptions that occur during the deletion of a Velociraptor client.
@@ -274,7 +247,6 @@ async def delete_agent_velociraptor(client_id: str) -> AgentModifyResponse:
247 """
248 try:
249 await delete_client(client_id=client_id)
277 - await ensure_client_deleted(client_id=client_id)
250 return AgentModifyResponse(success=True, message="Agent deleted successfully")
251 except Exception as e:
252 return handle_exception(e, client_id)
@@ -299,37 +271,3 @@ async def delete_client(client_id: str) -> dict:
271 return check_flow_success(flow, client_id)
272 except Exception as e:
273 return handle_exception(e, client_id)
302 -
303 -
304 -async def ensure_client_deleted(client_id: str) -> dict:
305 - """
306 - Ensures that a client is deleted from the server.
307 -
308 - Args:
309 - client_id (str): The ID of the client to be deleted.
310 -
311 - Returns:
312 - dict: The result of the deletion operation.
313 - """
314 - universal_service = await UniversalService.create("Velociraptor")
315 - try:
316 - query = create_query(
317 - "SELECT collect_client(client_id='server', artifacts=['Server.Information.Clients'], env=dict()) FROM scope()",
318 - )
319 - flow = execute_query(universal_service, query)
320 - flow_id = (
321 - flow.get("results")[0]
322 - .get(
323 - "collect_client(client_id='server', artifacts=['Server.Information.Clients'], env=dict())",
324 - )
325 - .get("flow_id")
326 - )
327 -
328 - results = universal_service.read_collection_results(
329 - client_id=client_id,
330 - flow_id=flow_id,
331 - artifact="Server.Information.Clients",
332 - )
333 - return check_client_in_results(results, client_id)
334 - except Exception as e:
335 - return handle_exception(e, client_id)