agentguardproxy 0.5.1__tar.gz → 0.5.2__tar.gz
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.
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/PKG-INFO +2 -3
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguard/__init__.py +61 -64
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguard/adapters/browseruse.py +14 -15
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguard/adapters/crewai.py +31 -58
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguard/adapters/langchain.py +30 -46
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguard/adapters/mcp.py +33 -35
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguardproxy.egg-info/PKG-INFO +2 -3
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/pyproject.toml +8 -9
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_adapters_extended.py +2 -2
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_crewai.py +2 -2
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_guard.py +1 -1
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_langchain.py +10 -3
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/README.md +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguard/adapters/__init__.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguardproxy.egg-info/SOURCES.txt +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguardproxy.egg-info/dependency_links.txt +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguardproxy.egg-info/requires.txt +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguardproxy.egg-info/top_level.txt +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/setup.cfg +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_adapters.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_browseruse.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_decorator.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_end_to_end_real_server.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_integration.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_mcp.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_mcp_fuzz.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_mcp_gateway.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_polling_jitter.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_redactor_property.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_sdk_integration.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_sdk_polish.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_tenant_routing.py +0 -0
- {agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/tests/test_wire_format.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentguardproxy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Python SDK for AgentGuard — the firewall for AI agents
|
|
5
5
|
Author-email: Cauã Ferraz <cauaferrazp@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -12,14 +12,13 @@ Keywords: ai,agents,firewall,policy,guardrails,safety
|
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
19
|
Classifier: Topic :: Security
|
|
21
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.10
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
Provides-Extra: langchain
|
|
25
24
|
Requires-Dist: langchain-core<2.0,>=0.3; extra == "langchain"
|
|
@@ -47,7 +47,7 @@ DECISION_REQUIRE_APPROVAL = "REQUIRE_APPROVAL"
|
|
|
47
47
|
|
|
48
48
|
# API endpoint paths. The leading "/v1" is added by Guard._url so a single
|
|
49
49
|
# code path handles both the legacy /v1/<suffix> URLs and the tenant-aware
|
|
50
|
-
# /v1/t/<tenant>/<suffix> form
|
|
50
|
+
# /v1/t/<tenant>/<suffix> form.
|
|
51
51
|
ENDPOINT_CHECK = "/check"
|
|
52
52
|
ENDPOINT_APPROVE = "/approve/"
|
|
53
53
|
ENDPOINT_DENY = "/deny/"
|
|
@@ -61,10 +61,10 @@ ENDPOINT_STATUS = "/status/"
|
|
|
61
61
|
LOCAL_TENANT_ID = "local"
|
|
62
62
|
|
|
63
63
|
# Fail-mode values for the Guard() constructor. "deny" fails closed when the
|
|
64
|
-
# AgentGuard proxy is unreachable (the
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
64
|
+
# AgentGuard proxy is unreachable (the default). "allow" fails open —
|
|
65
|
+
# permitted as an explicit opt-in for agents whose threat model treats
|
|
66
|
+
# AgentGuard as best-effort; the caller is responsible for any resulting
|
|
67
|
+
# safety implications.
|
|
68
68
|
FAIL_MODE_DENY = "deny"
|
|
69
69
|
FAIL_MODE_ALLOW = "allow"
|
|
70
70
|
_VALID_FAIL_MODES = (FAIL_MODE_DENY, FAIL_MODE_ALLOW)
|
|
@@ -95,16 +95,16 @@ class CheckResult:
|
|
|
95
95
|
# --- Typed exceptions raised by the @guarded decorator ---
|
|
96
96
|
#
|
|
97
97
|
# All three extend PermissionError so callers that already catch
|
|
98
|
-
# PermissionError
|
|
99
|
-
#
|
|
100
|
-
#
|
|
98
|
+
# PermissionError keep working. New callers can catch the specific
|
|
99
|
+
# subclass and read structured fields (result, approval_id,
|
|
100
|
+
# approval_url) instead of parsing error strings.
|
|
101
101
|
|
|
102
102
|
class AgentGuardError(PermissionError):
|
|
103
103
|
"""Base class for AgentGuard-raised permission failures.
|
|
104
104
|
|
|
105
105
|
Carries the originating :class:`CheckResult` (if any) so callers can
|
|
106
106
|
inspect the decision, matched rule, and approval metadata without
|
|
107
|
-
re-running the check. Messages preserve
|
|
107
|
+
re-running the check. Messages preserve a stable string format so
|
|
108
108
|
existing regex/text matchers continue to work.
|
|
109
109
|
"""
|
|
110
110
|
|
|
@@ -156,8 +156,8 @@ class AgentGuardAuthError(AgentGuardError):
|
|
|
156
156
|
``/v1/audit``).
|
|
157
157
|
|
|
158
158
|
Distinguishes "API key wrong / expired" from "approval poll timed
|
|
159
|
-
out" so callers can surface the right operator-facing error.
|
|
160
|
-
|
|
159
|
+
out" so callers can surface the right operator-facing error.
|
|
160
|
+
Extends :class:`AgentGuardError` so existing
|
|
161
161
|
``except PermissionError:`` handlers still catch it.
|
|
162
162
|
"""
|
|
163
163
|
|
|
@@ -171,15 +171,21 @@ class AgentGuardAuthError(AgentGuardError):
|
|
|
171
171
|
self.status = status
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
# AgentGuardTimeoutError is an alias for AgentGuardApprovalTimeout
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
# downstream `except AgentGuardApprovalTimeout:` handlers.
|
|
174
|
+
# AgentGuardTimeoutError is an alias for AgentGuardApprovalTimeout
|
|
175
|
+
# kept for backwards compatibility — `except AgentGuardApprovalTimeout:`
|
|
176
|
+
# handlers still match.
|
|
178
177
|
AgentGuardTimeoutError = AgentGuardApprovalTimeout
|
|
179
178
|
|
|
180
179
|
|
|
181
180
|
class Guard:
|
|
182
|
-
"""Client for the AgentGuard
|
|
181
|
+
"""Client for the AgentGuard server — the SDK enforcement layer.
|
|
182
|
+
|
|
183
|
+
Call ``guard.check(scope, ...)`` before every gated action, or use the
|
|
184
|
+
``@guarded`` decorator / framework adapters in ``agentguard.adapters``
|
|
185
|
+
for higher-level integration. For wire-level enforcement that needs no
|
|
186
|
+
agent code change, see the ``agentguard-mcp-gateway`` and
|
|
187
|
+
``agentguard-llm-proxy`` binaries.
|
|
188
|
+
"""
|
|
183
189
|
|
|
184
190
|
def __init__(
|
|
185
191
|
self,
|
|
@@ -199,26 +205,20 @@ class Guard:
|
|
|
199
205
|
timeout: HTTP timeout in seconds for individual calls.
|
|
200
206
|
api_key: Bearer token for /v1/approve, /v1/deny, /v1/status.
|
|
201
207
|
Falls back to AGENTGUARD_API_KEY.
|
|
202
|
-
fail_mode: Behavior when the proxy is unreachable. "deny"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
tenant_id: Optional tenant identifier (v0.5+). When set to a
|
|
208
|
+
fail_mode: Behavior when the proxy is unreachable. "deny"
|
|
209
|
+
(the default) returns a DENY result so the agent fails
|
|
210
|
+
closed. "allow" returns an ALLOW result — use only when
|
|
211
|
+
the threat model treats AgentGuard as best-effort and
|
|
212
|
+
the caller accepts the safety trade-off. An invalid
|
|
213
|
+
value raises ValueError at construction.
|
|
214
|
+
tenant_id: Optional tenant identifier. When set to a
|
|
210
215
|
non-empty value other than ``"local"``, every HTTP call is
|
|
211
216
|
routed through the tenant-aware ``/v1/t/{tenant_id}/...``
|
|
212
|
-
URL family instead of the legacy ``/v1/...`` path.
|
|
213
|
-
or ``"local"`` selects the legacy URLs
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
v0.5.0 parity note: the TypeScript SDK already honors `failMode`,
|
|
219
|
-
default `"deny"`. v0.5.0 will align both SDKs on explicit fail-mode
|
|
220
|
-
documentation. Adding `fail_mode` now is purely forward-compatible;
|
|
221
|
-
omitting it preserves v0.4.0 semantics exactly.
|
|
217
|
+
URL family instead of the legacy ``/v1/...`` path.
|
|
218
|
+
``None`` or ``"local"`` selects the legacy URLs. Falls
|
|
219
|
+
back to ``AGENTGUARD_TENANT_ID``. The bundled
|
|
220
|
+
FilePolicyProvider only recognises ``"local"``;
|
|
221
|
+
multi-tenant providers can register others.
|
|
222
222
|
"""
|
|
223
223
|
if fail_mode not in _VALID_FAIL_MODES:
|
|
224
224
|
raise ValueError(
|
|
@@ -314,13 +314,12 @@ class Guard:
|
|
|
314
314
|
|
|
315
315
|
try:
|
|
316
316
|
with request.urlopen(req, timeout=self.timeout) as resp:
|
|
317
|
-
# --- Honest response validation
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
# `{` would slip through. We now positively assert:
|
|
317
|
+
# --- Honest response validation ---
|
|
318
|
+
# A misconfigured reverse proxy serving an HTML error
|
|
319
|
+
# page or a chunked plaintext body would silently produce
|
|
320
|
+
# a JSONDecodeError that we *do* catch — but 200-OK plus
|
|
321
|
+
# a non-JSON body that happened to start with `{` would
|
|
322
|
+
# slip through. We positively assert:
|
|
324
323
|
# 1. status in 2xx
|
|
325
324
|
# 2. Content-Type is application/json (charset suffix ok)
|
|
326
325
|
# 3. body is a dict carrying a `decision` field
|
|
@@ -386,9 +385,9 @@ class Guard:
|
|
|
386
385
|
approval_url=body.get("approval_url", ""),
|
|
387
386
|
)
|
|
388
387
|
except (error.URLError, OSError, json.JSONDecodeError) as e:
|
|
389
|
-
# Transport failure. fail_mode picks the
|
|
390
|
-
#
|
|
391
|
-
#
|
|
388
|
+
# Transport failure. fail_mode picks the default: "deny"
|
|
389
|
+
# fails closed; "allow" is an opt-in for callers whose
|
|
390
|
+
# threat model treats AgentGuard as advisory.
|
|
392
391
|
#
|
|
393
392
|
# We catch three classes here:
|
|
394
393
|
# - URLError: urlopen() connection-phase failures (connect
|
|
@@ -411,7 +410,7 @@ class Guard:
|
|
|
411
410
|
|
|
412
411
|
Centralizes the fail-mode dispatch so transport failures and HTTP
|
|
413
412
|
contract violations (bad status, wrong content type, malformed
|
|
414
|
-
body) all flow through one decision point.
|
|
413
|
+
body) all flow through one decision point.
|
|
415
414
|
"""
|
|
416
415
|
decision = DECISION_ALLOW if self.fail_mode == FAIL_MODE_ALLOW else DECISION_DENY
|
|
417
416
|
return CheckResult(decision=decision, reason=reason)
|
|
@@ -459,19 +458,17 @@ class Guard:
|
|
|
459
458
|
Sends the API key on every poll because /v1/status is now auth-gated
|
|
460
459
|
on servers configured with --api-key.
|
|
461
460
|
|
|
462
|
-
|
|
461
|
+
Behavior:
|
|
463
462
|
- Polling is jittered to ``[0.8, 1.2] * poll_interval`` to avoid
|
|
464
463
|
synchronized retries from many clients waiting on the same
|
|
465
464
|
approval bursting the proxy at exact ``poll_interval`` boundaries.
|
|
466
|
-
``time.sleep`` is retained — it is the documented pattern for
|
|
467
|
-
this kind of polling loop and consistent with the v0.4.x SDK.
|
|
468
465
|
- HTTP 401 / 403 from the status endpoint raise
|
|
469
466
|
:class:`AgentGuardAuthError` immediately. Continuing to poll
|
|
470
467
|
would just spin until ``timeout`` elapsed and return a
|
|
471
468
|
synthetic "Approval timed out" DENY, masking the real cause
|
|
472
|
-
(wrong/expired API key). Other HTTPErrors and URLErrors
|
|
473
|
-
|
|
474
|
-
|
|
469
|
+
(wrong/expired API key). Other HTTPErrors and URLErrors are
|
|
470
|
+
swallowed and retried so transient network blips do not abort
|
|
471
|
+
a long-running approval wait.
|
|
475
472
|
"""
|
|
476
473
|
deadline = time.time() + timeout
|
|
477
474
|
while time.time() < deadline:
|
|
@@ -519,9 +516,9 @@ class Guard:
|
|
|
519
516
|
# ``Guard.check``. Anything outside this set is almost always a typo
|
|
520
517
|
# (e.g. ``agent="x"`` instead of ``meta={"agent":"x"}``) — and silently
|
|
521
518
|
# forwarding it to ``Guard.check`` would either be dropped on the floor
|
|
522
|
-
# (``check`` only inspects keyword args it knows about) or raise a
|
|
523
|
-
# TypeError much later in the call chain.
|
|
524
|
-
#
|
|
519
|
+
# (``check`` only inspects keyword args it knows about) or raise a
|
|
520
|
+
# confusing TypeError much later in the call chain. Reject at the
|
|
521
|
+
# decorator boundary so the typo surfaces at definition time.
|
|
525
522
|
_GUARDED_VALID_CHECK_KWARGS = frozenset({
|
|
526
523
|
"action",
|
|
527
524
|
"command",
|
|
@@ -570,17 +567,16 @@ def guarded(
|
|
|
570
567
|
decision: ALLOW runs the function, DENY raises :class:`AgentGuardDenied`,
|
|
571
568
|
a synthetic "Approval timed out" raises :class:`AgentGuardApprovalTimeout`.
|
|
572
569
|
With ``wait_for_approval=False`` (default), the wrapper raises
|
|
573
|
-
:class:`AgentGuardApprovalRequired` immediately
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
All raised exceptions extend :class:`PermissionError`, so existing
|
|
577
|
-
``except PermissionError:`` handlers continue to work unchanged.
|
|
570
|
+
:class:`AgentGuardApprovalRequired` immediately. That class extends
|
|
571
|
+
``PermissionError``, so existing ``except PermissionError:`` handlers
|
|
572
|
+
continue to work unchanged.
|
|
578
573
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
574
|
+
Unknown ``**check_kwargs`` raise ``TypeError`` at decoration time.
|
|
575
|
+
Only the keyword arguments :meth:`Guard.check` understands
|
|
576
|
+
(``action``, ``command``, ``path``, ``domain``, ``url``,
|
|
582
577
|
``session_id``, ``est_cost``, ``meta``) are accepted; a typo like
|
|
583
|
-
``agent="x"``
|
|
578
|
+
``agent="x"`` is rejected at definition time rather than silently
|
|
579
|
+
dropped.
|
|
584
580
|
"""
|
|
585
581
|
unknown = set(check_kwargs) - _GUARDED_VALID_CHECK_KWARGS
|
|
586
582
|
if unknown:
|
|
@@ -623,7 +619,8 @@ def guarded(
|
|
|
623
619
|
result=resolved,
|
|
624
620
|
)
|
|
625
621
|
raise AgentGuardApprovalRequired(
|
|
626
|
-
#
|
|
622
|
+
# Stable message text — text-matchers in caller code
|
|
623
|
+
# depend on it.
|
|
627
624
|
f"Action requires approval. Approve at: {result.approval_url}",
|
|
628
625
|
result=result,
|
|
629
626
|
approval_id=result.approval_id,
|
|
@@ -34,9 +34,10 @@ Design notes
|
|
|
34
34
|
the methods listed in :data:`_GATED_METHODS` plus a small allowlist of
|
|
35
35
|
read-only properties via :data:`_ALLOWED_PASSTHROUGH`.
|
|
36
36
|
- **Default deny on attribute access.** Anything not on the allowlist
|
|
37
|
-
raises ``AttributeError`` with a security explanation.
|
|
38
|
-
|
|
39
|
-
``self._page.click(...)
|
|
37
|
+
raises ``AttributeError`` with a security explanation. A permissive
|
|
38
|
+
``__getattr__`` would silently fall through to
|
|
39
|
+
``self._page.click(...)`` and leave every action method un-gated, so
|
|
40
|
+
the wrapper never adds one.
|
|
40
41
|
- **Form values are redacted before transmission.** ``check_form_input``
|
|
41
42
|
passes the field NAME through (operators need it for audit context)
|
|
42
43
|
but rewrites long values to ``<redacted; len=N>`` so audit logs do not
|
|
@@ -44,9 +45,8 @@ Design notes
|
|
|
44
45
|
through :func:`agentguard.adapters.mcp._redact` for the standard secret
|
|
45
46
|
patterns (Bearer tokens, AWS keys, ``secret=...`` pairs, etc.).
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
pkg/policy/engine_data_test.go for the engine-side close).
|
|
48
|
+
The data-scope contract is pinned by pkg/policy/engine_data_test.go
|
|
49
|
+
on the engine side.
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
52
|
from typing import Any, Optional
|
|
@@ -215,11 +215,10 @@ class GuardedPage:
|
|
|
215
215
|
|
|
216
216
|
Gated methods are explicit attributes on this class (see the bodies
|
|
217
217
|
below). Anything else is rejected via ``__getattr__`` against
|
|
218
|
-
:data:`_ALLOWED_PASSTHROUGH`.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
raw Page without ever consulting the policy.
|
|
218
|
+
:data:`_ALLOWED_PASSTHROUGH`. A permissive ``__getattr__`` that
|
|
219
|
+
proxied every attribute access would let ``page.click(...)``,
|
|
220
|
+
``page.fill(...)``, ``page.evaluate(...)`` flow through to the raw
|
|
221
|
+
Page without ever consulting the policy — so we never add one.
|
|
223
222
|
|
|
224
223
|
Read-only properties listed in :data:`_ALLOWED_PASSTHROUGH` (url,
|
|
225
224
|
title, content, viewport_size, is_closed, main_frame, context) are
|
|
@@ -529,10 +528,10 @@ class GuardedPage:
|
|
|
529
528
|
|
|
530
529
|
Anything else raises ``AttributeError`` with a security
|
|
531
530
|
explanation. The default-deny posture is intentional: silently
|
|
532
|
-
proxying unknown attributes to ``self._page``
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
531
|
+
proxying unknown attributes to ``self._page`` would let modern
|
|
532
|
+
Playwright APIs (route, expose_function, ...) skip every gate.
|
|
533
|
+
Adding a new method to the gated surface is preferable to
|
|
534
|
+
widening the allowlist.
|
|
536
535
|
"""
|
|
537
536
|
# Avoid infinite recursion when our own internals are accessed
|
|
538
537
|
# before __init__ completes.
|
|
@@ -1,67 +1,39 @@
|
|
|
1
1
|
"""
|
|
2
|
-
AgentGuard CrewAI Adapter
|
|
2
|
+
AgentGuard CrewAI Adapter — subclass + override.
|
|
3
3
|
|
|
4
4
|
Wraps CrewAI tools so every invocation passes through AgentGuard policy checks.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Concretely::
|
|
21
|
-
|
|
22
|
-
Agent(tools=[GuardedCrewTool(...)])
|
|
23
|
-
|
|
24
|
-
raises::
|
|
25
|
-
|
|
26
|
-
pydantic_core._pydantic_core.ValidationError: 1 validation error for
|
|
27
|
-
Agent tools.0 Input should be a valid dictionary or instance of
|
|
28
|
-
BaseTool [...] input_type=GuardedCrewTool
|
|
29
|
-
|
|
30
|
-
That same architectural class affects langgraph 1.0 + langchain_core 1.x
|
|
31
|
-
(``isinstance(thing, Runnable)`` in ``coerce_to_runnable``).
|
|
32
|
-
|
|
33
|
-
The v0.5.1 fix: actually subclass CrewAI's ``BaseTool`` so isinstance
|
|
34
|
-
passes natively, and preserve the policy-enforcement contract by
|
|
35
|
-
overriding **every** entry point the framework calls. The
|
|
36
|
-
"every-method-is-on-this-class" property substitutes for the
|
|
37
|
-
composition-era ``__getattr__`` allowlist:
|
|
38
|
-
|
|
39
|
-
- ``_run`` (abstract on the parent — we MUST override) gates every
|
|
40
|
-
sync dispatch path the framework drives. CrewAI's ``BaseTool.run``,
|
|
41
|
-
``invoke`` (when present on a subclass / wrapper), and
|
|
42
|
-
``to_structured_tool`` all read ``_run`` off the instance, so a
|
|
43
|
-
gated ``_run`` covers the entire sync surface.
|
|
6
|
+
Architecture
|
|
7
|
+
------------
|
|
8
|
+
``GuardedCrewTool`` subclasses CrewAI's ``BaseTool`` directly so
|
|
9
|
+
``isinstance(thing, BaseTool)`` checks succeed natively (CrewAI 1.x +
|
|
10
|
+
pydantic 2.12 reject virtual-subclass registration via
|
|
11
|
+
``BaseTool.register``). To keep the gate tight without an
|
|
12
|
+
``__getattr__`` allowlist, **every** entry point the framework calls is
|
|
13
|
+
explicitly overridden:
|
|
14
|
+
|
|
15
|
+
- ``_run`` (abstract on the parent) gates every sync dispatch path
|
|
16
|
+
CrewAI drives. CrewAI's ``BaseTool.run``, ``invoke`` (when present
|
|
17
|
+
on a subclass / wrapper), and ``to_structured_tool`` all read
|
|
18
|
+
``_run`` off the instance, so a gated ``_run`` covers the entire
|
|
19
|
+
sync surface.
|
|
44
20
|
- ``_arun`` is overridden because CrewAI's ``arun`` calls ``_arun``.
|
|
45
21
|
- ``run`` / ``arun`` / ``invoke`` / ``ainvoke`` / ``__call__`` are
|
|
46
22
|
overridden explicitly so the gate fires regardless of which entry
|
|
47
|
-
the agent runtime picks. Each one
|
|
48
|
-
underlying instance's matching method
|
|
49
|
-
accounting (``_claim_usage`` etc.)
|
|
50
|
-
those.
|
|
23
|
+
the agent runtime picks. Each one runs the tool through the
|
|
24
|
+
underlying instance's matching method to preserve CrewAI's usage
|
|
25
|
+
accounting (``_claim_usage`` etc.).
|
|
51
26
|
- ``to_structured_tool`` is overridden so CrewAI's
|
|
52
|
-
``to_structured_tool`` pipeline (which does ``func=self._run``)
|
|
53
|
-
up calling our gated ``_run``, not the wrapped tool's bare
|
|
54
|
-
|
|
55
|
-
The new defense contract
|
|
56
|
-
------------------------
|
|
57
|
-
Composition v0.5: "no parent methods are exposed; ``__getattr__`` is the
|
|
58
|
-
single chokepoint."
|
|
27
|
+
``to_structured_tool`` pipeline (which does ``func=self._run``)
|
|
28
|
+
ends up calling our gated ``_run``, not the wrapped tool's bare
|
|
29
|
+
``_run``.
|
|
59
30
|
|
|
60
|
-
|
|
61
|
-
class; the canary integration test (``test_at_real_crewai.py``)
|
|
62
|
-
upstream adds a new dispatch path that bypasses our overrides.
|
|
63
|
-
``PrivateAttr`` keeps internal references (``_tool``,
|
|
64
|
-
``_scope``) off ``model_fields`` and out of ``model_dump``
|
|
31
|
+
The defense contract: every gated method is explicitly overridden on
|
|
32
|
+
this class; the canary integration test (``test_at_real_crewai.py``)
|
|
33
|
+
trips if upstream adds a new dispatch path that bypasses our overrides.
|
|
34
|
+
Pydantic ``PrivateAttr`` keeps internal references (``_tool``,
|
|
35
|
+
``_guard``, ``_scope``) off ``model_fields`` and out of ``model_dump``
|
|
36
|
+
payloads.
|
|
65
37
|
|
|
66
38
|
Lazy framework import
|
|
67
39
|
---------------------
|
|
@@ -174,9 +146,10 @@ def _build_guarded_class() -> type:
|
|
|
174
146
|
"""Hybrid subclass-and-override wrapper around a CrewAI ``BaseTool``.
|
|
175
147
|
|
|
176
148
|
Subclasses ``BaseTool`` so framework-side ``isinstance`` checks
|
|
177
|
-
succeed natively (
|
|
178
|
-
|
|
179
|
-
overridden to gate via :meth:`Guard.check` before
|
|
149
|
+
succeed natively (pydantic 2.12 / CrewAI 1.x reject virtual-
|
|
150
|
+
subclass registration). Every entry point the framework calls
|
|
151
|
+
is explicitly overridden to gate via :meth:`Guard.check` before
|
|
152
|
+
forwarding.
|
|
180
153
|
"""
|
|
181
154
|
|
|
182
155
|
# Pydantic private attributes — held on the instance but not part
|
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
"""
|
|
2
|
-
AgentGuard LangChain Adapter
|
|
2
|
+
AgentGuard LangChain Adapter — subclass + override.
|
|
3
3
|
|
|
4
4
|
Wraps LangChain tools so every invocation passes through AgentGuard policy
|
|
5
5
|
checks.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
check. The integration test had to wrap GuardedTool in a
|
|
19
|
-
``Tool.from_function(func=lambda x: gt.invoke(x))`` to register it
|
|
20
|
-
with the agent — that workaround is no longer required after v0.5.1.
|
|
21
|
-
|
|
22
|
-
The v0.5.1 fix: subclass ``langchain_core.tools.BaseTool`` so isinstance
|
|
23
|
-
passes natively, and preserve the policy-enforcement contract by
|
|
24
|
-
overriding **every** entry point the framework calls. The
|
|
25
|
-
"every-method-is-on-this-class" property substitutes for the
|
|
26
|
-
composition-era ``__getattr__`` allowlist.
|
|
27
|
-
|
|
28
|
-
Gated methods (each calls ``Guard.check`` before forwarding):
|
|
7
|
+
Architecture
|
|
8
|
+
------------
|
|
9
|
+
``GuardedTool`` subclasses ``langchain_core.tools.BaseTool`` directly
|
|
10
|
+
so ``isinstance(thing, Runnable)`` and ``isinstance(thing, BaseTool)``
|
|
11
|
+
checks succeed natively. langgraph 1.0 + langchain_core 1.x's
|
|
12
|
+
``coerce_to_runnable`` and langchain 1.x's
|
|
13
|
+
``langchain.agents.create_agent`` both reject composition wrappers,
|
|
14
|
+
so the subclass approach is the only one that registers cleanly.
|
|
15
|
+
|
|
16
|
+
To keep the gate tight without an ``__getattr__`` allowlist, **every**
|
|
17
|
+
entry point the framework calls is explicitly overridden:
|
|
29
18
|
|
|
30
19
|
- ``_run`` (abstract on the parent — required by the subclass) — the
|
|
31
20
|
canonical sync dispatch path; ``BaseTool.run`` calls into ``_run``
|
|
@@ -39,19 +28,14 @@ Gated methods (each calls ``Guard.check`` before forwarding):
|
|
|
39
28
|
gating is a v0.6 issue.
|
|
40
29
|
- ``batch`` / ``abatch`` — gate each input independently; first DENY
|
|
41
30
|
raises for the whole batch (whole-batch-fails-on-first-deny).
|
|
42
|
-
- ``run`` / ``arun`` — legacy entries; preserve
|
|
43
|
-
semantics
|
|
44
|
-
|
|
45
|
-
The new defense contract
|
|
46
|
-
------------------------
|
|
47
|
-
Composition v0.5.0: "no parent methods are exposed; ``__getattr__`` is
|
|
48
|
-
the single chokepoint."
|
|
31
|
+
- ``run`` / ``arun`` — legacy entries; preserve string-on-deny
|
|
32
|
+
semantics for callers that depend on it.
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
class. Pydantic ``PrivateAttr`` keeps internal references off
|
|
34
|
+
The defense contract: every gated method is explicitly overridden on
|
|
35
|
+
this class. Pydantic ``PrivateAttr`` keeps internal references off
|
|
52
36
|
``model_fields`` and out of ``model_dump`` payloads. The canary
|
|
53
|
-
integration test (``test_at_real_langchain.py``) trips if upstream
|
|
54
|
-
a new dispatch path that bypasses our overrides.
|
|
37
|
+
integration test (``test_at_real_langchain.py``) trips if upstream
|
|
38
|
+
adds a new dispatch path that bypasses our overrides.
|
|
55
39
|
|
|
56
40
|
Lazy framework import
|
|
57
41
|
---------------------
|
|
@@ -65,10 +49,9 @@ the ``GuardedTool`` symbol is a callable factory that raises a clear
|
|
|
65
49
|
Stream gating limitation
|
|
66
50
|
========================
|
|
67
51
|
|
|
68
|
-
For ``stream
|
|
69
|
-
Mid-stream tool calls
|
|
70
|
-
|
|
71
|
-
title: ``langchain: per-chunk policy gating in stream()/astream()``.
|
|
52
|
+
For ``stream`` / ``astream`` the gate fires **once** at stream open.
|
|
53
|
+
Mid-stream tool calls bypass the gate. TODO(v0.6): per-chunk gating
|
|
54
|
+
in ``stream()`` / ``astream()``.
|
|
72
55
|
"""
|
|
73
56
|
|
|
74
57
|
from __future__ import annotations
|
|
@@ -143,10 +126,10 @@ def _build_guarded_tool_class() -> type:
|
|
|
143
126
|
"""Hybrid subclass-and-override wrapper around a LangChain ``BaseTool``.
|
|
144
127
|
|
|
145
128
|
Subclasses ``BaseTool`` so framework-side ``isinstance(thing,
|
|
146
|
-
Runnable)`` checks succeed natively (
|
|
147
|
-
|
|
148
|
-
framework calls is explicitly overridden to gate via
|
|
149
|
-
before forwarding.
|
|
129
|
+
Runnable)`` checks succeed natively (langgraph 1.0 /
|
|
130
|
+
langchain_core 1.x reject composition wrappers). Every entry
|
|
131
|
+
point the framework calls is explicitly overridden to gate via
|
|
132
|
+
Guard.check before forwarding.
|
|
150
133
|
"""
|
|
151
134
|
|
|
152
135
|
# Pydantic private attrs — held on the instance but not part of
|
|
@@ -504,7 +487,8 @@ def _build_guarded_tool_class() -> type:
|
|
|
504
487
|
return await self._tool.abatch(inputs, config=config, **kwargs)
|
|
505
488
|
|
|
506
489
|
# --------------------------------------------------------------
|
|
507
|
-
# Legacy API (run / arun) — string-on-deny preserved for
|
|
490
|
+
# Legacy API (run / arun) — string-on-deny shape preserved for
|
|
491
|
+
# callers that depend on it; new code should prefer invoke().
|
|
508
492
|
# --------------------------------------------------------------
|
|
509
493
|
|
|
510
494
|
def run( # type: ignore[override]
|
|
@@ -515,9 +499,9 @@ def _build_guarded_tool_class() -> type:
|
|
|
515
499
|
) -> Any:
|
|
516
500
|
"""Run the tool (legacy entry).
|
|
517
501
|
|
|
518
|
-
Returns a string message on DENY / REQUIRE_APPROVAL
|
|
519
|
-
|
|
520
|
-
|
|
502
|
+
Returns a string message on DENY / REQUIRE_APPROVAL (legacy
|
|
503
|
+
shape). New code should prefer ``invoke`` so failures surface
|
|
504
|
+
as exceptions.
|
|
521
505
|
"""
|
|
522
506
|
result = self._gate(tool_input)
|
|
523
507
|
inner = self._tool
|
|
@@ -76,7 +76,7 @@ def _infer_check_params_for(tool: "ToolDefinition", arguments: dict) -> dict:
|
|
|
76
76
|
Both the in-process server and the gateway need identical inference +
|
|
77
77
|
redaction. Defining the logic once at module scope keeps the gateway
|
|
78
78
|
from awkwardly invoking the unbound server method on a non-server
|
|
79
|
-
``self`` (the
|
|
79
|
+
``self`` (the gateway is its own class).
|
|
80
80
|
"""
|
|
81
81
|
from urllib.parse import urlparse # local import — only used here
|
|
82
82
|
|
|
@@ -181,8 +181,8 @@ class GuardedMCPServer:
|
|
|
181
181
|
"""Delegate to :func:`_infer_check_params_for`.
|
|
182
182
|
|
|
183
183
|
Kept as a method for back-compat with any subclass / test that
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
binds it; the real logic (including secret redaction) lives in
|
|
185
|
+
the module-level helper so :class:`GuardedMCPGateway` can
|
|
186
186
|
reuse it without instantiating a server.
|
|
187
187
|
"""
|
|
188
188
|
return _infer_check_params_for(tool, arguments)
|
|
@@ -194,19 +194,17 @@ class GuardedMCPServer:
|
|
|
194
194
|
params = request.get("params", {})
|
|
195
195
|
|
|
196
196
|
if method == "initialize":
|
|
197
|
-
# MCP clients advertise their protocol version in params. We pin
|
|
198
|
-
# MCP_PROTOCOL_VERSION and do not yet negotiate
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
# MUST go to stderr.
|
|
197
|
+
# MCP clients advertise their protocol version in params. We pin
|
|
198
|
+
# to MCP_PROTOCOL_VERSION and do not yet negotiate. If the client
|
|
199
|
+
# wants a different version (usually newer), log a single WARN to
|
|
200
|
+
# stderr so operators can see version drift, then respond with
|
|
201
|
+
# our pinned version. stdout is reserved for JSON-RPC on the
|
|
202
|
+
# stdio transport, so the warning MUST go to stderr.
|
|
204
203
|
client_version = params.get("protocolVersion") if isinstance(params, dict) else None
|
|
205
204
|
if client_version and client_version != MCP_PROTOCOL_VERSION:
|
|
206
205
|
sys.stderr.write(
|
|
207
206
|
f"WARN agentguard.mcp: client requested protocolVersion "
|
|
208
|
-
f"{client_version!r}, pinning to {MCP_PROTOCOL_VERSION!r}
|
|
209
|
-
f"(negotiation is a v0.5.0 design item)\n"
|
|
207
|
+
f"{client_version!r}, pinning to {MCP_PROTOCOL_VERSION!r}\n"
|
|
210
208
|
)
|
|
211
209
|
sys.stderr.flush()
|
|
212
210
|
return {
|
|
@@ -349,15 +347,15 @@ class GuardedMCPServer:
|
|
|
349
347
|
def run(self):
|
|
350
348
|
"""Run the MCP server on stdio (blocking).
|
|
351
349
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
350
|
+
Each frame is processed inside a guard so a single malformed
|
|
351
|
+
JSON line, a handler exception, or a downstream-side error does
|
|
352
|
+
not crash the adapter. Claude Desktop (and similar long-lived
|
|
353
|
+
MCP clients) keep one stdio session open for the whole session
|
|
354
|
+
— surviving a bad frame means the user does not have to restart
|
|
355
|
+
their editor every time a tool throws.
|
|
358
356
|
|
|
359
357
|
Behavior:
|
|
360
|
-
- blank / whitespace-only line: dropped silently
|
|
358
|
+
- blank / whitespace-only line: dropped silently.
|
|
361
359
|
- JSON parse error: log to stderr, drop the frame. Best-effort
|
|
362
360
|
JSON-RPC parse-error response is not emitted because the bad
|
|
363
361
|
frame had no recoverable id.
|
|
@@ -431,8 +429,7 @@ class _UpstreamProcess:
|
|
|
431
429
|
matching the stdio JSON-RPC pattern Claude Desktop and Cursor use.
|
|
432
430
|
Server-initiated notifications (``listChanged`` etc.) are not
|
|
433
431
|
forwarded; capability merging and namespaced tool prefixes are
|
|
434
|
-
deferred
|
|
435
|
-
upstream, request/response only.
|
|
432
|
+
deferred. Single upstream, request/response only.
|
|
436
433
|
"""
|
|
437
434
|
|
|
438
435
|
def __init__(self, command: list, env: Optional[dict] = None):
|
|
@@ -503,20 +500,21 @@ class _UpstreamProcess:
|
|
|
503
500
|
class GuardedMCPGateway:
|
|
504
501
|
"""Single-upstream MCP gateway with AgentGuard in the middle.
|
|
505
502
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
503
|
+
Spawns one downstream MCP server (e.g.
|
|
504
|
+
``npx -y @modelcontextprotocol/server-filesystem``), answers
|
|
505
|
+
``tools/list`` by forwarding to the downstream, and gates every
|
|
506
|
+
``tools/call`` through ``Guard.check`` before forwarding.
|
|
510
507
|
|
|
511
|
-
Limitations
|
|
508
|
+
Limitations:
|
|
512
509
|
- Single upstream — no capability merging.
|
|
513
510
|
- No tool-name prefix / namespace separation.
|
|
514
511
|
- Server-initiated notifications (``notifications/tools/list_changed``)
|
|
515
512
|
are not forwarded.
|
|
516
513
|
- No prompts/resources support — only ``tools/*`` is gated.
|
|
517
514
|
|
|
518
|
-
|
|
519
|
-
|
|
515
|
+
The full multi-upstream gateway lives in cmd/agentguard-mcp-gateway/
|
|
516
|
+
+ pkg/mcpgw/. Use :class:`GuardedMCPServer` directly when you want
|
|
517
|
+
to register tools in code.
|
|
520
518
|
"""
|
|
521
519
|
|
|
522
520
|
# Methods we always forward verbatim to the upstream.
|
|
@@ -549,7 +547,7 @@ class GuardedMCPGateway:
|
|
|
549
547
|
Scope defaults to ``"shell"`` for unknown tools; ``_infer_check_params``
|
|
550
548
|
upgrades to ``network`` / ``filesystem`` based on argument keys, so the
|
|
551
549
|
default is mostly cosmetic. We do NOT try to read the description or
|
|
552
|
-
schema to guess a smarter scope —
|
|
550
|
+
schema to guess a smarter scope — the simple inference is the contract.
|
|
553
551
|
"""
|
|
554
552
|
try:
|
|
555
553
|
resp = self._upstream.request({
|
|
@@ -699,16 +697,16 @@ def main():
|
|
|
699
697
|
"""Entry point for running as ``python -m agentguard.adapters.mcp``.
|
|
700
698
|
|
|
701
699
|
Modes:
|
|
702
|
-
- ``--upstream "<command...>"`` — gateway mode
|
|
700
|
+
- ``--upstream "<command...>"`` — gateway mode. Spawns the
|
|
703
701
|
downstream MCP server given by the command, bridges JSON-RPC,
|
|
704
702
|
and gates ``tools/call`` through AgentGuard. Example:
|
|
705
703
|
``python -m agentguard.adapters.mcp --guard-url http://localhost:8080
|
|
706
704
|
--upstream "npx -y @modelcontextprotocol/server-filesystem /tmp"``.
|
|
707
|
-
- no ``--upstream`` — empty-server mode.
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
705
|
+
- no ``--upstream`` — empty-server mode. Retained for callers that
|
|
706
|
+
``register tools in code`` via subclasses. Emits a stderr WARN
|
|
707
|
+
so an operator following the docs literally sees that no tools
|
|
708
|
+
are registered (instead of silently exposing a tool-less server
|
|
709
|
+
to Claude
|
|
712
710
|
Desktop).
|
|
713
711
|
"""
|
|
714
712
|
import argparse
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentguardproxy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Python SDK for AgentGuard — the firewall for AI agents
|
|
5
5
|
Author-email: Cauã Ferraz <cauaferrazp@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -12,14 +12,13 @@ Keywords: ai,agents,firewall,policy,guardrails,safety
|
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
19
|
Classifier: Topic :: Security
|
|
21
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.10
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
Provides-Extra: langchain
|
|
25
24
|
Requires-Dist: langchain-core<2.0,>=0.3; extra == "langchain"
|
|
@@ -4,18 +4,18 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentguardproxy"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
description = "Python SDK for AgentGuard — the firewall for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
|
-
# requires-python: 3.
|
|
11
|
+
# requires-python: 3.10 floor.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# `pip install agentguardproxy[
|
|
17
|
-
# 3.
|
|
18
|
-
requires-python = ">=3.
|
|
13
|
+
# 3.8 was dropped in v0.5.0 (upstream EOL October 2024). 3.9 was dropped
|
|
14
|
+
# after upstream EOL (October 2025) — the `mcp` PyPI extra requires >=3.10
|
|
15
|
+
# anyway, so keeping 3.9 in the trove invited a broken
|
|
16
|
+
# `pip install agentguardproxy[mcp]` resolution. The CI matrix exercises
|
|
17
|
+
# 3.10, 3.11, 3.12 on every push.
|
|
18
|
+
requires-python = ">=3.10"
|
|
19
19
|
authors = [
|
|
20
20
|
{ name = "Cauã Ferraz", email = "cauaferrazp@gmail.com" },
|
|
21
21
|
]
|
|
@@ -23,7 +23,6 @@ classifiers = [
|
|
|
23
23
|
"Development Status :: 3 - Alpha",
|
|
24
24
|
"Intended Audience :: Developers",
|
|
25
25
|
"Programming Language :: Python :: 3",
|
|
26
|
-
"Programming Language :: Python :: 3.9",
|
|
27
26
|
"Programming Language :: Python :: 3.10",
|
|
28
27
|
"Programming Language :: Python :: 3.11",
|
|
29
28
|
"Programming Language :: Python :: 3.12",
|
|
@@ -111,11 +111,11 @@ class TestLangChainExtended:
|
|
|
111
111
|
assert body["action"] == "write"
|
|
112
112
|
|
|
113
113
|
def test_attribute_proxy_blocked(self, mock_server):
|
|
114
|
-
"""v0.5.
|
|
114
|
+
"""v0.5.0: arbitrary attribute access is blocked.
|
|
115
115
|
|
|
116
116
|
v0.5.0 used a composition wrapper with a custom ``__getattr__``
|
|
117
117
|
allowlist that raised AttributeError with a "bypass" message.
|
|
118
|
-
v0.5.
|
|
118
|
+
v0.5.0 switches to subclassing ``langchain_core.tools.BaseTool``,
|
|
119
119
|
so unknown attributes raise pydantic's standard "no such field"
|
|
120
120
|
AttributeError instead. The security property is unchanged: the
|
|
121
121
|
wrapper does not proxy arbitrary attributes to the wrapped tool.
|
|
@@ -262,7 +262,7 @@ class TestApprovalPath:
|
|
|
262
262
|
|
|
263
263
|
|
|
264
264
|
# ---------------------------------------------------------------------------
|
|
265
|
-
# Defense contract under the v0.5.
|
|
265
|
+
# Defense contract under the v0.5.0 hybrid pattern.
|
|
266
266
|
#
|
|
267
267
|
# v0.5.0 used a composition wrapper with a strict ``__getattr__`` allowlist
|
|
268
268
|
# — every parent / unknown attribute access raised AttributeError. That
|
|
@@ -270,7 +270,7 @@ class TestApprovalPath:
|
|
|
270
270
|
# BaseTool)`` failed (virtual-subclass registrations are no longer honored
|
|
271
271
|
# by pydantic).
|
|
272
272
|
#
|
|
273
|
-
# v0.5.
|
|
273
|
+
# v0.5.0 switches to a hybrid pattern: GuardedCrewTool **subclasses**
|
|
274
274
|
# CrewAI's ``BaseTool`` so the framework's isinstance check passes, and
|
|
275
275
|
# every dispatch entry point (``run``, ``_run``, ``invoke``, ``ainvoke``,
|
|
276
276
|
# ``__call__``, ``_arun``, ``arun``, ``to_structured_tool``) is explicitly
|
|
@@ -134,7 +134,7 @@ class TestGuardCheck:
|
|
|
134
134
|
def test_check_unreachable_fail_open(self):
|
|
135
135
|
"""fail_mode='allow' turns unreachable into ALLOW.
|
|
136
136
|
|
|
137
|
-
Locks the Phase-5.
|
|
137
|
+
Locks the Phase-5.0 parity-prep contract: the default is still
|
|
138
138
|
'deny' (see test_check_unreachable_fails_closed), and an explicit
|
|
139
139
|
'allow' is required to opt into fail-open — mirroring the TS SDK
|
|
140
140
|
where `failMode: 'allow'` has always been an explicit opt-in.
|
|
@@ -275,8 +275,15 @@ class TestBatch:
|
|
|
275
275
|
|
|
276
276
|
out = gt.batch(["a", "b", "c"])
|
|
277
277
|
|
|
278
|
+
# `out` order matches input order (LangChain's batch preserves
|
|
279
|
+
# input/output alignment) but `calls` records execution order,
|
|
280
|
+
# which is non-deterministic when batch runs concurrently. The
|
|
281
|
+
# original assertion `calls == ["a","b","c"]` passed by luck on
|
|
282
|
+
# Python 3.10-3.12 schedulers but flaked on 3.13. Compare sorted
|
|
283
|
+
# so the assertion captures "all three inputs ran exactly once"
|
|
284
|
+
# without depending on the scheduler.
|
|
278
285
|
assert out == ["echo:a", "echo:b", "echo:c"]
|
|
279
|
-
assert calls == ["a", "b", "c"]
|
|
286
|
+
assert sorted(calls) == ["a", "b", "c"]
|
|
280
287
|
|
|
281
288
|
def test_guardedtool_batch_denies_first(self, mock_server_indexed):
|
|
282
289
|
"""First entry DENY → batch raises with index 0 in the message,
|
|
@@ -331,9 +338,9 @@ class TestBatch:
|
|
|
331
338
|
|
|
332
339
|
|
|
333
340
|
# ---------------------------------------------------------------------------
|
|
334
|
-
# Defense contract under the v0.5.
|
|
341
|
+
# Defense contract under the v0.5.0 hybrid pattern.
|
|
335
342
|
#
|
|
336
|
-
# v0.5.0 used composition + a strict ``__getattr__`` allowlist; v0.5.
|
|
343
|
+
# v0.5.0 used composition + a strict ``__getattr__`` allowlist; v0.5.0
|
|
337
344
|
# subclasses langchain_core.tools.BaseTool so isinstance(thing, Runnable)
|
|
338
345
|
# succeeds at framework boundaries (langgraph 1.0 / langchain_core 1.x)
|
|
339
346
|
# and overrides every dispatch entry to keep gating tight.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentguardproxy-0.5.1 → agentguardproxy-0.5.2}/agentguardproxy.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|