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.
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/CHANGELOG.md +16 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/PKG-INFO +3 -2
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/__init__.py +1 -1
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/_core.py +10 -6
- agentrust_py-0.0.6/agentrust_sdk/adapters/crewai.py +237 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/config.py +1 -1
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/pyproject.toml +3 -2
- agentrust_py-0.0.6/tests/adapters/_crews.py +29 -0
- agentrust_py-0.0.6/tests/adapters/test_crewai_baseline.py +28 -0
- agentrust_py-0.0.6/tests/adapters/test_crewai_failure_modes.py +107 -0
- agentrust_py-0.0.6/tests/adapters/test_crewai_governed_tools.py +215 -0
- agentrust_py-0.0.6/tests/adapters/test_crewai_holds_and_chain.py +177 -0
- agentrust_py-0.0.6/tests/adapters/test_crewai_quickstart_example.py +116 -0
- agentrust_py-0.0.6/tests/adapters/test_crewai_task_checks.py +170 -0
- agentrust_py-0.0.6/tests/adapters/test_langgraph_quickstart_example.py +133 -0
- agentrust_py-0.0.6/tests/conformance/bindings.py +193 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/conformance/test_c1_c10.py +4 -5
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/conftest.py +31 -0
- agentrust_py-0.0.6/tests/test_adapter_tier_gate.py +46 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_adapters.py +1 -2
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_async_advanced.py +3 -6
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_callback_url.py +1 -1
- agentrust_py-0.0.5/agentrust_sdk/adapters/crewai.py +0 -104
- agentrust_py-0.0.5/tests/conformance/bindings.py +0 -100
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/.gitignore +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/LICENSE +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/README.md +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/_evaluations.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/_real_validator.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/autogen.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/claude_agents.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/langgraph.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/mcp.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/adapters/openai_agents.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/audit/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/audit/logger.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/auth.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/auto.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/autoload.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/engine.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/profiles.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/classifier/system_prompt.md +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/cli.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/cli_certify.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/client.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/decorator.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/embedded.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/golden_suite.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/hitl.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/kernel.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/kill_switch.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/governance/modes.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/hooks.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/manifest.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/models.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/discord.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/dispatcher.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/notifications/slack.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/backends/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/backends/native.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/backends/opa.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/engine.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/policy/loader.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/py.typed +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/queue_replay.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/tiers.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/tool_tracking.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/version_negotiation.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/agentrust_sdk/webhooks.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/base.yaml +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/default-policy.json +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/financial.yaml +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/medical.yaml +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/profiles/advisory.json +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/profiles/balanced.json +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/policy/profiles/strict.json +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/_cfg.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/_graphs.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_baseline.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_failure_modes.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_governed_tools.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_holds_and_chain.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/adapters/test_langgraph_node.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/conformance/__init__.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_chaos.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_cli_certify.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_client.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_client_adapter_contract.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_config.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_decorator.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_embedded.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_embedded_adapter_contract.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_golden_suite.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_governor.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_hooks.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_manifest.py +0 -0
- {agentrust_py-0.0.5 → agentrust_py-0.0.6}/tests/test_policy_packaging.py +0 -0
- {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.
|
|
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<
|
|
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'
|
|
@@ -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
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentrust-py"
|
|
7
|
-
version = "0.0.
|
|
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>=
|
|
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"]
|