agentrust-py 0.0.5__tar.gz → 0.0.6__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 (106) hide show
  1. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/CHANGELOG.md +16 -0
  2. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/PKG-INFO +3 -2
  3. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/__init__.py +1 -1
  4. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/_core.py +10 -6
  5. agentrust_py-0.0.6/agentrust_sdk/adapters/crewai.py +237 -0
  6. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/config.py +1 -1
  7. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/pyproject.toml +3 -2
  8. agentrust_py-0.0.6/tests/adapters/_crews.py +29 -0
  9. agentrust_py-0.0.6/tests/adapters/test_crewai_baseline.py +28 -0
  10. agentrust_py-0.0.6/tests/adapters/test_crewai_failure_modes.py +107 -0
  11. agentrust_py-0.0.6/tests/adapters/test_crewai_governed_tools.py +215 -0
  12. agentrust_py-0.0.6/tests/adapters/test_crewai_holds_and_chain.py +177 -0
  13. agentrust_py-0.0.6/tests/adapters/test_crewai_quickstart_example.py +116 -0
  14. agentrust_py-0.0.6/tests/adapters/test_crewai_task_checks.py +170 -0
  15. agentrust_py-0.0.6/tests/adapters/test_langgraph_quickstart_example.py +133 -0
  16. agentrust_py-0.0.6/tests/conformance/bindings.py +193 -0
  17. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/conformance/test_c1_c10.py +4 -5
  18. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/conftest.py +31 -0
  19. agentrust_py-0.0.6/tests/test_adapter_tier_gate.py +46 -0
  20. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_adapters.py +1 -2
  21. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_async_advanced.py +3 -6
  22. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_callback_url.py +1 -1
  23. agentrust_py-0.0.5/agentrust_sdk/adapters/crewai.py +0 -104
  24. agentrust_py-0.0.5/tests/conformance/bindings.py +0 -100
  25. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/.gitignore +0 -0
  26. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/LICENSE +0 -0
  27. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/README.md +0 -0
  28. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust/__init__.py +0 -0
  29. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/_evaluations.py +0 -0
  30. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/_real_validator.py +0 -0
  31. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/__init__.py +0 -0
  32. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/autogen.py +0 -0
  33. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/claude_agents.py +0 -0
  34. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/langgraph.py +0 -0
  35. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/mcp.py +0 -0
  36. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/openai_agents.py +0 -0
  37. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/audit/__init__.py +0 -0
  38. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/audit/logger.py +0 -0
  39. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/auth.py +0 -0
  40. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/auto.py +0 -0
  41. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/autoload.py +0 -0
  42. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/__init__.py +0 -0
  43. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/engine.py +0 -0
  44. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/profiles.py +0 -0
  45. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/system_prompt.md +0 -0
  46. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/cli.py +0 -0
  47. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/cli_certify.py +0 -0
  48. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/client.py +0 -0
  49. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/decorator.py +0 -0
  50. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/embedded.py +0 -0
  51. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/golden_suite.py +0 -0
  52. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/__init__.py +0 -0
  53. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/hitl.py +0 -0
  54. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/kernel.py +0 -0
  55. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/kill_switch.py +0 -0
  56. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/modes.py +0 -0
  57. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/hooks.py +0 -0
  58. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/manifest.py +0 -0
  59. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/models.py +0 -0
  60. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/__init__.py +0 -0
  61. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/discord.py +0 -0
  62. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/dispatcher.py +0 -0
  63. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/slack.py +0 -0
  64. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/__init__.py +0 -0
  65. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/backends/__init__.py +0 -0
  66. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/backends/native.py +0 -0
  67. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/backends/opa.py +0 -0
  68. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/engine.py +0 -0
  69. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/loader.py +0 -0
  70. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/py.typed +0 -0
  71. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/queue_replay.py +0 -0
  72. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/tiers.py +0 -0
  73. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/tool_tracking.py +0 -0
  74. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/version_negotiation.py +0 -0
  75. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/webhooks.py +0 -0
  76. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/base.yaml +0 -0
  77. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/default-policy.json +0 -0
  78. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/financial.yaml +0 -0
  79. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/medical.yaml +0 -0
  80. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/profiles/advisory.json +0 -0
  81. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/profiles/balanced.json +0 -0
  82. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/profiles/strict.json +0 -0
  83. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/__init__.py +0 -0
  84. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/_cfg.py +0 -0
  85. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/__init__.py +0 -0
  86. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/_graphs.py +0 -0
  87. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_baseline.py +0 -0
  88. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_failure_modes.py +0 -0
  89. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_governed_tools.py +0 -0
  90. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_holds_and_chain.py +0 -0
  91. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_node.py +0 -0
  92. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/conformance/__init__.py +0 -0
  93. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_chaos.py +0 -0
  94. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_cli_certify.py +0 -0
  95. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_client.py +0 -0
  96. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_client_adapter_contract.py +0 -0
  97. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_config.py +0 -0
  98. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_decorator.py +0 -0
  99. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_embedded.py +0 -0
  100. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_embedded_adapter_contract.py +0 -0
  101. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_golden_suite.py +0 -0
  102. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_governor.py +0 -0
  103. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_hooks.py +0 -0
  104. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_manifest.py +0 -0
  105. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_policy_packaging.py +0 -0
  106. {agentrust_py-0.0.5 → agentrust_py-0.0.6}/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.6
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
@@ -25,8 +25,9 @@ Requires-Dist: pydantic<3.0.0,>=2.0.0
25
25
  Requires-Dist: pyyaml<7.0,>=6.0.3
