agentrust-py 0.0.4__tar.gz → 0.0.5__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 (96) hide show
  1. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/CHANGELOG.md +18 -0
  2. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/PKG-INFO +3 -2
  3. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/__init__.py +3 -1
  4. agentrust_py-0.0.5/agentrust_sdk/adapters/_core.py +372 -0
  5. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/autogen.py +1 -1
  6. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/claude_agents.py +2 -2
  7. agentrust_py-0.0.5/agentrust_sdk/adapters/langgraph.py +233 -0
  8. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/openai_agents.py +3 -3
  9. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/profiles.py +3 -1
  10. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/client.py +63 -3
  11. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/config.py +1 -1
  12. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/embedded.py +191 -27
  13. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/hooks.py +10 -3
  14. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/models.py +14 -0
  15. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/loader.py +12 -1
  16. agentrust_py-0.0.5/policy/base.yaml +176 -0
  17. agentrust_py-0.0.5/policy/default-policy.json +108 -0
  18. agentrust_py-0.0.5/policy/financial.yaml +105 -0
  19. agentrust_py-0.0.5/policy/medical.yaml +77 -0
  20. agentrust_py-0.0.5/policy/profiles/advisory.json +11 -0
  21. agentrust_py-0.0.5/policy/profiles/balanced.json +11 -0
  22. agentrust_py-0.0.5/policy/profiles/strict.json +11 -0
  23. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/pyproject.toml +6 -3
  24. agentrust_py-0.0.5/tests/adapters/__init__.py +0 -0
  25. agentrust_py-0.0.5/tests/adapters/_graphs.py +41 -0
  26. agentrust_py-0.0.5/tests/adapters/test_langgraph_baseline.py +29 -0
  27. agentrust_py-0.0.5/tests/adapters/test_langgraph_failure_modes.py +111 -0
  28. agentrust_py-0.0.5/tests/adapters/test_langgraph_governed_tools.py +190 -0
  29. agentrust_py-0.0.5/tests/adapters/test_langgraph_holds_and_chain.py +232 -0
  30. agentrust_py-0.0.5/tests/adapters/test_langgraph_node.py +179 -0
  31. agentrust_py-0.0.5/tests/conformance/__init__.py +0 -0
  32. agentrust_py-0.0.5/tests/conformance/bindings.py +100 -0
  33. agentrust_py-0.0.5/tests/conformance/test_c1_c10.py +265 -0
  34. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/conftest.py +64 -0
  35. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_callback_url.py +1 -1
  36. agentrust_py-0.0.5/tests/test_client_adapter_contract.py +125 -0
  37. agentrust_py-0.0.5/tests/test_embedded_adapter_contract.py +173 -0
  38. agentrust_py-0.0.5/tests/test_governor.py +408 -0
  39. agentrust_py-0.0.5/tests/test_policy_packaging.py +36 -0
  40. agentrust_py-0.0.4/agentrust_sdk/adapters/langgraph.py +0 -115
  41. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/.gitignore +0 -0
  42. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/LICENSE +0 -0
  43. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/README.md +0 -0
  44. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust/__init__.py +0 -0
  45. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/_evaluations.py +0 -0
  46. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/_real_validator.py +0 -0
  47. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/__init__.py +0 -0
  48. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/crewai.py +0 -0
  49. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/mcp.py +0 -0
  50. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/audit/__init__.py +0 -0
  51. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/audit/logger.py +0 -0
  52. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/auth.py +0 -0
  53. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/auto.py +0 -0
  54. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/autoload.py +0 -0
  55. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/__init__.py +0 -0
  56. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/engine.py +0 -0
  57. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/system_prompt.md +0 -0
  58. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/cli.py +0 -0
  59. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/cli_certify.py +0 -0
  60. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/decorator.py +0 -0
  61. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/golden_suite.py +0 -0
  62. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/__init__.py +0 -0
  63. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/hitl.py +0 -0
  64. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/kernel.py +0 -0
  65. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/kill_switch.py +0 -0
  66. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/modes.py +0 -0
  67. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/manifest.py +0 -0
  68. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/__init__.py +0 -0
  69. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/discord.py +0 -0
  70. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/dispatcher.py +0 -0
  71. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/slack.py +0 -0
  72. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/__init__.py +0 -0
  73. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/backends/__init__.py +0 -0
  74. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/backends/native.py +0 -0
  75. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/backends/opa.py +0 -0
  76. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/engine.py +0 -0
  77. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/py.typed +0 -0
  78. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/queue_replay.py +0 -0
  79. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/tiers.py +0 -0
  80. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/tool_tracking.py +0 -0
  81. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/version_negotiation.py +0 -0
  82. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/webhooks.py +0 -0
  83. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/__init__.py +0 -0
  84. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/_cfg.py +0 -0
  85. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_adapters.py +0 -0
  86. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_async_advanced.py +0 -0
  87. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_chaos.py +0 -0
  88. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_cli_certify.py +0 -0
  89. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_client.py +0 -0
  90. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_config.py +0 -0
  91. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_decorator.py +0 -0
  92. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_embedded.py +0 -0
  93. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_golden_suite.py +0 -0
  94. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_hooks.py +0 -0
  95. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_manifest.py +0 -0
  96. {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_webhooks.py +0 -0
@@ -7,6 +7,24 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.0.5] - 2026-10-26
11
+
12
+ ### Added
13
+ - `agentrust_sdk.adapters.langgraph.governed_tools` — every LangGraph tool call is pre-checked before it runs, held for human review on `escalate`, and post-checked; refusals become error `ToolMessage`s. See `docs/framework-adapters/langgraph.md`.
14
+ - `agentrust_sdk.adapters._core.Governor` — the shared pre-check / hold / run / post-check core for framework connectors, with `current_envelope` for the trust chain.
15
+ - `AgentTrustClient.get_resolution()` / `AsyncAgentTrustClient.get_resolution()`, `pre_check(parent_envelope_id=...)`, and `ValidateResponse.review_id`.
16
+ - `AgentTrustNode(on_block="route")` and `AgentTrustNode.as_runnable()`.
17
+ - `EmbeddedGateway(manifests=...)` and `register_manifest()` for the pre-check tool-trust gate.
18
+
19
+ ### Changed
20
+ - `AgentTrustNode` defaults its gateway URL to `SDK_CONFIG.gateway_url` and lets `GatewayUnavailableError` propagate in closed mode.
21
+ - The `langgraph` extra now allows LangGraph 1.x (`>=0.2,<2.0`).
22
+
23
+ ### Fixed
24
+ - The SDK wheel now ships its policy files (`base.yaml`, the financial and medical packs, `default-policy.json`, `profiles/`). Before, an installed SDK — including 0.0.4 from PyPI — ran the embedded gateway on fallback rules because the files were left out of the package. The release workflow now fails if a built wheel cannot find them.
25
+
26
+ ---
27
+
10
28
  ## [Unreleased]
