flock-core 0.5.8__py3-none-any.whl → 0.5.10__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/agent.py +149 -62
- flock/api/themes.py +6 -2
- flock/artifact_collector.py +6 -3
- flock/batch_accumulator.py +3 -1
- flock/cli.py +3 -1
- flock/components.py +45 -56
- flock/context_provider.py +531 -0
- flock/correlation_engine.py +8 -4
- flock/dashboard/collector.py +48 -29
- flock/dashboard/events.py +10 -4
- flock/dashboard/launcher.py +3 -1
- flock/dashboard/models/graph.py +9 -3
- flock/dashboard/service.py +143 -72
- flock/dashboard/websocket.py +17 -4
- flock/engines/dspy_engine.py +174 -98
- flock/engines/examples/simple_batch_engine.py +9 -3
- flock/examples.py +6 -2
- flock/frontend/src/services/indexeddb.test.ts +4 -4
- flock/frontend/src/services/indexeddb.ts +1 -1
- flock/helper/cli_helper.py +14 -1
- flock/logging/auto_trace.py +6 -1
- flock/logging/formatters/enum_builder.py +3 -1
- flock/logging/formatters/theme_builder.py +32 -17
- flock/logging/formatters/themed_formatter.py +38 -22
- flock/logging/logging.py +21 -7
- flock/logging/telemetry.py +9 -3
- flock/logging/telemetry_exporter/duckdb_exporter.py +27 -25
- flock/logging/trace_and_logged.py +14 -5
- flock/mcp/__init__.py +3 -6
- flock/mcp/client.py +49 -19
- flock/mcp/config.py +12 -6
- flock/mcp/manager.py +6 -2
- flock/mcp/servers/sse/flock_sse_server.py +9 -3
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +6 -2
- flock/mcp/tool.py +18 -6
- flock/mcp/types/handlers.py +3 -1
- flock/mcp/types/types.py +9 -3
- flock/orchestrator.py +204 -50
- flock/orchestrator_component.py +15 -5
- flock/patches/dspy_streaming_patch.py +12 -4
- flock/registry.py +9 -3
- flock/runtime.py +69 -18
- flock/service.py +19 -6
- flock/store.py +29 -10
- flock/subscription.py +6 -4
- flock/utilities.py +41 -13
- flock/utility/output_utility_component.py +31 -11
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/METADATA +134 -4
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/RECORD +52 -51
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/WHEEL +0 -0
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/licenses/LICENSE +0 -0
flock/agent.py
CHANGED
|
@@ -47,12 +47,14 @@ class MCPServerConfig(TypedDict, total=False):
|
|
|
47
47
|
>>> config: MCPServerConfig = {"roots": ["/workspace/data"]}
|
|
48
48
|
|
|
49
49
|
>>> # Tool whitelist only
|
|
50
|
-
>>> config: MCPServerConfig = {
|
|
50
|
+
>>> config: MCPServerConfig = {
|
|
51
|
+
... "tool_whitelist": ["read_file", "write_file"]
|
|
52
|
+
... }
|
|
51
53
|
|
|
52
54
|
>>> # Both restrictions
|
|
53
55
|
>>> config: MCPServerConfig = {
|
|
54
56
|
... "roots": ["/workspace/data"],
|
|
55
|
-
... "tool_whitelist": ["read_file"]
|
|
57
|
+
... "tool_whitelist": ["read_file"],
|
|
56
58
|
... }
|
|
57
59
|
"""
|
|
58
60
|
|
|
@@ -66,9 +68,9 @@ class AgentOutput:
|
|
|
66
68
|
default_visibility: Visibility
|
|
67
69
|
count: int = 1 # Number of artifacts to generate (fan-out)
|
|
68
70
|
filter_predicate: Callable[[BaseModel], bool] | None = None # Where clause
|
|
69
|
-
validate_predicate:
|
|
70
|
-
|
|
71
|
-
)
|
|
71
|
+
validate_predicate: (
|
|
72
|
+
Callable[[BaseModel], bool] | list[tuple[Callable, str]] | None
|
|
73
|
+
) = None # Validation logic
|
|
72
74
|
group_description: str | None = None # Group description override
|
|
73
75
|
|
|
74
76
|
def __post_init__(self):
|
|
@@ -127,6 +129,13 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
127
129
|
All public methods are automatically traced via OpenTelemetry.
|
|
128
130
|
"""
|
|
129
131
|
|
|
132
|
+
# Phase 6+7: Class-level streaming coordination (SHARED across ALL agent instances)
|
|
133
|
+
# These class variables enable all agents to coordinate CLI streaming behavior
|
|
134
|
+
_streaming_counter: int = 0 # Global count of agents currently streaming to CLI
|
|
135
|
+
_websocket_broadcast_global: Any = (
|
|
136
|
+
None # WebSocket broadcast wrapper (dashboard mode)
|
|
137
|
+
)
|
|
138
|
+
|
|
130
139
|
def __init__(self, name: str, *, orchestrator: Flock) -> None:
|
|
131
140
|
self.name = name
|
|
132
141
|
self.description: str | None = None
|
|
@@ -145,10 +154,16 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
145
154
|
self.tenant_id: str | None = None
|
|
146
155
|
self.model: str | None = None
|
|
147
156
|
self.prevent_self_trigger: bool = True # T065: Prevent infinite feedback loops
|
|
157
|
+
# Phase 3: Per-agent context provider (security fix)
|
|
158
|
+
self.context_provider: Any = None
|
|
148
159
|
# MCP integration
|
|
149
160
|
self.mcp_server_names: set[str] = set()
|
|
150
|
-
self.mcp_mount_points: list[
|
|
151
|
-
|
|
161
|
+
self.mcp_mount_points: list[
|
|
162
|
+
str
|
|
163
|
+
] = [] # Deprecated: Use mcp_server_mounts instead
|
|
164
|
+
self.mcp_server_mounts: dict[
|
|
165
|
+
str, list[str]
|
|
166
|
+
] = {} # Server-specific mount points
|
|
152
167
|
self.tool_whitelist: list[str] | None = None
|
|
153
168
|
|
|
154
169
|
@property
|
|
@@ -158,7 +173,9 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
158
173
|
|
|
159
174
|
@property
|
|
160
175
|
def identity(self) -> AgentIdentity:
|
|
161
|
-
return AgentIdentity(
|
|
176
|
+
return AgentIdentity(
|
|
177
|
+
name=self.name, labels=self.labels, tenant_id=self.tenant_id
|
|
178
|
+
)
|
|
162
179
|
|
|
163
180
|
@staticmethod
|
|
164
181
|
def _component_display_name(component: AgentComponent) -> str:
|
|
@@ -199,7 +216,9 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
199
216
|
self._resolve_utilities()
|
|
200
217
|
await self._run_initialize(ctx)
|
|
201
218
|
processed_inputs = await self._run_pre_consume(ctx, artifacts)
|
|
202
|
-
eval_inputs = EvalInputs(
|
|
219
|
+
eval_inputs = EvalInputs(
|
|
220
|
+
artifacts=processed_inputs, state=dict(ctx.state)
|
|
221
|
+
)
|
|
203
222
|
eval_inputs = await self._run_pre_evaluate(ctx, eval_inputs)
|
|
204
223
|
|
|
205
224
|
# Phase 3: Call engine ONCE PER OutputGroup
|
|
@@ -218,13 +237,19 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
218
237
|
# Loop over each output group
|
|
219
238
|
for group_idx, output_group in enumerate(self.output_groups):
|
|
220
239
|
# Prepare group-specific context
|
|
221
|
-
group_ctx = self._prepare_group_context(
|
|
240
|
+
group_ctx = self._prepare_group_context(
|
|
241
|
+
ctx, group_idx, output_group
|
|
242
|
+
)
|
|
222
243
|
|
|
223
244
|
# Phase 7: Single evaluation path with auto-detection
|
|
224
245
|
# Engine's evaluate() auto-detects batch/fan-out from ctx and output_group
|
|
225
|
-
result = await self._run_engines(
|
|
246
|
+
result = await self._run_engines(
|
|
247
|
+
group_ctx, eval_inputs, output_group
|
|
248
|
+
)
|
|
226
249
|
|
|
227
|
-
result = await self._run_post_evaluate(
|
|
250
|
+
result = await self._run_post_evaluate(
|
|
251
|
+
group_ctx, eval_inputs, result
|
|
252
|
+
)
|
|
228
253
|
|
|
229
254
|
# Extract outputs for THIS group only
|
|
230
255
|
group_outputs = await self._make_outputs_for_group(
|
|
@@ -290,7 +315,10 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
290
315
|
for tool_key, tool_entry in tools_dict.items():
|
|
291
316
|
if isinstance(tool_entry, dict):
|
|
292
317
|
original_name = tool_entry.get("original_name", None)
|
|
293
|
-
if
|
|
318
|
+
if (
|
|
319
|
+
original_name is not None
|
|
320
|
+
and original_name in tool_whitelist
|
|
321
|
+
):
|
|
294
322
|
filtered_tools[tool_key] = tool_entry
|
|
295
323
|
|
|
296
324
|
tools_dict = filtered_tools
|
|
@@ -315,7 +343,9 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
315
343
|
except Exception as e:
|
|
316
344
|
# Architecture Decision: AD007 - Graceful Degradation
|
|
317
345
|
# Agent continues with native tools only
|
|
318
|
-
logger.error(
|
|
346
|
+
logger.error(
|
|
347
|
+
f"Failed to load MCP tools for agent {self.name}: {e}", exc_info=True
|
|
348
|
+
)
|
|
319
349
|
return []
|
|
320
350
|
|
|
321
351
|
async def _run_initialize(self, ctx: Context) -> None:
|
|
@@ -336,7 +366,9 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
336
366
|
for engine in self.engines:
|
|
337
367
|
await engine.on_initialize(self, ctx)
|
|
338
368
|
|
|
339
|
-
async def _run_pre_consume(
|
|
369
|
+
async def _run_pre_consume(
|
|
370
|
+
self, ctx: Context, inputs: list[Artifact]
|
|
371
|
+
) -> list[Artifact]:
|
|
340
372
|
current = inputs
|
|
341
373
|
for component in self._sorted_utilities():
|
|
342
374
|
comp_name = self._component_display_name(component)
|
|
@@ -408,7 +440,13 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
408
440
|
if isinstance(result, BaseModel) and not isinstance(result, ER):
|
|
409
441
|
result = ER.from_object(result, agent=self)
|
|
410
442
|
|
|
411
|
-
|
|
443
|
+
artifacts = result.artifacts
|
|
444
|
+
for artifact in artifacts:
|
|
445
|
+
artifact.correlation_id = ctx.correlation_id
|
|
446
|
+
|
|
447
|
+
result = await engine.on_post_evaluate(
|
|
448
|
+
self, ctx, current_inputs, result
|
|
449
|
+
)
|
|
412
450
|
accumulated_logs.extend(result.logs)
|
|
413
451
|
accumulated_metrics.update(result.metrics)
|
|
414
452
|
merged_state = dict(current_inputs.state)
|
|
@@ -484,9 +522,12 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
484
522
|
if matching_artifact:
|
|
485
523
|
metadata["artifact_id"] = matching_artifact.id
|
|
486
524
|
|
|
487
|
-
artifact = output_decl.apply(
|
|
525
|
+
artifact = output_decl.apply(
|
|
526
|
+
payload, produced_by=self.name, metadata=metadata
|
|
527
|
+
)
|
|
488
528
|
produced.append(artifact)
|
|
489
|
-
|
|
529
|
+
# Phase 6: REMOVED publishing - orchestrator now handles it
|
|
530
|
+
# await ctx.board.publish(artifact)
|
|
490
531
|
|
|
491
532
|
return produced
|
|
492
533
|
|
|
@@ -604,7 +645,9 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
604
645
|
model_instance = model_cls(**artifact.payload)
|
|
605
646
|
for check, error_msg in output_decl.validate_predicate:
|
|
606
647
|
if not check(model_instance):
|
|
607
|
-
raise ValueError(
|
|
648
|
+
raise ValueError(
|
|
649
|
+
f"{error_msg}: {output_decl.spec.type_name}"
|
|
650
|
+
)
|
|
608
651
|
|
|
609
652
|
# 5. Apply visibility and publish artifacts (Phase 5)
|
|
610
653
|
for artifact_from_engine in matching_artifacts:
|
|
@@ -626,14 +669,20 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
626
669
|
|
|
627
670
|
# Re-wrap the artifact with agent metadata
|
|
628
671
|
artifact = output_decl.apply(
|
|
629
|
-
artifact_from_engine.payload,
|
|
672
|
+
artifact_from_engine.payload,
|
|
673
|
+
produced_by=self.name,
|
|
674
|
+
metadata=metadata,
|
|
630
675
|
)
|
|
631
676
|
produced.append(artifact)
|
|
632
|
-
|
|
677
|
+
# Phase 6 SECURITY FIX: REMOVED publishing - orchestrator now handles it
|
|
678
|
+
# This fixes Vulnerability #2 (WRITE Bypass) - agents can no longer publish directly
|
|
679
|
+
# await ctx.board.publish(artifact)
|
|
633
680
|
|
|
634
681
|
return produced
|
|
635
682
|
|
|
636
|
-
async def _run_post_publish(
|
|
683
|
+
async def _run_post_publish(
|
|
684
|
+
self, ctx: Context, artifacts: Sequence[Artifact]
|
|
685
|
+
) -> None:
|
|
637
686
|
components = self._sorted_utilities()
|
|
638
687
|
for artifact in artifacts:
|
|
639
688
|
for component in components:
|
|
@@ -718,7 +767,8 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
718
767
|
return []
|
|
719
768
|
|
|
720
769
|
default_engine = DSPyEngine(
|
|
721
|
-
model=self._orchestrator.model
|
|
770
|
+
model=self._orchestrator.model
|
|
771
|
+
or os.getenv("DEFAULT_MODEL", "openai/gpt-4.1"),
|
|
722
772
|
instructions=self.description,
|
|
723
773
|
)
|
|
724
774
|
self.engines = [default_engine]
|
|
@@ -830,11 +880,13 @@ class AgentBuilder:
|
|
|
830
880
|
def consumes(
|
|
831
881
|
self,
|
|
832
882
|
*types: type[BaseModel],
|
|
833
|
-
where: Callable[[BaseModel], bool]
|
|
883
|
+
where: Callable[[BaseModel], bool]
|
|
884
|
+
| Sequence[Callable[[BaseModel], bool]]
|
|
885
|
+
| None = None,
|
|
834
886
|
text: str | None = None,
|
|
835
887
|
min_p: float = 0.0,
|
|
836
888
|
from_agents: Iterable[str] | None = None,
|
|
837
|
-
|
|
889
|
+
tags: Iterable[str] | None = None,
|
|
838
890
|
join: dict | JoinSpec | None = None,
|
|
839
891
|
batch: dict | BatchSpec | None = None,
|
|
840
892
|
delivery: str = "exclusive",
|
|
@@ -853,7 +905,7 @@ class AgentBuilder:
|
|
|
853
905
|
text: Optional semantic text filter using embedding similarity
|
|
854
906
|
min_p: Minimum probability threshold for text similarity (0.0-1.0)
|
|
855
907
|
from_agents: Only consume artifacts from specific agents
|
|
856
|
-
|
|
908
|
+
tags: Only consume artifacts with matching tags
|
|
857
909
|
join: Join specification for coordinating multiple artifact types
|
|
858
910
|
batch: Batch specification for processing multiple artifacts together
|
|
859
911
|
delivery: Delivery mode - "exclusive" (one agent) or "broadcast" (all matching)
|
|
@@ -876,23 +928,17 @@ class AgentBuilder:
|
|
|
876
928
|
>>> # Multiple predicates (all must pass)
|
|
877
929
|
>>> agent.consumes(
|
|
878
930
|
... Order,
|
|
879
|
-
... where=[
|
|
880
|
-
... lambda o: o.total > 100,
|
|
881
|
-
... lambda o: o.status == "pending"
|
|
882
|
-
... ]
|
|
931
|
+
... where=[lambda o: o.total > 100, lambda o: o.status == "pending"],
|
|
883
932
|
... )
|
|
884
933
|
|
|
885
934
|
>>> # Consume from specific agents
|
|
886
935
|
>>> agent.consumes(Report, from_agents=["analyzer", "validator"])
|
|
887
936
|
|
|
888
937
|
>>> # Channel-based routing
|
|
889
|
-
>>> agent.consumes(Alert,
|
|
938
|
+
>>> agent.consumes(Alert, tags={"critical", "security"})
|
|
890
939
|
|
|
891
940
|
>>> # Batch processing
|
|
892
|
-
>>> agent.consumes(
|
|
893
|
-
... Email,
|
|
894
|
-
... batch={"size": 10, "timeout": 5.0}
|
|
895
|
-
... )
|
|
941
|
+
>>> agent.consumes(Email, batch={"size": 10, "timeout": 5.0})
|
|
896
942
|
"""
|
|
897
943
|
predicates: Sequence[Callable[[BaseModel], bool]] | None
|
|
898
944
|
if where is None:
|
|
@@ -911,7 +957,7 @@ class AgentBuilder:
|
|
|
911
957
|
where=predicates,
|
|
912
958
|
text_predicates=text_predicates,
|
|
913
959
|
from_agents=from_agents,
|
|
914
|
-
|
|
960
|
+
tags=tags,
|
|
915
961
|
join=join_spec,
|
|
916
962
|
batch=batch_spec,
|
|
917
963
|
delivery=delivery,
|
|
@@ -927,7 +973,9 @@ class AgentBuilder:
|
|
|
927
973
|
visibility: Visibility | Callable[[BaseModel], Visibility] | None = None,
|
|
928
974
|
fan_out: int | None = None,
|
|
929
975
|
where: Callable[[BaseModel], bool] | None = None,
|
|
930
|
-
validate: Callable[[BaseModel], bool]
|
|
976
|
+
validate: Callable[[BaseModel], bool]
|
|
977
|
+
| list[tuple[Callable, str]]
|
|
978
|
+
| None = None,
|
|
931
979
|
description: str | None = None,
|
|
932
980
|
) -> PublishBuilder:
|
|
933
981
|
"""Declare which artifact types this agent produces.
|
|
@@ -945,11 +993,17 @@ class AgentBuilder:
|
|
|
945
993
|
|
|
946
994
|
Examples:
|
|
947
995
|
>>> agent.publishes(Report) # Publish 1 Report
|
|
948
|
-
>>> agent.publishes(
|
|
996
|
+
>>> agent.publishes(
|
|
997
|
+
... Task, Task, Task
|
|
998
|
+
... ) # Publish 3 Tasks (duplicate counting)
|
|
949
999
|
>>> agent.publishes(Task, fan_out=3) # Same as above (sugar syntax)
|
|
950
1000
|
>>> agent.publishes(Task, where=lambda t: t.priority > 5) # With filtering
|
|
951
|
-
>>> agent.publishes(
|
|
952
|
-
|
|
1001
|
+
>>> agent.publishes(
|
|
1002
|
+
... Report, validate=lambda r: r.score > 0
|
|
1003
|
+
... ) # With validation
|
|
1004
|
+
>>> agent.publishes(
|
|
1005
|
+
... Task, description="Special instructions"
|
|
1006
|
+
... ) # With description
|
|
953
1007
|
|
|
954
1008
|
See Also:
|
|
955
1009
|
- PublicVisibility: Default, visible to all agents
|
|
@@ -1000,7 +1054,9 @@ class AgentBuilder:
|
|
|
1000
1054
|
# Create OutputGroup from outputs
|
|
1001
1055
|
group = OutputGroup(
|
|
1002
1056
|
outputs=outputs,
|
|
1003
|
-
shared_visibility=resolved_visibility
|
|
1057
|
+
shared_visibility=resolved_visibility
|
|
1058
|
+
if not callable(resolved_visibility)
|
|
1059
|
+
else None,
|
|
1004
1060
|
group_description=description,
|
|
1005
1061
|
)
|
|
1006
1062
|
|
|
@@ -1027,20 +1083,14 @@ class AgentBuilder:
|
|
|
1027
1083
|
|
|
1028
1084
|
Examples:
|
|
1029
1085
|
>>> # Rate limiting
|
|
1030
|
-
>>> agent.with_utilities(
|
|
1031
|
-
... RateLimiter(max_calls=10, window=60)
|
|
1032
|
-
... )
|
|
1086
|
+
>>> agent.with_utilities(RateLimiter(max_calls=10, window=60))
|
|
1033
1087
|
|
|
1034
1088
|
>>> # Budget control
|
|
1035
|
-
>>> agent.with_utilities(
|
|
1036
|
-
... TokenBudget(max_tokens=10000)
|
|
1037
|
-
... )
|
|
1089
|
+
>>> agent.with_utilities(TokenBudget(max_tokens=10000))
|
|
1038
1090
|
|
|
1039
1091
|
>>> # Multiple components (executed in order)
|
|
1040
1092
|
>>> agent.with_utilities(
|
|
1041
|
-
... RateLimiter(max_calls=5),
|
|
1042
|
-
... MetricsCollector(),
|
|
1043
|
-
... CacheLayer(ttl=3600)
|
|
1093
|
+
... RateLimiter(max_calls=5), MetricsCollector(), CacheLayer(ttl=3600)
|
|
1044
1094
|
... )
|
|
1045
1095
|
|
|
1046
1096
|
See Also:
|
|
@@ -1066,19 +1116,14 @@ class AgentBuilder:
|
|
|
1066
1116
|
|
|
1067
1117
|
Examples:
|
|
1068
1118
|
>>> # DSPy engine with specific model
|
|
1069
|
-
>>> agent.with_engines(
|
|
1070
|
-
... DSPyEngine(model="openai/gpt-4o")
|
|
1071
|
-
... )
|
|
1119
|
+
>>> agent.with_engines(DSPyEngine(model="openai/gpt-4o"))
|
|
1072
1120
|
|
|
1073
1121
|
>>> # Custom non-LLM engine
|
|
1074
|
-
>>> agent.with_engines(
|
|
1075
|
-
... RuleBasedEngine(rules=my_rules)
|
|
1076
|
-
... )
|
|
1122
|
+
>>> agent.with_engines(RuleBasedEngine(rules=my_rules))
|
|
1077
1123
|
|
|
1078
1124
|
>>> # Hybrid approach (multiple engines)
|
|
1079
1125
|
>>> agent.with_engines(
|
|
1080
|
-
... DSPyEngine(model="openai/gpt-4o-mini"),
|
|
1081
|
-
... FallbackEngine()
|
|
1126
|
+
... DSPyEngine(model="openai/gpt-4o-mini"), FallbackEngine()
|
|
1082
1127
|
... )
|
|
1083
1128
|
|
|
1084
1129
|
Note:
|
|
@@ -1113,6 +1158,39 @@ class AgentBuilder:
|
|
|
1113
1158
|
self._agent.tools.update(funcs)
|
|
1114
1159
|
return self
|
|
1115
1160
|
|
|
1161
|
+
def with_context(self, provider: Any) -> AgentBuilder:
|
|
1162
|
+
"""Configure a custom context provider for this agent (Phase 3 security fix).
|
|
1163
|
+
|
|
1164
|
+
Context providers control what artifacts an agent can see, enforcing
|
|
1165
|
+
visibility filtering at the security boundary layer.
|
|
1166
|
+
|
|
1167
|
+
Args:
|
|
1168
|
+
provider: ContextProvider instance for this agent
|
|
1169
|
+
|
|
1170
|
+
Returns:
|
|
1171
|
+
self for method chaining
|
|
1172
|
+
|
|
1173
|
+
Examples:
|
|
1174
|
+
>>> # Use custom provider for this agent
|
|
1175
|
+
>>> agent.with_context(MyCustomProvider())
|
|
1176
|
+
|
|
1177
|
+
>>> # Use FilteredContextProvider for declarative filtering
|
|
1178
|
+
>>> agent.with_context(
|
|
1179
|
+
... FilteredContextProvider(FilterConfig(tags={"important"}))
|
|
1180
|
+
... )
|
|
1181
|
+
|
|
1182
|
+
Note:
|
|
1183
|
+
Per-agent provider takes precedence over global provider configured
|
|
1184
|
+
on Flock(context_provider=...). If neither is set, DefaultContextProvider
|
|
1185
|
+
is used automatically.
|
|
1186
|
+
|
|
1187
|
+
See Also:
|
|
1188
|
+
- DefaultContextProvider: Default security boundary with visibility enforcement
|
|
1189
|
+
- FilteredContextProvider: Declarative filtering with FilterConfig
|
|
1190
|
+
"""
|
|
1191
|
+
self._agent.context_provider = provider
|
|
1192
|
+
return self
|
|
1193
|
+
|
|
1116
1194
|
def with_mcps(
|
|
1117
1195
|
self,
|
|
1118
1196
|
servers: (
|
|
@@ -1144,8 +1222,11 @@ class AgentBuilder:
|
|
|
1144
1222
|
|
|
1145
1223
|
>>> # New format: Server-specific config with roots and tool whitelist
|
|
1146
1224
|
>>> agent.with_mcps({
|
|
1147
|
-
... "filesystem": {
|
|
1148
|
-
...
|
|
1225
|
+
... "filesystem": {
|
|
1226
|
+
... "roots": ["/workspace/dir/data"],
|
|
1227
|
+
... "tool_whitelist": ["read_file"],
|
|
1228
|
+
... },
|
|
1229
|
+
... "github": {}, # No restrictions for github
|
|
1149
1230
|
... })
|
|
1150
1231
|
|
|
1151
1232
|
>>> # Old format: Direct list (backward compatible)
|
|
@@ -1180,7 +1261,11 @@ class AgentBuilder:
|
|
|
1180
1261
|
elif isinstance(server_config, dict):
|
|
1181
1262
|
# New format: MCPServerConfig with optional roots and tool_whitelist
|
|
1182
1263
|
mounts = server_config.get("roots", None)
|
|
1183
|
-
if
|
|
1264
|
+
if (
|
|
1265
|
+
mounts is not None
|
|
1266
|
+
and isinstance(mounts, list)
|
|
1267
|
+
and len(mounts) > 0
|
|
1268
|
+
):
|
|
1184
1269
|
server_mounts[server_name] = list(mounts)
|
|
1185
1270
|
|
|
1186
1271
|
config_whitelist = server_config.get("tool_whitelist", None)
|
|
@@ -1341,7 +1426,9 @@ class AgentBuilder:
|
|
|
1341
1426
|
|
|
1342
1427
|
# Get types agent publishes
|
|
1343
1428
|
publishing_types = {
|
|
1344
|
-
output.spec.type_name
|
|
1429
|
+
output.spec.type_name
|
|
1430
|
+
for group in self._agent.output_groups
|
|
1431
|
+
for output in group.outputs
|
|
1345
1432
|
}
|
|
1346
1433
|
|
|
1347
1434
|
# Check for overlap
|
flock/api/themes.py
CHANGED
|
@@ -59,7 +59,9 @@ async def get_theme(theme_name: str) -> dict[str, Any]:
|
|
|
59
59
|
theme_path = THEMES_DIR / f"{theme_name}.toml"
|
|
60
60
|
|
|
61
61
|
if not theme_path.exists():
|
|
62
|
-
raise HTTPException(
|
|
62
|
+
raise HTTPException(
|
|
63
|
+
status_code=404, detail=f"Theme '{theme_name}' not found"
|
|
64
|
+
)
|
|
63
65
|
|
|
64
66
|
# Load TOML theme
|
|
65
67
|
theme_data = toml.load(theme_path)
|
|
@@ -68,4 +70,6 @@ async def get_theme(theme_name: str) -> dict[str, Any]:
|
|
|
68
70
|
except HTTPException:
|
|
69
71
|
raise
|
|
70
72
|
except Exception as e:
|
|
71
|
-
raise HTTPException(
|
|
73
|
+
raise HTTPException(
|
|
74
|
+
status_code=500, detail=f"Failed to load theme '{theme_name}': {e!s}"
|
|
75
|
+
)
|
flock/artifact_collector.py
CHANGED
|
@@ -43,8 +43,8 @@ class ArtifactCollector:
|
|
|
43
43
|
# Structure: {(agent_name, subscription_index): {type_name: [artifact1, artifact2, ...]}}
|
|
44
44
|
# Example: {("diagnostician", 0): {"XRay": [artifact1], "LabResult": [artifact2]}}
|
|
45
45
|
# For count-based AND gates: {"TypeA": [artifact1, artifact2, artifact3]} (3 As collected)
|
|
46
|
-
self._waiting_pools: dict[tuple[str, int], dict[str, list[Artifact]]] =
|
|
47
|
-
lambda: defaultdict(list)
|
|
46
|
+
self._waiting_pools: dict[tuple[str, int], dict[str, list[Artifact]]] = (
|
|
47
|
+
defaultdict(lambda: defaultdict(list))
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
def add_artifact(
|
|
@@ -72,7 +72,10 @@ class ArtifactCollector:
|
|
|
72
72
|
- After returning complete=True, the pool is automatically cleared
|
|
73
73
|
"""
|
|
74
74
|
# Single-type subscription with count=1: No waiting needed (immediate trigger)
|
|
75
|
-
if
|
|
75
|
+
if (
|
|
76
|
+
len(subscription.type_names) == 1
|
|
77
|
+
and subscription.type_counts[artifact.type] == 1
|
|
78
|
+
):
|
|
76
79
|
return (True, [artifact])
|
|
77
80
|
|
|
78
81
|
# Multi-type or count-based subscription: Use waiting pool (AND gate logic)
|
flock/batch_accumulator.py
CHANGED
|
@@ -194,7 +194,9 @@ class BatchEngine:
|
|
|
194
194
|
|
|
195
195
|
return False # Not ready to flush yet
|
|
196
196
|
|
|
197
|
-
def flush_batch(
|
|
197
|
+
def flush_batch(
|
|
198
|
+
self, agent_name: str, subscription_index: int
|
|
199
|
+
) -> list[Artifact] | None:
|
|
198
200
|
"""
|
|
199
201
|
Flush a batch and return its artifacts.
|
|
200
202
|
|
flock/cli.py
CHANGED
|
@@ -123,7 +123,9 @@ def sqlite_maintenance(
|
|
|
123
123
|
try:
|
|
124
124
|
before_dt = datetime.fromisoformat(delete_before)
|
|
125
125
|
except ValueError as exc: # pragma: no cover - Typer handles but defensive
|
|
126
|
-
raise typer.BadParameter(
|
|
126
|
+
raise typer.BadParameter(
|
|
127
|
+
f"Invalid ISO timestamp: {delete_before}"
|
|
128
|
+
) from exc
|
|
127
129
|
deleted = await store.delete_before(before_dt)
|
|
128
130
|
if vacuum:
|
|
129
131
|
await store.vacuum()
|
flock/components.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING,
|
|
5
|
+
from typing import TYPE_CHECKING, Self
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field, create_model
|
|
8
8
|
from pydantic._internal._model_construction import ModelMetaclass
|
|
@@ -12,8 +12,6 @@ from flock.logging.auto_trace import AutoTracedMeta
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
15
|
-
from uuid import UUID
|
|
16
|
-
|
|
17
15
|
from flock.agent import Agent, OutputGroup
|
|
18
16
|
from flock.artifacts import Artifact
|
|
19
17
|
from flock.runtime import Context, EvalInputs, EvalResult
|
|
@@ -71,7 +69,9 @@ class AgentComponent(BaseModel, metaclass=TracedModelMeta):
|
|
|
71
69
|
) -> list[Artifact]:
|
|
72
70
|
return inputs
|
|
73
71
|
|
|
74
|
-
async def on_pre_evaluate(
|
|
72
|
+
async def on_pre_evaluate(
|
|
73
|
+
self, agent: Agent, ctx: Context, inputs: EvalInputs
|
|
74
|
+
) -> EvalInputs:
|
|
75
75
|
return inputs
|
|
76
76
|
|
|
77
77
|
async def on_post_evaluate(
|
|
@@ -89,7 +89,9 @@ class AgentComponent(BaseModel, metaclass=TracedModelMeta):
|
|
|
89
89
|
) -> None: # pragma: no cover - default
|
|
90
90
|
return None
|
|
91
91
|
|
|
92
|
-
async def on_terminate(
|
|
92
|
+
async def on_terminate(
|
|
93
|
+
self, agent: Agent, ctx: Context
|
|
94
|
+
) -> None: # pragma: no cover - default
|
|
93
95
|
return None
|
|
94
96
|
|
|
95
97
|
|
|
@@ -153,67 +155,54 @@ class EngineComponent(AgentComponent):
|
|
|
153
155
|
"""
|
|
154
156
|
raise NotImplementedError
|
|
155
157
|
|
|
156
|
-
|
|
158
|
+
def get_conversation_context(
|
|
157
159
|
self,
|
|
158
160
|
ctx: Context,
|
|
159
|
-
correlation_id: UUID | None = None,
|
|
160
161
|
max_artifacts: int | None = None,
|
|
161
|
-
) -> list[
|
|
162
|
-
"""
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
) -> list[Artifact]:
|
|
163
|
+
"""Get conversation context from Context (read-only helper).
|
|
164
|
+
|
|
165
|
+
Phase 8 Security Fix: This method now simply reads pre-filtered artifacts from
|
|
166
|
+
Context. The orchestrator evaluates context BEFORE creating Context, so engines
|
|
167
|
+
can no longer query arbitrary data.
|
|
168
|
+
|
|
169
|
+
REMOVED METHODS (Security Fix):
|
|
170
|
+
- fetch_conversation_context() - REMOVED (engines can't query anymore)
|
|
171
|
+
- get_latest_artifact_of_type() - REMOVED (engines can't query anymore)
|
|
172
|
+
|
|
173
|
+
Migration Guide:
|
|
174
|
+
Old (vulnerable): context = await self.fetch_conversation_context(ctx, agent, exclude_ids)
|
|
175
|
+
New (secure): context = ctx.artifacts # Pre-filtered by orchestrator!
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
Args:
|
|
178
|
+
ctx: Execution context with pre-filtered artifacts
|
|
179
|
+
max_artifacts: Optional limit (applies to already-filtered list)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of Artifact objects (pre-filtered by orchestrator via context provider)
|
|
183
|
+
with full metadata (type, payload, produced_by, created_at, tags, etc.)
|
|
184
|
+
"""
|
|
185
|
+
if not self.enable_context or not ctx:
|
|
168
186
|
return []
|
|
169
187
|
|
|
170
|
-
|
|
171
|
-
all_artifacts = await ctx.board.list()
|
|
188
|
+
context_items = list(ctx.artifacts) # Copy to avoid mutation
|
|
172
189
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
190
|
+
# Apply engine-level filtering (type exclusions)
|
|
191
|
+
if self.context_exclude_types:
|
|
192
|
+
context_items = [
|
|
193
|
+
item
|
|
194
|
+
for item in context_items
|
|
195
|
+
if item.type not in self.context_exclude_types
|
|
180
196
|
]
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
context = []
|
|
189
|
-
i = 0
|
|
190
|
-
for artifact in context_artifacts:
|
|
191
|
-
context.append(
|
|
192
|
-
{
|
|
193
|
-
"type": artifact.type,
|
|
194
|
-
"payload": artifact.payload,
|
|
195
|
-
"produced_by": artifact.produced_by,
|
|
196
|
-
"event_number": i,
|
|
197
|
-
# "created_at": artifact.created_at.isoformat(),
|
|
198
|
-
}
|
|
199
|
-
)
|
|
200
|
-
i += 1
|
|
201
|
-
|
|
202
|
-
return context
|
|
203
|
-
|
|
204
|
-
except Exception:
|
|
205
|
-
return []
|
|
198
|
+
# Apply max artifacts limit
|
|
199
|
+
max_limit = (
|
|
200
|
+
max_artifacts if max_artifacts is not None else self.context_max_artifacts
|
|
201
|
+
)
|
|
202
|
+
if max_limit is not None and max_limit > 0:
|
|
203
|
+
context_items = context_items[-max_limit:]
|
|
206
204
|
|
|
207
|
-
|
|
208
|
-
self,
|
|
209
|
-
ctx: Context,
|
|
210
|
-
artifact_type: str,
|
|
211
|
-
correlation_id: UUID | None = None,
|
|
212
|
-
) -> dict[str, Any] | None:
|
|
213
|
-
"""Get the most recent artifact of a specific type in the conversation."""
|
|
214
|
-
context = await self.fetch_conversation_context(ctx, correlation_id)
|
|
215
|
-
matching = [a for a in context if a["type"].endswith(artifact_type)]
|
|
216
|
-
return matching[-1] if matching else None
|
|
205
|
+
return context_items
|
|
217
206
|
|
|
218
207
|
def should_use_context(self, inputs: EvalInputs) -> bool:
|
|
219
208
|
"""Determine if context should be included based on the current inputs."""
|