agentrust-py 0.0.5__tar.gz → 0.0.7__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.
Files changed (110) hide show
  1. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/CHANGELOG.md +16 -0
  2. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/PKG-INFO +5 -2
  3. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/__init__.py +9 -2
  4. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/adapters/_core.py +10 -6
  5. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/adapters/autogen.py +129 -41
  6. agentrust_py-0.0.7/agentrust_sdk/adapters/crewai.py +237 -0
  7. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/config.py +1 -1
  8. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/decorator.py +12 -1
  9. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/manifest.py +1 -1
  10. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/pyproject.toml +5 -2
  11. agentrust_py-0.0.7/tests/adapters/_ag2.py +57 -0
  12. agentrust_py-0.0.7/tests/adapters/_crews.py +29 -0
  13. agentrust_py-0.0.7/tests/adapters/test_ag2_governed_function.py +232 -0
  14. agentrust_py-0.0.7/tests/adapters/test_ag2_reply_hook.py +157 -0
  15. agentrust_py-0.0.7/tests/adapters/test_ag2_research_team_example.py +122 -0
  16. agentrust_py-0.0.7/tests/adapters/test_crewai_baseline.py +28 -0
  17. agentrust_py-0.0.7/tests/adapters/test_crewai_failure_modes.py +107 -0
  18. agentrust_py-0.0.7/tests/adapters/test_crewai_governed_tools.py +215 -0
  19. agentrust_py-0.0.7/tests/adapters/test_crewai_holds_and_chain.py +177 -0
  20. agentrust_py-0.0.7/tests/adapters/test_crewai_quickstart_example.py +116 -0
  21. agentrust_py-0.0.7/tests/adapters/test_crewai_task_checks.py +170 -0
  22. agentrust_py-0.0.7/tests/adapters/test_langgraph_quickstart_example.py +133 -0
  23. agentrust_py-0.0.7/tests/conformance/bindings.py +193 -0
  24. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/conformance/test_c1_c10.py +4 -5
  25. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/conftest.py +31 -0
  26. agentrust_py-0.0.7/tests/test_adapter_tier_gate.py +46 -0
  27. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_adapters.py +1 -2
  28. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_async_advanced.py +3 -6
  29. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_callback_url.py +1 -1
  30. agentrust_py-0.0.5/agentrust_sdk/adapters/crewai.py +0 -104
  31. agentrust_py-0.0.5/tests/conformance/bindings.py +0 -100
  32. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/.gitignore +0 -0
  33. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/LICENSE +0 -0
  34. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/README.md +0 -0
  35. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust/__init__.py +0 -0
  36. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/_evaluations.py +0 -0
  37. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/_real_validator.py +0 -0
  38. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/adapters/__init__.py +0 -0
  39. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/adapters/claude_agents.py +0 -0
  40. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/adapters/langgraph.py +0 -0
  41. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/adapters/mcp.py +0 -0
  42. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/adapters/openai_agents.py +0 -0
  43. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/audit/__init__.py +0 -0
  44. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/audit/logger.py +0 -0
  45. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/auth.py +0 -0
  46. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/auto.py +0 -0
  47. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/autoload.py +0 -0
  48. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/classifier/__init__.py +0 -0
  49. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/classifier/engine.py +0 -0
  50. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/classifier/profiles.py +0 -0
  51. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/classifier/system_prompt.md +0 -0
  52. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/cli.py +0 -0
  53. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/cli_certify.py +0 -0
  54. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/client.py +0 -0
  55. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/embedded.py +0 -0
  56. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/golden_suite.py +0 -0
  57. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/governance/__init__.py +0 -0
  58. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/governance/hitl.py +0 -0
  59. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/governance/kernel.py +0 -0
  60. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/governance/kill_switch.py +0 -0
  61. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/governance/modes.py +0 -0
  62. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/hooks.py +0 -0
  63. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/models.py +0 -0
  64. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/notifications/__init__.py +0 -0
  65. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/notifications/discord.py +0 -0
  66. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/notifications/dispatcher.py +0 -0
  67. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/notifications/slack.py +0 -0
  68. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/policy/__init__.py +0 -0
  69. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/__init__.py +0 -0
  70. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/native.py +0 -0
  71. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/opa.py +0 -0
  72. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/policy/engine.py +0 -0
  73. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/policy/loader.py +0 -0
  74. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/py.typed +0 -0
  75. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/queue_replay.py +0 -0
  76. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/tiers.py +0 -0
  77. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/tool_tracking.py +0 -0
  78. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/version_negotiation.py +0 -0
  79. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/agentrust_sdk/webhooks.py +0 -0
  80. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/policy/base.yaml +0 -0
  81. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/policy/default-policy.json +0 -0
  82. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/policy/financial.yaml +0 -0
  83. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/policy/medical.yaml +0 -0
  84. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/policy/profiles/advisory.json +0 -0
  85. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/policy/profiles/balanced.json +0 -0
  86. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/policy/profiles/strict.json +0 -0
  87. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/__init__.py +0 -0
  88. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/_cfg.py +0 -0
  89. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/adapters/__init__.py +0 -0
  90. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/adapters/_graphs.py +0 -0
  91. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_baseline.py +0 -0
  92. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_failure_modes.py +0 -0
  93. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_governed_tools.py +0 -0
  94. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_holds_and_chain.py +0 -0
  95. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_node.py +0 -0
  96. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/conformance/__init__.py +0 -0
  97. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_chaos.py +0 -0
  98. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_cli_certify.py +0 -0
  99. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_client.py +0 -0
  100. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_client_adapter_contract.py +0 -0
  101. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_config.py +0 -0
  102. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_decorator.py +0 -0
  103. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_embedded.py +0 -0
  104. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_embedded_adapter_contract.py +0 -0
  105. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_golden_suite.py +0 -0
  106. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_governor.py +0 -0
  107. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_hooks.py +0 -0
  108. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_manifest.py +0 -0
  109. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_policy_packaging.py +0 -0
  110. {agentrust_py-0.0.5 → agentrust_py-0.0.7}/tests/test_webhooks.py +0 -0
