flock-core 0.5.11__py3-none-any.whl → 0.5.20__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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/__init__.py +1 -1
- flock/agent/__init__.py +30 -0
- flock/agent/builder_helpers.py +192 -0
- flock/agent/builder_validator.py +169 -0
- flock/agent/component_lifecycle.py +325 -0
- flock/agent/context_resolver.py +141 -0
- flock/agent/mcp_integration.py +212 -0
- flock/agent/output_processor.py +304 -0
- flock/api/__init__.py +20 -0
- flock/{api_models.py → api/models.py} +0 -2
- flock/{service.py → api/service.py} +3 -3
- flock/cli.py +2 -2
- flock/components/__init__.py +41 -0
- flock/components/agent/__init__.py +22 -0
- flock/{components.py → components/agent/base.py} +4 -3
- flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
- flock/components/orchestrator/__init__.py +22 -0
- flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
- flock/components/orchestrator/circuit_breaker.py +95 -0
- flock/components/orchestrator/collection.py +143 -0
- flock/components/orchestrator/deduplication.py +78 -0
- flock/core/__init__.py +30 -0
- flock/core/agent.py +953 -0
- flock/{artifacts.py → core/artifacts.py} +1 -1
- flock/{context_provider.py → core/context_provider.py} +3 -3
- flock/core/orchestrator.py +1102 -0
- flock/{store.py → core/store.py} +99 -454
- flock/{subscription.py → core/subscription.py} +1 -1
- flock/dashboard/collector.py +5 -5
- flock/dashboard/graph_builder.py +7 -7
- flock/dashboard/routes/__init__.py +21 -0
- flock/dashboard/routes/control.py +327 -0
- flock/dashboard/routes/helpers.py +340 -0
- flock/dashboard/routes/themes.py +76 -0
- flock/dashboard/routes/traces.py +521 -0
- flock/dashboard/routes/websocket.py +108 -0
- flock/dashboard/service.py +43 -1316
- flock/engines/dspy/__init__.py +20 -0
- flock/engines/dspy/artifact_materializer.py +216 -0
- flock/engines/dspy/signature_builder.py +474 -0
- flock/engines/dspy/streaming_executor.py +858 -0
- flock/engines/dspy_engine.py +45 -1330
- flock/engines/examples/simple_batch_engine.py +2 -2
- flock/examples.py +7 -7
- flock/logging/logging.py +1 -16
- flock/models/__init__.py +10 -0
- flock/orchestrator/__init__.py +45 -0
- flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
- flock/orchestrator/artifact_manager.py +168 -0
- flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
- flock/orchestrator/component_runner.py +389 -0
- flock/orchestrator/context_builder.py +167 -0
- flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
- flock/orchestrator/event_emitter.py +167 -0
- flock/orchestrator/initialization.py +184 -0
- flock/orchestrator/lifecycle_manager.py +226 -0
- flock/orchestrator/mcp_manager.py +202 -0
- flock/orchestrator/scheduler.py +189 -0
- flock/orchestrator/server_manager.py +234 -0
- flock/orchestrator/tracing.py +147 -0
- flock/storage/__init__.py +10 -0
- flock/storage/artifact_aggregator.py +158 -0
- flock/storage/in_memory/__init__.py +6 -0
- flock/storage/in_memory/artifact_filter.py +114 -0
- flock/storage/in_memory/history_aggregator.py +115 -0
- flock/storage/sqlite/__init__.py +10 -0
- flock/storage/sqlite/agent_history_queries.py +154 -0
- flock/storage/sqlite/consumption_loader.py +100 -0
- flock/storage/sqlite/query_builder.py +112 -0
- flock/storage/sqlite/query_params_builder.py +91 -0
- flock/storage/sqlite/schema_manager.py +168 -0
- flock/storage/sqlite/summary_queries.py +194 -0
- flock/utils/__init__.py +14 -0
- flock/utils/async_utils.py +67 -0
- flock/{runtime.py → utils/runtime.py} +3 -3
- flock/utils/time_utils.py +53 -0
- flock/utils/type_resolution.py +38 -0
- flock/{utilities.py → utils/utilities.py} +2 -2
- flock/utils/validation.py +57 -0
- flock/utils/visibility.py +79 -0
- flock/utils/visibility_utils.py +134 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/METADATA +18 -4
- {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/RECORD +89 -33
- flock/agent.py +0 -1578
- flock/orchestrator.py +0 -1983
- /flock/{visibility.py → core/visibility.py} +0 -0
- /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
- /flock/{helper → utils}/cli_helper.py +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/WHEEL +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Component lifecycle hook execution for orchestrator.
|
|
2
|
+
|
|
3
|
+
This module manages the execution of OrchestratorComponent hooks in priority order.
|
|
4
|
+
Components can modify artifacts, control scheduling decisions, and handle collection logic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from asyncio import Task
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from flock.components.orchestrator import OrchestratorComponent
|
|
16
|
+
from flock.core import Agent
|
|
17
|
+
from flock.core.artifacts import Artifact
|
|
18
|
+
from flock.core.subscription import Subscription
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ComponentRunner:
|
|
22
|
+
"""Executes orchestrator component hooks in priority order.
|
|
23
|
+
|
|
24
|
+
This class manages the component lifecycle including initialization,
|
|
25
|
+
artifact processing, scheduling decisions, and shutdown. All hooks
|
|
26
|
+
execute in priority order (lower priority number = earlier execution).
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
_components: List of orchestrator components (sorted by priority)
|
|
30
|
+
_logger: Logger instance for component execution tracking
|
|
31
|
+
_initialized: Flag to prevent multiple initializations
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
components: list[OrchestratorComponent],
|
|
37
|
+
logger: logging.Logger | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the component runner.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
components: List of orchestrator components (should be pre-sorted by priority)
|
|
43
|
+
logger: Logger instance (defaults to module logger if not provided)
|
|
44
|
+
"""
|
|
45
|
+
self._components = components
|
|
46
|
+
self._logger = logger or logging.getLogger(__name__)
|
|
47
|
+
self._initialized = False
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def components(self) -> list[OrchestratorComponent]:
|
|
51
|
+
"""Get the list of registered components."""
|
|
52
|
+
return self._components
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_initialized(self) -> bool:
|
|
56
|
+
"""Check if components have been initialized."""
|
|
57
|
+
return self._initialized
|
|
58
|
+
|
|
59
|
+
async def run_initialize(self, orchestrator: Any) -> None:
|
|
60
|
+
"""Initialize all components in priority order (called once).
|
|
61
|
+
|
|
62
|
+
Executes on_initialize hook for each component. Sets _initialized
|
|
63
|
+
flag to prevent multiple initializations.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
orchestrator: The Flock orchestrator instance
|
|
67
|
+
"""
|
|
68
|
+
if self._initialized:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
self._logger.info(
|
|
72
|
+
f"Initializing {len(self._components)} orchestrator components"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
for component in self._components:
|
|
76
|
+
comp_name = component.name or component.__class__.__name__
|
|
77
|
+
self._logger.debug(
|
|
78
|
+
f"Initializing component: name={comp_name}, priority={component.priority}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
await component.on_initialize(orchestrator)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self._logger.exception(
|
|
85
|
+
f"Component initialization failed: name={comp_name}, error={e!s}"
|
|
86
|
+
)
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
self._initialized = True
|
|
90
|
+
self._logger.info(f"All components initialized: count={len(self._components)}")
|
|
91
|
+
|
|
92
|
+
async def run_artifact_published(
|
|
93
|
+
self, orchestrator: Any, artifact: Artifact
|
|
94
|
+
) -> Artifact | None:
|
|
95
|
+
"""Run on_artifact_published hooks (returns modified artifact or None to block).
|
|
96
|
+
|
|
97
|
+
Components execute in priority order, each receiving the artifact from the
|
|
98
|
+
previous component (chaining). If any component returns None, the artifact
|
|
99
|
+
is blocked and scheduling stops.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
orchestrator: The Flock orchestrator instance
|
|
103
|
+
artifact: The artifact being published
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Modified artifact or None if blocked by a component
|
|
107
|
+
"""
|
|
108
|
+
current_artifact = artifact
|
|
109
|
+
|
|
110
|
+
for component in self._components:
|
|
111
|
+
comp_name = component.name or component.__class__.__name__
|
|
112
|
+
self._logger.debug(
|
|
113
|
+
f"Running on_artifact_published: component={comp_name}, "
|
|
114
|
+
f"artifact_type={current_artifact.type}, artifact_id={current_artifact.id}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
result = await component.on_artifact_published(
|
|
119
|
+
orchestrator, current_artifact
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if result is None:
|
|
123
|
+
self._logger.info(
|
|
124
|
+
f"Artifact blocked by component: component={comp_name}, "
|
|
125
|
+
f"artifact_type={current_artifact.type}, artifact_id={current_artifact.id}"
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
current_artifact = result
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self._logger.exception(
|
|
132
|
+
f"Component hook failed: component={comp_name}, "
|
|
133
|
+
f"hook=on_artifact_published, error={e!s}"
|
|
134
|
+
)
|
|
135
|
+
raise
|
|
136
|
+
|
|
137
|
+
return current_artifact
|
|
138
|
+
|
|
139
|
+
async def run_before_schedule(
|
|
140
|
+
self,
|
|
141
|
+
orchestrator: Any,
|
|
142
|
+
artifact: Artifact,
|
|
143
|
+
agent: Agent,
|
|
144
|
+
subscription: Subscription,
|
|
145
|
+
) -> Any:
|
|
146
|
+
"""Run on_before_schedule hooks (returns CONTINUE, SKIP, or DEFER).
|
|
147
|
+
|
|
148
|
+
Components execute in priority order. First component to return SKIP or
|
|
149
|
+
DEFER stops execution and returns that decision.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
orchestrator: The Flock orchestrator instance
|
|
153
|
+
artifact: The artifact being scheduled
|
|
154
|
+
agent: The target agent
|
|
155
|
+
subscription: The subscription being evaluated
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
ScheduleDecision (CONTINUE, SKIP, or DEFER)
|
|
159
|
+
"""
|
|
160
|
+
from flock.components.orchestrator import ScheduleDecision
|
|
161
|
+
|
|
162
|
+
for component in self._components:
|
|
163
|
+
comp_name = component.name or component.__class__.__name__
|
|
164
|
+
|
|
165
|
+
self._logger.debug(
|
|
166
|
+
f"Running on_before_schedule: component={comp_name}, "
|
|
167
|
+
f"agent={agent.name}, artifact_type={artifact.type}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
decision = await component.on_before_schedule(
|
|
172
|
+
orchestrator, artifact, agent, subscription
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if decision == ScheduleDecision.SKIP:
|
|
176
|
+
self._logger.info(
|
|
177
|
+
f"Scheduling skipped by component: component={comp_name}, "
|
|
178
|
+
f"agent={agent.name}, artifact_type={artifact.type}, decision=SKIP"
|
|
179
|
+
)
|
|
180
|
+
return ScheduleDecision.SKIP
|
|
181
|
+
|
|
182
|
+
if decision == ScheduleDecision.DEFER:
|
|
183
|
+
self._logger.debug(
|
|
184
|
+
f"Scheduling deferred by component: component={comp_name}, "
|
|
185
|
+
f"agent={agent.name}, decision=DEFER"
|
|
186
|
+
)
|
|
187
|
+
return ScheduleDecision.DEFER
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self._logger.exception(
|
|
191
|
+
f"Component hook failed: component={comp_name}, "
|
|
192
|
+
f"hook=on_before_schedule, error={e!s}"
|
|
193
|
+
)
|
|
194
|
+
raise
|
|
195
|
+
|
|
196
|
+
return ScheduleDecision.CONTINUE
|
|
197
|
+
|
|
198
|
+
async def run_collect_artifacts(
|
|
199
|
+
self,
|
|
200
|
+
orchestrator: Any,
|
|
201
|
+
artifact: Artifact,
|
|
202
|
+
agent: Agent,
|
|
203
|
+
subscription: Subscription,
|
|
204
|
+
) -> Any:
|
|
205
|
+
"""Run on_collect_artifacts hooks (returns first non-None result).
|
|
206
|
+
|
|
207
|
+
Components execute in priority order. First component to return non-None
|
|
208
|
+
wins (short-circuit). If all return None, default is immediate scheduling.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
orchestrator: The Flock orchestrator instance
|
|
212
|
+
artifact: The artifact being collected
|
|
213
|
+
agent: The target agent
|
|
214
|
+
subscription: The subscription being evaluated
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
CollectionResult (complete=True/False, artifacts=[...])
|
|
218
|
+
"""
|
|
219
|
+
from flock.components.orchestrator import CollectionResult
|
|
220
|
+
|
|
221
|
+
for component in self._components:
|
|
222
|
+
comp_name = component.name or component.__class__.__name__
|
|
223
|
+
|
|
224
|
+
self._logger.debug(
|
|
225
|
+
f"Running on_collect_artifacts: component={comp_name}, "
|
|
226
|
+
f"agent={agent.name}, artifact_type={artifact.type}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
result = await component.on_collect_artifacts(
|
|
231
|
+
orchestrator, artifact, agent, subscription
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if result is not None:
|
|
235
|
+
self._logger.debug(
|
|
236
|
+
f"Collection handled by component: component={comp_name}, "
|
|
237
|
+
f"complete={result.complete}, artifact_count={len(result.artifacts)}"
|
|
238
|
+
)
|
|
239
|
+
return result
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self._logger.exception(
|
|
242
|
+
f"Component hook failed: component={comp_name}, "
|
|
243
|
+
f"hook=on_collect_artifacts, error={e!s}"
|
|
244
|
+
)
|
|
245
|
+
raise
|
|
246
|
+
|
|
247
|
+
# Default: immediate scheduling with single artifact
|
|
248
|
+
self._logger.debug(
|
|
249
|
+
f"No component handled collection, using default: "
|
|
250
|
+
f"agent={agent.name}, artifact_type={artifact.type}"
|
|
251
|
+
)
|
|
252
|
+
return CollectionResult.immediate([artifact])
|
|
253
|
+
|
|
254
|
+
async def run_before_agent_schedule(
|
|
255
|
+
self, orchestrator: Any, agent: Agent, artifacts: list[Artifact]
|
|
256
|
+
) -> list[Artifact] | None:
|
|
257
|
+
"""Run on_before_agent_schedule hooks (returns modified artifacts or None to block).
|
|
258
|
+
|
|
259
|
+
Components execute in priority order, each receiving artifacts from the
|
|
260
|
+
previous component (chaining). If any component returns None, scheduling
|
|
261
|
+
is blocked.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
orchestrator: The Flock orchestrator instance
|
|
265
|
+
agent: The target agent
|
|
266
|
+
artifacts: List of artifacts to schedule
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Modified artifacts list or None if scheduling blocked
|
|
270
|
+
"""
|
|
271
|
+
current_artifacts = artifacts
|
|
272
|
+
|
|
273
|
+
for component in self._components:
|
|
274
|
+
comp_name = component.name or component.__class__.__name__
|
|
275
|
+
|
|
276
|
+
self._logger.debug(
|
|
277
|
+
f"Running on_before_agent_schedule: component={comp_name}, "
|
|
278
|
+
f"agent={agent.name}, artifact_count={len(current_artifacts)}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
result = await component.on_before_agent_schedule(
|
|
283
|
+
orchestrator, agent, current_artifacts
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if result is None:
|
|
287
|
+
self._logger.info(
|
|
288
|
+
f"Agent scheduling blocked by component: component={comp_name}, "
|
|
289
|
+
f"agent={agent.name}"
|
|
290
|
+
)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
current_artifacts = result
|
|
294
|
+
except Exception as e:
|
|
295
|
+
self._logger.exception(
|
|
296
|
+
f"Component hook failed: component={comp_name}, "
|
|
297
|
+
f"hook=on_before_agent_schedule, error={e!s}"
|
|
298
|
+
)
|
|
299
|
+
raise
|
|
300
|
+
|
|
301
|
+
return current_artifacts
|
|
302
|
+
|
|
303
|
+
async def run_agent_scheduled(
|
|
304
|
+
self,
|
|
305
|
+
orchestrator: Any,
|
|
306
|
+
agent: Agent,
|
|
307
|
+
artifacts: list[Artifact],
|
|
308
|
+
task: Task[Any],
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Run on_agent_scheduled hooks (notification only, non-blocking).
|
|
311
|
+
|
|
312
|
+
Components execute in priority order. Exceptions are logged but don't
|
|
313
|
+
prevent other components from executing or block scheduling.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
orchestrator: The Flock orchestrator instance
|
|
317
|
+
agent: The scheduled agent
|
|
318
|
+
artifacts: List of artifacts for the agent
|
|
319
|
+
task: The asyncio task for agent execution
|
|
320
|
+
"""
|
|
321
|
+
for component in self._components:
|
|
322
|
+
comp_name = component.name or component.__class__.__name__
|
|
323
|
+
|
|
324
|
+
self._logger.debug(
|
|
325
|
+
f"Running on_agent_scheduled: component={comp_name}, "
|
|
326
|
+
f"agent={agent.name}, artifact_count={len(artifacts)}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
await component.on_agent_scheduled(orchestrator, agent, artifacts, task)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
self._logger.warning(
|
|
333
|
+
f"Component notification hook failed (non-critical): "
|
|
334
|
+
f"component={comp_name}, hook=on_agent_scheduled, error={e!s}"
|
|
335
|
+
)
|
|
336
|
+
# Don't propagate - this is a notification hook
|
|
337
|
+
|
|
338
|
+
async def run_idle(self, orchestrator: Any) -> None:
|
|
339
|
+
"""Run on_orchestrator_idle hooks when orchestrator becomes idle.
|
|
340
|
+
|
|
341
|
+
Components execute in priority order. Exceptions are logged but don't
|
|
342
|
+
prevent other components from executing.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
orchestrator: The Flock orchestrator instance
|
|
346
|
+
"""
|
|
347
|
+
self._logger.debug(
|
|
348
|
+
f"Running on_orchestrator_idle hooks: component_count={len(self._components)}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
for component in self._components:
|
|
352
|
+
comp_name = component.name or component.__class__.__name__
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
await component.on_orchestrator_idle(orchestrator)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
self._logger.warning(
|
|
358
|
+
f"Component idle hook failed (non-critical): "
|
|
359
|
+
f"component={comp_name}, hook=on_orchestrator_idle, error={e!s}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
async def run_shutdown(self, orchestrator: Any) -> None:
|
|
363
|
+
"""Run on_shutdown hooks when orchestrator shuts down.
|
|
364
|
+
|
|
365
|
+
Components execute in priority order. Exceptions are logged but don't
|
|
366
|
+
prevent shutdown of other components (best-effort cleanup).
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
orchestrator: The Flock orchestrator instance
|
|
370
|
+
"""
|
|
371
|
+
self._logger.info(
|
|
372
|
+
f"Shutting down {len(self._components)} orchestrator components"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
for component in self._components:
|
|
376
|
+
comp_name = component.name or component.__class__.__name__
|
|
377
|
+
self._logger.debug(f"Shutting down component: name={comp_name}")
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
await component.on_shutdown(orchestrator)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
self._logger.exception(
|
|
383
|
+
f"Component shutdown failed: component={comp_name}, "
|
|
384
|
+
f"hook=on_shutdown, error={e!s}"
|
|
385
|
+
)
|
|
386
|
+
# Continue shutting down other components
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
__all__ = ["ComponentRunner"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Execution context building with security boundary enforcement.
|
|
2
|
+
|
|
3
|
+
Phase 5A: Extracted from orchestrator.py to eliminate code duplication.
|
|
4
|
+
|
|
5
|
+
This module implements the security boundary pattern for context creation,
|
|
6
|
+
consolidating duplicated code from direct_invoke(), invoke(), and _run_agent_task().
|
|
7
|
+
|
|
8
|
+
SECURITY CRITICAL: This module enforces the Phase 8 context provider pattern
|
|
9
|
+
that prevents identity spoofing and READ capability bypass.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
from uuid import UUID, uuid4
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from flock.core import Agent
|
|
21
|
+
from flock.core.artifacts import Artifact
|
|
22
|
+
from flock.core.store import BlackboardStore
|
|
23
|
+
from flock.utils.runtime import Context
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContextBuilder:
|
|
27
|
+
"""Builds execution contexts with security boundary enforcement.
|
|
28
|
+
|
|
29
|
+
This module implements the security boundary pattern:
|
|
30
|
+
1. Resolve provider (agent > global > default)
|
|
31
|
+
2. Wrap with BoundContextProvider (prevent identity spoofing)
|
|
32
|
+
3. Evaluate context artifacts (orchestrator controls READ)
|
|
33
|
+
4. Create Context with data-only (no capabilities)
|
|
34
|
+
|
|
35
|
+
Phase 5A: Extracted to eliminate duplication across 3 methods and
|
|
36
|
+
reduce _run_agent_task complexity from C(11) to B or A.
|
|
37
|
+
|
|
38
|
+
SECURITY NOTICE: Changes to this module affect the security boundary
|
|
39
|
+
between agents and the blackboard. Review carefully.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
store: BlackboardStore,
|
|
46
|
+
default_context_provider: Any | None = None,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize ContextBuilder with blackboard store and default provider.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
store: BlackboardStore instance for context provider queries
|
|
52
|
+
default_context_provider: Global context provider (Phase 3 security fix).
|
|
53
|
+
If None, agents use DefaultContextProvider. Can be overridden per-agent.
|
|
54
|
+
"""
|
|
55
|
+
self._store = store
|
|
56
|
+
self._default_context_provider = default_context_provider
|
|
57
|
+
self._logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
async def build_execution_context(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
agent: Agent,
|
|
63
|
+
artifacts: list[Artifact],
|
|
64
|
+
correlation_id: UUID | None = None,
|
|
65
|
+
is_batch: bool = False,
|
|
66
|
+
) -> Context:
|
|
67
|
+
"""Build Context with pre-filtered artifacts (Phase 8 security fix).
|
|
68
|
+
|
|
69
|
+
Implements the security boundary pattern:
|
|
70
|
+
1. Resolve provider (agent > global > default)
|
|
71
|
+
2. Wrap with BoundContextProvider (prevent identity spoofing)
|
|
72
|
+
3. Evaluate context artifacts (orchestrator controls READ)
|
|
73
|
+
4. Create Context with data-only (no capabilities)
|
|
74
|
+
|
|
75
|
+
SECURITY NOTICE: This method enforces the security boundary between
|
|
76
|
+
agents and the blackboard. Agents receive pre-filtered context data
|
|
77
|
+
and cannot bypass visibility controls.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
agent: Agent instance being executed
|
|
81
|
+
artifacts: Input artifacts that triggered execution
|
|
82
|
+
correlation_id: Optional correlation ID for grouping related work
|
|
83
|
+
is_batch: Whether this is a batch execution (affects context metadata)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Context with pre-filtered artifacts and agent identity
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
>>> # Direct invocation
|
|
90
|
+
>>> context = await builder.build_execution_context(
|
|
91
|
+
... agent=pizza_agent,
|
|
92
|
+
... artifacts=[input_artifact],
|
|
93
|
+
... correlation_id=uuid4(),
|
|
94
|
+
... is_batch=False,
|
|
95
|
+
... )
|
|
96
|
+
>>> outputs = await agent.execute(context, artifacts)
|
|
97
|
+
|
|
98
|
+
>>> # Batch execution
|
|
99
|
+
>>> context = await builder.build_execution_context(
|
|
100
|
+
... agent=batch_agent,
|
|
101
|
+
... artifacts=batch_artifacts,
|
|
102
|
+
... correlation_id=batch_correlation,
|
|
103
|
+
... is_batch=True,
|
|
104
|
+
... )
|
|
105
|
+
"""
|
|
106
|
+
# Phase 8: Evaluate context BEFORE creating Context (security fix)
|
|
107
|
+
# Provider resolution: per-agent > global > DefaultContextProvider
|
|
108
|
+
from flock.core.context_provider import (
|
|
109
|
+
BoundContextProvider,
|
|
110
|
+
ContextRequest,
|
|
111
|
+
DefaultContextProvider,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Resolve correlation ID
|
|
115
|
+
resolved_correlation_id = correlation_id or (
|
|
116
|
+
artifacts[0].correlation_id
|
|
117
|
+
if artifacts and artifacts[0].correlation_id
|
|
118
|
+
else uuid4()
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Step 1: Resolve provider (agent > global > default)
|
|
122
|
+
inner_provider = (
|
|
123
|
+
getattr(agent, "context_provider", None)
|
|
124
|
+
or self._default_context_provider
|
|
125
|
+
or DefaultContextProvider()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Step 2: SECURITY FIX - Wrap provider with BoundContextProvider
|
|
129
|
+
# This prevents identity spoofing by binding the provider to the agent's identity
|
|
130
|
+
provider = BoundContextProvider(inner_provider, agent.identity)
|
|
131
|
+
|
|
132
|
+
# Step 3: Evaluate context using provider (orchestrator controls READ capability)
|
|
133
|
+
# Engines will receive pre-filtered artifacts via ctx.artifacts
|
|
134
|
+
request = ContextRequest(
|
|
135
|
+
agent=agent,
|
|
136
|
+
correlation_id=resolved_correlation_id,
|
|
137
|
+
store=self._store,
|
|
138
|
+
agent_identity=agent.identity,
|
|
139
|
+
exclude_ids={a.id for a in artifacts}, # Exclude input artifacts
|
|
140
|
+
)
|
|
141
|
+
context_artifacts = await provider(request)
|
|
142
|
+
|
|
143
|
+
# Step 4: Create Context with pre-filtered data (no capabilities!)
|
|
144
|
+
# SECURITY: Context is now just data - engines can't query anything
|
|
145
|
+
from flock.utils.runtime import Context
|
|
146
|
+
|
|
147
|
+
ctx = Context(
|
|
148
|
+
artifacts=context_artifacts, # Pre-filtered conversation context
|
|
149
|
+
agent_identity=agent.identity,
|
|
150
|
+
task_id=str(uuid4()),
|
|
151
|
+
correlation_id=resolved_correlation_id,
|
|
152
|
+
is_batch=is_batch,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Log context creation for debugging
|
|
156
|
+
self._logger.debug(
|
|
157
|
+
f"Context built: agent={agent.name}, "
|
|
158
|
+
f"correlation_id={resolved_correlation_id}, "
|
|
159
|
+
f"is_batch={is_batch}, "
|
|
160
|
+
f"context_artifacts={len(context_artifacts)}, "
|
|
161
|
+
f"input_artifacts={len(artifacts)}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return ctx
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["ContextBuilder"]
|
|
@@ -16,8 +16,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
-
from flock.artifacts import Artifact
|
|
20
|
-
from flock.subscription import JoinSpec, Subscription
|
|
19
|
+
from flock.core.artifacts import Artifact
|
|
20
|
+
from flock.core.subscription import JoinSpec, Subscription
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class CorrelationGroup:
|