predicate-secure 0.1.0__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.
- predicate_secure/__init__.py +833 -0
- predicate_secure/adapters.py +388 -0
- predicate_secure/config.py +156 -0
- predicate_secure/detection.py +194 -0
- predicate_secure/py.typed +0 -0
- predicate_secure/tracing.py +511 -0
- predicate_secure-0.1.0.dist-info/METADATA +341 -0
- predicate_secure-0.1.0.dist-info/RECORD +13 -0
- predicate_secure-0.1.0.dist-info/WHEEL +5 -0
- predicate_secure-0.1.0.dist-info/licenses/LICENSE +24 -0
- predicate_secure-0.1.0.dist-info/licenses/LICENSE-APACHE +201 -0
- predicate_secure-0.1.0.dist-info/licenses/LICENSE-MIT +21 -0
- predicate_secure-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
"""
|
|
2
|
+
predicate-secure: Drop-in security wrapper for AI agents.
|
|
3
|
+
|
|
4
|
+
Adds authorization, verification, and audit to any agent framework
|
|
5
|
+
(browser-use, LangChain, Playwright, etc.) in 3 lines of code.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from predicate_secure import SecureAgent
|
|
9
|
+
from browser_use import Agent
|
|
10
|
+
|
|
11
|
+
# Wrap your existing agent
|
|
12
|
+
secure_agent = SecureAgent(
|
|
13
|
+
agent=Agent(task="Buy headphones", llm=my_model),
|
|
14
|
+
policy="policies/shopping.yaml",
|
|
15
|
+
mode="strict",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Run with full authorization + verification loop
|
|
19
|
+
secure_agent.run()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from .adapters import (
|
|
28
|
+
AdapterError,
|
|
29
|
+
AdapterResult,
|
|
30
|
+
create_adapter,
|
|
31
|
+
create_browser_use_adapter,
|
|
32
|
+
create_browser_use_runtime,
|
|
33
|
+
create_langchain_adapter,
|
|
34
|
+
create_playwright_adapter,
|
|
35
|
+
create_pydantic_ai_adapter,
|
|
36
|
+
)
|
|
37
|
+
from .config import SecureAgentConfig, WrappedAgent
|
|
38
|
+
from .detection import DetectionResult, Framework, FrameworkDetector, UnsupportedFrameworkError
|
|
39
|
+
from .tracing import (
|
|
40
|
+
DebugTracer,
|
|
41
|
+
PolicyDecision,
|
|
42
|
+
SnapshotDiff,
|
|
43
|
+
TraceEvent,
|
|
44
|
+
TraceFormat,
|
|
45
|
+
VerificationResult,
|
|
46
|
+
create_debug_tracer,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__version__ = "0.1.0"
|
|
50
|
+
|
|
51
|
+
# Public API
|
|
52
|
+
__all__ = [
|
|
53
|
+
"SecureAgent",
|
|
54
|
+
"SecureAgentConfig",
|
|
55
|
+
"WrappedAgent",
|
|
56
|
+
# Framework detection
|
|
57
|
+
"Framework",
|
|
58
|
+
"FrameworkDetector",
|
|
59
|
+
"DetectionResult",
|
|
60
|
+
# Adapters
|
|
61
|
+
"AdapterResult",
|
|
62
|
+
"AdapterError",
|
|
63
|
+
"create_adapter",
|
|
64
|
+
"create_browser_use_adapter",
|
|
65
|
+
"create_browser_use_runtime",
|
|
66
|
+
"create_playwright_adapter",
|
|
67
|
+
"create_langchain_adapter",
|
|
68
|
+
"create_pydantic_ai_adapter",
|
|
69
|
+
# Tracing
|
|
70
|
+
"DebugTracer",
|
|
71
|
+
"TraceEvent",
|
|
72
|
+
"TraceFormat",
|
|
73
|
+
"PolicyDecision",
|
|
74
|
+
"SnapshotDiff",
|
|
75
|
+
"VerificationResult",
|
|
76
|
+
"create_debug_tracer",
|
|
77
|
+
# Modes
|
|
78
|
+
"MODE_STRICT",
|
|
79
|
+
"MODE_PERMISSIVE",
|
|
80
|
+
"MODE_DEBUG",
|
|
81
|
+
"MODE_AUDIT",
|
|
82
|
+
# Exceptions
|
|
83
|
+
"AuthorizationDenied",
|
|
84
|
+
"VerificationFailed",
|
|
85
|
+
"PolicyLoadError",
|
|
86
|
+
"UnsupportedFrameworkError",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# Mode constants
|
|
90
|
+
MODE_STRICT = "strict"
|
|
91
|
+
MODE_PERMISSIVE = "permissive"
|
|
92
|
+
MODE_DEBUG = "debug"
|
|
93
|
+
MODE_AUDIT = "audit"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AuthorizationDenied(Exception):
|
|
97
|
+
"""Raised when an action is denied by the policy engine."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, message: str, decision: Any = None):
|
|
100
|
+
super().__init__(message)
|
|
101
|
+
self.decision = decision
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class VerificationFailed(Exception):
|
|
105
|
+
"""Raised when post-execution verification fails."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, message: str, predicate: str | None = None):
|
|
108
|
+
super().__init__(message)
|
|
109
|
+
self.predicate = predicate
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PolicyLoadError(Exception):
|
|
113
|
+
"""Raised when a policy file cannot be loaded."""
|
|
114
|
+
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SecureAgent:
|
|
119
|
+
"""
|
|
120
|
+
Drop-in security wrapper for AI agents.
|
|
121
|
+
|
|
122
|
+
Wraps any agent framework (browser-use, LangChain, Playwright, etc.)
|
|
123
|
+
and adds:
|
|
124
|
+
- Pre-action authorization via policy engine
|
|
125
|
+
- Post-execution verification via snapshot engine
|
|
126
|
+
- Cryptographic audit receipts
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
secure_agent = SecureAgent(
|
|
130
|
+
agent=my_browser_use_agent,
|
|
131
|
+
policy="policies/shopping.yaml",
|
|
132
|
+
mode="strict",
|
|
133
|
+
)
|
|
134
|
+
secure_agent.run()
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
config: SecureAgentConfig with all configuration
|
|
138
|
+
wrapped: WrappedAgent with detected framework info
|
|
139
|
+
authority_context: Initialized AuthorityClient context (lazy)
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
agent: Any,
|
|
145
|
+
policy: str | Path | None = None,
|
|
146
|
+
mode: str = MODE_STRICT,
|
|
147
|
+
principal_id: str | None = None,
|
|
148
|
+
tenant_id: str | None = None,
|
|
149
|
+
session_id: str | None = None,
|
|
150
|
+
sidecar_url: str | None = None,
|
|
151
|
+
signing_key: str | None = None,
|
|
152
|
+
mandate_ttl_seconds: int = 300,
|
|
153
|
+
trace_format: str = "console",
|
|
154
|
+
trace_file: str | Path | None = None,
|
|
155
|
+
trace_colors: bool = True,
|
|
156
|
+
trace_verbose: bool = True,
|
|
157
|
+
):
|
|
158
|
+
"""
|
|
159
|
+
Initialize SecureAgent wrapper.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
agent: The agent to wrap (browser-use Agent, Playwright page, etc.)
|
|
163
|
+
policy: Policy file path or None for env var fallback
|
|
164
|
+
mode: Execution mode (strict, permissive, debug, audit)
|
|
165
|
+
principal_id: Agent principal ID (auto-detect from env if not provided)
|
|
166
|
+
tenant_id: Tenant ID for multi-tenant deployments
|
|
167
|
+
session_id: Session ID for tracking
|
|
168
|
+
sidecar_url: Sidecar URL (None for embedded mode)
|
|
169
|
+
signing_key: Secret key for mandate signing
|
|
170
|
+
mandate_ttl_seconds: TTL for issued mandates
|
|
171
|
+
trace_format: Format for debug trace output ("console" or "json")
|
|
172
|
+
trace_file: Path to trace output file (None for stderr)
|
|
173
|
+
trace_colors: Whether to use ANSI colors in console output
|
|
174
|
+
trace_verbose: Whether to output verbose trace information
|
|
175
|
+
"""
|
|
176
|
+
# Build config from kwargs
|
|
177
|
+
self._config = SecureAgentConfig.from_kwargs(
|
|
178
|
+
policy=policy,
|
|
179
|
+
mode=mode,
|
|
180
|
+
principal_id=principal_id,
|
|
181
|
+
tenant_id=tenant_id,
|
|
182
|
+
session_id=session_id,
|
|
183
|
+
sidecar_url=sidecar_url,
|
|
184
|
+
signing_key=signing_key,
|
|
185
|
+
mandate_ttl_seconds=mandate_ttl_seconds,
|
|
186
|
+
trace_format=trace_format,
|
|
187
|
+
trace_file=trace_file,
|
|
188
|
+
trace_colors=trace_colors,
|
|
189
|
+
trace_verbose=trace_verbose,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Detect framework and wrap agent
|
|
193
|
+
self._wrapped = self._wrap_agent(agent)
|
|
194
|
+
|
|
195
|
+
# Lazy-initialized authority context
|
|
196
|
+
self._authority_context: Any = None
|
|
197
|
+
|
|
198
|
+
# Debug tracer (initialized when mode="debug")
|
|
199
|
+
self._tracer: DebugTracer | None = None
|
|
200
|
+
if self._config.is_debug_mode:
|
|
201
|
+
self._tracer = create_debug_tracer(
|
|
202
|
+
format=self._config.trace_format,
|
|
203
|
+
file_path=self._config.effective_trace_file,
|
|
204
|
+
use_colors=self._config.trace_colors,
|
|
205
|
+
verbose=self._config.trace_verbose,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Legacy attribute access (for backward compat with tests)
|
|
209
|
+
self._agent = agent
|
|
210
|
+
self._policy = policy
|
|
211
|
+
self._mode = mode
|
|
212
|
+
self._principal_id = principal_id
|
|
213
|
+
self._sidecar_url = sidecar_url
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def config(self) -> SecureAgentConfig:
|
|
217
|
+
"""Get the configuration."""
|
|
218
|
+
return self._config
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def wrapped(self) -> WrappedAgent:
|
|
222
|
+
"""Get the wrapped agent with framework info."""
|
|
223
|
+
return self._wrapped
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def framework(self) -> Framework:
|
|
227
|
+
"""Get the detected framework."""
|
|
228
|
+
return Framework(self._wrapped.framework)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def tracer(self) -> DebugTracer | None:
|
|
232
|
+
"""Get the debug tracer (available when mode='debug')."""
|
|
233
|
+
return self._tracer
|
|
234
|
+
|
|
235
|
+
def _wrap_agent(self, agent: Any) -> WrappedAgent:
|
|
236
|
+
"""
|
|
237
|
+
Detect framework and wrap agent.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
agent: The agent to wrap
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
WrappedAgent with framework info
|
|
244
|
+
"""
|
|
245
|
+
detection = FrameworkDetector.detect(agent)
|
|
246
|
+
|
|
247
|
+
return WrappedAgent(
|
|
248
|
+
original=agent,
|
|
249
|
+
framework=detection.framework.value,
|
|
250
|
+
agent_runtime=None, # Initialized lazily when needed
|
|
251
|
+
executor=self._extract_executor(agent, detection),
|
|
252
|
+
metadata=detection.metadata,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _extract_executor(self, agent: Any, detection: DetectionResult) -> Any | None:
|
|
256
|
+
"""
|
|
257
|
+
Extract LLM executor from agent if available.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
agent: The agent object
|
|
261
|
+
detection: Detection result
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
LLM executor or None
|
|
265
|
+
"""
|
|
266
|
+
# browser-use Agent has .llm attribute
|
|
267
|
+
if detection.framework == Framework.BROWSER_USE:
|
|
268
|
+
return getattr(agent, "llm", None)
|
|
269
|
+
|
|
270
|
+
# LangChain AgentExecutor has .agent or .llm
|
|
271
|
+
if detection.framework == Framework.LANGCHAIN:
|
|
272
|
+
return getattr(agent, "llm", None) or getattr(agent, "agent", None)
|
|
273
|
+
|
|
274
|
+
# PydanticAI agents have .model
|
|
275
|
+
if detection.framework == Framework.PYDANTIC_AI:
|
|
276
|
+
return getattr(agent, "model", None)
|
|
277
|
+
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def _get_authority_context(self) -> Any:
|
|
281
|
+
"""
|
|
282
|
+
Get or initialize the authority context.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
LocalAuthorizationContext from AuthorityClient
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
PolicyLoadError: If policy cannot be loaded
|
|
289
|
+
"""
|
|
290
|
+
if self._authority_context is not None:
|
|
291
|
+
return self._authority_context
|
|
292
|
+
|
|
293
|
+
policy_path = self._config.effective_policy_path
|
|
294
|
+
if policy_path is None:
|
|
295
|
+
# No policy = no authorization enforcement
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
# Import here to avoid hard dependency
|
|
300
|
+
from predicate_authority.client import AuthorityClient
|
|
301
|
+
|
|
302
|
+
self._authority_context = AuthorityClient.from_policy_file(
|
|
303
|
+
policy_file=policy_path,
|
|
304
|
+
secret_key=self._config.effective_signing_key,
|
|
305
|
+
ttl_seconds=self._config.mandate_ttl_seconds,
|
|
306
|
+
)
|
|
307
|
+
return self._authority_context
|
|
308
|
+
except ImportError:
|
|
309
|
+
raise PolicyLoadError(
|
|
310
|
+
"predicate-authority is required for policy enforcement. "
|
|
311
|
+
"Install with: pip install predicate-secure[authority]"
|
|
312
|
+
)
|
|
313
|
+
except FileNotFoundError as e:
|
|
314
|
+
raise PolicyLoadError(f"Policy file not found: {policy_path}") from e
|
|
315
|
+
except Exception as e:
|
|
316
|
+
raise PolicyLoadError(f"Failed to load policy: {e}") from e
|
|
317
|
+
|
|
318
|
+
def _create_pre_action_authorizer(self) -> Any:
|
|
319
|
+
"""
|
|
320
|
+
Create a pre-action authorizer callback for RuntimeAgent.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Callable that takes ActionRequest and returns decision
|
|
324
|
+
"""
|
|
325
|
+
context = self._get_authority_context()
|
|
326
|
+
if context is None:
|
|
327
|
+
# No policy = allow all
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def authorizer(request: Any) -> Any:
|
|
331
|
+
"""Pre-action authorization callback."""
|
|
332
|
+
# Trace authorization request
|
|
333
|
+
if self._tracer:
|
|
334
|
+
action = getattr(request, "action", str(request))
|
|
335
|
+
resource = getattr(request, "resource", "")
|
|
336
|
+
self._tracer.trace_authorization_request(
|
|
337
|
+
action=action,
|
|
338
|
+
resource=resource,
|
|
339
|
+
principal=self._config.effective_principal_id,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
decision = context.client.authorize(request)
|
|
343
|
+
|
|
344
|
+
# Trace policy decision
|
|
345
|
+
if self._tracer:
|
|
346
|
+
action = getattr(request, "action", str(request))
|
|
347
|
+
resource = getattr(request, "resource", "")
|
|
348
|
+
reason = None
|
|
349
|
+
if hasattr(decision, "reason") and decision.reason:
|
|
350
|
+
reason = (
|
|
351
|
+
decision.reason.value
|
|
352
|
+
if hasattr(decision.reason, "value")
|
|
353
|
+
else str(decision.reason)
|
|
354
|
+
)
|
|
355
|
+
self._tracer.trace_policy_decision(
|
|
356
|
+
PolicyDecision(
|
|
357
|
+
action=action,
|
|
358
|
+
resource=resource,
|
|
359
|
+
allowed=decision.allowed,
|
|
360
|
+
reason=reason,
|
|
361
|
+
principal=self._config.effective_principal_id,
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if not decision.allowed and self._config.fail_closed:
|
|
366
|
+
raise AuthorizationDenied(
|
|
367
|
+
f"Action denied: {decision.reason.value if decision.reason else 'policy'}",
|
|
368
|
+
decision=decision,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return decision
|
|
372
|
+
|
|
373
|
+
return authorizer
|
|
374
|
+
|
|
375
|
+
def run(self, task: str | None = None) -> Any:
|
|
376
|
+
"""
|
|
377
|
+
Execute the agent with full authorization + verification loop.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
task: Optional task override
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Agent execution result
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
AuthorizationDenied: If an action is denied (in strict mode)
|
|
387
|
+
VerificationFailed: If post-execution verification fails
|
|
388
|
+
UnsupportedFrameworkError: If framework is not supported
|
|
389
|
+
"""
|
|
390
|
+
if self._wrapped.framework == Framework.UNKNOWN.value:
|
|
391
|
+
detection = FrameworkDetector.detect(self._wrapped.original)
|
|
392
|
+
raise UnsupportedFrameworkError(detection)
|
|
393
|
+
|
|
394
|
+
# Trace session start
|
|
395
|
+
if self._tracer:
|
|
396
|
+
self._tracer.trace_session_start(
|
|
397
|
+
framework=self._wrapped.framework,
|
|
398
|
+
mode=self._config.mode,
|
|
399
|
+
policy=self._config.effective_policy_path,
|
|
400
|
+
principal_id=self._config.effective_principal_id,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
# Framework-specific execution
|
|
405
|
+
if self._wrapped.framework == Framework.BROWSER_USE.value:
|
|
406
|
+
result = self._run_browser_use(task)
|
|
407
|
+
elif self._wrapped.framework == Framework.PLAYWRIGHT.value:
|
|
408
|
+
result = self._run_playwright(task)
|
|
409
|
+
elif self._wrapped.framework == Framework.LANGCHAIN.value:
|
|
410
|
+
result = self._run_langchain(task)
|
|
411
|
+
elif self._wrapped.framework == Framework.PYDANTIC_AI.value:
|
|
412
|
+
result = self._run_pydantic_ai(task)
|
|
413
|
+
else:
|
|
414
|
+
raise NotImplementedError(
|
|
415
|
+
f"run() not implemented for framework: {self._wrapped.framework}"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Trace session end (success)
|
|
419
|
+
if self._tracer:
|
|
420
|
+
self._tracer.trace_session_end(success=True)
|
|
421
|
+
|
|
422
|
+
return result
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
# Trace session end (failure)
|
|
426
|
+
if self._tracer:
|
|
427
|
+
self._tracer.trace_session_end(success=False, error=str(e))
|
|
428
|
+
raise
|
|
429
|
+
|
|
430
|
+
def _run_browser_use(self, task: str | None) -> Any:
|
|
431
|
+
"""Run browser-use agent with authorization."""
|
|
432
|
+
# Import here to avoid hard dependency
|
|
433
|
+
try:
|
|
434
|
+
import asyncio
|
|
435
|
+
|
|
436
|
+
agent = self._wrapped.original
|
|
437
|
+
|
|
438
|
+
# Override task if provided
|
|
439
|
+
if task is not None and hasattr(agent, "task"):
|
|
440
|
+
setattr(agent, "task", task)
|
|
441
|
+
|
|
442
|
+
# Check if agent has a run method
|
|
443
|
+
if hasattr(agent, "run"):
|
|
444
|
+
# browser-use Agent.run() is typically async
|
|
445
|
+
if asyncio.iscoroutinefunction(agent.run):
|
|
446
|
+
return asyncio.get_event_loop().run_until_complete(agent.run())
|
|
447
|
+
return agent.run()
|
|
448
|
+
|
|
449
|
+
raise NotImplementedError(
|
|
450
|
+
"browser-use Agent.run() integration not fully implemented. "
|
|
451
|
+
"For now, use the agent directly with pre_action_authorizer callback."
|
|
452
|
+
)
|
|
453
|
+
except ImportError:
|
|
454
|
+
raise NotImplementedError(
|
|
455
|
+
"browser-use integration requires the browser-use package. "
|
|
456
|
+
"Install with: pip install predicate-secure[browser-use]"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def _run_playwright(self, task: str | None) -> Any:
|
|
460
|
+
"""Run Playwright page with authorization."""
|
|
461
|
+
raise NotImplementedError(
|
|
462
|
+
"Playwright direct integration not yet implemented. "
|
|
463
|
+
"Use with RuntimeAgent for Playwright pages."
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
def _run_langchain(self, task: str | None) -> Any:
|
|
467
|
+
"""Run LangChain agent with authorization."""
|
|
468
|
+
agent = self._wrapped.original
|
|
469
|
+
|
|
470
|
+
# LangChain agents have .invoke() method
|
|
471
|
+
if hasattr(agent, "invoke"):
|
|
472
|
+
if task is not None:
|
|
473
|
+
return agent.invoke({"input": task})
|
|
474
|
+
raise ValueError("Task is required for LangChain agents")
|
|
475
|
+
|
|
476
|
+
raise NotImplementedError(
|
|
477
|
+
"LangChain integration requires AgentExecutor with invoke() method."
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
def _run_pydantic_ai(self, task: str | None) -> Any:
|
|
481
|
+
"""Run PydanticAI agent with authorization."""
|
|
482
|
+
raise NotImplementedError("PydanticAI integration not yet implemented.")
|
|
483
|
+
|
|
484
|
+
def trace_step(
|
|
485
|
+
self,
|
|
486
|
+
action: str,
|
|
487
|
+
resource: str = "",
|
|
488
|
+
metadata: dict | None = None,
|
|
489
|
+
) -> int | None:
|
|
490
|
+
"""
|
|
491
|
+
Trace a step start (for manual step tracking).
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
action: Action being performed
|
|
495
|
+
resource: Resource being acted upon
|
|
496
|
+
metadata: Additional metadata
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Step number (None if not in debug mode)
|
|
500
|
+
|
|
501
|
+
Example:
|
|
502
|
+
step = secure.trace_step("click", "button#submit")
|
|
503
|
+
# ... perform action ...
|
|
504
|
+
secure.trace_step_end(step, success=True)
|
|
505
|
+
"""
|
|
506
|
+
if self._tracer:
|
|
507
|
+
return self._tracer.trace_step_start(
|
|
508
|
+
action=action,
|
|
509
|
+
resource=resource,
|
|
510
|
+
metadata=metadata,
|
|
511
|
+
)
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
def trace_step_end(
|
|
515
|
+
self,
|
|
516
|
+
step_number: int | None,
|
|
517
|
+
success: bool = True,
|
|
518
|
+
result: Any = None,
|
|
519
|
+
error: str | None = None,
|
|
520
|
+
) -> None:
|
|
521
|
+
"""
|
|
522
|
+
Trace a step end (for manual step tracking).
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
step_number: Step number from trace_step()
|
|
526
|
+
success: Whether the step succeeded
|
|
527
|
+
result: Step result (optional)
|
|
528
|
+
error: Error message if failed
|
|
529
|
+
"""
|
|
530
|
+
if self._tracer and step_number is not None:
|
|
531
|
+
self._tracer.trace_step_end(
|
|
532
|
+
step_number=step_number,
|
|
533
|
+
success=success,
|
|
534
|
+
result=result,
|
|
535
|
+
error=error,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def trace_snapshot_diff(
|
|
539
|
+
self,
|
|
540
|
+
before: dict | None = None,
|
|
541
|
+
after: dict | None = None,
|
|
542
|
+
diff: dict | None = None,
|
|
543
|
+
label: str = "State Change",
|
|
544
|
+
) -> None:
|
|
545
|
+
"""
|
|
546
|
+
Trace a snapshot diff (before/after state change).
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
before: Before snapshot (for computing diff)
|
|
550
|
+
after: After snapshot (for computing diff)
|
|
551
|
+
diff: Pre-computed diff (if before/after not provided)
|
|
552
|
+
label: Label for the diff
|
|
553
|
+
"""
|
|
554
|
+
if not self._tracer:
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
if diff:
|
|
558
|
+
self._tracer.trace_snapshot_diff(SnapshotDiff(**diff), label=label)
|
|
559
|
+
elif before is not None and after is not None:
|
|
560
|
+
# Compute simple diff
|
|
561
|
+
computed_diff = SnapshotDiff(
|
|
562
|
+
added=[k for k in after if k not in before],
|
|
563
|
+
removed=[k for k in before if k not in after],
|
|
564
|
+
changed=[
|
|
565
|
+
{"element": k, "before": before[k], "after": after[k]}
|
|
566
|
+
for k in before
|
|
567
|
+
if k in after and before[k] != after[k]
|
|
568
|
+
],
|
|
569
|
+
)
|
|
570
|
+
self._tracer.trace_snapshot_diff(computed_diff, label=label)
|
|
571
|
+
|
|
572
|
+
def trace_verification(
|
|
573
|
+
self,
|
|
574
|
+
predicate: str,
|
|
575
|
+
passed: bool,
|
|
576
|
+
message: str | None = None,
|
|
577
|
+
expected: Any = None,
|
|
578
|
+
actual: Any = None,
|
|
579
|
+
) -> None:
|
|
580
|
+
"""
|
|
581
|
+
Trace a verification predicate result.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
predicate: Predicate name or expression
|
|
585
|
+
passed: Whether verification passed
|
|
586
|
+
message: Optional message
|
|
587
|
+
expected: Expected value (for failed verifications)
|
|
588
|
+
actual: Actual value (for failed verifications)
|
|
589
|
+
"""
|
|
590
|
+
if self._tracer:
|
|
591
|
+
self._tracer.trace_verification_result(
|
|
592
|
+
VerificationResult(
|
|
593
|
+
predicate=predicate,
|
|
594
|
+
passed=passed,
|
|
595
|
+
message=message,
|
|
596
|
+
expected=expected,
|
|
597
|
+
actual=actual,
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
@classmethod
|
|
602
|
+
def attach(cls, agent: Any, **kwargs: Any) -> SecureAgent:
|
|
603
|
+
"""
|
|
604
|
+
Attach SecureAgent to an existing agent (factory method).
|
|
605
|
+
|
|
606
|
+
This mirrors the SentienceDebugger.attach() pattern.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
agent: The agent to wrap
|
|
610
|
+
**kwargs: Additional configuration
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
SecureAgent instance
|
|
614
|
+
"""
|
|
615
|
+
return cls(agent=agent, **kwargs)
|
|
616
|
+
|
|
617
|
+
def get_pre_action_authorizer(self) -> Any:
|
|
618
|
+
"""
|
|
619
|
+
Get a pre-action authorizer callback for use with RuntimeAgent.
|
|
620
|
+
|
|
621
|
+
This allows integrating SecureAgent authorization with existing
|
|
622
|
+
RuntimeAgent-based workflows.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Callable for pre_action_authorizer parameter
|
|
626
|
+
|
|
627
|
+
Example:
|
|
628
|
+
secure = SecureAgent(agent=my_agent, policy="policy.yaml")
|
|
629
|
+
runtime_agent = RuntimeAgent(
|
|
630
|
+
runtime=runtime,
|
|
631
|
+
executor=executor,
|
|
632
|
+
pre_action_authorizer=secure.get_pre_action_authorizer(),
|
|
633
|
+
)
|
|
634
|
+
"""
|
|
635
|
+
return self._create_pre_action_authorizer()
|
|
636
|
+
|
|
637
|
+
def get_adapter(
|
|
638
|
+
self,
|
|
639
|
+
tracer: Any | None = None,
|
|
640
|
+
snapshot_options: Any | None = None,
|
|
641
|
+
predicate_api_key: str | None = None,
|
|
642
|
+
**kwargs: Any,
|
|
643
|
+
) -> AdapterResult:
|
|
644
|
+
"""
|
|
645
|
+
Get an adapter for the wrapped agent.
|
|
646
|
+
|
|
647
|
+
This creates the appropriate adapter based on the detected framework,
|
|
648
|
+
wiring together BrowserUseAdapter, PredicateBrowserUsePlugin,
|
|
649
|
+
SentienceLangChainCore, or AgentRuntime.from_playwright_page().
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
tracer: Optional Tracer for event emission
|
|
653
|
+
snapshot_options: Optional SnapshotOptions
|
|
654
|
+
predicate_api_key: Optional API key for Predicate API
|
|
655
|
+
**kwargs: Additional framework-specific options
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
AdapterResult with initialized components
|
|
659
|
+
|
|
660
|
+
Raises:
|
|
661
|
+
AdapterError: If adapter initialization fails
|
|
662
|
+
|
|
663
|
+
Example:
|
|
664
|
+
secure = SecureAgent(agent=my_browser_use_agent, policy="policy.yaml")
|
|
665
|
+
adapter = secure.get_adapter()
|
|
666
|
+
|
|
667
|
+
# Use plugin lifecycle hooks
|
|
668
|
+
result = await agent.run(
|
|
669
|
+
on_step_start=adapter.plugin.on_step_start,
|
|
670
|
+
on_step_end=adapter.plugin.on_step_end,
|
|
671
|
+
)
|
|
672
|
+
"""
|
|
673
|
+
return create_adapter(
|
|
674
|
+
agent=self._wrapped.original,
|
|
675
|
+
framework=self.framework,
|
|
676
|
+
tracer=tracer,
|
|
677
|
+
snapshot_options=snapshot_options,
|
|
678
|
+
predicate_api_key=predicate_api_key,
|
|
679
|
+
**kwargs,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
async def get_runtime_async(
|
|
683
|
+
self,
|
|
684
|
+
tracer: Any | None = None,
|
|
685
|
+
snapshot_options: Any | None = None,
|
|
686
|
+
predicate_api_key: str | None = None,
|
|
687
|
+
) -> Any:
|
|
688
|
+
"""
|
|
689
|
+
Get an initialized AgentRuntime for the wrapped agent (async).
|
|
690
|
+
|
|
691
|
+
This is useful for browser-use agents where runtime initialization
|
|
692
|
+
requires async operations.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
tracer: Optional Tracer for event emission
|
|
696
|
+
snapshot_options: Optional SnapshotOptions
|
|
697
|
+
predicate_api_key: Optional API key for Predicate API
|
|
698
|
+
|
|
699
|
+
Returns:
|
|
700
|
+
AgentRuntime instance
|
|
701
|
+
|
|
702
|
+
Raises:
|
|
703
|
+
AdapterError: If runtime initialization fails
|
|
704
|
+
|
|
705
|
+
Example:
|
|
706
|
+
secure = SecureAgent(agent=my_browser_use_agent, policy="policy.yaml")
|
|
707
|
+
runtime = await secure.get_runtime_async()
|
|
708
|
+
|
|
709
|
+
# Use with RuntimeAgent
|
|
710
|
+
from predicate.runtime_agent import RuntimeAgent
|
|
711
|
+
runtime_agent = RuntimeAgent(
|
|
712
|
+
runtime=runtime,
|
|
713
|
+
executor=my_llm,
|
|
714
|
+
pre_action_authorizer=secure.get_pre_action_authorizer(),
|
|
715
|
+
)
|
|
716
|
+
"""
|
|
717
|
+
if self.framework == Framework.BROWSER_USE:
|
|
718
|
+
result = await create_browser_use_runtime(
|
|
719
|
+
agent=self._wrapped.original,
|
|
720
|
+
tracer=tracer,
|
|
721
|
+
snapshot_options=snapshot_options,
|
|
722
|
+
predicate_api_key=predicate_api_key,
|
|
723
|
+
)
|
|
724
|
+
# Cache the runtime
|
|
725
|
+
self._wrapped.agent_runtime = result.agent_runtime
|
|
726
|
+
return result.agent_runtime
|
|
727
|
+
|
|
728
|
+
if self.framework == Framework.PLAYWRIGHT:
|
|
729
|
+
adapter = create_playwright_adapter(
|
|
730
|
+
page=self._wrapped.original,
|
|
731
|
+
tracer=tracer,
|
|
732
|
+
snapshot_options=snapshot_options,
|
|
733
|
+
predicate_api_key=predicate_api_key,
|
|
734
|
+
)
|
|
735
|
+
self._wrapped.agent_runtime = adapter.agent_runtime
|
|
736
|
+
return adapter.agent_runtime
|
|
737
|
+
|
|
738
|
+
raise AdapterError(
|
|
739
|
+
f"get_runtime_async() not supported for framework: {self.framework.value}",
|
|
740
|
+
self.framework,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
def get_browser_use_plugin(
|
|
744
|
+
self,
|
|
745
|
+
tracer: Any | None = None,
|
|
746
|
+
snapshot_options: Any | None = None,
|
|
747
|
+
predicate_api_key: str | None = None,
|
|
748
|
+
) -> Any:
|
|
749
|
+
"""
|
|
750
|
+
Get a PredicateBrowserUsePlugin for browser-use lifecycle hooks.
|
|
751
|
+
|
|
752
|
+
This is the recommended way to integrate with browser-use agents.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
tracer: Optional Tracer for event emission
|
|
756
|
+
snapshot_options: Optional SnapshotOptions
|
|
757
|
+
predicate_api_key: Optional API key for Predicate API
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
PredicateBrowserUsePlugin instance
|
|
761
|
+
|
|
762
|
+
Raises:
|
|
763
|
+
AdapterError: If framework is not browser-use
|
|
764
|
+
|
|
765
|
+
Example:
|
|
766
|
+
secure = SecureAgent(agent=my_agent, policy="policy.yaml")
|
|
767
|
+
plugin = secure.get_browser_use_plugin()
|
|
768
|
+
|
|
769
|
+
# Run with lifecycle hooks
|
|
770
|
+
result = await agent.run(
|
|
771
|
+
on_step_start=plugin.on_step_start,
|
|
772
|
+
on_step_end=plugin.on_step_end,
|
|
773
|
+
)
|
|
774
|
+
"""
|
|
775
|
+
if self.framework != Framework.BROWSER_USE:
|
|
776
|
+
raise AdapterError(
|
|
777
|
+
"get_browser_use_plugin() only available for browser-use agents",
|
|
778
|
+
self.framework,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
adapter = create_browser_use_adapter(
|
|
782
|
+
agent=self._wrapped.original,
|
|
783
|
+
tracer=tracer,
|
|
784
|
+
snapshot_options=snapshot_options,
|
|
785
|
+
predicate_api_key=predicate_api_key,
|
|
786
|
+
)
|
|
787
|
+
return adapter.plugin
|
|
788
|
+
|
|
789
|
+
def get_langchain_core(
|
|
790
|
+
self,
|
|
791
|
+
browser: Any | None = None,
|
|
792
|
+
tracer: Any | None = None,
|
|
793
|
+
snapshot_options: Any | None = None,
|
|
794
|
+
predicate_api_key: str | None = None,
|
|
795
|
+
) -> Any:
|
|
796
|
+
"""
|
|
797
|
+
Get a SentienceLangChainCore for LangChain tool interception.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
browser: Optional browser instance for browser tools
|
|
801
|
+
tracer: Optional Tracer for event emission
|
|
802
|
+
snapshot_options: Optional SnapshotOptions
|
|
803
|
+
predicate_api_key: Optional API key for Predicate API
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
SentienceLangChainCore instance
|
|
807
|
+
|
|
808
|
+
Raises:
|
|
809
|
+
AdapterError: If framework is not LangChain
|
|
810
|
+
"""
|
|
811
|
+
if self.framework != Framework.LANGCHAIN:
|
|
812
|
+
raise AdapterError(
|
|
813
|
+
"get_langchain_core() only available for LangChain agents",
|
|
814
|
+
self.framework,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
adapter = create_langchain_adapter(
|
|
818
|
+
agent=self._wrapped.original,
|
|
819
|
+
browser=browser,
|
|
820
|
+
tracer=tracer,
|
|
821
|
+
snapshot_options=snapshot_options,
|
|
822
|
+
predicate_api_key=predicate_api_key,
|
|
823
|
+
)
|
|
824
|
+
return adapter.plugin
|
|
825
|
+
|
|
826
|
+
def __repr__(self) -> str:
|
|
827
|
+
return (
|
|
828
|
+
f"SecureAgent("
|
|
829
|
+
f"framework={self._wrapped.framework}, "
|
|
830
|
+
f"mode={self._config.mode}, "
|
|
831
|
+
f"policy={self._config.effective_policy_path or 'None'}"
|
|
832
|
+
f")"
|
|
833
|
+
)
|