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.
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/CHANGELOG.md +18 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/PKG-INFO +3 -2
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/__init__.py +3 -1
- agentrust_py-0.0.5/agentrust_sdk/adapters/_core.py +372 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/autogen.py +1 -1
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/claude_agents.py +2 -2
- agentrust_py-0.0.5/agentrust_sdk/adapters/langgraph.py +233 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/openai_agents.py +3 -3
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/profiles.py +3 -1
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/client.py +63 -3
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/config.py +1 -1
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/embedded.py +191 -27
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/hooks.py +10 -3
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/models.py +14 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/loader.py +12 -1
- agentrust_py-0.0.5/policy/base.yaml +176 -0
- agentrust_py-0.0.5/policy/default-policy.json +108 -0
- agentrust_py-0.0.5/policy/financial.yaml +105 -0
- agentrust_py-0.0.5/policy/medical.yaml +77 -0
- agentrust_py-0.0.5/policy/profiles/advisory.json +11 -0
- agentrust_py-0.0.5/policy/profiles/balanced.json +11 -0
- agentrust_py-0.0.5/policy/profiles/strict.json +11 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/pyproject.toml +6 -3
- agentrust_py-0.0.5/tests/adapters/__init__.py +0 -0
- agentrust_py-0.0.5/tests/adapters/_graphs.py +41 -0
- agentrust_py-0.0.5/tests/adapters/test_langgraph_baseline.py +29 -0
- agentrust_py-0.0.5/tests/adapters/test_langgraph_failure_modes.py +111 -0
- agentrust_py-0.0.5/tests/adapters/test_langgraph_governed_tools.py +190 -0
- agentrust_py-0.0.5/tests/adapters/test_langgraph_holds_and_chain.py +232 -0
- agentrust_py-0.0.5/tests/adapters/test_langgraph_node.py +179 -0
- agentrust_py-0.0.5/tests/conformance/__init__.py +0 -0
- agentrust_py-0.0.5/tests/conformance/bindings.py +100 -0
- agentrust_py-0.0.5/tests/conformance/test_c1_c10.py +265 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/conftest.py +64 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_callback_url.py +1 -1
- agentrust_py-0.0.5/tests/test_client_adapter_contract.py +125 -0
- agentrust_py-0.0.5/tests/test_embedded_adapter_contract.py +173 -0
- agentrust_py-0.0.5/tests/test_governor.py +408 -0
- agentrust_py-0.0.5/tests/test_policy_packaging.py +36 -0
- agentrust_py-0.0.4/agentrust_sdk/adapters/langgraph.py +0 -115
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/.gitignore +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/LICENSE +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/README.md +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/_evaluations.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/_real_validator.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/crewai.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/adapters/mcp.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/audit/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/audit/logger.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/auth.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/auto.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/autoload.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/engine.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/classifier/system_prompt.md +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/cli.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/cli_certify.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/decorator.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/golden_suite.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/hitl.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/kernel.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/kill_switch.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/governance/modes.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/manifest.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/discord.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/dispatcher.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/notifications/slack.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/backends/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/backends/native.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/backends/opa.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/policy/engine.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/py.typed +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/queue_replay.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/tiers.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/tool_tracking.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/version_negotiation.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/agentrust_sdk/webhooks.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/__init__.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/_cfg.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_adapters.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_async_advanced.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_chaos.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_cli_certify.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_client.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_config.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_decorator.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_embedded.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_golden_suite.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_hooks.py +0 -0
- {agentrust_py-0.0.4 → agentrust_py-0.0.5}/tests/test_manifest.py +0 -0
- {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.
|
|
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<
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
147
|
+
framework: str = "Claude Agents",
|
|
148
148
|
) -> None:
|
|
149
149
|
_check_tier(api_key)
|
|
150
150
|
self._agent_id = agent_id
|