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
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""Context Provider - Security Boundary Layer.
|
|
2
|
+
|
|
3
|
+
The Context Provider is the CRITICAL SECURITY BOUNDARY between agents
|
|
4
|
+
(untrusted business logic) and the blackboard store (infrastructure).
|
|
5
|
+
|
|
6
|
+
SECURITY FIX (2025-10-16): This module implements the fix for three
|
|
7
|
+
critical security vulnerabilities:
|
|
8
|
+
|
|
9
|
+
- Vulnerability #1 (READ BYPASS): Agents could bypass visibility via ctx.board.list()
|
|
10
|
+
- Vulnerability #2 (WRITE BYPASS): Agents could bypass validation via ctx.board.publish()
|
|
11
|
+
- Vulnerability #3 (GOD MODE): Agents had unlimited ctx.orchestrator access
|
|
12
|
+
|
|
13
|
+
Solution: Context Provider enforces visibility filtering BEFORE agents see data.
|
|
14
|
+
Agents can NO LONGER bypass security because they don't have direct store access.
|
|
15
|
+
|
|
16
|
+
References:
|
|
17
|
+
- .flock/flock-research/context-provider/SECURITY_ANALYSIS.md
|
|
18
|
+
- docs/specs/007-context-provider-security-fix/PLAN.md
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from datetime import datetime, timedelta
|
|
26
|
+
from typing import Any, Protocol
|
|
27
|
+
from uuid import UUID
|
|
28
|
+
|
|
29
|
+
from flock.artifacts import Artifact
|
|
30
|
+
from flock.store import BlackboardStore, FilterConfig
|
|
31
|
+
from flock.visibility import AgentIdentity
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ContextRequest:
|
|
36
|
+
"""Request for agent context.
|
|
37
|
+
|
|
38
|
+
This carries all information needed for providers to filter context
|
|
39
|
+
with mandatory visibility enforcement.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
agent: Agent instance requesting context
|
|
43
|
+
correlation_id: Workflow identifier for filtering
|
|
44
|
+
store: Blackboard store for querying artifacts
|
|
45
|
+
agent_identity: Agent identity for visibility checks (includes labels, tenant_id)
|
|
46
|
+
exclude_ids: Set of artifact IDs to exclude from context (e.g., input artifacts)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
agent: Any # Agent type to avoid circular import
|
|
50
|
+
correlation_id: UUID
|
|
51
|
+
store: BlackboardStore
|
|
52
|
+
agent_identity: AgentIdentity
|
|
53
|
+
exclude_ids: set[UUID] | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ContextProvider(Protocol):
|
|
57
|
+
"""Protocol for context providers.
|
|
58
|
+
|
|
59
|
+
Context Providers are the MANDATORY security boundary between agents
|
|
60
|
+
and the blackboard store. All providers MUST enforce visibility filtering.
|
|
61
|
+
|
|
62
|
+
SECURITY REQUIREMENT: Every provider implementation MUST call
|
|
63
|
+
artifact.visibility.allows(agent_identity) before returning artifacts.
|
|
64
|
+
Any provider that doesn't enforce visibility is a SECURITY BUG.
|
|
65
|
+
|
|
66
|
+
Implementations:
|
|
67
|
+
- DefaultContextProvider: Filters by correlation_id + visibility (default behavior)
|
|
68
|
+
- FilteredContextProvider: Wraps FilterConfig for declarative filtering + visibility
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
# Global provider
|
|
72
|
+
flock = Flock(context_provider=MyProvider())
|
|
73
|
+
|
|
74
|
+
# Per-agent provider
|
|
75
|
+
agent.with_context(MyProvider())
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
async def __call__(self, request: ContextRequest) -> list[Artifact]:
|
|
79
|
+
"""Fetch context with MANDATORY visibility enforcement.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
request: Context request with agent identity and correlation
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of Artifact objects that agent is allowed to see.
|
|
86
|
+
|
|
87
|
+
SECURITY: Implementation MUST filter by visibility using:
|
|
88
|
+
artifact.visibility.allows(request.agent_identity)
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BaseContextProvider(ABC):
|
|
94
|
+
"""Base class enforcing MANDATORY visibility filtering for all context providers.
|
|
95
|
+
|
|
96
|
+
**SECURITY BY DESIGN**: Subclasses implement get_artifacts() to query/filter
|
|
97
|
+
artifacts. Visibility filtering and exclude_ids handling are enforced by this
|
|
98
|
+
base class and CANNOT BE BYPASSED.
|
|
99
|
+
|
|
100
|
+
This makes it architecturally impossible to create an insecure provider that
|
|
101
|
+
forgets to check visibility. The security logic is centralized and guaranteed.
|
|
102
|
+
|
|
103
|
+
Architecture:
|
|
104
|
+
- Subclass implements: get_artifacts() - custom query/filtering logic
|
|
105
|
+
- Base class enforces: visibility filtering, exclude_ids
|
|
106
|
+
- Result: 75% less code, 100% security coverage
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
>>> class MyProvider(BaseContextProvider):
|
|
110
|
+
... async def get_artifacts(self, request):
|
|
111
|
+
... # Just return artifacts - base class handles visibility!
|
|
112
|
+
... artifacts, _ = await request.store.query_artifacts(...)
|
|
113
|
+
... return artifacts
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
|
|
118
|
+
"""Query and filter artifacts (will be visibility-filtered automatically).
|
|
119
|
+
|
|
120
|
+
Subclasses implement this to define their filtering logic:
|
|
121
|
+
- DefaultContextProvider: Query all artifacts
|
|
122
|
+
- CorrelatedContextProvider: Query by correlation_id
|
|
123
|
+
- RecentContextProvider: Query all + sort by time + limit
|
|
124
|
+
- TimeWindowContextProvider: Query all + filter by time window
|
|
125
|
+
- EmptyContextProvider: Return empty list
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
request: Context request with agent identity, store, correlation_id
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of artifacts (will be visibility-filtered by base class)
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
async def __call__(self, request: ContextRequest) -> list[Artifact]:
|
|
135
|
+
"""Fetch context with MANDATORY visibility enforcement (cannot be bypassed).
|
|
136
|
+
|
|
137
|
+
SECURITY IMPLEMENTATION (enforced by base class):
|
|
138
|
+
1. Get artifacts from subclass (custom filtering logic)
|
|
139
|
+
2. Filter by visibility (security filtering) - MANDATORY, CANNOT BE BYPASSED
|
|
140
|
+
3. Exclude specific artifacts (if requested)
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
request: Context request with agent identity
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of Artifact objects agent can see (visibility-filtered)
|
|
147
|
+
"""
|
|
148
|
+
# Step 1: Get artifacts from subclass implementation
|
|
149
|
+
artifacts = await self.get_artifacts(request)
|
|
150
|
+
|
|
151
|
+
# Step 2: CRITICAL SECURITY STEP - Filter by visibility (ENFORCED BY BASE CLASS)
|
|
152
|
+
# This is the FIX for Vulnerability #1 (READ BYPASS)
|
|
153
|
+
# Subclasses CANNOT bypass this - it's architecturally impossible
|
|
154
|
+
visible_artifacts = [
|
|
155
|
+
artifact
|
|
156
|
+
for artifact in artifacts
|
|
157
|
+
if artifact.visibility.allows(request.agent_identity)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Step 3: Exclude specific artifacts (e.g., input artifacts to avoid duplication)
|
|
161
|
+
if request.exclude_ids:
|
|
162
|
+
visible_artifacts = [
|
|
163
|
+
artifact
|
|
164
|
+
for artifact in visible_artifacts
|
|
165
|
+
if artifact.id not in request.exclude_ids
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
return visible_artifacts
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DefaultContextProvider(BaseContextProvider):
|
|
172
|
+
"""Default context provider - shows ALL artifacts on blackboard with MANDATORY visibility enforcement.
|
|
173
|
+
|
|
174
|
+
**EXPLICIT IS BETTER THAN IMPLICIT**: This provider shows agents everything on the
|
|
175
|
+
blackboard they're allowed to see (visibility-filtered). No magic correlation filtering!
|
|
176
|
+
|
|
177
|
+
If you want correlation-based filtering, use CorrelatedContextProvider explicitly.
|
|
178
|
+
|
|
179
|
+
This provider implements the secure replacement for the old vulnerable pattern:
|
|
180
|
+
Old (INSECURE): all_artifacts = await ctx.board.list()
|
|
181
|
+
New (SECURE): context = await provider(request)
|
|
182
|
+
|
|
183
|
+
Security Properties:
|
|
184
|
+
- ✅ Shows ALL artifacts on blackboard (no hidden filtering)
|
|
185
|
+
- ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
|
|
186
|
+
- ✅ Returns only artifacts agent is allowed to see
|
|
187
|
+
- ✅ No direct store access exposed to agents
|
|
188
|
+
|
|
189
|
+
This fixes Vulnerability #1 (READ BYPASS) where agents could access
|
|
190
|
+
any artifact regardless of visibility by calling ctx.board.list().
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
>>> # Global: All agents see everything they're allowed to
|
|
194
|
+
>>> flock = Flock(context_provider=DefaultContextProvider())
|
|
195
|
+
>>>
|
|
196
|
+
>>> # Per-agent: This agent sees full blackboard
|
|
197
|
+
>>> agent.context_provider = DefaultContextProvider()
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
|
|
201
|
+
"""Query ALL artifacts from blackboard (no filtering).
|
|
202
|
+
|
|
203
|
+
Visibility filtering is enforced by BaseContextProvider automatically.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
request: Context request with store access
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
All artifacts from blackboard (will be visibility-filtered by base class)
|
|
210
|
+
"""
|
|
211
|
+
artifacts, _ = await request.store.query_artifacts(
|
|
212
|
+
FilterConfig(), # Empty filter = get everything
|
|
213
|
+
limit=-1, # Get all artifacts
|
|
214
|
+
)
|
|
215
|
+
return artifacts
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class FilteredContextProvider(BaseContextProvider):
|
|
219
|
+
"""Context provider with declarative filtering + MANDATORY visibility enforcement.
|
|
220
|
+
|
|
221
|
+
This provider combines declarative filtering (FilterConfig) with security
|
|
222
|
+
enforcement (visibility). It implements Phase 4 of the security fix.
|
|
223
|
+
|
|
224
|
+
Security Properties:
|
|
225
|
+
- ✅ Filters by FilterConfig (declarative filtering: tags, types, correlation, etc.)
|
|
226
|
+
- ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
|
|
227
|
+
- ✅ Returns only artifacts matching BOTH filters AND visibility
|
|
228
|
+
- ✅ No direct store access exposed to agents
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
>>> # Filter by tags + enforce visibility
|
|
232
|
+
>>> provider = FilteredContextProvider(
|
|
233
|
+
... FilterConfig(tags={"important", "urgent"}), limit=10
|
|
234
|
+
... )
|
|
235
|
+
>>> agent.with_context(provider)
|
|
236
|
+
|
|
237
|
+
>>> # Filter by type + enforce visibility
|
|
238
|
+
>>> provider = FilteredContextProvider(
|
|
239
|
+
... FilterConfig(type_names={"Task", "Report"}), limit=50
|
|
240
|
+
... )
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(self, filter_config: FilterConfig, limit: int = 50):
|
|
244
|
+
"""Initialize FilteredContextProvider with declarative filters.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
filter_config: FilterConfig specifying declarative filters
|
|
248
|
+
limit: Maximum number of artifacts to return (default: 50)
|
|
249
|
+
"""
|
|
250
|
+
self.filter_config = filter_config
|
|
251
|
+
self.limit = limit
|
|
252
|
+
|
|
253
|
+
async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
|
|
254
|
+
"""Query artifacts using FilterConfig (declarative filtering).
|
|
255
|
+
|
|
256
|
+
Visibility filtering is enforced by BaseContextProvider automatically.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
request: Context request with store access
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Artifacts matching FilterConfig (will be visibility-filtered by base class)
|
|
263
|
+
"""
|
|
264
|
+
artifacts, _ = await request.store.query_artifacts(
|
|
265
|
+
self.filter_config,
|
|
266
|
+
limit=self.limit,
|
|
267
|
+
)
|
|
268
|
+
return artifacts
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class BoundContextProvider:
|
|
272
|
+
"""Security wrapper that binds a provider to a specific agent identity.
|
|
273
|
+
|
|
274
|
+
SECURITY FIX (2025-10-17): This wrapper prevents engines from forging
|
|
275
|
+
Context objects with fake agent_identity values. Even if an engine creates
|
|
276
|
+
a fake Context with a different agent_identity, this wrapper will use the
|
|
277
|
+
trusted identity that was bound at creation time by the orchestrator.
|
|
278
|
+
|
|
279
|
+
The orchestrator creates a BoundContextProvider for each agent execution,
|
|
280
|
+
binding it to the agent's true identity. Engines cannot bypass this because
|
|
281
|
+
they would need to create a fake BoundContextProvider, but they don't have
|
|
282
|
+
access to the real bound identity.
|
|
283
|
+
|
|
284
|
+
Example Attack (prevented):
|
|
285
|
+
>>> # Malicious engine tries to escalate privileges
|
|
286
|
+
>>> fake_ctx = Context(
|
|
287
|
+
... ...
|
|
288
|
+
... agent_identity=AgentIdentity(name="admin", labels={"admin"}), # FAKE
|
|
289
|
+
... )
|
|
290
|
+
>>> # Provider ignores fake identity, uses bound identity instead
|
|
291
|
+
>>> context = await bound_provider(
|
|
292
|
+
... request
|
|
293
|
+
... ) # Still filters as original agent
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def __init__(
|
|
297
|
+
self, inner_provider: ContextProvider, bound_agent_identity: AgentIdentity
|
|
298
|
+
):
|
|
299
|
+
"""Create provider bound to specific agent identity.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
inner_provider: Wrapped provider (e.g., DefaultContextProvider)
|
|
303
|
+
bound_agent_identity: Trusted agent identity from orchestrator
|
|
304
|
+
"""
|
|
305
|
+
self._inner = inner_provider
|
|
306
|
+
self._bound_identity = bound_agent_identity
|
|
307
|
+
|
|
308
|
+
async def __call__(self, request: ContextRequest) -> list[Artifact]:
|
|
309
|
+
"""Fetch context using BOUND agent identity (ignoring request.agent_identity).
|
|
310
|
+
|
|
311
|
+
SECURITY: This method ignores request.agent_identity because it could
|
|
312
|
+
come from untrusted engine code. Instead, it uses the bound identity
|
|
313
|
+
that was set by the orchestrator at Context creation time.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
request: Context request (agent_identity field is IGNORED)
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
List of Artifact objects filtered by BOUND identity (not request identity)
|
|
320
|
+
"""
|
|
321
|
+
# SECURITY: Replace untrusted agent_identity with trusted bound identity
|
|
322
|
+
secure_request = ContextRequest(
|
|
323
|
+
agent=request.agent,
|
|
324
|
+
correlation_id=request.correlation_id,
|
|
325
|
+
store=request.store,
|
|
326
|
+
agent_identity=self._bound_identity, # Use trusted identity, ignore request
|
|
327
|
+
exclude_ids=request.exclude_ids,
|
|
328
|
+
)
|
|
329
|
+
return await self._inner(secure_request)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class CorrelatedContextProvider(BaseContextProvider):
|
|
333
|
+
"""Context provider that filters by correlation_id + visibility.
|
|
334
|
+
|
|
335
|
+
**EXPLICIT WORKFLOW ISOLATION**: Use this when you want agents to see only
|
|
336
|
+
artifacts from their specific workflow (correlation_id).
|
|
337
|
+
|
|
338
|
+
This is the explicit version of what DefaultContextProvider used to do implicitly.
|
|
339
|
+
Now you choose: full blackboard (DefaultContextProvider) or workflow-scoped
|
|
340
|
+
(CorrelatedContextProvider).
|
|
341
|
+
|
|
342
|
+
Security Properties:
|
|
343
|
+
- ✅ Filters by correlation_id (workflow boundary)
|
|
344
|
+
- ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
|
|
345
|
+
- ✅ Returns only workflow artifacts agent is allowed to see
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> # Global: All agents only see their workflow
|
|
349
|
+
>>> flock = Flock(context_provider=CorrelatedContextProvider())
|
|
350
|
+
>>>
|
|
351
|
+
>>> # Per-agent: This agent only sees workflow artifacts
|
|
352
|
+
>>> agent.context_provider = CorrelatedContextProvider()
|
|
353
|
+
>>>
|
|
354
|
+
>>> # Use case: Multi-tenant SaaS with workflow isolation
|
|
355
|
+
>>> # Each workflow (correlation_id) is isolated from others
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
|
|
359
|
+
"""Query artifacts by correlation_id (workflow filtering).
|
|
360
|
+
|
|
361
|
+
Visibility filtering is enforced by BaseContextProvider automatically.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
request: Context request with correlation_id
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Workflow artifacts (will be visibility-filtered by base class)
|
|
368
|
+
"""
|
|
369
|
+
artifacts, _ = await request.store.query_artifacts(
|
|
370
|
+
FilterConfig(correlation_id=str(request.correlation_id)),
|
|
371
|
+
limit=-1, # Get all workflow artifacts
|
|
372
|
+
)
|
|
373
|
+
return artifacts
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class RecentContextProvider(BaseContextProvider):
|
|
377
|
+
"""Context provider that shows only the N most recent artifacts.
|
|
378
|
+
|
|
379
|
+
**TOKEN COST CONTROL**: Perfect for keeping context small and relevant by
|
|
380
|
+
showing only the most recent artifacts (sorted by creation time).
|
|
381
|
+
|
|
382
|
+
Security Properties:
|
|
383
|
+
- ✅ Limits context to N most recent artifacts
|
|
384
|
+
- ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
|
|
385
|
+
- ✅ Reduces token costs by limiting context size
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
>>> # Global: All agents see only last 10 artifacts
|
|
389
|
+
>>> flock = Flock(context_provider=RecentContextProvider(limit=10))
|
|
390
|
+
>>>
|
|
391
|
+
>>> # Per-agent: This agent sees only last 50 artifacts
|
|
392
|
+
>>> agent.context_provider = RecentContextProvider(limit=50)
|
|
393
|
+
>>>
|
|
394
|
+
>>> # Use case: High-volume systems where full history is too expensive
|
|
395
|
+
>>> # Agent only needs recent context to make decisions
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
def __init__(self, limit: int = 50):
|
|
399
|
+
"""Initialize RecentContextProvider with artifact limit.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
limit: Maximum number of recent artifacts to return (default: 50)
|
|
403
|
+
"""
|
|
404
|
+
self.limit = limit
|
|
405
|
+
|
|
406
|
+
async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
|
|
407
|
+
"""Query all artifacts and return N most recent.
|
|
408
|
+
|
|
409
|
+
Visibility filtering is enforced by BaseContextProvider automatically.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
request: Context request with store access
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
N most recent artifacts (will be visibility-filtered by base class)
|
|
416
|
+
"""
|
|
417
|
+
artifacts, _ = await request.store.query_artifacts(
|
|
418
|
+
FilterConfig(),
|
|
419
|
+
limit=-1, # Get all artifacts
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Sort by creation time (most recent first) and limit
|
|
423
|
+
artifacts.sort(key=lambda a: a.created_at, reverse=True)
|
|
424
|
+
return artifacts[: self.limit]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class TimeWindowContextProvider(BaseContextProvider):
|
|
428
|
+
"""Context provider that shows only artifacts from the last X hours.
|
|
429
|
+
|
|
430
|
+
**TIME-BASED FILTERING**: Perfect for real-time monitoring or event-driven
|
|
431
|
+
systems where only recent data is relevant.
|
|
432
|
+
|
|
433
|
+
Security Properties:
|
|
434
|
+
- ✅ Filters artifacts by time window (last X hours)
|
|
435
|
+
- ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
|
|
436
|
+
- ✅ Automatic cleanup of old context (no manual pruning needed)
|
|
437
|
+
|
|
438
|
+
Example:
|
|
439
|
+
>>> # Global: All agents see only last hour
|
|
440
|
+
>>> flock = Flock(context_provider=TimeWindowContextProvider(hours=1))
|
|
441
|
+
>>>
|
|
442
|
+
>>> # Per-agent: This agent sees last 24 hours
|
|
443
|
+
>>> agent.context_provider = TimeWindowContextProvider(hours=24)
|
|
444
|
+
>>>
|
|
445
|
+
>>> # Use case: Real-time monitoring dashboard
|
|
446
|
+
>>> # Only show events from last hour, ignore old data
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def __init__(self, hours: int = 1):
|
|
450
|
+
"""Initialize TimeWindowContextProvider with time window.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
hours: Number of hours to look back (default: 1)
|
|
454
|
+
"""
|
|
455
|
+
self.hours = hours
|
|
456
|
+
|
|
457
|
+
async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
|
|
458
|
+
"""Query all artifacts and filter by time window.
|
|
459
|
+
|
|
460
|
+
Visibility filtering is enforced by BaseContextProvider automatically.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
request: Context request with store access
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Artifacts within time window (will be visibility-filtered by base class)
|
|
467
|
+
"""
|
|
468
|
+
cutoff = datetime.now() - timedelta(hours=self.hours)
|
|
469
|
+
|
|
470
|
+
artifacts, _ = await request.store.query_artifacts(
|
|
471
|
+
FilterConfig(),
|
|
472
|
+
limit=-1, # Get all artifacts
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Filter by time window
|
|
476
|
+
return [artifact for artifact in artifacts if artifact.created_at >= cutoff]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class EmptyContextProvider(BaseContextProvider):
|
|
480
|
+
"""Context provider that returns NO historical context.
|
|
481
|
+
|
|
482
|
+
**STATELESS AGENTS**: Use this for purely functional agents that only
|
|
483
|
+
transform input → output without needing any historical context.
|
|
484
|
+
|
|
485
|
+
This is the ultimate token saver - zero context overhead!
|
|
486
|
+
|
|
487
|
+
Security Properties:
|
|
488
|
+
- ✅ Returns empty context (no artifacts)
|
|
489
|
+
- ✅ Enforces visibility (N/A - no artifacts to filter)
|
|
490
|
+
- ✅ Maximum token savings (zero context tokens)
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
>>> # Global: All agents are stateless (no context)
|
|
494
|
+
>>> flock = Flock(context_provider=EmptyContextProvider())
|
|
495
|
+
>>>
|
|
496
|
+
>>> # Per-agent: This agent is purely functional
|
|
497
|
+
>>> translator.context_provider = EmptyContextProvider()
|
|
498
|
+
>>>
|
|
499
|
+
>>> # Use case: Simple transformation agents
|
|
500
|
+
>>> # Agent: English → Spanish (no history needed)
|
|
501
|
+
>>> # Agent: Markdown → HTML (no history needed)
|
|
502
|
+
>>> # Agent: Image → Thumbnail (no history needed)
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
|
|
506
|
+
"""Return no artifacts (stateless agent).
|
|
507
|
+
|
|
508
|
+
Visibility filtering is enforced by BaseContextProvider automatically
|
|
509
|
+
(though there's nothing to filter).
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
request: Context request (ignored)
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Empty list (no artifacts)
|
|
516
|
+
"""
|
|
517
|
+
return []
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
__all__ = [
|
|
521
|
+
"BaseContextProvider",
|
|
522
|
+
"BoundContextProvider",
|
|
523
|
+
"ContextProvider",
|
|
524
|
+
"ContextRequest",
|
|
525
|
+
"CorrelatedContextProvider",
|
|
526
|
+
"DefaultContextProvider",
|
|
527
|
+
"EmptyContextProvider",
|
|
528
|
+
"FilteredContextProvider",
|
|
529
|
+
"RecentContextProvider",
|
|
530
|
+
"TimeWindowContextProvider",
|
|
531
|
+
]
|
flock/correlation_engine.py
CHANGED
|
@@ -44,7 +44,9 @@ class CorrelationGroup:
|
|
|
44
44
|
self.created_at_sequence = (
|
|
45
45
|
created_at_sequence # Global sequence when first artifact arrived
|
|
46
46
|
)
|
|
47
|
-
self.created_at_time: datetime | None =
|
|
47
|
+
self.created_at_time: datetime | None = (
|
|
48
|
+
None # Timestamp when first artifact arrived
|
|
49
|
+
)
|
|
48
50
|
|
|
49
51
|
# Waiting pool: type -> list of artifacts
|
|
50
52
|
self.waiting_artifacts: dict[str, list[Artifact]] = defaultdict(list)
|
|
@@ -121,8 +123,8 @@ class CorrelationEngine:
|
|
|
121
123
|
# Correlation state per (agent, subscription_index)
|
|
122
124
|
# Key: (agent_name, subscription_index)
|
|
123
125
|
# Value: dict[correlation_key, CorrelationGroup]
|
|
124
|
-
self.correlation_groups: dict[tuple[str, int], dict[Any, CorrelationGroup]] =
|
|
125
|
-
dict
|
|
126
|
+
self.correlation_groups: dict[tuple[str, int], dict[Any, CorrelationGroup]] = (
|
|
127
|
+
defaultdict(dict)
|
|
126
128
|
)
|
|
127
129
|
|
|
128
130
|
def add_artifact(
|
|
@@ -210,7 +212,9 @@ class CorrelationEngine:
|
|
|
210
212
|
|
|
211
213
|
# Remove expired groups
|
|
212
214
|
expired_keys = [
|
|
213
|
-
key
|
|
215
|
+
key
|
|
216
|
+
for key, group in groups.items()
|
|
217
|
+
if group.is_expired(self.global_sequence)
|
|
214
218
|
]
|
|
215
219
|
|
|
216
220
|
for key in expired_keys:
|