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.
- agentrust/__init__.py +72 -0
- agentrust_py-0.0.3.dist-info/METADATA +193 -0
- agentrust_py-0.0.3.dist-info/RECORD +29 -0
- agentrust_py-0.0.3.dist-info/WHEEL +4 -0
- agentrust_py-0.0.3.dist-info/entry_points.txt +2 -0
- agentrust_py-0.0.3.dist-info/licenses/LICENSE +177 -0
- agentrust_sdk/__init__.py +124 -0
- agentrust_sdk/adapters/__init__.py +1 -0
- agentrust_sdk/adapters/autogen.py +235 -0
- agentrust_sdk/adapters/claude_agents.py +225 -0
- agentrust_sdk/adapters/crewai.py +98 -0
- agentrust_sdk/adapters/langgraph.py +109 -0
- agentrust_sdk/adapters/mcp.py +193 -0
- agentrust_sdk/adapters/openai_agents.py +263 -0
- agentrust_sdk/auth.py +192 -0
- agentrust_sdk/auto.py +397 -0
- agentrust_sdk/autoload.py +95 -0
- agentrust_sdk/cli.py +736 -0
- agentrust_sdk/client.py +790 -0
- agentrust_sdk/config.py +192 -0
- agentrust_sdk/decorator.py +276 -0
- agentrust_sdk/embedded.py +428 -0
- agentrust_sdk/hooks.py +461 -0
- agentrust_sdk/models.py +81 -0
- agentrust_sdk/py.typed +0 -0
- agentrust_sdk/queue_replay.py +204 -0
- agentrust_sdk/tiers.py +180 -0
- agentrust_sdk/version_negotiation.py +290 -0
- agentrust_sdk/webhooks.py +782 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
queue_replay.py — Replay-on-reconnect helper for AgentTrust SDK.
|
|
3
|
+
|
|
4
|
+
After each successful validate() call, maybe_drain_on_success() checks
|
|
5
|
+
whether there are pending records in the local queue DB and, if so,
|
|
6
|
+
drains up to 50 of them in a daemon background thread.
|
|
7
|
+
|
|
8
|
+
A module-level cooldown (_DRAIN_COOLDOWN = 60 s) prevents spawning a
|
|
9
|
+
drain thread more than once per minute regardless of how many successful
|
|
10
|
+
validate() calls arrive in quick succession.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import sqlite3
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Cooldown state
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
_last_drain_attempt: float = 0.0
|
|
28
|
+
_DRAIN_COOLDOWN: float = 60.0
|
|
29
|
+
_drain_lock = threading.Lock()
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Public API
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
_MAX_DRAIN_ITEMS = 50
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def maybe_drain_on_success(
|
|
39
|
+
gateway_url: str,
|
|
40
|
+
headers: dict,
|
|
41
|
+
queue_db: Path,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Call this after each successful validate() response.
|
|
44
|
+
|
|
45
|
+
If the queue DB has pending rows *and* the cooldown has elapsed, a
|
|
46
|
+
daemon thread is spawned to drain up to _MAX_DRAIN_ITEMS records.
|
|
47
|
+
Returns immediately (non-blocking).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
gateway_url: Full base URL of the AgentTrust gateway
|
|
51
|
+
(e.g. ``"https://gateway.agentrust.ai"``).
|
|
52
|
+
headers: HTTP headers to use for replay requests
|
|
53
|
+
(should include ``X-AgentTrust-Token`` etc.).
|
|
54
|
+
queue_db: :class:`pathlib.Path` to the SQLite queue database.
|
|
55
|
+
"""
|
|
56
|
+
global _last_drain_attempt
|
|
57
|
+
|
|
58
|
+
# Fast path — no DB file at all.
|
|
59
|
+
if not queue_db.exists():
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
now = time.monotonic()
|
|
63
|
+
|
|
64
|
+
with _drain_lock:
|
|
65
|
+
if now - _last_drain_attempt < _DRAIN_COOLDOWN:
|
|
66
|
+
return # Still within cooldown window; skip.
|
|
67
|
+
|
|
68
|
+
# Check whether the DB actually has pending rows before committing
|
|
69
|
+
# to spawning a thread.
|
|
70
|
+
pending = _pending_count(queue_db)
|
|
71
|
+
if pending == 0:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
_last_drain_attempt = now # Claim the slot inside the lock.
|
|
75
|
+
|
|
76
|
+
logger.debug(
|
|
77
|
+
"[AgentTrust] replay-on-reconnect: %d pending record(s) found; "
|
|
78
|
+
"spawning background drain (max %d).",
|
|
79
|
+
pending,
|
|
80
|
+
_MAX_DRAIN_ITEMS,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
t = threading.Thread(
|
|
84
|
+
target=_drain_worker,
|
|
85
|
+
args=(gateway_url, headers, queue_db),
|
|
86
|
+
daemon=True,
|
|
87
|
+
name="agentrust-queue-drain",
|
|
88
|
+
)
|
|
89
|
+
t.start()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Internal helpers
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def _pending_count(queue_db: Path) -> int:
|
|
97
|
+
"""Return the number of rows in the queue table, or 0 on any error."""
|
|
98
|
+
try:
|
|
99
|
+
con = sqlite3.connect(str(queue_db))
|
|
100
|
+
try:
|
|
101
|
+
row = con.execute("SELECT COUNT(*) FROM queue").fetchone()
|
|
102
|
+
return int(row[0]) if row else 0
|
|
103
|
+
finally:
|
|
104
|
+
con.close()
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
logger.debug("[AgentTrust] queue_replay: could not count queue rows: %s", exc)
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _drain_worker(
|
|
111
|
+
gateway_url: str,
|
|
112
|
+
headers: dict,
|
|
113
|
+
queue_db: Path,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Background thread: drain up to _MAX_DRAIN_ITEMS from the queue.
|
|
116
|
+
|
|
117
|
+
Imports drain_queue() from client at call-time to avoid circular
|
|
118
|
+
imports at module load. All exceptions are caught and logged.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
from agentrust_sdk.client import drain_queue # local import avoids circular dep
|
|
122
|
+
|
|
123
|
+
sent, failed = _drain_limited(
|
|
124
|
+
gateway_url=gateway_url,
|
|
125
|
+
headers=headers,
|
|
126
|
+
queue_db=queue_db,
|
|
127
|
+
limit=_MAX_DRAIN_ITEMS,
|
|
128
|
+
)
|
|
129
|
+
logger.info(
|
|
130
|
+
"[AgentTrust] replay-on-reconnect complete: sent=%d failed=%d",
|
|
131
|
+
sent,
|
|
132
|
+
failed,
|
|
133
|
+
)
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
logger.error(
|
|
136
|
+
"[AgentTrust] replay-on-reconnect background thread raised: %s",
|
|
137
|
+
exc,
|
|
138
|
+
exc_info=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _drain_limited(
|
|
143
|
+
gateway_url: str,
|
|
144
|
+
headers: dict,
|
|
145
|
+
queue_db: Path,
|
|
146
|
+
limit: int,
|
|
147
|
+
) -> tuple[int, int]:
|
|
148
|
+
"""Replay up to *limit* records from queue_db against gateway_url.
|
|
149
|
+
|
|
150
|
+
This is a self-contained implementation that mirrors the logic in
|
|
151
|
+
``drain_queue()`` but accepts explicit gateway_url / headers and
|
|
152
|
+
caps the number of rows processed so that a very large backlog does
|
|
153
|
+
not block the daemon thread indefinitely.
|
|
154
|
+
"""
|
|
155
|
+
import json
|
|
156
|
+
|
|
157
|
+
import httpx
|
|
158
|
+
|
|
159
|
+
from .config import SDK_CONFIG # for _VALIDATE_PATH equivalent
|
|
160
|
+
|
|
161
|
+
_VALIDATE_PATH = "/v1/runtime/validate"
|
|
162
|
+
_url = gateway_url.rstrip("/")
|
|
163
|
+
|
|
164
|
+
if not queue_db.exists():
|
|
165
|
+
return 0, 0
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
con = sqlite3.connect(str(queue_db))
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
logger.error("[AgentTrust] replay-on-reconnect: cannot open queue DB: %s", exc)
|
|
171
|
+
return 0, 0
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
rows = con.execute(
|
|
175
|
+
"SELECT id, payload FROM queue ORDER BY id LIMIT ?", (limit,)
|
|
176
|
+
).fetchall()
|
|
177
|
+
except Exception as exc:
|
|
178
|
+
logger.error("[AgentTrust] replay-on-reconnect: cannot read queue DB: %s", exc)
|
|
179
|
+
con.close()
|
|
180
|
+
return 0, 0
|
|
181
|
+
|
|
182
|
+
sent = 0
|
|
183
|
+
failed = 0
|
|
184
|
+
timeout = SDK_CONFIG.timeout_sec
|
|
185
|
+
|
|
186
|
+
for row_id, raw in rows:
|
|
187
|
+
try:
|
|
188
|
+
payload = json.loads(raw)
|
|
189
|
+
with httpx.Client(base_url=_url, headers=headers, timeout=timeout) as http:
|
|
190
|
+
resp = http.post(_VALIDATE_PATH, json=payload)
|
|
191
|
+
resp.raise_for_status()
|
|
192
|
+
con.execute("DELETE FROM queue WHERE id = ?", (row_id,))
|
|
193
|
+
con.commit()
|
|
194
|
+
sent += 1
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
logger.warning(
|
|
197
|
+
"[AgentTrust] replay-on-reconnect: record %d failed: %s",
|
|
198
|
+
row_id,
|
|
199
|
+
exc,
|
|
200
|
+
)
|
|
201
|
+
failed += 1
|
|
202
|
+
|
|
203
|
+
con.close()
|
|
204
|
+
return sent, failed
|
agentrust_sdk/tiers.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tier and capability definitions for AgentTrust SDK.
|
|
3
|
+
|
|
4
|
+
Every capability maps to a minimum tier. The SDK reads the tier from the
|
|
5
|
+
API key JWT and silently skips (never blocks) capabilities above the tier.
|
|
6
|
+
|
|
7
|
+
Tier order (ascending): oss → free → developer → team → enterprise
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Tier(str, Enum):
|
|
15
|
+
OSS = "oss"
|
|
16
|
+
FREE = "free"
|
|
17
|
+
DEVELOPER = "developer"
|
|
18
|
+
TEAM = "team"
|
|
19
|
+
ENTERPRISE = "enterprise"
|
|
20
|
+
|
|
21
|
+
def __ge__(self, other: "Tier") -> bool:
|
|
22
|
+
return _TIER_ORDER[self] >= _TIER_ORDER[other]
|
|
23
|
+
|
|
24
|
+
def __gt__(self, other: "Tier") -> bool:
|
|
25
|
+
return _TIER_ORDER[self] > _TIER_ORDER[other]
|
|
26
|
+
|
|
27
|
+
def __le__(self, other: "Tier") -> bool:
|
|
28
|
+
return _TIER_ORDER[self] <= _TIER_ORDER[other]
|
|
29
|
+
|
|
30
|
+
def __lt__(self, other: "Tier") -> bool:
|
|
31
|
+
return _TIER_ORDER[self] < _TIER_ORDER[other]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_TIER_ORDER: dict[Tier, int] = {
|
|
35
|
+
Tier.OSS: 0,
|
|
36
|
+
Tier.FREE: 1,
|
|
37
|
+
Tier.DEVELOPER: 2,
|
|
38
|
+
Tier.TEAM: 3,
|
|
39
|
+
Tier.ENTERPRISE: 4,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Capability(str, Enum):
|
|
44
|
+
# ── Validation ──────────────────────────────────────────────────────────
|
|
45
|
+
SCHEMA_VALIDATION = "schema_validation" # OSS
|
|
46
|
+
EVIDENCE_VALIDATION = "evidence_validation" # Free
|
|
47
|
+
TOOL_TRUST_CHECK = "tool_trust_check" # Free
|
|
48
|
+
CONTRADICTION_DETECTION = "contradiction_detection" # Free
|
|
49
|
+
CONSISTENCY_CHECK = "consistency_check" # Free
|
|
50
|
+
|
|
51
|
+
# ── Confidence & Risk ───────────────────────────────────────────────────
|
|
52
|
+
CONFIDENCE_ENGINE = "confidence_engine" # Developer
|
|
53
|
+
RISK_SCORING = "risk_scoring" # Developer
|
|
54
|
+
HISTORICAL_RELIABILITY = "historical_reliability" # Developer
|
|
55
|
+
LLM_JUDGE_OLLAMA = "llm_judge_ollama" # Enterprise
|
|
56
|
+
LLM_JUDGE_CLAUDE = "llm_judge_claude" # Enterprise
|
|
57
|
+
|
|
58
|
+
# ── Decision & Policy ───────────────────────────────────────────────────
|
|
59
|
+
AUTO_DECISION = "auto_decision" # Free
|
|
60
|
+
BUILTIN_POLICY_PACKS = "builtin_policy_packs" # Developer
|
|
61
|
+
CUSTOM_POLICIES = "custom_policies" # Team
|
|
62
|
+
POLICY_VERSIONING = "policy_versioning" # Team
|
|
63
|
+
POLICY_SYNC = "policy_sync" # Team
|
|
64
|
+
|
|
65
|
+
# ── Audit & Observability ───────────────────────────────────────────────
|
|
66
|
+
LOCAL_AUDIT = "local_audit" # Free
|
|
67
|
+
CENTRAL_AUDIT = "central_audit" # Team
|
|
68
|
+
HASH_CHAIN_AUDIT = "hash_chain_audit" # Enterprise
|
|
69
|
+
ANALYTICS = "analytics" # Team
|
|
70
|
+
SOC2_EXPORT = "soc2_export" # Enterprise
|
|
71
|
+
|
|
72
|
+
# ── Operations & Collaboration ──────────────────────────────────────────
|
|
73
|
+
REVIEW_QUEUE = "review_queue" # Team
|
|
74
|
+
ALERT_ENGINE = "alert_engine" # Team
|
|
75
|
+
WEBHOOKS = "webhooks" # Team
|
|
76
|
+
TEAM_MANAGEMENT = "team_management" # Team
|
|
77
|
+
SSO_SAML = "sso_saml" # Enterprise
|
|
78
|
+
|
|
79
|
+
# ── Multi-Agent & Trust ─────────────────────────────────────────────────
|
|
80
|
+
TRUST_CHAIN = "trust_chain" # Enterprise
|
|
81
|
+
RATE_LIMITER = "rate_limiter" # Enterprise
|
|
82
|
+
|
|
83
|
+
# ── SDK & Integrations ──────────────────────────────────────────────────
|
|
84
|
+
LANGGRAPH_ADAPTER = "langgraph_adapter" # Team
|
|
85
|
+
CREWAI_ADAPTER = "crewai_adapter" # Team
|
|
86
|
+
MCP_ADAPTER = "mcp_adapter" # Developer
|
|
87
|
+
AUTOGEN_ADAPTER = "autogen_adapter" # Team
|
|
88
|
+
CLAUDE_AGENTS_ADAPTER = "claude_agents_adapter" # Team
|
|
89
|
+
OPENAI_AGENTS_ADAPTER = "openai_agents_adapter" # Team
|
|
90
|
+
CLI_FULL = "cli_full" # Developer
|
|
91
|
+
SELF_HOSTED = "self_hosted" # Enterprise
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Maps each capability to the minimum tier required
|
|
95
|
+
CAPABILITY_MIN_TIER: dict[Capability, Tier] = {
|
|
96
|
+
# Validation
|
|
97
|
+
Capability.SCHEMA_VALIDATION: Tier.OSS,
|
|
98
|
+
Capability.EVIDENCE_VALIDATION: Tier.FREE,
|
|
99
|
+
Capability.TOOL_TRUST_CHECK: Tier.FREE,
|
|
100
|
+
Capability.CONTRADICTION_DETECTION: Tier.FREE,
|
|
101
|
+
Capability.CONSISTENCY_CHECK: Tier.FREE,
|
|
102
|
+
|
|
103
|
+
# Confidence & Risk
|
|
104
|
+
Capability.CONFIDENCE_ENGINE: Tier.DEVELOPER,
|
|
105
|
+
Capability.RISK_SCORING: Tier.DEVELOPER,
|
|
106
|
+
Capability.HISTORICAL_RELIABILITY: Tier.DEVELOPER,
|
|
107
|
+
Capability.LLM_JUDGE_OLLAMA: Tier.ENTERPRISE,
|
|
108
|
+
Capability.LLM_JUDGE_CLAUDE: Tier.ENTERPRISE,
|
|
109
|
+
|
|
110
|
+
# Decision & Policy
|
|
111
|
+
Capability.AUTO_DECISION: Tier.FREE,
|
|
112
|
+
Capability.BUILTIN_POLICY_PACKS: Tier.DEVELOPER,
|
|
113
|
+
Capability.CUSTOM_POLICIES: Tier.TEAM,
|
|
114
|
+
Capability.POLICY_VERSIONING: Tier.TEAM,
|
|
115
|
+
Capability.POLICY_SYNC: Tier.TEAM,
|
|
116
|
+
|
|
117
|
+
# Audit
|
|
118
|
+
Capability.LOCAL_AUDIT: Tier.FREE,
|
|
119
|
+
Capability.CENTRAL_AUDIT: Tier.TEAM,
|
|
120
|
+
Capability.HASH_CHAIN_AUDIT: Tier.ENTERPRISE,
|
|
121
|
+
Capability.ANALYTICS: Tier.TEAM,
|
|
122
|
+
Capability.SOC2_EXPORT: Tier.ENTERPRISE,
|
|
123
|
+
|
|
124
|
+
# Operations
|
|
125
|
+
Capability.REVIEW_QUEUE: Tier.TEAM,
|
|
126
|
+
Capability.ALERT_ENGINE: Tier.TEAM,
|
|
127
|
+
Capability.WEBHOOKS: Tier.TEAM,
|
|
128
|
+
Capability.TEAM_MANAGEMENT: Tier.TEAM,
|
|
129
|
+
Capability.SSO_SAML: Tier.ENTERPRISE,
|
|
130
|
+
|
|
131
|
+
# Multi-agent
|
|
132
|
+
Capability.TRUST_CHAIN: Tier.ENTERPRISE,
|
|
133
|
+
Capability.RATE_LIMITER: Tier.ENTERPRISE,
|
|
134
|
+
|
|
135
|
+
# SDK
|
|
136
|
+
Capability.LANGGRAPH_ADAPTER: Tier.TEAM,
|
|
137
|
+
Capability.CREWAI_ADAPTER: Tier.TEAM,
|
|
138
|
+
Capability.MCP_ADAPTER: Tier.DEVELOPER,
|
|
139
|
+
Capability.AUTOGEN_ADAPTER: Tier.TEAM,
|
|
140
|
+
Capability.CLAUDE_AGENTS_ADAPTER: Tier.TEAM,
|
|
141
|
+
Capability.OPENAI_AGENTS_ADAPTER: Tier.TEAM,
|
|
142
|
+
Capability.CLI_FULL: Tier.DEVELOPER,
|
|
143
|
+
Capability.SELF_HOSTED: Tier.ENTERPRISE,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_allowed(capability: Capability, tier: Tier) -> bool:
|
|
148
|
+
"""Return True if the given tier can use this capability."""
|
|
149
|
+
min_tier = CAPABILITY_MIN_TIER.get(capability, Tier.ENTERPRISE)
|
|
150
|
+
return tier >= min_tier
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def allowed_capabilities(tier: Tier) -> list[Capability]:
|
|
154
|
+
"""Return all capabilities available on a given tier."""
|
|
155
|
+
return [cap for cap, min_t in CAPABILITY_MIN_TIER.items() if tier >= min_t]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Human-readable upgrade messages
|
|
159
|
+
UPGRADE_MESSAGES: dict[Capability, str] = {
|
|
160
|
+
Capability.CONFIDENCE_ENGINE: "Confidence scoring requires Developer tier ($29/mo). Upgrade: agentrust upgrade",
|
|
161
|
+
Capability.RISK_SCORING: "Risk scoring requires Developer tier ($29/mo). Upgrade: agentrust upgrade",
|
|
162
|
+
Capability.BUILTIN_POLICY_PACKS: "Policy packs require Developer tier ($29/mo). Upgrade: agentrust upgrade",
|
|
163
|
+
Capability.CUSTOM_POLICIES: "Custom policies require Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
164
|
+
Capability.POLICY_SYNC: "Policy sync requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
165
|
+
Capability.CENTRAL_AUDIT: "Central audit requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
166
|
+
Capability.ANALYTICS: "Analytics require Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
167
|
+
Capability.REVIEW_QUEUE: "Review queue requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
168
|
+
Capability.ALERT_ENGINE: "Alert engine requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
169
|
+
Capability.LANGGRAPH_ADAPTER: "LangGraph adapter requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
170
|
+
Capability.CREWAI_ADAPTER: "CrewAI adapter requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
171
|
+
Capability.MCP_ADAPTER: "MCP adapter requires Developer tier ($29/mo). Upgrade: agentrust upgrade",
|
|
172
|
+
Capability.AUTOGEN_ADAPTER: "AutoGen adapter requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
173
|
+
Capability.CLAUDE_AGENTS_ADAPTER: "Claude Agents adapter requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
174
|
+
Capability.OPENAI_AGENTS_ADAPTER: "OpenAI Agents adapter requires Team tier ($149/mo). Upgrade: agentrust upgrade",
|
|
175
|
+
Capability.TRUST_CHAIN: "Trust chain requires Enterprise tier. Contact: sales@agentrust.io",
|
|
176
|
+
Capability.LLM_JUDGE_CLAUDE: "LLM Judge requires Enterprise tier. Contact: sales@agentrust.io",
|
|
177
|
+
Capability.SSO_SAML: "SSO/SAML requires Enterprise tier. Contact: sales@agentrust.io",
|
|
178
|
+
Capability.SELF_HOSTED: "Self-hosted control plane requires Enterprise tier. Contact: sales@agentrust.io",
|
|
179
|
+
Capability.SOC2_EXPORT: "SOC2 audit export requires Enterprise tier. Contact: sales@agentrust.io",
|
|
180
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentTrust SDK — version negotiation.
|
|
3
|
+
|
|
4
|
+
Handles SDK/gateway version compatibility checks and header-based negotiation.
|
|
5
|
+
|
|
6
|
+
Supported gateway versions for forward-compatibility checks:
|
|
7
|
+
SUPPORTED_GATEWAY_VERSIONS
|
|
8
|
+
|
|
9
|
+
Typical usage
|
|
10
|
+
-------------
|
|
11
|
+
from .version_negotiation import check_response_version
|
|
12
|
+
check_response_version(response.headers, SDK_CONFIG.sdk_version)
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from .config import SDK_CONFIG
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Module-level constants
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
SUPPORTED_GATEWAY_VERSIONS: list[str] = ["0.0.1a1", "0.0.2", "0.1.0"]
|
|
26
|
+
|
|
27
|
+
_HEADER_MIN_SDK = "X-AgentTrust-Min-SDK-Version"
|
|
28
|
+
_HEADER_GATEWAY = "X-AgentTrust-Gateway-Version"
|
|
29
|
+
|
|
30
|
+
# Matches "1.2.3", "0.0.1a1", "1.2.3b4", "0.1.0rc2", etc.
|
|
31
|
+
_SEMVER_RE = re.compile(
|
|
32
|
+
r"^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:[a-zA-Z].*)?$"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Exception
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VersionNegotiationError(Exception):
|
|
42
|
+
"""Raised when SDK and gateway versions are incompatible.
|
|
43
|
+
|
|
44
|
+
Attributes
|
|
45
|
+
----------
|
|
46
|
+
sdk_ver : SDK version string reported by this library.
|
|
47
|
+
gateway_ver : Gateway version string extracted from the response headers
|
|
48
|
+
(may be None if the gateway did not advertise one).
|
|
49
|
+
min_required : Minimum SDK version the gateway requires.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
message: str,
|
|
55
|
+
sdk_ver: str,
|
|
56
|
+
gateway_ver: Optional[str],
|
|
57
|
+
min_required: str,
|
|
58
|
+
) -> None:
|
|
59
|
+
super().__init__(message)
|
|
60
|
+
self.sdk_ver = sdk_ver
|
|
61
|
+
self.gateway_ver = gateway_ver
|
|
62
|
+
self.min_required = min_required
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
65
|
+
return (
|
|
66
|
+
f"VersionNegotiationError({str(self)!r}, "
|
|
67
|
+
f"sdk_ver={self.sdk_ver!r}, "
|
|
68
|
+
f"gateway_ver={self.gateway_ver!r}, "
|
|
69
|
+
f"min_required={self.min_required!r})"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Core helpers
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_semver(v: str) -> tuple[int, int, int]:
|
|
79
|
+
"""Parse a version string into a (major, minor, patch) integer tuple.
|
|
80
|
+
|
|
81
|
+
Pre-release labels (``a1``, ``b2``, ``rc3``, …) and build metadata
|
|
82
|
+
attached to the patch segment are stripped before comparison so that
|
|
83
|
+
``"0.0.1a1"`` and ``"0.0.1"`` compare as equal by numeric precedence.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
v:
|
|
88
|
+
A version string such as ``"1.2.3"``, ``"0.0.1a1"``, or ``"1.0.0rc2"``.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
tuple[int, int, int]
|
|
93
|
+
``(major, minor, patch)`` as integers.
|
|
94
|
+
|
|
95
|
+
Raises
|
|
96
|
+
------
|
|
97
|
+
ValueError
|
|
98
|
+
If *v* cannot be parsed as a semantic version.
|
|
99
|
+
"""
|
|
100
|
+
v = v.strip()
|
|
101
|
+
m = _SEMVER_RE.match(v)
|
|
102
|
+
if not m:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Cannot parse {v!r} as a semantic version — "
|
|
105
|
+
"expected MAJOR.MINOR.PATCH[pre-release]"
|
|
106
|
+
)
|
|
107
|
+
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_compatible(
|
|
111
|
+
sdk_version: str,
|
|
112
|
+
gateway_version: str,
|
|
113
|
+
min_gateway: str,
|
|
114
|
+
) -> tuple[bool, str]:
|
|
115
|
+
"""Check whether *sdk_version* satisfies the gateway's minimum requirement.
|
|
116
|
+
|
|
117
|
+
Compatibility rule (semver precedence, pre-release labels ignored):
|
|
118
|
+
sdk_version >= min_gateway
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
sdk_version:
|
|
123
|
+
The version of this SDK (e.g. ``SDK_CONFIG.sdk_version``).
|
|
124
|
+
gateway_version:
|
|
125
|
+
The version the gateway reported (used only for diagnostic messages).
|
|
126
|
+
min_gateway:
|
|
127
|
+
The minimum SDK version the gateway will accept.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
tuple[bool, str]
|
|
132
|
+
``(True, "")`` when compatible; ``(False, reason)`` otherwise.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
sdk_tuple = parse_semver(sdk_version)
|
|
136
|
+
except ValueError as exc:
|
|
137
|
+
return False, f"Invalid SDK version {sdk_version!r}: {exc}"
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
min_tuple = parse_semver(min_gateway)
|
|
141
|
+
except ValueError as exc:
|
|
142
|
+
return False, f"Invalid minimum gateway version {min_gateway!r}: {exc}"
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
parse_semver(gateway_version)
|
|
146
|
+
except ValueError:
|
|
147
|
+
# Gateway version is informational only; don't fail on bad value.
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
if sdk_tuple >= min_tuple:
|
|
151
|
+
return True, ""
|
|
152
|
+
|
|
153
|
+
return False, (
|
|
154
|
+
f"SDK version {sdk_version} is below the minimum required "
|
|
155
|
+
f"{min_gateway} (gateway {gateway_version}). "
|
|
156
|
+
"Please upgrade the agentrust-sdk package."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Header extraction
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def negotiate_header(response_headers: dict) -> Optional[str]:
|
|
166
|
+
"""Extract ``X-AgentTrust-Min-SDK-Version`` from *response_headers*.
|
|
167
|
+
|
|
168
|
+
Header lookup is case-insensitive to accommodate both raw ``http.client``
|
|
169
|
+
responses (which lowercase headers) and ``requests``/``httpx`` mappings
|
|
170
|
+
(which preserve original casing but expose case-insensitive access only
|
|
171
|
+
via their own classes — here we normalise manually).
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
response_headers:
|
|
176
|
+
A plain dict or any mapping of response header names → values.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
str or None
|
|
181
|
+
The header value if present, ``None`` otherwise.
|
|
182
|
+
"""
|
|
183
|
+
target_lower = _HEADER_MIN_SDK.lower()
|
|
184
|
+
for key, value in response_headers.items():
|
|
185
|
+
if key.lower() == target_lower:
|
|
186
|
+
return str(value).strip()
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _extract_gateway_version(response_headers: dict) -> Optional[str]:
|
|
191
|
+
"""Extract ``X-AgentTrust-Gateway-Version`` for diagnostic use."""
|
|
192
|
+
target_lower = _HEADER_GATEWAY.lower()
|
|
193
|
+
for key, value in response_headers.items():
|
|
194
|
+
if key.lower() == target_lower:
|
|
195
|
+
return str(value).strip()
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Primary public entry point
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def check_response_version(
|
|
205
|
+
resp_headers: dict,
|
|
206
|
+
sdk_version: str,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Validate that the current SDK satisfies the gateway's version requirement.
|
|
209
|
+
|
|
210
|
+
Reads ``X-AgentTrust-Min-SDK-Version`` from *resp_headers*. If the header
|
|
211
|
+
is absent no check is performed (the gateway is treated as compatible).
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
resp_headers:
|
|
216
|
+
Response headers from the gateway (plain dict or mapping).
|
|
217
|
+
sdk_version:
|
|
218
|
+
The version of this SDK to validate against the requirement.
|
|
219
|
+
|
|
220
|
+
Raises
|
|
221
|
+
------
|
|
222
|
+
VersionNegotiationError
|
|
223
|
+
When the SDK version is below the gateway's stated minimum.
|
|
224
|
+
"""
|
|
225
|
+
min_required = negotiate_header(resp_headers)
|
|
226
|
+
if min_required is None:
|
|
227
|
+
# Gateway did not advertise a minimum; assume compatible.
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
gateway_ver = _extract_gateway_version(resp_headers)
|
|
231
|
+
|
|
232
|
+
ok, reason = is_compatible(
|
|
233
|
+
sdk_version=sdk_version,
|
|
234
|
+
gateway_version=gateway_ver or "unknown",
|
|
235
|
+
min_gateway=min_required,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not ok:
|
|
239
|
+
raise VersionNegotiationError(
|
|
240
|
+
reason,
|
|
241
|
+
sdk_ver=sdk_version,
|
|
242
|
+
gateway_ver=gateway_ver,
|
|
243
|
+
min_required=min_required,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Metadata helper
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_version_info() -> dict:
|
|
253
|
+
"""Return a dict with full version metadata for this SDK installation.
|
|
254
|
+
|
|
255
|
+
Keys
|
|
256
|
+
----
|
|
257
|
+
sdk_version Current SDK version string.
|
|
258
|
+
min_gateway_version Minimum gateway version this SDK requires.
|
|
259
|
+
supported_gateway_versions
|
|
260
|
+
List of gateway versions explicitly tested for
|
|
261
|
+
forward-compatibility.
|
|
262
|
+
sdk_semver ``(major, minor, patch)`` tuple for the SDK version.
|
|
263
|
+
min_gateway_semver ``(major, minor, patch)`` tuple for the minimum
|
|
264
|
+
gateway version.
|
|
265
|
+
negotiation_header The HTTP header name used for version negotiation.
|
|
266
|
+
gateway_version_header The HTTP header name used by the gateway to
|
|
267
|
+
advertise its own version.
|
|
268
|
+
"""
|
|
269
|
+
sdk_ver = SDK_CONFIG.sdk_version
|
|
270
|
+
min_gw = SDK_CONFIG.min_gateway_version
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
sdk_semver = parse_semver(sdk_ver)
|
|
274
|
+
except ValueError:
|
|
275
|
+
sdk_semver = None # type: ignore[assignment]
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
min_gw_semver = parse_semver(min_gw)
|
|
279
|
+
except ValueError:
|
|
280
|
+
min_gw_semver = None # type: ignore[assignment]
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"sdk_version": sdk_ver,
|
|
284
|
+
"min_gateway_version": min_gw,
|
|
285
|
+
"supported_gateway_versions": list(SUPPORTED_GATEWAY_VERSIONS),
|
|
286
|
+
"sdk_semver": sdk_semver,
|
|
287
|
+
"min_gateway_semver": min_gw_semver,
|
|
288
|
+
"negotiation_header": _HEADER_MIN_SDK,
|
|
289
|
+
"gateway_version_header": _HEADER_GATEWAY,
|
|
290
|
+
}
|