@@ -7,6 +7,22 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.0.6] - 2026-10-26
11
+
12
+ ### Added
13
+ - **CrewAI tool calls are checked before they run.** `governed_tools(tools, agent_id=...)` (`agentrust_sdk.adapters.crewai`) wraps the tools given to a CrewAI `Agent`: each call is pre-checked (tool trust, kill switch, trust chain), held for a person when the decision is `escalate`, then post-checked. A refused call reaches the agent as the tool's result (`Blocked by AgentTrust: ...`), so the crew finishes. Proven against real CrewAI by the same C1–C10 conformance suite as LangGraph.
14
+ - `guardrail(agent_id=...)` for `Task(guardrail=...)`: a blocked task output returns `(False, "Blocked by AgentTrust: ...")`, so CrewAI asks the agent for a new answer instead of ending the crew.
15
+ - `examples/crewai_quickstart.py`, the calculator crew governed by AgentTrust.
16
+
17
+ ### Changed
18
+ - **`agentrust-py[crewai]` now needs CrewAI 1.x** (`crewai>=1.0,<2.0`). CrewAI 0.x is no longer supported.
19
+ - **`AgentTrustCallback`:** the gateway URL defaults to `AGENTRUST_GATEWAY_URL` instead of a hardcoded `http://localhost:8000`; with `AGENTRUST_FAILURE_MODE=closed` an unreachable gateway now raises `GatewayUnavailableError` instead of passing the output through. Its constructor arguments and its behaviour in open mode are unchanged.
20
+
21
+ ### Fixed
22
+ - **Console agent keys (`at_live_...`) were refused by the SDK's client-side tier check** with `<framework> adapter requires Team tier`, a paying Team customer's included, so a connection needed `AGENTRUST_ENV=development`. Only a JWT key states its tier, so only a JWT key is checked on the client; for any other key the gateway, which knows the owner's plan, decides. Affects `governed_tools` for LangGraph and the CrewAI adapter.
23
+
24
+ ---
25
+
10
26
  ## [0.0.5] - 2026-10-26
11
27
 
12
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentrust-py
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: AgentTrust Edge — Full Governance Platform. Policy Engine + MAD Classifier + HITL + Kill Switch.
5
5
  Project-URL: Homepage, https://agentrust.io
6
6
  Project-URL: Documentation, https://docs.agentrust.io
@@ -23,10 +23,13 @@ Requires-Python: >=3.10
23
23
  Requires-Dist: httpx<1.0.0,>=0.27.0
24
24
  Requires-Dist: pydantic<3.0.0,>=2.0.0
25
25
  Requires-Dist: pyyaml<7.0,>=6.0.3
26
+ Provides-Extra: ag2
27
+ Requires-Dist: ag2<1.0,>=0.9; extra == 'ag2'
26
28
  Provides-Extra: autoload
27
29
  Provides-Extra: crewai
28
- Requires-Dist: crewai<1.0.0,>=0.80.0; extra == 'crewai'
30
+ Requires-Dist: crewai<2.0,>=1.0; extra == 'crewai'
29
31
  Provides-Extra: dev