11
29
 
12
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentrust-py
3
- Version: 0.0.4
3
+ Version: 0.0.5
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
@@ -29,6 +29,7 @@ Requires-Dist: crewai<1.0.0,>=0.80.0; extra == 'crewai'
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: fastapi>=0.137.1; extra == 'dev'
31
31
  Requires-Dist: httpx<1.0.0,>=0.27.0; extra == 'dev'
32
+ Requires-Dist: langgraph<2.0.0,>=0.2.0; extra == 'dev'
32
33
  Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
33
34
  Requires-Dist: pytest>=9.1.0; extra == 'dev'
34
35
  Requires-Dist: respx>=0.21.0; extra == 'dev'
@@ -47,7 +48,7 @@ Requires-Dist: uvicorn<1.0.0,>=0.29.0; extra == 'full'
47
48
  Provides-Extra: langchain
48
49
  Requires-Dist: langchain<1.0.0,>=0.2.0; extra == 'langchain'
49
50
  Provides-Extra: langgraph
50
- Requires-Dist: langgraph<1.0.0,>=0.2.0; extra == 'langgraph'
51
+ Requires-Dist: langgraph<2.0.0,>=0.2.0; extra == 'langgraph'
51
52
  Provides-Extra: llm
52
53
  Requires-Dist: openai<2.0.0,>=1.0.0; extra == 'llm'
53
54
  Provides-Extra: openai
@@ -41,6 +41,8 @@ from .adapters.mcp import mcp_tool_guard, MCPServerGuard
41
41
  from .adapters.autogen import AgentTrustReplyHook, AgentTrustGroupChatMonitor
42
42
  from .adapters.claude_agents import ClaudeAgentGuard
43
43
  from .adapters.openai_agents import agent_guard as openai_agent_guard, OpenAIAgentsGuard, SwarmGuard