26
26
  Provides-Extra: autoload
27
27
  Provides-Extra: crewai
28
- Requires-Dist: crewai<1.0.0,>=0.80.0; extra == 'crewai'
28
+ Requires-Dist: crewai<2.0,>=1.0; extra == 'crewai'
29
29
  Provides-Extra: dev
30
+ Requires-Dist: crewai<2.0,>=1.0; extra == 'dev'
30
31
  Requires-Dist: fastapi>=0.137.1; extra == 'dev'
31
32
  Requires-Dist: httpx<1.0.0,>=0.27.0; extra == 'dev'
32
33
  Requires-Dist: langgraph<2.0.0,>=0.2.0; extra == 'dev'
@@ -147,4 +147,4 @@ __all__ = [
147
147
  "validate",
148
148
  ]
149
149
 
150
- __version__ = "0.0.5"
150
+ __version__ = "0.0.6"
@@ -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:
@@ -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.6"
51
51
  _MIN_GATEWAY_VERSION = "0.0.1a1"
52
52
 
53
53
 
@@ -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.6"
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,7 @@ 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
44
  openai = ["openai>=2.41.1,<3.0.0"]
45
45
  langchain = ["langchain>=0.2.0,<1.0.0"]
46
46
  # Retry/backoff (recommended for production)
@@ -68,6 +68,7 @@ dev = [
68
68
  "uvicorn>=0.29.0",
69
69
  "httpx>=0.27.0,<1.0.0",
70
70
  "langgraph>=0.2.0,<2.0.0",
71
+ "crewai>=1.0,<2.0",
71
72
  ]
72
73
 
73
74
  [project.scripts]
