proxilion 0.0.1__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.
Files changed (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,641 @@
1
+ """
2
+ LangChain integration for Proxilion.
3
+
4
+ This module provides authorization wrappers for LangChain tools,
5
+ enabling secure tool execution with user-context authorization.
6
+
7
+ Features:
8
+ - ProxilionTool: Wraps LangChain BaseTool with authorization
9
+ - ProxilionCallbackHandler: Intercepts and logs tool invocations
10
+ - wrap_langchain_tools: Convenience function for bulk wrapping
11
+
12
+ Note:
13
+ LangChain is an optional dependency. Install with:
14
+ pip install proxilion[langchain]
15
+
16
+ Example:
17
+ >>> from langchain.tools import Tool
18
+ >>> from proxilion import Proxilion, Policy
19
+ >>> from proxilion.contrib.langchain import ProxilionTool, wrap_langchain_tools
20
+ >>>
21
+ >>> auth = Proxilion()
22
+ >>>
23
+ >>> @auth.policy("search")
24
+ ... class SearchPolicy(Policy):
25
+ ... def can_execute(self, context):
26
+ ... return "search_user" in self.user.roles
27
+ >>>
28
+ >>> # Wrap a single tool
29
+ >>> secure_tool = ProxilionTool(
30
+ ... original_tool=search_tool,
31
+ ... proxilion=auth,
32
+ ... resource="search",
33
+ ... )
34
+ >>>
35
+ >>> # Or wrap multiple tools at once
36
+ >>> secure_tools = wrap_langchain_tools(
37
+ ... tools=[search_tool, calc_tool],
38
+ ... proxilion=auth,
39
+ ... )
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import asyncio
45
+ import contextvars
46
+ import logging
47
+ from dataclasses import dataclass, field
48
+ from datetime import datetime, timezone
49
+ from typing import Any
50
+
51
+ from proxilion.exceptions import AuthorizationError, ProxilionError
52
+ from proxilion.types import AgentContext, UserContext
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+ # Context variable for user context in LangChain callbacks
57
+ _langchain_user_context: contextvars.ContextVar[UserContext | None] = contextvars.ContextVar(
58
+ "langchain_user_context", default=None
59
+ )
60
+
61
+ _langchain_agent_context: contextvars.ContextVar[AgentContext | None] = contextvars.ContextVar(
62
+ "langchain_agent_context", default=None
63
+ )
64
+
65
+
66
+ def set_langchain_user(user: UserContext) -> contextvars.Token[UserContext | None]:
67
+ """Set the current user for LangChain tool execution."""
68
+ return _langchain_user_context.set(user)
69
+
70
+
71
+ def get_langchain_user() -> UserContext | None:
72
+ """Get the current user for LangChain tool execution."""
73
+ return _langchain_user_context.get()
74
+
75
+
76
+ def set_langchain_agent(agent: AgentContext) -> contextvars.Token[AgentContext | None]:
77
+ """Set the current agent for LangChain tool execution."""
78
+ return _langchain_agent_context.set(agent)
79
+
80
+
81
+ def get_langchain_agent() -> AgentContext | None:
82
+ """Get the current agent for LangChain tool execution."""
83
+ return _langchain_agent_context.get()
84
+
85
+
86
+ class LangChainIntegrationError(ProxilionError):
87
+ """Error in LangChain integration."""
88
+ pass
89
+
90
+
91
+ @dataclass
92
+ class ToolInvocation:
93
+ """Record of a tool invocation for audit purposes."""
94
+ tool_name: str
95
+ input_str: str
96
+ user_id: str | None
97
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
98
+ authorized: bool = False
99
+ output: str | None = None
100
+ error: str | None = None
101
+ duration_ms: float = 0.0
102
+
103
+
104
+ class ProxilionToolMixin:
105
+ """
106
+ Mixin class providing Proxilion authorization for LangChain tools.
107
+
108
+ This mixin can be combined with LangChain's BaseTool to create
109
+ authorized tools without requiring LangChain as a dependency
110
+ at import time.
111
+ """
112
+
113
+ proxilion: Any
114
+ resource: str
115
+ action: str
116
+ original_tool: Any
117
+ require_user: bool
118
+
119
+ def _get_user_context(self) -> UserContext | None:
120
+ """Get user context from context variable or Proxilion."""
121
+ # Try context variable first
122
+ user = get_langchain_user()
123
+ if user is not None:
124
+ return user
125
+
126
+ # Try Proxilion's context
127
+ from proxilion.core import get_current_user
128
+ return get_current_user()
129
+
130
+ def _authorize(self, tool_input: str) -> None:
131
+ """Check authorization and raise if denied."""
132
+ user = self._get_user_context()
133
+
134
+ if user is None:
135
+ if self.require_user:
136
+ raise AuthorizationError(
137
+ user="unknown",
138
+ action=self.action,
139
+ resource=self.resource,
140
+ reason="No user context available for LangChain tool",
141
+ )
142
+ return # Allow if user not required
143
+
144
+ # Build context from input
145
+ context = {
146
+ "tool_input": tool_input,
147
+ "tool_name": getattr(self.original_tool, "name", self.resource),
148
+ }
149
+
150
+ # Check authorization
151
+ result = self.proxilion.check(user, self.action, self.resource, context)
152
+ if not result.allowed:
153
+ raise AuthorizationError(
154
+ user=user.user_id,
155
+ action=self.action,
156
+ resource=self.resource,
157
+ reason=result.reason,
158
+ )
159
+
160
+
161
+ class ProxilionTool:
162
+ """
163
+ Wraps a LangChain tool with Proxilion authorization.
164
+
165
+ This wrapper intercepts tool calls and applies authorization
166
+ checks before delegating to the original tool.
167
+
168
+ Works without LangChain installed by duck-typing the BaseTool
169
+ interface. When LangChain is available, the wrapped tool can
170
+ be used anywhere a BaseTool is expected.
171
+
172
+ Example:
173
+ >>> from proxilion import Proxilion, Policy, UserContext
174
+ >>> from proxilion.contrib.langchain import ProxilionTool, set_langchain_user
175
+ >>>
176
+ >>> auth = Proxilion()
177
+ >>>
178
+ >>> @auth.policy("calculator")
179
+ ... class CalculatorPolicy(Policy):
180
+ ... def can_execute(self, context):
181
+ ... return True
182
+ >>>
183
+ >>> # Create a mock tool
184
+ >>> class CalcTool:
185
+ ... name = "calculator"
186
+ ... description = "Perform calculations"
187
+ ... def _run(self, query):
188
+ ... return eval(query)
189
+ >>>
190
+ >>> secure_calc = ProxilionTool(
191
+ ... original_tool=CalcTool(),
192
+ ... proxilion=auth,
193
+ ... resource="calculator",
194
+ ... )
195
+ >>>
196
+ >>> # Set user context and run
197
+ >>> set_langchain_user(UserContext(user_id="alice", roles=["user"]))
198
+ >>> result = secure_calc.run("2 + 2")
199
+ """
200
+
201
+ def __init__(
202
+ self,
203
+ original_tool: Any,
204
+ proxilion: Any,
205
+ resource: str | None = None,
206
+ action: str = "execute",
207
+ require_user: bool = True,
208
+ ) -> None:
209
+ """
210
+ Initialize the Proxilion-wrapped tool.
211
+
212
+ Args:
213
+ original_tool: The original LangChain tool to wrap.
214
+ proxilion: Proxilion instance for authorization.
215
+ resource: Resource name for policies (default: tool name).
216
+ action: Action name for authorization (default: "execute").
217
+ require_user: Whether to require user context (default: True).
218
+ """
219
+ self.original_tool = original_tool
220
+ self.proxilion = proxilion
221
+ self.resource = resource or getattr(original_tool, "name", "unknown_tool")
222
+ self.action = action
223
+ self.require_user = require_user
224
+
225
+ # Copy attributes from original tool
226
+ self.name = getattr(original_tool, "name", self.resource)
227
+ self.description = getattr(original_tool, "description", "")
228
+
229
+ # Copy other common attributes
230
+ for attr in ["args_schema", "return_direct", "verbose"]:
231
+ if hasattr(original_tool, attr):
232
+ setattr(self, attr, getattr(original_tool, attr))
233
+
234
+ def _get_user_context(self) -> UserContext | None:
235
+ """Get user context from context variable or Proxilion."""
236
+ user = get_langchain_user()
237
+ if user is not None:
238
+ return user
239
+
240
+ from proxilion.core import get_current_user
241
+ return get_current_user()
242
+
243
+ def _authorize(self, tool_input: str | dict[str, Any]) -> None:
244
+ """Check authorization and raise if denied."""
245
+ user = self._get_user_context()
246
+
247
+ if user is None:
248
+ if self.require_user:
249
+ raise AuthorizationError(
250
+ user="unknown",
251
+ action=self.action,
252
+ resource=self.resource,
253
+ reason="No user context available for LangChain tool",
254
+ )
255
+ return
256
+
257
+ # Build context from input
258
+ if isinstance(tool_input, dict):
259
+ context = {"tool_input": tool_input, **tool_input}
260
+ else:
261
+ context = {"tool_input": str(tool_input)}
262
+
263
+ context["tool_name"] = self.name
264
+
265
+ result = self.proxilion.check(user, self.action, self.resource, context)
266
+ if not result.allowed:
267
+ raise AuthorizationError(
268
+ user=user.user_id,
269
+ action=self.action,
270
+ resource=self.resource,
271
+ reason=result.reason,
272
+ )
273
+
274
+ def run(self, tool_input: str | dict[str, Any], **kwargs: Any) -> str:
275
+ """
276
+ Run the tool with authorization.
277
+
278
+ Args:
279
+ tool_input: Input for the tool.
280
+ **kwargs: Additional arguments.
281
+
282
+ Returns:
283
+ Tool output as string.
284
+ """
285
+ self._authorize(tool_input)
286
+ return self.original_tool.run(tool_input, **kwargs)
287
+
288
+ async def arun(self, tool_input: str | dict[str, Any], **kwargs: Any) -> str:
289
+ """
290
+ Run the tool asynchronously with authorization.
291
+
292
+ Args:
293
+ tool_input: Input for the tool.
294
+ **kwargs: Additional arguments.
295
+
296
+ Returns:
297
+ Tool output as string.
298
+ """
299
+ self._authorize(tool_input)
300
+
301
+ if hasattr(self.original_tool, "arun"):
302
+ return await self.original_tool.arun(tool_input, **kwargs)
303
+ else:
304
+ # Fall back to sync run in executor
305
+ loop = asyncio.get_event_loop()
306
+ return await loop.run_in_executor(
307
+ None,
308
+ lambda: self.original_tool.run(tool_input, **kwargs),
309
+ )
310
+
311
+ def _run(self, *args: Any, **kwargs: Any) -> str:
312
+ """Internal run method for LangChain compatibility."""
313
+ tool_input = args[0] if args else kwargs.get("tool_input", "")
314
+ self._authorize(tool_input)
315
+
316
+ if hasattr(self.original_tool, "_run"):
317
+ return self.original_tool._run(*args, **kwargs)
318
+ return self.original_tool.run(tool_input, **kwargs)
319
+
320
+ async def _arun(self, *args: Any, **kwargs: Any) -> str:
321
+ """Internal async run method for LangChain compatibility."""
322
+ tool_input = args[0] if args else kwargs.get("tool_input", "")
323
+ self._authorize(tool_input)
324
+
325
+ if hasattr(self.original_tool, "_arun"):
326
+ return await self.original_tool._arun(*args, **kwargs)
327
+ elif hasattr(self.original_tool, "arun"):
328
+ return await self.original_tool.arun(tool_input, **kwargs)
329
+ else:
330
+ loop = asyncio.get_event_loop()
331
+ return await loop.run_in_executor(
332
+ None,
333
+ lambda: self.original_tool._run(*args, **kwargs)
334
+ if hasattr(self.original_tool, "_run")
335
+ else self.original_tool.run(tool_input, **kwargs),
336
+ )
337
+
338
+ def __call__(self, tool_input: str | dict[str, Any], **kwargs: Any) -> str:
339
+ """Call the tool directly."""
340
+ return self.run(tool_input, **kwargs)
341
+
342
+
343
+ class ProxilionCallbackHandler:
344
+ """
345
+ LangChain callback handler for logging and authorization.
346
+
347
+ Intercepts tool calls in LangChain agents to:
348
+ - Log all tool invocations to audit trail
349
+ - Apply authorization checks
350
+ - Track execution timing
351
+
352
+ Compatible with LangChain's callback system without requiring
353
+ LangChain as a dependency.
354
+
355
+ Example:
356
+ >>> from proxilion import Proxilion
357
+ >>> from proxilion.contrib.langchain import ProxilionCallbackHandler
358
+ >>>
359
+ >>> auth = Proxilion()
360
+ >>> handler = ProxilionCallbackHandler(
361
+ ... proxilion=auth,
362
+ ... user_context=user,
363
+ ... )
364
+ >>>
365
+ >>> # Use with LangChain agent
366
+ >>> agent.run("query", callbacks=[handler])
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ proxilion: Any,
372
+ user_context: UserContext | None = None,
373
+ agent_context: AgentContext | None = None,
374
+ log_inputs: bool = True,
375
+ log_outputs: bool = True,
376
+ block_unauthorized: bool = True,
377
+ ) -> None:
378
+ """
379
+ Initialize the callback handler.
380
+
381
+ Args:
382
+ proxilion: Proxilion instance.
383
+ user_context: User context for authorization.
384
+ agent_context: Optional agent context.
385
+ log_inputs: Whether to log tool inputs.
386
+ log_outputs: Whether to log tool outputs.
387
+ block_unauthorized: Whether to block unauthorized calls.
388
+ """
389
+ self.proxilion = proxilion
390
+ self.user_context = user_context
391
+ self.agent_context = agent_context
392
+ self.log_inputs = log_inputs
393
+ self.log_outputs = log_outputs
394
+ self.block_unauthorized = block_unauthorized
395
+
396
+ self._invocations: list[ToolInvocation] = []
397
+ self._current_invocation: ToolInvocation | None = None
398
+ self._start_time: float | None = None
399
+
400
+ @property
401
+ def invocations(self) -> list[ToolInvocation]:
402
+ """Get list of recorded tool invocations."""
403
+ return list(self._invocations)
404
+
405
+ def on_tool_start(
406
+ self,
407
+ serialized: dict[str, Any],
408
+ input_str: str,
409
+ **kwargs: Any,
410
+ ) -> None:
411
+ """
412
+ Called when a tool starts execution.
413
+
414
+ Args:
415
+ serialized: Serialized tool information.
416
+ input_str: Tool input string.
417
+ **kwargs: Additional arguments.
418
+ """
419
+ import time
420
+
421
+ tool_name = serialized.get("name", "unknown")
422
+
423
+ # Get user context
424
+ user = self.user_context or get_langchain_user()
425
+ user_id = user.user_id if user else None
426
+
427
+ # Create invocation record
428
+ self._current_invocation = ToolInvocation(
429
+ tool_name=tool_name,
430
+ input_str=input_str if self.log_inputs else "[REDACTED]",
431
+ user_id=user_id,
432
+ )
433
+ self._start_time = time.time()
434
+
435
+ # Check authorization if blocking is enabled
436
+ if self.block_unauthorized and user is not None:
437
+ context = {"tool_input": input_str}
438
+ result = self.proxilion.check(user, "execute", tool_name, context)
439
+
440
+ self._current_invocation.authorized = result.allowed
441
+
442
+ if not result.allowed:
443
+ self._current_invocation.error = result.reason
444
+ self._invocations.append(self._current_invocation)
445
+ raise AuthorizationError(
446
+ user=user.user_id,
447
+ action="execute",
448
+ resource=tool_name,
449
+ reason=result.reason,
450
+ )
451
+ else:
452
+ self._current_invocation.authorized = True
453
+
454
+ logger.debug(f"Tool started: {tool_name} for user {user_id}")
455
+
456
+ def on_tool_end(self, output: str, **kwargs: Any) -> None:
457
+ """
458
+ Called when a tool finishes execution.
459
+
460
+ Args:
461
+ output: Tool output string.
462
+ **kwargs: Additional arguments.
463
+ """
464
+ import time
465
+
466
+ if self._current_invocation is not None:
467
+ if self._start_time is not None:
468
+ self._current_invocation.duration_ms = (
469
+ time.time() - self._start_time
470
+ ) * 1000
471
+
472
+ self._current_invocation.output = (
473
+ output if self.log_outputs else "[REDACTED]"
474
+ )
475
+ self._invocations.append(self._current_invocation)
476
+
477
+ logger.debug(
478
+ f"Tool ended: {self._current_invocation.tool_name} "
479
+ f"({self._current_invocation.duration_ms:.1f}ms)"
480
+ )
481
+
482
+ self._current_invocation = None
483
+ self._start_time = None
484
+
485
+ def on_tool_error(self, error: Exception, **kwargs: Any) -> None:
486
+ """
487
+ Called when a tool raises an error.
488
+
489
+ Args:
490
+ error: The exception raised.
491
+ **kwargs: Additional arguments.
492
+ """
493
+ import time
494
+
495
+ if self._current_invocation is not None:
496
+ if self._start_time is not None:
497
+ self._current_invocation.duration_ms = (
498
+ time.time() - self._start_time
499
+ ) * 1000
500
+
501
+ self._current_invocation.error = str(error)
502
+ self._invocations.append(self._current_invocation)
503
+
504
+ logger.warning(
505
+ f"Tool error: {self._current_invocation.tool_name} - {error}"
506
+ )
507
+
508
+ self._current_invocation = None
509
+ self._start_time = None
510
+
511
+ # LangChain callback handler interface methods
512
+ def on_llm_start(self, *args: Any, **kwargs: Any) -> None:
513
+ """Called when LLM starts (no-op)."""
514
+ pass
515
+
516
+ def on_llm_end(self, *args: Any, **kwargs: Any) -> None:
517
+ """Called when LLM ends (no-op)."""
518
+ pass
519
+
520
+ def on_llm_error(self, *args: Any, **kwargs: Any) -> None:
521
+ """Called on LLM error (no-op)."""
522
+ pass
523
+
524
+ def on_chain_start(self, *args: Any, **kwargs: Any) -> None:
525
+ """Called when chain starts (no-op)."""
526
+ pass
527
+
528
+ def on_chain_end(self, *args: Any, **kwargs: Any) -> None:
529
+ """Called when chain ends (no-op)."""
530
+ pass
531
+
532
+ def on_chain_error(self, *args: Any, **kwargs: Any) -> None:
533
+ """Called on chain error (no-op)."""
534
+ pass
535
+
536
+ def on_agent_action(self, *args: Any, **kwargs: Any) -> None:
537
+ """Called on agent action (no-op)."""
538
+ pass
539
+
540
+ def on_agent_finish(self, *args: Any, **kwargs: Any) -> None:
541
+ """Called when agent finishes (no-op)."""
542
+ pass
543
+
544
+
545
+ def wrap_langchain_tools(
546
+ tools: list[Any],
547
+ proxilion: Any,
548
+ resource_prefix: str = "",
549
+ action: str = "execute",
550
+ require_user: bool = True,
551
+ ) -> list[ProxilionTool]:
552
+ """
553
+ Wrap multiple LangChain tools with Proxilion authorization.
554
+
555
+ Convenience function for bulk wrapping of tools.
556
+
557
+ Args:
558
+ tools: List of LangChain tools to wrap.
559
+ proxilion: Proxilion instance.
560
+ resource_prefix: Optional prefix for resource names.
561
+ action: Action name for authorization.
562
+ require_user: Whether to require user context.
563
+
564
+ Returns:
565
+ List of wrapped ProxilionTool instances.
566
+
567
+ Example:
568
+ >>> tools = [search_tool, calc_tool, file_tool]
569
+ >>> secure_tools = wrap_langchain_tools(
570
+ ... tools=tools,
571
+ ... proxilion=auth,
572
+ ... resource_prefix="agent_",
573
+ ... )
574
+ """
575
+ wrapped = []
576
+
577
+ for tool in tools:
578
+ tool_name = getattr(tool, "name", f"tool_{len(wrapped)}")
579
+ resource = f"{resource_prefix}{tool_name}" if resource_prefix else tool_name
580
+
581
+ wrapped.append(
582
+ ProxilionTool(
583
+ original_tool=tool,
584
+ proxilion=proxilion,
585
+ resource=resource,
586
+ action=action,
587
+ require_user=require_user,
588
+ )
589
+ )
590
+
591
+ return wrapped
592
+
593
+
594
+ class LangChainUserContextManager:
595
+ """
596
+ Context manager for setting user context in LangChain operations.
597
+
598
+ Example:
599
+ >>> with LangChainUserContextManager(user):
600
+ ... result = agent.run("query")
601
+ """
602
+
603
+ def __init__(
604
+ self,
605
+ user: UserContext,
606
+ agent: AgentContext | None = None,
607
+ ) -> None:
608
+ self.user = user
609
+ self.agent = agent
610
+ self._user_token: contextvars.Token[UserContext | None] | None = None
611
+ self._agent_token: contextvars.Token[AgentContext | None] | None = None
612
+
613
+ def __enter__(self) -> LangChainUserContextManager:
614
+ self._user_token = set_langchain_user(self.user)
615
+ if self.agent:
616
+ self._agent_token = set_langchain_agent(self.agent)
617
+ return self
618
+
619
+ def __exit__(self, *args: Any) -> None:
620
+ if self._user_token is not None:
621
+ _langchain_user_context.reset(self._user_token)
622
+ if self._agent_token is not None:
623
+ _langchain_agent_context.reset(self._agent_token)
624
+
625
+
626
+ def langchain_user_context(user: UserContext, agent: AgentContext | None = None):
627
+ """
628
+ Decorator/context manager for setting user context.
629
+
630
+ Can be used as a decorator or context manager.
631
+
632
+ Example as decorator:
633
+ >>> @langchain_user_context(user)
634
+ ... def run_agent():
635
+ ... return agent.run("query")
636
+
637
+ Example as context manager:
638
+ >>> with langchain_user_context(user):
639
+ ... result = agent.run("query")
640
+ """
641
+ return LangChainUserContextManager(user, agent)