44
+ from .adapters.crewai import AgentTrustCallback
45
+ from .adapters.langgraph import AgentTrustNode
44
46
  from .decorator import BlockedError, harness, validate # validate = alias
45
47
  from .tool_tracking import track_tool, timed_tool
46
48
  from .embedded import EmbeddedGateway, embed_gateway
@@ -145,4 +147,4 @@ __all__ = [
145
147
  "validate",
146
148
  ]
147
149
 
148
- __version__ = "0.0.4"
150
+ __version__ = "0.0.5"
@@ -0,0 +1,372 @@
1
+ """
2
+ Governor — the shared governance core every framework connector runs on.
3
+
4
+ A connector (a "binding") only finds the framework's tool-execution and
5
+ output hooks and translates framework objects. Everything else lives here:
6
+
7
+ guard_tool(name, args, call)
8
+ pre-check ─► approve/retry ─► call() ─► post-check ─► Allowed(result)
9
+ ─► block ─► Refused
10
+ ─► escalate ─► hold for the human review
11
+ approved ─► call() ... else ─► Refused
12
+ ─► gateway down ─► closed: Refused open/queue: call()
13
+
14
+ check_output(input, output) → /validate on a final answer
15
+
16
+ While ``call()`` runs, ``current_envelope`` holds the pre-check envelope id,
17
+ so a governed call made inside it (a tool that runs a sub-agent) sends it as
18
+ ``parent_envelope_id`` and the gateway checks the trust chain.
19
+
20
+ ``on_block="refuse"`` returns a ``Refused`` the binding hands back to the
21
+ agent as a tool result, so the agent can replan. ``on_block="raise"`` raises
22
+ ``BlockedError`` (or ``GatewayUnavailableError`` in closed mode) instead.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ import logging
28
+ import os
29
+ import time
30
+ from contextvars import ContextVar
31
+ from dataclasses import dataclass
32
+ from typing import Any, Awaitable, Callable, Literal
33
+
34
+ from .. import client as _client
35
+ from ..client import GatewayUnavailableError
36
+ from ..config import SDK_CONFIG
37
+ from ..decorator import BlockedError
38
+ from ..models import ValidateResponse
39
+ from ..tiers import UPGRADE_MESSAGES, Capability, is_allowed
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Envelope of the governed call currently running in this context.
44
+ current_envelope: ContextVar[str | None] = ContextVar("agentrust_current_envelope", default=None)
45
+
46
+ # The no-op response a client returns when the SDK is disabled or the gateway
47
+ # is unreachable in open/queue mode. It is not a real ledger row, so it is
48
+ # never used as a parent and never post-checked.
49
+ _NOOP_ENVELOPE = "disabled"
50
+
51
+ _PROCEED = {"approve", "retry"}
52
+ _HOLD = {"escalate", "request_evidence"}
53
+
54
+ OnBlock = Literal["refuse", "raise"]
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class Allowed:
59
+ """The call ran (or the output passed); *result* is what to hand back."""
60
+
61
+ result: Any
62
+ envelope_id: str | None = None
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Refused:
67
+ """The call did not run, or its result is withheld."""
68
+
69
+ message: str
70
+ reason: str
71
+ envelope_id: str | None = None
72
+ reason_code: str | None = None
73
+ outcome: str = "block"
74
+
75
+
76
+ def _real_envelope(envelope_id: str | None) -> str | None:
77
+ return envelope_id if envelope_id and envelope_id != _NOOP_ENVELOPE else None
78
+
79
+
80
+ def check_adapter_tier(capability: Capability, api_key: str | None = None) -> None:
81
+ """Raise RuntimeError when *api_key*'s tier does not include *capability*.
82
+
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
+ """
86
+ env = os.environ.get("AGENTRUST_ENV", "production").lower()
87
+ key = api_key or SDK_CONFIG.api_key
88
+ if env in ("development", "dev", "demo", "test") or not key or not key.startswith("at_"):
89
+ return
90
+ from ..auth import resolve_key
91
+
92
+ if not is_allowed(capability, resolve_key(key).tier):
93
+ msg = UPGRADE_MESSAGES.get(capability, f"{capability.value} is not available on this tier.")
94
+ raise RuntimeError(f"[AgentTrust] {msg}")
95
+
96
+
97
+ class Governor:
98
+ """Pre-check, hold, run and post-check one agent's tool calls."""
99
+
100
+ def __init__(
101
+ self,
102
+ agent_id: str,
103
+ *,
104
+ framework: str,
105
+ on_block: OnBlock = "refuse",
106
+ review_timeout_s: float = 300.0,
107
+ user: str = "unknown",
108
+ base_url: str | None = None,
109
+ api_key: str | None = None,
110
+ poll_initial_s: float = 1.0,
111
+ poll_max_s: float = 5.0,
112
+ client_factory: Callable[[], Any] | None = None,
113
+ async_client_factory: Callable[[], Any] | None = None,
114
+ clock: Callable[[], float] = time.monotonic,
115
+ sleep: Callable[[float], None] = time.sleep,
116
+ asleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
117
+ ) -> None:
118
+ if on_block not in ("refuse", "raise"):
119
+ raise ValueError("on_block must be 'refuse' or 'raise'")
120
+ self.agent_id = agent_id
121
+ self.framework = framework
122
+ self.on_block = on_block
123
+ self.review_timeout_s = review_timeout_s
124
+ self.user = user
125
+ self._client_factory = client_factory or (
126
+ lambda: _client.AgentTrustClient(base_url=base_url, api_key=api_key)
127
+ )
128
+ self._async_client_factory = async_client_factory or (
129
+ lambda: _client.AsyncAgentTrustClient(base_url=base_url, api_key=api_key)
130
+ )
131
+ self._poll_initial_s = poll_initial_s
132
+ self._poll_max_s = poll_max_s
133
+ self._clock = clock
134
+ self._sleep = sleep
135
+ self._asleep = asleep
136
+
137
+ # ------------------------------------------------------------------
138
+ # Tool calls
139
+ # ------------------------------------------------------------------
140
+
141
+ def guard_tool(
142
+ self, name: str, args: dict[str, Any], call: Callable[[], Any], *, input: str = "",
143
+ ) -> Allowed | Refused:
144
+ """Govern one tool call; *call* runs only when the gateway allows it."""
145
+ if not SDK_CONFIG.enabled:
146
+ return Allowed(call())
147
+
148
+ with self._client_factory() as client:
149
+ try:
150
+ pre = client.pre_check(**self._pre_check_kwargs(name, args, input))
151
+ except GatewayUnavailableError as exc:
152
+ return self._unavailable(exc)
153
+
154
+ outcome = pre.decision.outcome
155
+ if outcome in _HOLD:
156
+ held = self._hold(client, pre)
157
+ if held is not None:
158
+ return held
159
+ elif outcome not in _PROCEED:
160
+ return self._refuse_decision(pre)
161
+
162
+ envelope_id = _real_envelope(pre.envelope_id)
163
+ t0 = time.perf_counter()
164
+ token = current_envelope.set(envelope_id) if envelope_id else None
165
+ try:
166
+ result = call()
167
+ finally:
168
+ if token is not None:
169
+ current_envelope.reset(token)
170
+ if envelope_id is None:
171
+ return Allowed(result)
172
+
173
+ try:
174
+ post = client.post_check(**self._post_check_kwargs(envelope_id, name, args, result, t0))
175
+ except GatewayUnavailableError as exc:
176
+ return self._unavailable(exc)
177
+ return self._after_post_check(post, result, envelope_id)
178
+
179
+ async def aguard_tool(
180
+ self, name: str, args: dict[str, Any], call: Callable[[], Awaitable[Any]], *, input: str = "",
181
+ ) -> Allowed | Refused:
182
+ """Async counterpart of guard_tool; *call* returns an awaitable."""
183
+ if not SDK_CONFIG.enabled:
184
+ return Allowed(await call())
185
+
186
+ async with self._async_client_factory() as client:
187
+ try:
188
+ pre = await client.pre_check(**self._pre_check_kwargs(name, args, input))
189
+ except GatewayUnavailableError as exc:
190
+ return self._unavailable(exc)
191
+
192
+ outcome = pre.decision.outcome
193
+ if outcome in _HOLD:
194
+ held = await self._ahold(client, pre)
195
+ if held is not None:
196
+ return held
197
+ elif outcome not in _PROCEED:
198
+ return self._refuse_decision(pre)
199
+
200
+ envelope_id = _real_envelope(pre.envelope_id)
201
+ t0 = time.perf_counter()
202
+ token = current_envelope.set(envelope_id) if envelope_id else None
203
+ try:
204
+ result = await call()
205
+ finally:
206
+ if token is not None:
207
+ current_envelope.reset(token)
208
+ if envelope_id is None:
209
+ return Allowed(result)
210
+
211
+ try:
212
+ post = await client.post_check(
213
+ **self._post_check_kwargs(envelope_id, name, args, result, t0)
214
+ )
215
+ except GatewayUnavailableError as exc:
216
+ return self._unavailable(exc)
217
+ return self._after_post_check(post, result, envelope_id)
218
+
219
+ # ------------------------------------------------------------------
220
+ # Final output
221
+ # ------------------------------------------------------------------
222
+
223
+ def check_output(self, *, input: str, output: dict[str, Any], user: str | None = None) -> Allowed | Refused:
224
+ """Validate a final answer. ``Allowed.result`` is the ValidateResponse.
225
+
226
+ Closed mode lets ``GatewayUnavailableError`` propagate: there is no
227
+ call to refuse, so the caller must stop.
228
+ """
229
+ with self._client_factory() as client:
230
+ resp = client.validate(**self._validate_kwargs(input, output, user))
231
+ return self._output_verdict(resp)
232
+
233
+ async def acheck_output(
234
+ self, *, input: str, output: dict[str, Any], user: str | None = None,
235
+ ) -> Allowed | Refused:
236
+ async with self._async_client_factory() as client:
237
+ resp = await client.validate(**self._validate_kwargs(input, output, user))
238
+ return self._output_verdict(resp)
239
+
240
+ # ------------------------------------------------------------------
241
+ # Internals
242
+ # ------------------------------------------------------------------
243
+
244
+ def _pre_check_kwargs(self, name: str, args: dict[str, Any], input: str) -> dict[str, Any]:
245
+ return {
246
+ "agent_id": self.agent_id,
247
+ "action": name,
248
+ "params": args,
249
+ "user": self.user,
250
+ "input": input,
251
+ "framework": self.framework,
252
+ "parent_envelope_id": current_envelope.get(),
253
+ }
254
+
255
+ @staticmethod
256
+ def _post_check_kwargs(
257
+ envelope_id: str, name: str, args: dict[str, Any], result: Any, t0: float,
258
+ ) -> dict[str, Any]:
259
+ output = result if isinstance(result, dict) else {"result": str(result)}
260
+ return {
261
+ "pre_check_envelope_id": envelope_id,
262
+ "output": output,
263
+ "latency_ms": (time.perf_counter() - t0) * 1000,
264
+ "tools_called": [{"name": name, "arguments": args, "result": result}],
265
+ }
266
+
267
+ def _validate_kwargs(self, input: str, output: dict[str, Any], user: str | None) -> dict[str, Any]:
268
+ return {
269
+ "agent_id": self.agent_id,
270
+ "user": user or self.user,
271
+ "input": input,
272
+ "output": output,
273
+ "framework": self.framework,
274
+ "parent_envelope_id": current_envelope.get(),
275
+ }
276
+
277
+ def _after_post_check(self, post: ValidateResponse, result: Any, envelope_id: str) -> Allowed | Refused:
278
+ if post.decision.outcome == "block":
279
+ return self._refuse_decision(post)
280
+ return Allowed(result, envelope_id)
281
+
282
+ def _output_verdict(self, resp: ValidateResponse) -> Allowed | Refused:
283
+ if resp.decision.outcome == "block":
284
+ return self._refused(
285
+ resp.decision.reason, _real_envelope(resp.envelope_id), resp.decision.reason_code,
286
+ outcome="block", raise_blocked=False,
287
+ )
288
+ return Allowed(resp, _real_envelope(resp.envelope_id))
289
+
290
+ def _hold(self, client: Any, pre: ValidateResponse) -> Refused | None:
291
+ """Wait for the review; None means approved (run the call)."""
292
+ if not pre.review_id:
293
+ return self._refused_no_review(pre)
294
+ deadline = self._clock() + self.review_timeout_s
295
+ interval = self._poll_initial_s
296
+ while True:
297
+ status = client.get_resolution(pre.envelope_id)
298
+ if status.status != "pending":
299
+ return self._resolved(pre, status)
300
+ remaining = deadline - self._clock()
301
+ if remaining <= 0:
302
+ return self._timed_out(pre)
303
+ self._sleep(min(interval, remaining))
304
+ interval = min(interval * 2, self._poll_max_s)
305
+
306
+ async def _ahold(self, client: Any, pre: ValidateResponse) -> Refused | None:
307
+ if not pre.review_id:
308
+ return self._refused_no_review(pre)
309
+ deadline = self._clock() + self.review_timeout_s
310
+ interval = self._poll_initial_s
311
+ while True:
312
+ status = await client.get_resolution(pre.envelope_id)
313
+ if status.status != "pending":
314
+ return self._resolved(pre, status)
315
+ remaining = deadline - self._clock()
316
+ if remaining <= 0:
317
+ return self._timed_out(pre)
318
+ await self._asleep(min(interval, remaining))
319
+ interval = min(interval * 2, self._poll_max_s)
320
+
321
+ def _resolved(self, pre: ValidateResponse, status: Any) -> Refused | None:
322
+ if status.status == "approved":
323
+ logger.info("[AgentTrust] review approved for envelope %s", pre.envelope_id)
324
+ return None
325
+ reason = f"human review {status.status}" + (f": {status.reason}" if status.reason else "")
326
+ return self._refused(reason, pre.envelope_id, f"review_{status.status}", outcome=pre.decision.outcome)
327
+
328
+ def _timed_out(self, pre: ValidateResponse) -> Refused:
329
+ logger.warning(
330
+ "[AgentTrust] review %s for envelope %s not decided within %.0fs; denying",
331
+ pre.review_id, pre.envelope_id, self.review_timeout_s,
332
+ )
333
+ return self._refused(
334
+ f"human review not decided within {self.review_timeout_s:.0f}s",
335
+ pre.envelope_id, "review_timeout", outcome=pre.decision.outcome,
336
+ )
337
+
338
+ def _refused_no_review(self, pre: ValidateResponse) -> Refused:
339
+ return self._refused(
340
+ f"needs human review but no review was opened ({pre.decision.reason})",
341
+ pre.envelope_id, "review_unavailable", outcome=pre.decision.outcome,
342
+ )
343
+
344
+ def _refuse_decision(self, resp: ValidateResponse) -> Refused:
345
+ return self._refused(
346
+ resp.decision.reason, _real_envelope(resp.envelope_id), resp.decision.reason_code,
347
+ outcome=resp.decision.outcome,
348
+ )
349
+
350
+ def _unavailable(self, exc: GatewayUnavailableError) -> Refused:
351
+ if self.on_block == "raise":
352
+ raise exc
353
+ logger.warning("[AgentTrust] gateway unavailable, refusing (fail-closed): %s", exc)
354
+ return self._refused(
355
+ "AgentTrust gateway unavailable and AGENTRUST_FAILURE_MODE=closed",
356
+ None, "gateway_unavailable", raise_blocked=False,
357
+ )
358
+
359
+ def _refused(
360
+ self, reason: str, envelope_id: str | None, reason_code: str | None,
361
+ *, outcome: str = "block", raise_blocked: bool = True,
362
+ ) -> Refused:
363
+ if raise_blocked and self.on_block == "raise":
364
+ raise BlockedError(outcome, reason, envelope_id or "")
365
+ where = f" (envelope {envelope_id})" if envelope_id else ""
366
+ return Refused(
367
+ message=f"Blocked by AgentTrust: {reason}{where}",
368
+ reason=reason,
369
+ envelope_id=envelope_id,
370
+ reason_code=reason_code,
371
+ outcome=outcome,
372
+ )
@@ -207,7 +207,7 @@ class AgentTrustGroupChatMonitor:
207
207
  api_key: str | None = None,
208
208
  user: str = "autogen-group",
209
209
  block_on_block: bool = False, # Default False — group chats tolerate review
210
- framework: str = "AutoGen-GroupChat",
210
+ framework: str = "AutoGen",
211
211
  ) -> None:
212
212
  _check_tier(api_key)
213
213
  self._agent_id = agent_id
@@ -25,7 +25,7 @@ Usage::
25
25
  def run_agent(query: str) -> str:
26
26
  ...
27
27
 
28
- Requires Developer tier.
28
+ Requires Team tier.
29
29
  """
30
30
  from __future__ import annotations
31
31
 
@@ -144,7 +144,7 @@ class ClaudeAgentGuard:
144
144
  api_key: str | None = None,
145
145
  user: str = "claude-agent",
146
146
  block_on_block: bool = True,
147
- framework: str = "Claude-Agents",
147
+ framework: str = "Claude Agents",
148
148
  ) -> None:
149
149
  _check_tier(api_key)
150
150
  self._agent_id = agent_id