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.
@@ -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
+ )