44from fastapi import Depends
55
66from src .adapters .streams .adapter_redis import DRedisStreamRepository
7+ from src .api .schemas .authorization_types import AgentexResource
78from src .domain .entities .agents import ACPType , AgentEntity
89from src .domain .entities .events import EventEntity
910from src .domain .entities .task_message_updates import TaskMessageUpdateEntity
1415from src .domain .repositories .task_repository import DTaskRepository
1516from src .domain .repositories .task_state_repository import DTaskStateRepository
1617from src .domain .services .agent_acp_service import DAgentACPService
18+ from src .domain .services .authorization_service import DAuthorizationService
19+ from src .utils .feature_flags import DFeatureFlagProvider , FeatureFlagName
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,19 +36,24 @@ def __init__(
3336 task_repository : DTaskRepository ,
3437 event_repository : DEventRepository ,
3538 stream_repository : DRedisStreamRepository ,
39+ authorization_service : DAuthorizationService ,
40+ feature_flags : DFeatureFlagProvider ,
3641 ):
3742 self .acp_client = acp_client
3843 self .task_state_repository = task_state_repository
3944 self .task_repository = task_repository
4045 self .event_repository = event_repository
4146 self .stream_repository = stream_repository
47+ self .authorization_service = authorization_service
48+ self .feature_flags = feature_flags
4249
4350 async def create_task (
4451 self ,
4552 agent : AgentEntity ,
4653 task_name : str | None = None ,
4754 task_params : dict [str , Any ] | None = None ,
4855 task_metadata : dict [str , Any ] | None = None ,
56+ account_id : str | None = None ,
4957 ) -> TaskEntity :
5058 """
5159 Create a new task record in the repository with single agent (maintains existing interface).
@@ -56,28 +64,107 @@ async def create_task(
5664 task_params: The parameters for the task
5765 task_metadata: Caller-provided metadata to persist on the task row.
5866 Not forwarded to the agent.
67+ account_id: Caller-resolved account scope. When provided and the
68+ FGAC_TASKS_DUAL_WRITE flag is enabled for it, the task is also
69+ registered in Spark AuthZ.
5970 Returns:
6071 Task containing the created task info
6172 """
73+ principal_context = self .authorization_service .principal_context
74+ creator_user_id = getattr (principal_context , "user_id" , None )
75+ creator_service_account_id = getattr (
76+ principal_context , "service_account_id" , None
77+ )
78+
79+ task_id = orm_id ()
80+ zedtoken : str | None = None
81+
82+ if self .feature_flags .is_enabled (
83+ FeatureFlagName .FGAC_TASKS_DUAL_WRITE , account_id
84+ ):
85+ zedtoken = await self ._register_task_in_spark_authz (
86+ task_id = task_id ,
87+ account_id = account_id ,
88+ creator_user_id = creator_user_id ,
89+ creator_service_account_id = creator_service_account_id ,
90+ )
6291
6392 task_entity = await self .task_repository .create (
6493 agent_id = agent .id ,
6594 task = TaskEntity (
66- id = orm_id () ,
95+ id = task_id ,
6796 name = task_name ,
6897 status = TaskStatus .RUNNING ,
6998 status_reason = "Task created, forwarding to ACP server" ,
7099 params = task_params ,
71100 task_metadata = task_metadata ,
101+ creator_user_id = creator_user_id ,
102+ creator_service_account_id = creator_service_account_id ,
103+ spark_authz_zedtoken = zedtoken ,
72104 ),
73105 )
74106 return task_entity
75107
108+ async def _register_task_in_spark_authz (
109+ self ,
110+ * ,
111+ task_id : str ,
112+ account_id : str | None ,
113+ creator_user_id : str | None ,
114+ creator_service_account_id : str | None ,
115+ ) -> str | None :
116+ """Register a new task in Spark AuthZ with creator as owner.
117+
118+ Called BEFORE the Postgres write — a failure raises and prevents the
119+ row from being persisted, so there is no compensating action to take.
120+ Mirrors the KB FGAC pattern at
121+ ``packages/egp-api-backend/.../knowledge_base_v2_use_case.py:374-388``.
122+
123+ The current ``Provider.spark`` adapter returns ``{}`` from ``grant``;
124+ no ZedToken is surfaced today, so we always return ``None`` for the
125+ new-write-isolation column. A follow-up will plumb the token through
126+ once the adapter exposes it.
127+ """
128+ if creator_user_id is None and creator_service_account_id is None :
129+ logger .warning (
130+ "Skipping Spark AuthZ task registration: no creator resolvable" ,
131+ extra = {"task_id" : task_id , "account_id" : account_id },
132+ )
133+ return None
134+ await self .authorization_service .grant (
135+ resource = AgentexResource .task (task_id ),
136+ )
137+ return None
138+
139+ async def deregister_task_from_spark_authz (
140+ self , * , task_id : str , account_id : str | None
141+ ) -> None :
142+ """Best-effort revocation of a task's Spark AuthZ tuples on delete.
143+
144+ Only invoked when the FGAC_TASKS_DUAL_WRITE flag is enabled for the
145+ caller's account. Failures are logged but do not block the delete.
146+ """
147+ if not self .feature_flags .is_enabled (
148+ FeatureFlagName .FGAC_TASKS_DUAL_WRITE , account_id
149+ ):
150+ return
151+ try :
152+ await self .authorization_service .revoke (
153+ resource = AgentexResource .task (task_id ),
154+ )
155+ except Exception :
156+ logger .warning (
157+ "Spark AuthZ revoke failed for task" ,
158+ extra = {"task_id" : task_id , "account_id" : account_id },
159+ exc_info = True ,
160+ )
161+
76162 async def create_task_and_forward_to_acp (
77163 self ,
78164 agent : AgentEntity ,
79165 task_name : str | None = None ,
80166 task_params : dict [str , Any ] | None = None ,
167+ account_id : str | None = None ,
81168 ) -> TaskEntity :
82169 """
83170 Create a new task record in the repository with single agent (maintains existing interface).
@@ -86,12 +173,17 @@ async def create_task_and_forward_to_acp(
86173 Args:
87174 agent: The agent to create the task for
88175 task_params: The parameters for the task to be sent to the ACP server
176+ account_id: Caller-resolved account scope; threaded through to
177+ :meth:`create_task` for FGAC dual-write gating.
89178
90179 Returns:
91180 Task containing the created task info
92181 """
93182 task_entity = await self .create_task (
94- agent = agent , task_name = task_name , task_params = task_params
183+ agent = agent ,
184+ task_name = task_name ,
185+ task_params = task_params ,
186+ account_id = account_id ,
95187 )
96188
97189 if agent .acp_type == ACPType .SYNC :
0 commit comments