33
44from fastapi import Depends
55
6+ from src .adapters .crud_store .exceptions import ItemDoesNotExist
67from src .adapters .streams .adapter_redis import DRedisStreamRepository
8+ from src .api .schemas .authorization_types import AgentexResource
79from src .domain .entities .agents import ACPType , AgentEntity
810from src .domain .entities .events import EventEntity
911from src .domain .entities .task_message_updates import TaskMessageUpdateEntity
1416from src .domain .repositories .task_repository import DTaskRepository
1517from src .domain .repositories .task_state_repository import DTaskStateRepository
1618from src .domain .services .agent_acp_service import DAgentACPService
19+ from src .domain .services .authorization_service import DAuthorizationService
1720from src .utils .ids import orm_id
1821from src .utils .logging import make_logger
1922from src .utils .stream_topics import get_task_event_stream_topic
@@ -33,12 +36,14 @@ def __init__(
3336 task_repository : DTaskRepository ,
3437 event_repository : DEventRepository ,
3538 stream_repository : DRedisStreamRepository ,
39+ authorization_service : DAuthorizationService ,
3640 ):
3741 self .acp_client = acp_client
3842 self .task_state_repository = task_state_repository
3943 self .task_repository = task_repository
4044 self .event_repository = event_repository
4145 self .stream_repository = stream_repository
46+ self .authorization_service = authorization_service
4247
4348 async def create_task (
4449 self ,
@@ -59,19 +64,33 @@ async def create_task(
5964 Returns:
6065 Task containing the created task info
6166 """
62-
63- task_entity = await self .task_repository .create (
64- agent_id = agent .id ,
65- task = TaskEntity (
66- id = orm_id (),
67- name = task_name ,
68- status = TaskStatus .RUNNING ,
69- status_reason = "Task created, forwarding to ACP server" ,
70- params = task_params ,
71- task_metadata = task_metadata ,
72- ),
67+ # Register in the authorization service before persisting: a registration
68+ # failure aborts the request with no orphaned row. If the persist fails
69+ # after a successful registration, the compensating deregister_resource
70+ # below prevents a dangling authorization entry. Both calls are no-ops
71+ # when the authorization service is disabled for this account.
72+ task_entity = TaskEntity (
73+ id = orm_id (),
74+ name = task_name ,
75+ status = TaskStatus .RUNNING ,
76+ status_reason = "Task created, forwarding to ACP server" ,
77+ params = task_params ,
78+ task_metadata = task_metadata ,
79+ )
80+ await self .authorization_service .register_resource (
81+ AgentexResource .task (task_entity .id ),
82+ parent = AgentexResource .agent (agent .id ),
7383 )
74- return task_entity
84+ try :
85+ return await self .task_repository .create (
86+ agent_id = agent .id ,
87+ task = task_entity ,
88+ )
89+ except Exception :
90+ await self .authorization_service .deregister_resource (
91+ AgentexResource .task (task_entity .id ),
92+ )
93+ raise
7594
7695 async def create_task_and_forward_to_acp (
7796 self ,
@@ -91,7 +110,9 @@ async def create_task_and_forward_to_acp(
91110 Task containing the created task info
92111 """
93112 task_entity = await self .create_task (
94- agent = agent , task_name = task_name , task_params = task_params
113+ agent = agent ,
114+ task_name = task_name ,
115+ task_params = task_params ,
95116 )
96117
97118 if agent .acp_type == ACPType .SYNC :
@@ -214,8 +235,35 @@ async def delete_task(self, id: str | None = None, name: str | None = None) -> N
214235 """
215236 Delete a task from the repository.
216237 """
238+ # Delete first (Postgres is the source of truth for existence), then
239+ # deregister best-effort: a deregister failure is logged and swallowed
240+ # rather than failing a delete that already succeeded.
241+ # Resolve the id before the delete so we can pass it to deregister_resource;
242+ # looking it up by name afterwards would race. If the name doesn't resolve,
243+ # swallow ItemDoesNotExist and let delete() surface its own native error
244+ # so the missing-task error contract is unchanged.
245+ task_id_for_deregister : str | None = id
246+ if task_id_for_deregister is None and name is not None :
247+ try :
248+ task = await self .task_repository .get (name = name )
249+ task_id_for_deregister = task .id
250+ except ItemDoesNotExist :
251+ task_id_for_deregister = None
252+
217253 await self .task_repository .delete (id = id , name = name )
218254
255+ if task_id_for_deregister is not None :
256+ try :
257+ await self .authorization_service .deregister_resource (
258+ AgentexResource .task (task_id_for_deregister ),
259+ )
260+ except Exception :
261+ logger .exception (
262+ "task authorization deregister failed for task %s after successful delete; "
263+ "the deregistration failure has been swallowed" ,
264+ task_id_for_deregister ,
265+ )
266+
219267 async def list_tasks (
220268 self ,
221269 * ,
0 commit comments