32
+ Requires-Dist: crewai<2.0,>=1.0; extra == 'dev'
30
33
  Requires-Dist: fastapi>=0.137.1; extra == 'dev'
31
34
  Requires-Dist: httpx<1.0.0,>=0.27.0; extra == 'dev'
32
35
  Requires-Dist: langgraph<2.0.0,>=0.2.0; extra == 'dev'
@@ -38,7 +38,12 @@ from .client import (
38
38
  from .config import SDK_CONFIG
39
39
  from .hooks import auto_instrument, auto_wrap
40
40
  from .adapters.mcp import mcp_tool_guard, MCPServerGuard
41
- from .adapters.autogen import AgentTrustReplyHook, AgentTrustGroupChatMonitor
41
+ from .adapters.autogen import (
42
+ AgentTrustGroupChatMonitor,
43
+ AgentTrustReplyHook,
44
+ governed_function,
45
+ governed_functions,
46
+ )
42
47
  from .adapters.claude_agents import ClaudeAgentGuard
43
48
  from .adapters.openai_agents import agent_guard as openai_agent_guard, OpenAIAgentsGuard, SwarmGuard
44
49
  from .adapters.crewai import AgentTrustCallback
@@ -87,6 +92,8 @@ from .webhooks import (
87
92
  __all__ = [
88
93
  # Primary API
89
94
  "harness",
95
+ "governed_function",
96
+ "governed_functions",
90
97
  # Embedded gateway (zero-dep plug-and-play)
91
98
  "embed_gateway",
92
99
  "EmbeddedGateway",
@@ -147,4 +154,4 @@ __all__ = [
147
154
  "validate",
148
155
  ]
149
156
 
150
- __version__ = "0.0.5"
157
+ __version__ = "0.0.7"
@@ -28,7 +28,7 @@ import logging
28
28
  import os
29
29
  import time
30
30
  from contextvars import ContextVar
31
- from dataclasses import dataclass
31
+ from dataclasses import dataclass, replace
32
32
  from typing import Any, Awaitable, Callable, Literal
33
33
 
34
34
  from .. import client as _client
@@ -71,6 +71,8 @@ class Refused:
71
71
  envelope_id: str | None = None
72
72
  reason_code: str | None = None
73
73
  outcome: str = "block"
74
+ # The gateway answer behind a refused output check, when there is one.
75
+ response: ValidateResponse | None = None
74
76
 
75
77
 
76
78
  def _real_envelope(envelope_id: str | None) -> str | None:
@@ -80,12 +82,14 @@ def _real_envelope(envelope_id: str | None) -> str | None:
80
82
  def check_adapter_tier(capability: Capability, api_key: str | None = None) -> None:
81
83
  """Raise RuntimeError when *api_key*'s tier does not include *capability*.
82
84
 
83
- Skipped in development/demo/test environments, without a key, and for
84
- keys that are not AgentTrust ``at_`` keys (e.g. an embedded gateway token).
85
+ Only a JWT key states its tier, so only a JWT key is checked here. An
86
+ opaque key, such as a console agent key (``at_live_...``), carries no
87
+ tier: the gateway knows the owner's plan and decides. Also skipped in
88
+ development/demo/test environments and without a key.
85
89
  """
86
90
  env = os.environ.get("AGENTRUST_ENV", "production").lower()
87
91
  key = api_key or SDK_CONFIG.api_key
88
- if env in ("development", "dev", "demo", "test") or not key or not key.startswith("at_"):
92
+ if env in ("development", "dev", "demo", "test") or not key or key.count(".") != 2:
89
93
  return
90
94
  from ..auth import resolve_key
91
95
 
@@ -281,10 +285,10 @@ class Governor:
281
285
 
282
286
  def _output_verdict(self, resp: ValidateResponse) -> Allowed | Refused:
283
287
  if resp.decision.outcome == "block":
284
- return self._refused(
288
+ return replace(self._refused(
285
289
  resp.decision.reason, _real_envelope(resp.envelope_id), resp.decision.reason_code,
286
290
  outcome="block", raise_blocked=False,
287
- )
291
+ ), response=resp)
288
292
  return Allowed(resp, _real_envelope(resp.envelope_id))
289
293
 
290
294
  def _hold(self, client: Any, pre: ValidateResponse) -> Refused | None:
@@ -5,6 +5,11 @@ Architecture doc (two-pager): "Connects to … AutoGen agents."
5
5
 
6
6
  Supports both AutoGen v0.2 (pyautogen) and AutoGen v0.4 (autogen-agentchat):
7
7
 
8
+ Pattern 0 — governed_function (AG2 / pyautogen 0.2 tools):
9
+ Wrap each tool before register_function so every call is pre-checked,
10
+ held for approval when needed, run, post-checked and audited. A refusal
11
+ is returned as the tool result, so the chat keeps going.
12
+
8
13
  Pattern 1 — AgentTrustReplyHook (v0.2 + v0.4):
9
14
  Attach to any ConversableAgent / AssistantAgent to validate every reply
10
15
  before it is sent.
@@ -25,19 +30,111 @@ Requires Team tier.
25
30
  """
26
31
  from __future__ import annotations
27
32
 
33
+ import functools
34
+ import inspect
28
35
  import logging
29
36
  import time
30
- from typing import Any, Callable
37
+ from typing import Any, Callable, Sequence
38
+
39
+ from ..tiers import Capability
40
+ from ._core import Allowed, Governor, OnBlock, Refused, check_adapter_tier
31
41
 
32
42
  logger = logging.getLogger(__name__)
33
43
 
44
+ _FRAMEWORK = "ag2"
45
+
46
+
47
+ def governed_function(
48
+ fn: Callable[..., Any],
49
+ *,
50
+ agent_id: str,
51
+ name: str | None = None,
52
+ on_block: OnBlock = "refuse",
53
+ review_timeout_s: float = 300.0,
54
+ user: str = "ag2",
55
+ base_url: str | None = None,
56
+ api_key: str | None = None,
57
+ governor: Governor | None = None,
58
+ ) -> Callable[..., Any]:
59
+ """Wrap an AG2 tool function so AgentTrust governs every call.
60
+
61
+ The result keeps the name, docstring and signature of *fn*, so AG2 builds
62
+ the same tool schema from it. Pass it to ``register_function`` or
63
+ ``register_for_llm``. Async functions stay async.
64
+ """
65
+ check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
66
+ gov = governor or Governor(
67
+ agent_id,
68
+ framework=_FRAMEWORK,
69
+ on_block=on_block,
70
+ review_timeout_s=review_timeout_s,
71
+ user=user,
72
+ base_url=base_url,
73
+ api_key=api_key,
74
+ )
75
+ tool_name = name or fn.__name__
76
+ signature = inspect.signature(fn)
77
+
78
+ def call_args(args: tuple, kwargs: dict) -> dict[str, Any]:
79
+ bound = signature.bind(*args, **kwargs)
80
+ bound.apply_defaults()
81
+ return dict(bound.arguments)
82
+
83
+ def unwrap(outcome: Allowed | Refused) -> Any:
84
+ # The refusal text is the tool result the agent reads next.
85
+ return outcome.message if isinstance(outcome, Refused) else outcome.result
86
+
87
+ if inspect.iscoroutinefunction(fn):
88
+
89
+ @functools.wraps(fn)
90
+ async def governed(*args: Any, **kwargs: Any) -> Any:
91
+ params = call_args(args, kwargs)
92
+ outcome = await gov.aguard_tool(
93
+ tool_name, params, lambda: fn(*args, **kwargs), input=_describe_call(tool_name, params),
94
+ )
95
+ return unwrap(outcome)
96
+
97
+ else:
34
98
 
35
- def _check_tier(api_key: str | None = None) -> None:
36
- from agentrust_sdk.auth import resolve_key
37
- from agentrust_sdk.tiers import Capability, is_allowed
38
- info = resolve_key(api_key)
39
- if not is_allowed(Capability.CREWAI_ADAPTER, info.tier):
40
- raise RuntimeError("[AgentTrust] AutoGen adapter requires Team tier or higher.")
99
+ @functools.wraps(fn)
100
+ def governed(*args: Any, **kwargs: Any) -> Any:
101
+ params = call_args(args, kwargs)
102
+ outcome = gov.guard_tool(
103
+ tool_name, params, lambda: fn(*args, **kwargs), input=_describe_call(tool_name, params),
104
+ )
105
+ return unwrap(outcome)
106
+
107
+ return governed
108
+
109
+
110
+ def governed_functions(
111
+ fns: Sequence[Callable[..., Any]],
112
+ *,
113
+ agent_id: str,
114
+ on_block: OnBlock = "refuse",
115
+ review_timeout_s: float = 300.0,
116
+ user: str = "ag2",
117
+ base_url: str | None = None,
118
+ api_key: str | None = None,
119
+ governor: Governor | None = None,
120
+ ) -> list[Callable[..., Any]]:
121
+ """Wrap each function with :func:`governed_function`, sharing one Governor."""
122
+ check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
123
+ gov = governor or Governor(
124
+ agent_id,
125
+ framework=_FRAMEWORK,
126
+ on_block=on_block,
127
+ review_timeout_s=review_timeout_s,
128
+ user=user,
129
+ base_url=base_url,
130
+ api_key=api_key,
131
+ )
132
+ return [governed_function(fn, agent_id=agent_id, governor=gov) for fn in fns]
133
+
134
+
135
+ def _describe_call(name: str, args: dict[str, Any]) -> str:
136
+ """The proposed call as the pre-check's request input."""
137
+ return f"tool call {name}({', '.join(f'{k}={v!r}' for k, v in args.items())})"
41
138
 
42
139
 
43
140
  def _validate_sync(
@@ -50,7 +147,8 @@ def _validate_sync(
50
147
  framework: str,
51
148
  block_on_block: bool,
52
149
  ) -> None:
53
- from agentrust_sdk.client import AgentTrustClient
150
+ from agentrust_sdk.client import AgentTrustClient, GatewayUnavailableError
151
+ from agentrust_sdk.config import SDK_CONFIG
54
152
  from agentrust_sdk.decorator import _check_decision, BlockedError
55
153
 
56
154
  try:
@@ -63,9 +161,11 @@ def _validate_sync(
63
161
  framework=framework,
64
162
  )
65
163
  _check_decision(resp, block_on_block, False)
66
- except BlockedError:
164
+ except (BlockedError, GatewayUnavailableError):
67
165
  raise
68
166
  except Exception as exc:
167
+ if SDK_CONFIG.failure_mode == "closed":
168
+ raise
69
169
  logger.warning("[AgentTrust] AutoGen validate failed (non-fatal): %s", exc)
70
170
 
71
171
 
@@ -99,7 +199,7 @@ class AgentTrustReplyHook:
99
199
  block_on_block: bool = True,
100
200
  framework: str = "AutoGen",
101
201
  ) -> None:
102
- _check_tier(api_key)
202
+ check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
103
203
  self._agent_id = agent_id
104
204
  self._base_url = base_url
105
205
  self._api_key = api_key
@@ -111,47 +211,35 @@ class AgentTrustReplyHook:
111
211
  # AutoGen v0.2
112
212
  # ------------------------------------------------------------------
113
213
 
114
- def _reply_func(
115
- self,
116
- recipient: Any,
117
- messages: list[dict] | None = None,
118
- sender: Any = None,
119
- config: Any = None,
120
- ) -> tuple[bool, str | None]:
121
- """AutoGen v0.2 reply function — registered via register_reply()."""
122
- if not messages:
123
- return False, None
124
-
125
- last = messages[-1]
126
- content = last.get("content", "") or ""
127
- role = last.get("role", "assistant")
128
-
129
- if role not in ("assistant", "user"):
130
- return False, None
214
+ def _check_outgoing(
215
+ self, sender: Any = None, message: Any = None, recipient: Any = None, silent: bool = False,
216
+ ) -> Any:
217
+ """AG2 ``process_message_before_send`` hook: check the agent's own reply."""
218
+ content = message.get("content") if isinstance(message, dict) else message
219
+ if not isinstance(content, str) or not content:
220
+ return message
131
221
 
132
222
  t0 = time.perf_counter()
133
- output = {"content": content[:4000], "role": role}
134
223
  _validate_sync(
135
- self._agent_id, self._base_url, self._api_key,
136
- self._user, content[:500], output, self._framework, self._block_on_block,
224
+ self._agent_id, self._base_url, self._api_key, self._user,
225
+ content[:500], {"content": content[:4000], "role": "assistant"},
226
+ self._framework, self._block_on_block,
137
227
  )
138
228
  logger.debug(
139
229
  "[AgentTrust] AutoGen reply validated in %.1f ms", (time.perf_counter() - t0) * 1000
140
230
  )
141
- return False, None # False = don't override reply; None = no replacement content
231
+ return message
142
232
 
143
233
  def attach(self, agent: Any) -> None:
144
- """Attach to an AutoGen v0.2 ConversableAgent / AssistantAgent."""
145
- if not hasattr(agent, "register_reply"):
234
+ """Attach to an AG2 / AutoGen v0.2 ConversableAgent.
235
+
236
+ Checks every message the agent sends, before it leaves the agent.
237
+ """
238
+ if not hasattr(agent, "register_hook"):
146
239
  raise TypeError(
147
- f"Expected a ConversableAgent with register_reply(), got {type(agent).__name__}"
240
+ f"Expected a ConversableAgent with register_hook(), got {type(agent).__name__}"
148
241
  )
149
- agent.register_reply(
150
- trigger=[],
151
- reply_func=self._reply_func,
152
- position=0,
153
- name="agentrust_reply_hook",
154
- )
242
+ agent.register_hook("process_message_before_send", self._check_outgoing)
155
243
  logger.info(
156
244
  "[AgentTrust] AutoGen reply hook attached to agent '%s'", getattr(agent, "name", "?")
157
245
  )
@@ -209,7 +297,7 @@ class AgentTrustGroupChatMonitor:
209
297
  block_on_block: bool = False, # Default False — group chats tolerate review
210
298
  framework: str = "AutoGen",
211
299
  ) -> None:
212
- _check_tier(api_key)
300
+ check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
213
301
  self._agent_id = agent_id
214
302
  self._base_url = base_url
215
303
  self._api_key = api_key
@@ -0,0 +1,237 @@
1
+ """
2
+ CrewAI adapter.
3
+
4
+ - ``governed_tools(tools, agent_id=...)`` wraps each tool so every call is
5
+ pre-checked before it runs (tool trust, kill switch, review holds,
6
+ fail-closed) and its result post-checked. A refused call reaches the agent
7
+ as the tool's result text, so the crew keeps running.
8
+ - ``AgentTrustCallback`` validates a task's output after the task finished.
9
+
10
+ Requires Team tier. Gated at construction time.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import inspect
16
+ import logging
17
+ from functools import lru_cache
18
+ from typing import Any, Callable, Sequence
19
+
20
+ from ._core import Allowed, Governor, OnBlock, Refused, check_adapter_tier
21
+ from ..client import GatewayUnavailableError
22
+ from ..tiers import Capability
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _FRAMEWORK = "CrewAI"
27
+
28
+
29
+ def governed_tools(
30
+ tools: Sequence[Any],
31
+ *,
32
+ agent_id: str,
33
+ on_block: OnBlock = "refuse",
34
+ review_timeout_s: float = 300.0,
35
+ user: str = "crewai",
36
+ base_url: str | None = None,
37
+ api_key: str | None = None,
38
+ governor: Governor | None = None,
39
+ ) -> list[Any]:
40
+ """Wrap CrewAI tools so AgentTrust governs every call.
41
+
42
+ Accepts ``BaseTool`` instances (subclasses and the ``@tool`` decorator)
43
+ and plain functions with a docstring. Each wrapped tool keeps its name,
44
+ description and argument schema, so the agent sees the same definition as
45
+ before. Pass the result as the ``Agent``'s ``tools=``.
46
+ """
47
+ check_adapter_tier(Capability.CREWAI_ADAPTER, api_key)
48
+ gov = governor or Governor(
49
+ agent_id,
50
+ framework=_FRAMEWORK,
51
+ on_block=on_block,
52
+ review_timeout_s=review_timeout_s,
53
+ user=user,
54
+ base_url=base_url,
55
+ api_key=api_key,
56
+ )
57
+ from crewai.tools import BaseTool, tool as as_tool
58
+
59
+ wrapper = _governed_tool_class()
60
+ return [
61
+ wrapper.wrap(t if isinstance(t, BaseTool) else as_tool(t), gov)
62
+ for t in tools
63
+ ]
64
+
65
+
66
+ def _describe_call(name: str, args: dict[str, Any]) -> str:
67
+ """The proposed call as the pre-check's request input."""
68
+ return f"tool call {name}({', '.join(f'{k}={v!r}' for k, v in args.items())})"
69
+
70
+
71
+ def _never_cache(*_: Any, **__: Any) -> bool:
72
+ # A cached call skips ``_run``, so it would skip the pre-check too.
73
+ return False
74
+
75
+
76
+ def _runs_async(tool: Any) -> bool:
77
+ """True when the tool has a real ``_arun`` rather than the base stub."""
78
+ func = getattr(tool, "func", None)
79
+ if func is not None:
80
+ return inspect.iscoroutinefunction(func)
81
+ from crewai.tools import BaseTool
82
+
83
+ return type(tool)._arun is not BaseTool._arun
84
+
85
+
86
+ @lru_cache(maxsize=None)
87
+ def _governed_tool_class() -> type:
88
+ """Build GovernedTool on first use, so crewai stays optional."""
89
+ from crewai.tools import BaseTool
90
+
91
+ class GovernedTool(BaseTool):
92
+ """A tool whose every call goes through an AgentTrust Governor."""
93
+
94
+ inner: Any
95
+ governor: Any
96
+
97
+ @classmethod
98
+ def wrap(cls, inner: BaseTool, governor: Governor) -> "GovernedTool":
99
+ return cls(
100
+ name=inner.name,
101
+ description=inner.description,
102
+ args_schema=inner.args_schema,
103
+ result_as_answer=inner.result_as_answer,
104
+ cache_function=_never_cache,
105
+ inner=inner,
106
+ governor=governor,
107
+ )
108
+
109
+ def _run(self, *args: Any, **kwargs: Any) -> Any:
110
+ outcome = self.governor.guard_tool(
111
+ self.name, kwargs,
112
+ lambda: self.inner.run(**kwargs),
113
+ input=_describe_call(self.name, kwargs),
114
+ )
115
+ return self._unwrap(outcome)
116
+
117
+ async def _arun(self, *args: Any, **kwargs: Any) -> Any:
118
+ async def call() -> Any:
119
+ if _runs_async(self.inner):
120
+ return await self.inner.arun(**kwargs)
121
+ return await asyncio.to_thread(lambda: self.inner.run(**kwargs))
122
+
123
+ outcome = await self.governor.aguard_tool(
124
+ self.name, kwargs, call, input=_describe_call(self.name, kwargs),
125
+ )
126
+ return self._unwrap(outcome)
127
+
128
+ @staticmethod
129
+ def _unwrap(outcome: Allowed | Refused) -> Any:
130
+ # The refusal text is the observation the agent reads next.
131
+ return outcome.message if isinstance(outcome, Refused) else outcome.result
132
+
133
+ return GovernedTool
134
+
135
+
136
+ def _task_payload(output: Any) -> dict[str, Any]:
137
+ """A task's output as the dict /validate expects."""
138
+ raw = getattr(output, "raw", output)
139
+ return raw if isinstance(raw, dict) else {"result": str(raw)}
140
+
141
+
142
+ def guardrail(
143
+ *,
144
+ agent_id: str,
145
+ user: str = "crewai",
146
+ base_url: str | None = None,
147
+ api_key: str | None = None,
148
+ governor: Governor | None = None,
149
+ ) -> Callable[[Any], tuple[bool, Any]]:
150
+ """A ``Task(guardrail=...)`` that checks the task's output with AgentTrust.
151
+
152
+ Approved output passes through. A blocked output returns
153
+ ``(False, "Blocked by AgentTrust: ...")``, so CrewAI hands the refusal to
154
+ the agent and asks it to try again. In closed mode a gateway outage raises
155
+ ``GatewayUnavailableError``.
156
+ """
157
+ check_adapter_tier(Capability.CREWAI_ADAPTER, api_key)
158
+ gov = governor or Governor(
159
+ agent_id, framework=_FRAMEWORK, user=user, base_url=base_url, api_key=api_key,
160
+ )
161
+
162
+ # No return annotation: CrewAI checks it at runtime and rejects the string
163
+ # form that ``from __future__ import annotations`` produces.
164
+ def check(output):
165
+ verdict = gov.check_output(input="crewai-task", output=_task_payload(output))
166
+ if isinstance(verdict, Refused):
167
+ return False, verdict.message
168
+ return True, output
169
+
170
+ return check
171
+
172
+
173
+ class AgentTrustCallback:
174
+ """
175
+ CrewAI task callback that validates output via AgentTrust.
176
+
177
+ Requires Team tier ($149/mo). Prefer ``guardrail()`` when a blocked output
178
+ should send the agent back to try again rather than raise.
179
+
180
+ Usage::
181
+
182
+ from crewai import Task
183
+ from agentrust_sdk.adapters.crewai import AgentTrustCallback
184
+
185
+ cb = AgentTrustCallback(agent_id="research-crew")
186
+ task = Task(description="...", callback=cb.on_task_complete)
187
+ """
188
+
189
+ def __init__(
190
+ self,
191
+ agent_id: str,
192
+ *,
193
+ base_url: str | None = None,
194
+ api_key: str | None = None,
195
+ user: str = "crewai",
196
+ block_on_block: bool = True,
197
+ block_on_review: bool = False,
198
+ framework: str = _FRAMEWORK,
199
+ governor: Governor | None = None,
200
+ ) -> None:
201
+ check_adapter_tier(Capability.CREWAI_ADAPTER, api_key)
202
+ self._agent_id = agent_id
203
+ self._block_on_block = block_on_block
204
+ self._block_on_review = block_on_review
205
+ self._governor = governor or Governor(
206
+ agent_id, framework=framework, user=user, base_url=base_url, api_key=api_key,
207
+ )
208
+ self._last_result: Any = None
209
+
210
+ def on_task_complete(self, output: Any) -> Any:
211
+ from agentrust_sdk.decorator import _check_decision
212
+
213
+ try:
214
+ verdict = self._governor.check_output(input="crewai-task", output=_task_payload(output))
215
+ except GatewayUnavailableError:
216
+ raise # closed mode: the caller must stop
217
+ except Exception as exc: # kept from before: an unexpected client error is not fatal
218
+ logger.warning("[AgentTrust] CrewAI callback failed (non-fatal): %s", exc)
219
+ return output
220
+ resp = verdict.response if isinstance(verdict, Refused) else verdict.result
221
+ self._last_result = resp
222
+ _check_decision(resp, self._block_on_block, self._block_on_review)
223
+ return output
224
+
225
+ def task_callback(self, fn: Callable) -> Callable:
226
+ import functools
227
+
228
+ @functools.wraps(fn)
229
+ def wrapper(output: Any) -> Any:
230
+ self.on_task_complete(output)
231
+ return fn(output)
232
+
233
+ return wrapper
234
+
235
+ @property
236
+ def last_result(self) -> Any:
237
+ return self._last_result
@@ -47,7 +47,7 @@ from pathlib import Path
47
47
  logger = logging.getLogger(__name__)
48
48
 
49
49
  _VALID_FAILURE_MODES = frozenset({"open", "closed", "queue"})
50
- _SDK_VERSION = "0.0.5"
50
+ _SDK_VERSION = "0.0.7"
51
51
  _MIN_GATEWAY_VERSION = "0.0.1a1"
52
52
 
53
53
 
@@ -329,6 +329,17 @@ def _check_decision(resp: Any, block_on_block: bool, block_on_review: bool) -> N
329
329
  raise BlockedError(outcome, resp.decision.reason, resp.envelope_id)
330
330
 
331
331
 
332
+ def _ag2_installed() -> bool:
333
+ """AG2 installs the ``autogen`` module under the distribution name ``ag2``."""
334
+ from importlib.metadata import PackageNotFoundError, version
335
+
336
+ try:
337
+ version("ag2")
338
+ except PackageNotFoundError:
339
+ return False
340
+ return True
341
+
342
+
332
343
  def _detect_framework() -> str:
333
344
  """Best-effort framework detection from installed packages."""
334
345
  try:
@@ -341,7 +352,7 @@ def _detect_framework() -> str:
341
352
  ("llama_index", "LlamaIndex"),
342
353
  ]:
343
354
  if importlib.util.find_spec(name):
344
- return label
355
+ return "AG2" if name == "autogen" and _ag2_installed() else label
345
356
  except Exception:
346
357
  pass
347
358
  return "REST"
@@ -50,7 +50,7 @@ class CertifyManifest(BaseModel):
50
50
  display_name: str = ""
51
51
  framework: str = Field(
52
52
  default="rest",
53
- pattern=r"^(langchain|langgraph|crewai|autogen|openai-agents|mcp|rest)$",
53
+ pattern=r"^(langchain|langgraph|crewai|autogen|ag2|openai-agents|mcp|rest)$",
54
54
  )
55
55
  language: str = Field(default="python", pattern=r"^(python|typescript|rest)$")
56
56
  model: ModelDeclaration = Field(default_factory=ModelDeclaration)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentrust-py"
7
- version = "0.0.5"
7
+ version = "0.0.7"
8
8
  description = "AgentTrust Edge — Full Governance Platform. Policy Engine + MAD Classifier + HITL + Kill Switch."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -40,7 +40,9 @@ llm = [
40
40
  ]
41
41
  # Framework adapters (optional, tier-gated)
42
42
  langgraph = ["langgraph>=0.2.0,<2.0.0"]
43
- crewai = ["crewai>=0.80.0,<1.0.0"]
43
+ crewai = ["crewai>=1.0,<2.0"]
44
+ # AG2 0.x keeps the ConversableAgent API (module `autogen`); 1.x is a new API
45
+ ag2 = ["ag2>=0.9,<1.0"]
44
46
  openai = ["openai>=2.41.1,<3.0.0"]
45
47
  langchain = ["langchain>=0.2.0,<1.0.0"]
46
48
  # Retry/backoff (recommended for production)
@@ -68,6 +70,7 @@ dev = [
68
70
  "uvicorn>=0.29.0",
69
71
  "httpx>=0.27.0,<1.0.0",
70
72
  "langgraph>=0.2.0,<2.0.0",
73
+ "crewai>=1.0,<2.0",
71
74
  ]
72
75
 
73
76
  [project.scripts]