@@ -0,0 +1,29 @@
1
+ """Small real CrewAI crews shared by the CrewAI binding tests."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+
8
+ def tool_call(name: str, args: dict[str, Any]) -> str:
9
+ """The ReAct reply in which the agent calls a tool."""
10
+ return f"Thought: I should use {name}\nAction: {name}\nAction Input: {json.dumps(args)}"
11
+
12
+
13
+ def final(text: str = "done") -> str:
14
+ return f"Thought: I now know the final answer\nFinal Answer: {text}"
15
+
16
+
17
+ def one_agent_crew(tools, llm, *, guardrail=None, max_iter=25):
18
+ """One agent, one task, the given tools. ``max_iter`` caps the agent's LLM turns."""
19
+ from crewai import Agent, Crew, Task
20
+
21
+ agent = Agent(
22
+ role="Tester", goal="Use the tools", backstory="A test agent.",
23
+ tools=list(tools), llm=llm, allow_delegation=False, verbose=False, max_iter=max_iter,
24
+ )
25
+ task = Task(
26
+ description="Do the work.", expected_output="A short answer.",
27
+ agent=agent, guardrail=guardrail,
28
+ )
29
+ return Crew(agents=[agent], tasks=[task], verbose=False)
@@ -0,0 +1,28 @@
1
+ """Baseline: the harness runs a real CrewAI crew with no governance."""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+
6
+ from tests.adapters._crews import final, one_agent_crew, tool_call
7
+
8
+ pytest.importorskip("crewai")
9
+ pytestmark = pytest.mark.framework
10
+
11
+
12
+ def test_real_crew_runs_a_tool_and_finishes(scripted_crewai_llm):
13
+ from crewai.tools import tool
14
+
15
+ calls: list[float] = []
16
+
17
+ @tool
18
+ def refund(amount: float) -> str:
19
+ """Refund an amount to the customer."""
20
+ calls.append(amount)
21
+ return f"refunded {amount}"
22
+
23
+ llm = scripted_crewai_llm(tool_call("refund", {"amount": 50}), final("all done"))
24
+ output = one_agent_crew([refund], llm).kickoff()
25
+
26
+ assert calls == [50.0]
27
+ assert "all done" in output.raw
28
+ assert any("refunded 50.0" in p for p in llm.prompts)
@@ -0,0 +1,107 @@
1
+ """Kill switch and gateway failure modes through governed CrewAI tools."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ import sqlite3
7
+
8
+ import pytest
9
+
10
+ pytest.importorskip("crewai")
11
+ pytestmark = pytest.mark.framework
12
+
13
+ from crewai.tools import tool
14
+
15
+ from agentrust_sdk.adapters.crewai import governed_tools
16
+ from tests.adapters._crews import final, one_agent_crew, tool_call
17
+ from tests.conftest import _free_port
18
+
19
+ AGENT = "crew-failure-agent"
20
+
21
+
22
+ def _tools(calls):
23
+ @tool
24
+ def lookup(order_id: str) -> str:
25
+ """Look up an order."""
26
+ calls.append(("lookup", order_id))
27
+ return f"order {order_id} shipped"
28
+
29
+ @tool
30
+ def refund(amount: float) -> str:
31
+ """Refund an amount."""
32
+ calls.append(("refund", amount))
33
+ return f"refunded {amount}"
34
+
35
+ return [lookup, refund]
36
+
37
+
38
+ def _run(scripted_crewai_llm, calls):
39
+ llm = scripted_crewai_llm(
40
+ tool_call("lookup", {"order_id": "42"}),
41
+ tool_call("refund", {"amount": 5}),
42
+ final(),
43
+ )
44
+ output = one_agent_crew(governed_tools(_tools(calls), agent_id=AGENT), llm).kickoff()
45
+ return output, llm
46
+
47
+
48
+ @pytest.fixture
49
+ def dead_gateway(monkeypatch):
50
+ from agentrust_sdk.embedded import _make_embedded_jwt
51
+
52
+ monkeypatch.setenv("AGENTRUST_KEY", _make_embedded_jwt("team"))
53
+ monkeypatch.setenv("AGENTRUST_GATEWAY_URL", f"http://127.0.0.1:{_free_port()}")
54
+ monkeypatch.setenv("AGENTRUST_RETRY_ATTEMPTS", "1")
55
+ monkeypatch.setenv("AGENTRUST_TIMEOUT_SEC", "1")
56
+
57
+
58
+ def test_kill_switch_blocks_every_tool(embedded_gw, scripted_crewai_llm):
59
+ from agentrust_sdk.governance.kill_switch import get_kill_switch
60
+
61
+ embedded_gw()
62
+ get_kill_switch().kill_agent(AGENT, "incident 7")
63
+ calls: list = []
64
+
65
+ output, llm = _run(scripted_crewai_llm, calls)
66
+
67
+ assert calls == []
68
+ assert sum("Kill switch" in p for p in llm.prompts) >= 1
69
+ assert "done" in output.raw
70
+
71
+
72
+ def test_closed_mode_gateway_down_runs_no_tool(monkeypatch, dead_gateway, scripted_crewai_llm):
73
+ monkeypatch.setenv("AGENTRUST_FAILURE_MODE", "closed")
74
+ calls: list = []
75
+
76
+ output, llm = _run(scripted_crewai_llm, calls)
77
+
78
+ assert calls == []
79
+ assert any("gateway unavailable" in p for p in llm.prompts)
80
+ assert "done" in output.raw
81
+
82
+
83
+ def test_open_mode_gateway_down_runs_tool_and_warns(
84
+ monkeypatch, dead_gateway, scripted_crewai_llm, caplog,
85
+ ):
86
+ monkeypatch.setenv("AGENTRUST_FAILURE_MODE", "open")
87
+ calls: list = []
88
+
89
+ with caplog.at_level(logging.WARNING):
90
+ _run(scripted_crewai_llm, calls)
91
+
92
+ assert calls == [("lookup", "42"), ("refund", 5.0)]
93
+ assert "fail-open" in caplog.text
94
+
95
+
96
+ def test_queue_mode_gateway_down_buffers_request(
97
+ monkeypatch, dead_gateway, scripted_crewai_llm, tmp_path,
98
+ ):
99
+ monkeypatch.setenv("AGENTRUST_FAILURE_MODE", "queue")
100
+ monkeypatch.setenv("AGENTRUST_QUEUE_DB", str(tmp_path / "q.db"))
101
+ calls: list = []
102
+
103
+ _run(scripted_crewai_llm, calls)
104
+
105
+ assert len(calls) == 2
106
+ rows = sqlite3.connect(tmp_path / "q.db").execute("SELECT payload FROM queue").fetchall()
107
+ assert sorted(json.loads(r[0])["action"] for r in rows) == ["lookup", "refund"]