agentrust-py 0.0.3__py3-none-any.whl

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.
@@ -0,0 +1,192 @@
1
+ """
2
+ AgentTrust SDK — centralized configuration.
3
+
4
+ Config is resolved lazily on first access so that env-var injection at
5
+ runtime (secrets managers, test fixtures) takes effect. The singleton
6
+ SDK_CONFIG is still the primary entry point; call SDK_CONFIG.reload() after
7
+ mutating os.environ in tests or container entrypoints.
8
+
9
+ Environment variables
10
+ ---------------------
11
+ AGENTRUST_ENABLED "true"/"false" — master kill-switch (default true)
12
+ AGENTRUST_GATEWAY_URL Gateway base URL (default http://localhost:8000)
13
+ AGENTRUST_KEY API key (overrides ~/.agentrust/config.yaml)
14
+ AGENTRUST_TIMEOUT_SEC Per-request HTTP timeout in seconds (default 10)
15
+ AGENTRUST_RETRY_ATTEMPTS Max retry attempts on transient failure (default 3)
16
+ AGENTRUST_RETRY_BACKOFF Initial backoff in seconds for exponential retry (default 0.5)
17
+ AGENTRUST_FAILURE_MODE open | closed | queue (default open)
18
+ open — log and continue on gateway unreachable
19
+ closed — raise BlockedError on gateway unreachable
20
+ queue — buffer locally and replay when gateway returns
21
+ AGENTRUST_QUEUE_DB SQLite path for failure-mode=queue buffer
22
+ (default ~/.agentrust/queue.db)
23
+ AGENTRUST_SDK_VERSION Set automatically; override only in tests
24
+
25
+ Webhook environment variables (Team tier and above)
26
+ ----------------------------------------------------
27
+ AGENTRUST_WEBHOOK_URL Single webhook URL to register on SDK init.
28
+ Supports Discord (https://discord.com/api/webhooks/...)
29
+ and any generic HTTPS endpoint.
30
+ AGENTRUST_WEBHOOK_EVENTS Comma-separated decision filter for the env-var webhook.
31
+ Valid values: all | approve | block | escalate | request_evidence
32
+ Defaults to "all" when AGENTRUST_WEBHOOK_URL is set.
33
+ AGENTRUST_WEBHOOK_DB SQLite path for the webhook registry
34
+ (default ~/.agentrust/webhooks.db)
35
+ AGENTRUST_WEBHOOK_TIMEOUT Per-dispatch HTTP timeout in seconds (default 5)
36
+ AGENTRUST_WEBHOOK_AUTO_DISPATCH
37
+ "true"/"false" — fire webhooks automatically after
38
+ every validate() call when a dispatcher is attached
39
+ to the client (default true)
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import logging
44
+ import os
45
+ from pathlib import Path
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ _VALID_FAILURE_MODES = frozenset({"open", "closed", "queue"})
50
+ _SDK_VERSION = "0.0.1a1"
51
+ _MIN_GATEWAY_VERSION = "0.0.1a1"
52
+
53
+
54
+ def _bool(key: str, default: bool) -> bool:
55
+ val = os.environ.get(key, "").lower()
56
+ if val in ("1", "true", "yes"):
57
+ return True
58
+ if val in ("0", "false", "no"):
59
+ return False
60
+ return default
61
+
62
+
63
+ def _float(key: str, default: float) -> float:
64
+ try:
65
+ return float(os.environ[key])
66
+ except (KeyError, ValueError):
67
+ return default
68
+
69
+
70
+ def _int(key: str, default: int) -> int:
71
+ try:
72
+ return int(os.environ[key])
73
+ except (KeyError, ValueError):
74
+ return default
75
+
76
+
77
+ def _failure_mode(key: str, default: str) -> str:
78
+ val = os.environ.get(key, default).lower()
79
+ if val not in _VALID_FAILURE_MODES:
80
+ logger.warning(
81
+ "[AgentTrust] Invalid %s=%r — must be one of %s. Falling back to 'open'.",
82
+ key, val, sorted(_VALID_FAILURE_MODES),
83
+ )
84
+ return "open"
85
+ return val
86
+
87
+
88
+ class _SDKConfig:
89
+ """
90
+ Lazily-resolved env-var config. All properties are read from os.environ
91
+ on each attribute access so runtime mutations (secrets managers, test
92
+ fixtures) take effect without a process restart.
93
+
94
+ Call reload() to invalidate any cached state (currently a no-op since
95
+ nothing is cached, but provided for forward-compat and test ergonomics).
96
+ """
97
+
98
+ # Immutable constants — never come from env
99
+ sdk_version: str = _SDK_VERSION
100
+ min_gateway_version: str = _MIN_GATEWAY_VERSION
101
+
102
+ @property
103
+ def enabled(self) -> bool:
104
+ return _bool("AGENTRUST_ENABLED", True)
105
+
106
+ @property
107
+ def gateway_url(self) -> str:
108
+ return os.environ.get("AGENTRUST_GATEWAY_URL", "http://localhost:8000")
109
+
110
+ @gateway_url.setter
111
+ def gateway_url(self, value: str) -> None:
112
+ # Allow embed_gateway() to override after the fact.
113
+ os.environ["AGENTRUST_GATEWAY_URL"] = value
114
+
115
+ @property
116
+ def api_key(self) -> str | None:
117
+ return os.environ.get("AGENTRUST_KEY") or None
118
+
119
+ @property
120
+ def timeout_sec(self) -> float:
121
+ return _float("AGENTRUST_TIMEOUT_SEC", 10.0)
122
+
123
+ @property
124
+ def retry_attempts(self) -> int:
125
+ return _int("AGENTRUST_RETRY_ATTEMPTS", 3)
126
+
127
+ @property
128
+ def retry_backoff_sec(self) -> float:
129
+ return _float("AGENTRUST_RETRY_BACKOFF", 0.5)
130
+
131
+ @property
132
+ def failure_mode(self) -> str:
133
+ return _failure_mode("AGENTRUST_FAILURE_MODE", "open")
134
+
135
+ @property
136
+ def queue_db(self) -> Path:
137
+ return Path(
138
+ os.environ.get("AGENTRUST_QUEUE_DB",
139
+ str(Path.home() / ".agentrust" / "queue.db"))
140
+ )
141
+
142
+ # ── Webhook configuration (Team tier and above) ──────────────────────────
143
+
144
+ @property
145
+ def webhook_url(self) -> str | None:
146
+ """Single webhook URL bootstrapped from AGENTRUST_WEBHOOK_URL (optional)."""
147
+ return os.environ.get("AGENTRUST_WEBHOOK_URL") or None
148
+
149
+ @property
150
+ def webhook_events(self) -> list[str]:
151
+ """
152
+ Decision filter for the env-var webhook.
153
+
154
+ Parsed from AGENTRUST_WEBHOOK_EVENTS (comma-separated).
155
+ Defaults to ["all"] when the env var is absent or empty.
156
+ """
157
+ raw = os.environ.get("AGENTRUST_WEBHOOK_EVENTS", "all").strip()
158
+ parts = [e.strip() for e in raw.split(",") if e.strip()]
159
+ return parts if parts else ["all"]
160
+
161
+ @property
162
+ def webhook_db(self) -> Path:
163
+ """SQLite path for the webhook registry (default ~/.agentrust/webhooks.db)."""
164
+ return Path(
165
+ os.environ.get(
166
+ "AGENTRUST_WEBHOOK_DB",
167
+ str(Path.home() / ".agentrust" / "webhooks.db"),
168
+ )
169
+ )
170
+
171
+ @property
172
+ def webhook_timeout(self) -> float:
173
+ """Per-dispatch HTTP timeout in seconds (default 5)."""
174
+ return _float("AGENTRUST_WEBHOOK_TIMEOUT", 5.0)
175
+
176
+ @property
177
+ def webhook_auto_dispatch(self) -> bool:
178
+ """
179
+ Whether the client should fire webhooks automatically after every
180
+ validate() call when a dispatcher is attached (default True).
181
+ """
182
+ return _bool("AGENTRUST_WEBHOOK_AUTO_DISPATCH", True)
183
+
184
+ def is_failure_mode(self, mode: str) -> bool:
185
+ return self.failure_mode == mode
186
+
187
+ def reload(self) -> None:
188
+ """No-op — all properties are already live. Provided for test ergonomics."""
189
+
190
+
191
+ # Module-level singleton — import as `from .config import SDK_CONFIG`
192
+ SDK_CONFIG = _SDKConfig()
@@ -0,0 +1,276 @@
1
+ """
2
+ @harness — the single-line AgentTrust integration for any agent function.
3
+
4
+ Replaces the old @validate decorator. Reads config automatically from
5
+ ~/.agentrust/config.yaml so no arguments are required in most cases.
6
+
7
+ Usage (zero config after `agentrust init`)::
8
+
9
+ from agentrust import harness
10
+
11
+ @harness
12
+ def run_payment(user: str, input: str) -> dict:
13
+ return {"amount": 500, "status": "processed"}
14
+
15
+ Advanced usage::
16
+
17
+ @harness(
18
+ agent_id="payment-agent", # override auto-detected name
19
+ block_on_block=True, # raise BlockedError when blocked
20
+ block_on_review=False,
21
+ raise_on_tier_gate=False, # don't raise when capability above tier
22
+ )
23
+ async def run_async_agent(user: str, input: str) -> dict:
24
+ return {"result": "done"}
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import asyncio
29
+ import functools
30
+ import inspect
31
+ import logging
32
+ import time
33
+ from typing import Any, Callable
34
+
35
+ from .auth import read_config, resolve_key
36
+ from .config import SDK_CONFIG
37
+ from .tiers import Capability, Tier, is_allowed
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # BlockedError
44
+ # ---------------------------------------------------------------------------
45
+
46
+ class BlockedError(RuntimeError):
47
+ """Raised when AgentTrust blocks or escalates an execution."""
48
+
49
+ def __init__(self, outcome: str, reason: str, envelope_id: str) -> None:
50
+ super().__init__(f"AgentTrust [{outcome}]: {reason}")
51
+ self.outcome = outcome
52
+ self.reason = reason
53
+ self.envelope_id = envelope_id
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # @harness — supports both @harness and @harness(...) usage
58
+ # ---------------------------------------------------------------------------
59
+
60
+ def harness(
61
+ fn: Callable | None = None,
62
+ *,
63
+ agent_id: str | None = None,
64
+ user_kwarg: str = "user",
65
+ input_kwarg: str = "input",
66
+ base_url: str | None = None,
67
+ api_key: str | None = None,
68
+ block_on_block: bool = True,
69
+ block_on_review: bool = False,
70
+ raise_on_tier_gate: bool = False,
71
+ framework: str | None = None,
72
+ raise_on_error: bool = False,
73
+ ) -> Any:
74
+ """
75
+ Decorator that validates agent output via AgentTrust after the function returns.
76
+
77
+ Can be used bare (@harness) or with arguments (@harness(...)).
78
+ Config is auto-resolved from ~/.agentrust/config.yaml if not provided.
79
+ """
80
+ def _decorator(f: Callable) -> Callable:
81
+ # ── kill-switch: AGENTRUST_ENABLED=false → return original function unchanged ──
82
+ if not SDK_CONFIG.enabled:
83
+ return f
84
+
85
+ _agent_id = agent_id or f.__name__
86
+ _framework = framework or _detect_framework()
87
+ _base_url = base_url or _resolve_base_url()
88
+ _api_key = api_key
89
+
90
+ if inspect.iscoroutinefunction(f):
91
+ return _async_wrapper(
92
+ f, _agent_id, user_kwarg, input_kwarg,
93
+ _base_url, _api_key, block_on_block, block_on_review,
94
+ raise_on_tier_gate, _framework, raise_on_error,
95
+ )
96
+ return _sync_wrapper(
97
+ f, _agent_id, user_kwarg, input_kwarg,
98
+ _base_url, _api_key, block_on_block, block_on_review,
99
+ raise_on_tier_gate, _framework, raise_on_error,
100
+ )
101
+
102
+ if fn is not None:
103
+ if isinstance(fn, str):
104
+ # Used as @validate("agent-id") — legacy positional agent_id arg
105
+ _positional_id = fn
106
+ def _decorator_with_id(f: Callable) -> Callable:
107
+ _aid = _positional_id
108
+ _framework = framework or _detect_framework()
109
+ _base_url = base_url or _resolve_base_url()
110
+ if inspect.iscoroutinefunction(f):
111
+ return _async_wrapper(
112
+ f, _aid, user_kwarg, input_kwarg,
113
+ _base_url, api_key, block_on_block, block_on_review,
114
+ raise_on_tier_gate, _framework, raise_on_error,
115
+ )
116
+ return _sync_wrapper(
117
+ f, _aid, user_kwarg, input_kwarg,
118
+ _base_url, api_key, block_on_block, block_on_review,
119
+ raise_on_tier_gate, _framework, raise_on_error,
120
+ )
121
+ return _decorator_with_id
122
+ # Used as @harness (no parentheses)
123
+ return _decorator(fn)
124
+
125
+ # Used as @harness(...) (with arguments)
126
+ return _decorator
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Internal sync/async wrappers
131
+ # ---------------------------------------------------------------------------
132
+
133
+ def _sync_wrapper(
134
+ fn: Callable,
135
+ agent_id: str,
136
+ user_kwarg: str,
137
+ input_kwarg: str,
138
+ base_url: str,
139
+ api_key: str | None,
140
+ block_on_block: bool,
141
+ block_on_review: bool,
142
+ raise_on_tier_gate: bool,
143
+ framework: str,
144
+ raise_on_error: bool,
145
+ ) -> Callable:
146
+ from .client import AgentTrustClient
147
+
148
+ @functools.wraps(fn)
149
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
150
+ t0 = time.perf_counter()
151
+ result = fn(*args, **kwargs)
152
+ latency_ms = (time.perf_counter() - t0) * 1000
153
+
154
+ user = kwargs.get(user_kwarg, "unknown")
155
+ inp = kwargs.get(input_kwarg, "")
156
+ output = result if isinstance(result, dict) else {"result": str(result)}
157
+
158
+ try:
159
+ with AgentTrustClient(
160
+ base_url=base_url,
161
+ api_key=api_key,
162
+ raise_on_tier_gate=raise_on_tier_gate,
163
+ ) as client:
164
+ resp = client.validate(
165
+ agent_id=agent_id,
166
+ user=str(user),
167
+ input=str(inp),
168
+ output=output,
169
+ framework=framework,
170
+ latency_ms=latency_ms,
171
+ )
172
+ _check_decision(resp, block_on_block, block_on_review)
173
+ except BlockedError:
174
+ raise
175
+ except Exception as exc:
176
+ if raise_on_error:
177
+ raise
178
+ logger.warning("[AgentTrust] Validation failed (non-fatal): %s", exc)
179
+
180
+ return result
181
+
182
+ return wrapper
183
+
184
+
185
+ def _async_wrapper(
186
+ fn: Callable,
187
+ agent_id: str,
188
+ user_kwarg: str,
189
+ input_kwarg: str,
190
+ base_url: str,
191
+ api_key: str | None,
192
+ block_on_block: bool,
193
+ block_on_review: bool,
194
+ raise_on_tier_gate: bool,
195
+ framework: str,
196
+ raise_on_error: bool,
197
+ ) -> Callable:
198
+ from .client import AsyncAgentTrustClient
199
+
200
+ @functools.wraps(fn)
201
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
202
+ t0 = time.perf_counter()
203
+ result = await fn(*args, **kwargs)
204
+ latency_ms = (time.perf_counter() - t0) * 1000
205
+
206
+ user = kwargs.get(user_kwarg, "unknown")
207
+ inp = kwargs.get(input_kwarg, "")
208
+ output = result if isinstance(result, dict) else {"result": str(result)}
209
+
210
+ try:
211
+ async with AsyncAgentTrustClient(
212
+ base_url=base_url,
213
+ api_key=api_key,
214
+ raise_on_tier_gate=raise_on_tier_gate,
215
+ ) as client:
216
+ resp = await client.validate(
217
+ agent_id=agent_id,
218
+ user=str(user),
219
+ input=str(inp),
220
+ output=output,
221
+ framework=framework,
222
+ latency_ms=latency_ms,
223
+ )
224
+ _check_decision(resp, block_on_block, block_on_review)
225
+ except BlockedError:
226
+ raise
227
+ except Exception as exc:
228
+ if raise_on_error:
229
+ raise
230
+ logger.warning("[AgentTrust] Async validation failed (non-fatal): %s", exc)
231
+
232
+ return result
233
+
234
+ return wrapper
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Helpers
239
+ # ---------------------------------------------------------------------------
240
+
241
+ def _check_decision(resp: Any, block_on_block: bool, block_on_review: bool) -> None:
242
+ outcome = resp.decision.outcome
243
+ if block_on_block and outcome == "block":
244
+ raise BlockedError(outcome, resp.decision.reason, resp.envelope_id)
245
+ if block_on_review and outcome in ("escalate", "request_evidence"):
246
+ raise BlockedError(outcome, resp.decision.reason, resp.envelope_id)
247
+
248
+
249
+ def _detect_framework() -> str:
250
+ """Best-effort framework detection from installed packages."""
251
+ try:
252
+ import importlib
253
+ for name, label in [
254
+ ("langgraph", "LangGraph"),
255
+ ("crewai", "CrewAI"),
256
+ ("langchain", "LangChain"),
257
+ ("autogen", "AutoGen"),
258
+ ("llama_index", "LlamaIndex"),
259
+ ]:
260
+ if importlib.util.find_spec(name):
261
+ return label
262
+ except Exception:
263
+ pass
264
+ return "REST"
265
+
266
+
267
+ def _resolve_base_url() -> str:
268
+ # Env var takes priority, then config file, then default
269
+ if SDK_CONFIG.gateway_url != "http://localhost:8000":
270
+ return SDK_CONFIG.gateway_url
271
+ cfg = read_config("project") or read_config("global")
272
+ return cfg.get("control_plane_url", SDK_CONFIG.gateway_url)
273
+
274
+
275
+ # Keep old name as alias for backward compat
276
+ validate = harness