agentrust-py 0.0.6__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 (108) hide show
  1. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/PKG-INFO +3 -1
  2. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/__init__.py +9 -2
  3. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/autogen.py +129 -41
  4. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/config.py +1 -1
  5. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/decorator.py +12 -1
  6. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/manifest.py +1 -1
  7. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/pyproject.toml +3 -1
  8. agentrust_py-0.0.7/tests/adapters/_ag2.py +57 -0
  9. agentrust_py-0.0.7/tests/adapters/test_ag2_governed_function.py +232 -0
  10. agentrust_py-0.0.7/tests/adapters/test_ag2_reply_hook.py +157 -0
  11. agentrust_py-0.0.7/tests/adapters/test_ag2_research_team_example.py +122 -0
  12. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_callback_url.py +1 -1
  13. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/.gitignore +0 -0
  14. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/CHANGELOG.md +0 -0
  15. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/LICENSE +0 -0
  16. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/README.md +0 -0
  17. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust/__init__.py +0 -0
  18. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/_evaluations.py +0 -0
  19. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/_real_validator.py +0 -0
  20. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/__init__.py +0 -0
  21. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/_core.py +0 -0
  22. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/claude_agents.py +0 -0
  23. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/crewai.py +0 -0
  24. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/langgraph.py +0 -0
  25. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/mcp.py +0 -0
  26. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/openai_agents.py +0 -0
  27. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/audit/__init__.py +0 -0
  28. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/audit/logger.py +0 -0
  29. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/auth.py +0 -0
  30. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/auto.py +0 -0
  31. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/autoload.py +0 -0
  32. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/__init__.py +0 -0
  33. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/engine.py +0 -0
  34. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/profiles.py +0 -0
  35. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/system_prompt.md +0 -0
  36. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/cli.py +0 -0
  37. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/cli_certify.py +0 -0
  38. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/client.py +0 -0
  39. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/embedded.py +0 -0
  40. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/golden_suite.py +0 -0
  41. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/__init__.py +0 -0
  42. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/hitl.py +0 -0
  43. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/kernel.py +0 -0
  44. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/kill_switch.py +0 -0
  45. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/modes.py +0 -0
  46. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/hooks.py +0 -0
  47. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/models.py +0 -0
  48. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/__init__.py +0 -0
  49. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/discord.py +0 -0
  50. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/dispatcher.py +0 -0
  51. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/slack.py +0 -0
  52. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/__init__.py +0 -0
  53. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/__init__.py +0 -0
  54. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/native.py +0 -0
  55. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/opa.py +0 -0
  56. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/engine.py +0 -0
  57. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/loader.py +0 -0
  58. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/py.typed +0 -0
  59. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/queue_replay.py +0 -0
  60. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/tiers.py +0 -0
  61. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/tool_tracking.py +0 -0
  62. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/version_negotiation.py +0 -0
  63. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/webhooks.py +0 -0
  64. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/base.yaml +0 -0
  65. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/default-policy.json +0 -0
  66. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/financial.yaml +0 -0
  67. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/medical.yaml +0 -0
  68. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/profiles/advisory.json +0 -0
  69. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/profiles/balanced.json +0 -0
  70. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/profiles/strict.json +0 -0
  71. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/__init__.py +0 -0
  72. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/_cfg.py +0 -0
  73. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/__init__.py +0 -0
  74. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/_crews.py +0 -0
  75. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/_graphs.py +0 -0
  76. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_baseline.py +0 -0
  77. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_failure_modes.py +0 -0
  78. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_governed_tools.py +0 -0
  79. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_holds_and_chain.py +0 -0
  80. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_quickstart_example.py +0 -0
  81. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_task_checks.py +0 -0
  82. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_baseline.py +0 -0
  83. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_failure_modes.py +0 -0
  84. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_governed_tools.py +0 -0
  85. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_holds_and_chain.py +0 -0
  86. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_node.py +0 -0
  87. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_quickstart_example.py +0 -0
  88. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conformance/__init__.py +0 -0
  89. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conformance/bindings.py +0 -0
  90. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conformance/test_c1_c10.py +0 -0
  91. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conftest.py +0 -0
  92. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_adapter_tier_gate.py +0 -0
  93. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_adapters.py +0 -0
  94. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_async_advanced.py +0 -0
  95. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_chaos.py +0 -0
  96. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_cli_certify.py +0 -0
  97. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_client.py +0 -0
  98. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_client_adapter_contract.py +0 -0
  99. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_config.py +0 -0
  100. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_decorator.py +0 -0
  101. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_embedded.py +0 -0
  102. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_embedded_adapter_contract.py +0 -0
  103. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_golden_suite.py +0 -0
  104. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_governor.py +0 -0
  105. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_hooks.py +0 -0
  106. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_manifest.py +0 -0
  107. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_policy_packaging.py +0 -0
  108. {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentrust-py
3
- Version: 0.0.6
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,6 +23,8 @@ 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
30
  Requires-Dist: crewai<2.0,>=1.0; extra == 'crewai'
@@ -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.6"
157
+ __version__ = "0.0.7"
@@ -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
@@ -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.6"
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.6"
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" }
@@ -41,6 +41,8 @@ llm = [
41
41
  # Framework adapters (optional, tier-gated)
42
42
  langgraph = ["langgraph>=0.2.0,<2.0.0"]
43
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)
@@ -0,0 +1,57 @@
1
+ """A scripted chat model for real AG2 agents, shared by the AG2 binding tests."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from types import SimpleNamespace as NS
6
+ from typing import Any
7
+
8
+
9
+ def tool_call(name: str, args: dict[str, Any]) -> tuple[str, dict[str, Any]]:
10
+ """The reply in which the agent calls a tool."""
11
+ return name, args
12
+
13
+
14
+ def final(text: str = "done DONE") -> str:
15
+ return text
16
+
17
+
18
+ class ScriptedModel:
19
+ """Stands in for ``OpenAIWrapper.create``: replays *replies* in order."""
20
+
21
+ def __init__(self, *replies):
22
+ self.replies = list(replies)
23
+ self.prompts: list[str] = []
24
+
25
+ def create(self, wrapper, **params):
26
+ self.prompts.append(json.dumps(params.get("messages", []), default=str))
27
+ reply = self.replies.pop(0)
28
+ if isinstance(reply, str):
29
+ message = {"role": "assistant", "content": reply}
30
+ msg = NS(content=reply, tool_calls=None, function_call=None, role="assistant")
31
+ else:
32
+ name, args = reply
33
+ call = {"id": "call-1", "type": "function",
34
+ "function": {"name": name, "arguments": json.dumps(args)}}
35
+ message = {"role": "assistant", "content": None, "tool_calls": [call]}
36
+ msg = NS(content=None, tool_calls=[call], function_call=None, role="assistant")
37
+ msg.model_dump = lambda: message
38
+ return NS(
39
+ choices=[NS(message=msg, finish_reason="stop")], model="scripted", cost=0, id="r",
40
+ usage=NS(prompt_tokens=0, completion_tokens=0, total_tokens=0),
41
+ )
42
+
43
+ @staticmethod
44
+ def extract(response):
45
+ msg = response.choices[0].message
46
+ return [msg.model_dump()] if msg.tool_calls else [msg.content]
47
+
48
+
49
+ def install(monkeypatch, *replies) -> ScriptedModel:
50
+ from autogen.oai.client import OpenAIWrapper
51
+
52
+ model = ScriptedModel(*replies)
53
+ monkeypatch.setattr(OpenAIWrapper, "create", lambda self, **kw: model.create(self, **kw))
54
+ monkeypatch.setattr(
55
+ OpenAIWrapper, "extract_text_or_completion_object", classmethod(lambda cls, r: model.extract(r)),
56
+ )
57
+ return model
@@ -0,0 +1,232 @@
1
+ """governed_function: every AG2 tool call is pre-checked before it runs."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import inspect
6
+ import json
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ pytest.importorskip("autogen")
12
+
13
+ from autogen import ConversableAgent, register_function
14
+
15
+ from agentrust_sdk.adapters.autogen import governed_function, governed_functions
16
+ from tests.test_governor import FakeAsyncClient, FakeClient, _gov, _resp
17
+
18
+ AGENT = "ag2-agent"
19
+
20
+
21
+ def _refund(calls):
22
+ def refund(amount: float, reason: str = "") -> str:
23
+ """Refund an amount to the customer."""
24
+ calls.append(amount)
25
+ return f"refunded {amount}"
26
+
27
+ return refund
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Unit: a real Governor on a scripted client
32
+ # ---------------------------------------------------------------------------
33
+
34
+ def test_wrapper_keeps_name_docstring_and_signature():
35
+ inner = _refund([])
36
+ wrapped = governed_function(inner, agent_id="a", governor=_gov(FakeClient()))
37
+
38
+ assert wrapped.__name__ == "refund"
39
+ assert wrapped.__doc__ == inner.__doc__
40
+ assert inspect.signature(wrapped) == inspect.signature(inner)
41
+
42
+
43
+ def test_allowed_call_is_pre_checked_runs_and_is_post_checked():
44
+ calls, client = [], FakeClient()
45
+ wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
46
+
47
+ assert wrapped(5, reason="late") == "refunded 5"
48
+
49
+ assert calls == [5]
50
+ [pre] = client.pre_calls
51
+ assert (pre["action"], pre["params"]) == ("refund", {"amount": 5, "reason": "late"})
52
+ assert pre["input"] == "tool call refund(amount=5, reason='late')"
53
+ assert len(client.post_calls) == 1
54
+
55
+
56
+ def test_defaults_are_part_of_the_checked_arguments():
57
+ client = FakeClient()
58
+ governed_function(_refund([]), agent_id="a", governor=_gov(client))(amount=1.5)
59
+
60
+ assert client.pre_calls[0]["params"] == {"amount": 1.5, "reason": ""}
61
+
62
+
63
+ def test_blocked_call_does_not_run_and_returns_the_refusal_text():
64
+ calls = []
65
+ client = FakeClient(pre=[_resp("block", reason="over the limit")])
66
+ wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
67
+
68
+ result = wrapped(9999)
69
+
70
+ assert calls == []
71
+ assert "over the limit" in result
72
+ assert client.post_calls == []
73
+
74
+
75
+ def test_held_call_runs_once_a_reviewer_approves():
76
+ calls = []
77
+ client = FakeClient(
78
+ pre=[_resp("escalate", review_id="r1")], resolutions=["pending", "approved"],
79
+ )
80
+ wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
81
+
82
+ assert wrapped(5) == "refunded 5"
83
+ assert calls == [5]
84
+
85
+
86
+ def test_held_call_is_refused_when_the_review_times_out():
87
+ calls = []
88
+ client = FakeClient(pre=[_resp("escalate", review_id="r1")])
89
+ wrapped = governed_function(
90
+ _refund(calls), agent_id="a", governor=_gov(client, review_timeout_s=10),
91
+ )
92
+
93
+ result = wrapped(5)
94
+
95
+ assert calls == []
96
+ assert isinstance(result, str) and result
97
+
98
+
99
+ def test_async_function_stays_async_and_is_governed():
100
+ calls, client = [], FakeAsyncClient()
101
+
102
+ async def lookup(order_id: str) -> str:
103
+ """Look up an order."""
104
+ calls.append(order_id)
105
+ return f"order {order_id}"
106
+
107
+ wrapped = governed_function(lookup, agent_id="a", governor=_gov(client))
108
+
109
+ assert inspect.iscoroutinefunction(wrapped)
110
+ assert asyncio.run(wrapped("42")) == "order 42"
111
+ assert calls == ["42"] and len(client.pre_calls) == 1
112
+
113
+
114
+ def test_blocked_async_call_does_not_run():
115
+ calls = []
116
+
117
+ async def lookup(order_id: str) -> str:
118
+ """Look up an order."""
119
+ calls.append(order_id)
120
+ return "x"
121
+
122
+ client = FakeAsyncClient(pre=[_resp("block", reason="nope")])
123
+ wrapped = governed_function(lookup, agent_id="a", governor=_gov(client))
124
+
125
+ assert "nope" in asyncio.run(wrapped("42"))
126
+ assert calls == []
127
+
128
+
129
+ def test_function_without_arguments():
130
+ client = FakeClient()
131
+
132
+ def ping() -> str:
133
+ """Ping."""
134
+ return "pong"
135
+
136
+ assert governed_function(ping, agent_id="a", governor=_gov(client))() == "pong"
137
+ assert client.pre_calls[0]["params"] == {}
138
+
139
+
140
+ def test_exception_from_the_tool_propagates_and_skips_the_post_check():
141
+ client = FakeClient()
142
+
143
+ def boom() -> str:
144
+ """Fail."""
145
+ raise ValueError("bad")
146
+
147
+ with pytest.raises(ValueError):
148
+ governed_function(boom, agent_id="a", governor=_gov(client))()
149
+ assert client.post_calls == []
150
+
151
+
152
+ def test_kill_switch_off_runs_the_function_plainly(monkeypatch):
153
+ monkeypatch.setenv("AGENTRUST_ENABLED", "false")
154
+ calls, client = [], FakeClient()
155
+ wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
156
+
157
+ assert wrapped(5) == "refunded 5"
158
+ assert client.pre_calls == []
159
+
160
+
161
+ def test_governed_functions_share_one_governor():
162
+ client = FakeClient()
163
+ a, b = governed_functions([_refund([]), _refund([])], agent_id="a", governor=_gov(client))
164
+
165
+ assert a.__name__ == b.__name__ == "refund"
166
+
167
+
168
+ def test_gateway_down_in_closed_mode_refuses(monkeypatch):
169
+ from agentrust_sdk.client import GatewayUnavailableError
170
+
171
+ calls = []
172
+ client = FakeClient(pre_error=GatewayUnavailableError("down"))
173
+ wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
174
+
175
+ assert "gateway unavailable" in wrapped(5)
176
+ assert calls == []
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Real AG2: the tool schema is unchanged and registration works
181
+ # ---------------------------------------------------------------------------
182
+
183
+ def _schema(fn):
184
+ from autogen import LLMConfig
185
+
186
+ cfg = LLMConfig({"model": "m", "api_key": "k", "api_type": "openai"})
187
+ caller = ConversableAgent("caller", llm_config=cfg)
188
+ executor = ConversableAgent("executor", llm_config=False)
189
+ register_function(fn, caller=caller, executor=executor, name=fn.__name__, description="d")
190
+ return caller.llm_config.tools if hasattr(caller.llm_config, "tools") else caller.llm_config["tools"]
191
+
192
+
193
+ def test_ag2_builds_the_same_tool_schema_from_the_wrapper():
194
+ inner = _refund([])
195
+ wrapped = governed_function(inner, agent_id="a", governor=_gov(FakeClient()))
196
+
197
+ assert _schema(wrapped) == _schema(inner)
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Embedded gateway: what lands in the ledger
202
+ # ---------------------------------------------------------------------------
203
+
204
+ def _ledger(gw):
205
+ rows = httpx.get(f"{gw.url}/v1/audit/executions", params={"limit": 100},
206
+ headers={"Authorization": f"Bearer {gw._token}"}).json()["items"]
207
+ return sorted(rows, key=lambda r: r["id"])
208
+
209
+
210
+ def test_declared_tool_leaves_pre_and_post_check_rows(embedded_gw):
211
+ gw = embedded_gw(manifests={AGENT: {"declared_tools": [{"name": "refund"}]}})
212
+ calls = []
213
+
214
+ result = governed_function(_refund(calls), agent_id=AGENT)(5)
215
+
216
+ assert result == "refunded 5" and calls == [5]
217
+ assert [(r["phase"], r["decision"]) for r in _ledger(gw)] == [
218
+ ("pre_check", "approve"), ("post_check", "approve"),
219
+ ]
220
+
221
+
222
+ def test_undeclared_tool_is_refused_before_it_runs(embedded_gw):
223
+ gw = embedded_gw(manifests={AGENT: {"declared_tools": [{"name": "calculator"}]}})
224
+ calls = []
225
+
226
+ result = governed_function(_refund(calls), agent_id=AGENT)(5)
227
+
228
+ assert calls == []
229
+ assert "is not in this agent's declared_tools" in result
230
+ first = _ledger(gw)[0]
231
+ assert (first["phase"], first["decision"]) == ("pre_check", "block")
232
+ assert json.loads(first["request_json"])["metadata"]["action"] == "refund"