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