agentbadge 0.1.0__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.
- agentbadge/__init__.py +55 -0
- agentbadge/auth.py +29 -0
- agentbadge/client.py +296 -0
- agentbadge/types.py +101 -0
- agentbadge/verify.py +20 -0
- agentbadge-0.1.0.dist-info/METADATA +39 -0
- agentbadge-0.1.0.dist-info/RECORD +8 -0
- agentbadge-0.1.0.dist-info/WHEEL +4 -0
agentbadge/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Agent Badge Python SDK."""
|
|
2
|
+
|
|
3
|
+
from agentbadge_core.agent_card import AgentCard, badge_fragment, patch_badge
|
|
4
|
+
from agentbadge_core.protocols import (
|
|
5
|
+
SUPPORTED_PROTOCOLS,
|
|
6
|
+
badge_headers,
|
|
7
|
+
find_badge,
|
|
8
|
+
placement_table,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from agentbadge.auth import agent_auth_headers
|
|
12
|
+
from agentbadge.client import AgentBadgeClient
|
|
13
|
+
from agentbadge.types import (
|
|
14
|
+
AckResponse,
|
|
15
|
+
AgentBadgeAPIError,
|
|
16
|
+
AgentMessage,
|
|
17
|
+
BadgeDescriptor,
|
|
18
|
+
BadgeFeatures,
|
|
19
|
+
BadgeMetadata,
|
|
20
|
+
ChallengeInfo,
|
|
21
|
+
EnrollResponse,
|
|
22
|
+
MessagesResponse,
|
|
23
|
+
SendMessageResponse,
|
|
24
|
+
SubscribeResponse,
|
|
25
|
+
VerificationResponse,
|
|
26
|
+
)
|
|
27
|
+
from agentbadge.verify import verify_agent_card, verify_badge
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"SUPPORTED_PROTOCOLS",
|
|
33
|
+
"AckResponse",
|
|
34
|
+
"AgentBadgeAPIError",
|
|
35
|
+
"AgentBadgeClient",
|
|
36
|
+
"AgentCard",
|
|
37
|
+
"AgentMessage",
|
|
38
|
+
"BadgeDescriptor",
|
|
39
|
+
"BadgeFeatures",
|
|
40
|
+
"BadgeMetadata",
|
|
41
|
+
"ChallengeInfo",
|
|
42
|
+
"EnrollResponse",
|
|
43
|
+
"MessagesResponse",
|
|
44
|
+
"SendMessageResponse",
|
|
45
|
+
"SubscribeResponse",
|
|
46
|
+
"VerificationResponse",
|
|
47
|
+
"agent_auth_headers",
|
|
48
|
+
"badge_fragment",
|
|
49
|
+
"badge_headers",
|
|
50
|
+
"find_badge",
|
|
51
|
+
"patch_badge",
|
|
52
|
+
"placement_table",
|
|
53
|
+
"verify_agent_card",
|
|
54
|
+
"verify_badge",
|
|
55
|
+
]
|
agentbadge/auth.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Agent-side authentication for the messaging endpoints.
|
|
2
|
+
|
|
3
|
+
The agent proves control of its card key by signing
|
|
4
|
+
"<badgeId>:<timestamp>:<nonce>" with the Ed25519 key its AgentCard exposes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import secrets
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from agentbadge_core.crypto import Ed25519KeyPair
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def agent_auth_headers(
|
|
15
|
+
badge_id: str,
|
|
16
|
+
keypair: Ed25519KeyPair,
|
|
17
|
+
timestamp: int | None = None,
|
|
18
|
+
nonce: str | None = None,
|
|
19
|
+
) -> dict[str, str]:
|
|
20
|
+
"""The X-Agent-Signature / X-Agent-Timestamp / X-Agent-Nonce headers for one
|
|
21
|
+
request. Each call uses a fresh nonce — headers must not be reused."""
|
|
22
|
+
ts = timestamp if timestamp is not None else int(time.time())
|
|
23
|
+
n = nonce if nonce is not None else secrets.token_hex(16)
|
|
24
|
+
signature = keypair.sign(f"{badge_id}:{ts}:{n}".encode("ascii"))
|
|
25
|
+
return {
|
|
26
|
+
"X-Agent-Signature": base64.b64encode(signature).decode("ascii"),
|
|
27
|
+
"X-Agent-Timestamp": str(ts),
|
|
28
|
+
"X-Agent-Nonce": n,
|
|
29
|
+
}
|
agentbadge/client.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Async HTTP client for the Agent Badge API."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from agentbadge_core.agent_card import AgentCard
|
|
8
|
+
from agentbadge_core.constants import EXTENSION_URI
|
|
9
|
+
from agentbadge_core.crypto import Ed25519KeyPair
|
|
10
|
+
|
|
11
|
+
from agentbadge.auth import agent_auth_headers
|
|
12
|
+
from agentbadge.types import (
|
|
13
|
+
AckResponse,
|
|
14
|
+
AgentBadgeAPIError,
|
|
15
|
+
BadgeFeatures,
|
|
16
|
+
BadgeMetadata,
|
|
17
|
+
EnrollResponse,
|
|
18
|
+
MessagesResponse,
|
|
19
|
+
SendMessageResponse,
|
|
20
|
+
SubscribeResponse,
|
|
21
|
+
VerificationResponse,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentBadgeClient:
|
|
26
|
+
"""Thin typed client for the Agent Badge API.
|
|
27
|
+
|
|
28
|
+
An `httpx.AsyncClient` may be injected (tests pass one backed by
|
|
29
|
+
`httpx.MockTransport`); it is used both for API calls and for fetching
|
|
30
|
+
AgentCards in `verify_agent_card`. Operator-side messaging
|
|
31
|
+
(`send_message`) requires `admin_api_key`; agent-side messaging (poll,
|
|
32
|
+
ack, webhooks) signs each request with the agent's Ed25519 keypair.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
base_url: str = "http://localhost:8000",
|
|
38
|
+
client: httpx.AsyncClient | None = None,
|
|
39
|
+
admin_api_key: str | None = None,
|
|
40
|
+
):
|
|
41
|
+
self.base_url = base_url.rstrip("/")
|
|
42
|
+
self._client = client
|
|
43
|
+
self._owns_client = client is None
|
|
44
|
+
self.admin_api_key = admin_api_key
|
|
45
|
+
|
|
46
|
+
async def _http(self) -> httpx.AsyncClient:
|
|
47
|
+
if self._client is None:
|
|
48
|
+
self._client = httpx.AsyncClient(timeout=10.0, follow_redirects=True)
|
|
49
|
+
return self._client
|
|
50
|
+
|
|
51
|
+
async def aclose(self) -> None:
|
|
52
|
+
if self._owns_client and self._client is not None:
|
|
53
|
+
await self._client.aclose()
|
|
54
|
+
|
|
55
|
+
async def __aenter__(self) -> "AgentBadgeClient":
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
async def __aexit__(self, *exc_info: object) -> None:
|
|
59
|
+
await self.aclose()
|
|
60
|
+
|
|
61
|
+
async def _request(self, method: str, path_or_url: str, **kwargs: Any) -> Any:
|
|
62
|
+
url = path_or_url if path_or_url.startswith("http") else f"{self.base_url}{path_or_url}"
|
|
63
|
+
client = await self._http()
|
|
64
|
+
response = await client.request(method, url, **kwargs)
|
|
65
|
+
if response.status_code >= 400:
|
|
66
|
+
try:
|
|
67
|
+
body = response.json()
|
|
68
|
+
except ValueError:
|
|
69
|
+
body = {}
|
|
70
|
+
raise AgentBadgeAPIError(
|
|
71
|
+
status_code=response.status_code,
|
|
72
|
+
error=body.get("error", "http_error"),
|
|
73
|
+
message=body.get("message", response.text[:200]),
|
|
74
|
+
request_id=body.get("requestId"),
|
|
75
|
+
)
|
|
76
|
+
if response.status_code == 204 or not response.content:
|
|
77
|
+
return None
|
|
78
|
+
try:
|
|
79
|
+
return response.json()
|
|
80
|
+
except ValueError as exc:
|
|
81
|
+
raise AgentBadgeAPIError(
|
|
82
|
+
status_code=response.status_code,
|
|
83
|
+
error="invalid_response",
|
|
84
|
+
message=f"Response from {url} is not valid JSON",
|
|
85
|
+
) from exc
|
|
86
|
+
|
|
87
|
+
async def verify_badge(self, badge_id: str) -> VerificationResponse:
|
|
88
|
+
"""GET /v1/verify/{badge_id} as a typed VerificationResponse."""
|
|
89
|
+
data = await self._request("GET", f"/v1/verify/{badge_id}")
|
|
90
|
+
return VerificationResponse.model_validate(data)
|
|
91
|
+
|
|
92
|
+
async def verify_agent_card(self, card_url: str) -> VerificationResponse:
|
|
93
|
+
"""Fetch an AgentCard, find its Agent Badge extension, verify the badge."""
|
|
94
|
+
card = await self._request("GET", card_url)
|
|
95
|
+
if not isinstance(card, dict):
|
|
96
|
+
raise AgentBadgeAPIError(
|
|
97
|
+
200, "card_invalid", f"AgentCard at {card_url} is not an object"
|
|
98
|
+
)
|
|
99
|
+
extension = AgentCard.find_extension(card, EXTENSION_URI)
|
|
100
|
+
if extension is None:
|
|
101
|
+
raise AgentBadgeAPIError(
|
|
102
|
+
200,
|
|
103
|
+
"badge_extension_not_found",
|
|
104
|
+
f"AgentCard at {card_url} carries no Agent Badge extension ({EXTENSION_URI})",
|
|
105
|
+
)
|
|
106
|
+
badge_id = (extension.get("params") or {}).get("badgeId")
|
|
107
|
+
if not badge_id:
|
|
108
|
+
raise AgentBadgeAPIError(
|
|
109
|
+
200,
|
|
110
|
+
"badge_id_missing",
|
|
111
|
+
"Agent Badge extension params carry no badgeId",
|
|
112
|
+
)
|
|
113
|
+
return await self.verify_badge(badge_id)
|
|
114
|
+
|
|
115
|
+
async def get_credential(self, badge_id: str) -> dict:
|
|
116
|
+
"""GET /v1/credentials/{badge_id} — the full flattened JWS credential."""
|
|
117
|
+
return await self._request("GET", f"/v1/credentials/{badge_id}")
|
|
118
|
+
|
|
119
|
+
async def create_subscribe_intent(
|
|
120
|
+
self,
|
|
121
|
+
card_url: str,
|
|
122
|
+
plan: str = "monthly",
|
|
123
|
+
badge_type: str = "trust",
|
|
124
|
+
evidence: dict | None = None,
|
|
125
|
+
linked_credentials: list[dict] | None = None,
|
|
126
|
+
) -> dict:
|
|
127
|
+
"""POST /v1/subscribe/intents — free, and the first half of buying a badge.
|
|
128
|
+
|
|
129
|
+
Returns the terms the agent's own wallet needs in order to pay:
|
|
130
|
+
`subscribe_url`, `amount_usdc_base_units`, `pay_to`, `network`,
|
|
131
|
+
`asset_contract`. The SDK deliberately stops here — it holds no key and
|
|
132
|
+
never pays.
|
|
133
|
+
"""
|
|
134
|
+
body: dict = {"card_url": card_url, "plan": plan, "badge_type": badge_type}
|
|
135
|
+
if evidence is not None:
|
|
136
|
+
body["evidence"] = evidence
|
|
137
|
+
if linked_credentials is not None:
|
|
138
|
+
body["linked_credentials"] = linked_credentials
|
|
139
|
+
return await self._request("POST", "/v1/subscribe/intents", json=body)
|
|
140
|
+
|
|
141
|
+
async def subscribe_status(self, intent_id: str) -> dict:
|
|
142
|
+
"""GET /v1/subscribe/status — has this intent been paid?
|
|
143
|
+
|
|
144
|
+
A subscription carrying a settle tx is the proof of payment; poll this
|
|
145
|
+
rather than trusting any party's self-report.
|
|
146
|
+
"""
|
|
147
|
+
return await self._request("GET", "/v1/subscribe/status", params={"intent_id": intent_id})
|
|
148
|
+
|
|
149
|
+
async def wear(self, intent_id: str, card_path: str, *, dry_run: bool = False) -> dict:
|
|
150
|
+
"""Place the badge bought under `intent_id` onto this agent's own card.
|
|
151
|
+
|
|
152
|
+
Runs entirely in the caller's process against the caller's own file:
|
|
153
|
+
the SDK fetches the descriptor it already paid for, then patches the
|
|
154
|
+
local card. Nothing here writes to anybody else's document — placement
|
|
155
|
+
is the badge holder's own act, and this only removes the JSON surgery.
|
|
156
|
+
|
|
157
|
+
Raises if the intent has not settled: there is no descriptor to wear
|
|
158
|
+
until it has been paid for.
|
|
159
|
+
"""
|
|
160
|
+
from agentbadge_core.wear import wear_badge
|
|
161
|
+
|
|
162
|
+
status = await self.subscribe_status(intent_id)
|
|
163
|
+
descriptor = status.get("badge")
|
|
164
|
+
if not descriptor:
|
|
165
|
+
raise AgentBadgeAPIError(
|
|
166
|
+
409,
|
|
167
|
+
"not_settled",
|
|
168
|
+
f"Intent {intent_id} has no badge to wear yet (status: {status.get('status')}). "
|
|
169
|
+
"Pay the subscribe_url first.",
|
|
170
|
+
)
|
|
171
|
+
return wear_badge(descriptor, card_path, dry_run=dry_run)
|
|
172
|
+
|
|
173
|
+
async def enroll(
|
|
174
|
+
self, card_url: str, badge_type: str, evidence: dict | None = None
|
|
175
|
+
) -> EnrollResponse:
|
|
176
|
+
"""Removed — badges are bought, not enrolled.
|
|
177
|
+
|
|
178
|
+
Raises instead of calling the endpoint so an integration pinned to the
|
|
179
|
+
old SDK gets an error that says where issuance went, rather than a bare
|
|
180
|
+
410 from the wire.
|
|
181
|
+
"""
|
|
182
|
+
raise AgentBadgeAPIError(
|
|
183
|
+
410,
|
|
184
|
+
"enroll_gone",
|
|
185
|
+
"Free enrollment was removed. Call create_subscribe_intent(...), have the "
|
|
186
|
+
"agent's own wallet pay the returned subscribe_url with an x402 `exact` "
|
|
187
|
+
"payment, then poll subscribe_status(intent_id).",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def get_jwks(self) -> dict:
|
|
191
|
+
"""GET /.well-known/jwks.json."""
|
|
192
|
+
return await self._request("GET", "/.well-known/jwks.json")
|
|
193
|
+
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
# Messaging channel
|
|
196
|
+
|
|
197
|
+
def _admin_headers(self) -> dict[str, str]:
|
|
198
|
+
if not self.admin_api_key:
|
|
199
|
+
raise AgentBadgeAPIError(
|
|
200
|
+
0, "admin_api_key_required", "This call requires admin_api_key on the client"
|
|
201
|
+
)
|
|
202
|
+
return {"X-API-Key": self.admin_api_key}
|
|
203
|
+
|
|
204
|
+
async def send_message(
|
|
205
|
+
self,
|
|
206
|
+
badge_id: str,
|
|
207
|
+
payload: dict,
|
|
208
|
+
message_type: str | None = None,
|
|
209
|
+
expires_at: str | datetime | None = None,
|
|
210
|
+
) -> SendMessageResponse:
|
|
211
|
+
"""Operator → agent: POST /v1/agent-messages/{badge_id}.
|
|
212
|
+
|
|
213
|
+
The message type defaults to payload["type"] (or "arbitrary").
|
|
214
|
+
"""
|
|
215
|
+
body: dict = {
|
|
216
|
+
"message_type": message_type or payload.get("type", "arbitrary"),
|
|
217
|
+
"payload": payload,
|
|
218
|
+
}
|
|
219
|
+
if expires_at is not None:
|
|
220
|
+
body["expires_at"] = (
|
|
221
|
+
expires_at.isoformat() if isinstance(expires_at, datetime) else expires_at
|
|
222
|
+
)
|
|
223
|
+
data = await self._request(
|
|
224
|
+
"POST",
|
|
225
|
+
f"/v1/agent-messages/{badge_id}",
|
|
226
|
+
json=body,
|
|
227
|
+
headers=self._admin_headers(),
|
|
228
|
+
)
|
|
229
|
+
return SendMessageResponse.model_validate(data)
|
|
230
|
+
|
|
231
|
+
async def poll_messages(
|
|
232
|
+
self,
|
|
233
|
+
badge_id: str,
|
|
234
|
+
keypair: Ed25519KeyPair,
|
|
235
|
+
since: str | datetime | None = None,
|
|
236
|
+
) -> MessagesResponse:
|
|
237
|
+
"""Agent: GET /v1/agent-messages/{badge_id} with a signed request."""
|
|
238
|
+
params: dict = {}
|
|
239
|
+
if since is not None:
|
|
240
|
+
params["since"] = since.isoformat() if isinstance(since, datetime) else since
|
|
241
|
+
data = await self._request(
|
|
242
|
+
"GET",
|
|
243
|
+
f"/v1/agent-messages/{badge_id}",
|
|
244
|
+
params=params,
|
|
245
|
+
headers=agent_auth_headers(badge_id, keypair),
|
|
246
|
+
)
|
|
247
|
+
return MessagesResponse.model_validate(data)
|
|
248
|
+
|
|
249
|
+
async def ack_messages(
|
|
250
|
+
self, badge_id: str, keypair: Ed25519KeyPair, message_ids: list[str]
|
|
251
|
+
) -> AckResponse:
|
|
252
|
+
"""Agent: POST /v1/agent-messages/{badge_id}/ack with a signed request."""
|
|
253
|
+
data = await self._request(
|
|
254
|
+
"POST",
|
|
255
|
+
f"/v1/agent-messages/{badge_id}/ack",
|
|
256
|
+
json={"message_ids": message_ids},
|
|
257
|
+
headers=agent_auth_headers(badge_id, keypair),
|
|
258
|
+
)
|
|
259
|
+
return AckResponse.model_validate(data)
|
|
260
|
+
|
|
261
|
+
async def register_webhook(
|
|
262
|
+
self, badge_id: str, keypair: Ed25519KeyPair, webhook_url: str
|
|
263
|
+
) -> SubscribeResponse:
|
|
264
|
+
"""Agent: POST /v1/agent-subscriptions. The returned HMAC secret is
|
|
265
|
+
shown exactly once — store it."""
|
|
266
|
+
data = await self._request(
|
|
267
|
+
"POST",
|
|
268
|
+
"/v1/agent-subscriptions",
|
|
269
|
+
json={"badge_id": badge_id, "webhook_url": webhook_url},
|
|
270
|
+
headers=agent_auth_headers(badge_id, keypair),
|
|
271
|
+
)
|
|
272
|
+
return SubscribeResponse.model_validate(data)
|
|
273
|
+
|
|
274
|
+
async def unregister_webhook(
|
|
275
|
+
self, subscription_id: str, badge_id: str, keypair: Ed25519KeyPair
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Agent: DELETE /v1/agent-subscriptions/{id}, signed with the badge
|
|
278
|
+
that owns the subscription."""
|
|
279
|
+
await self._request(
|
|
280
|
+
"DELETE",
|
|
281
|
+
f"/v1/agent-subscriptions/{subscription_id}",
|
|
282
|
+
headers=agent_auth_headers(badge_id, keypair),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# ------------------------------------------------------------------
|
|
286
|
+
# Live metadata (Strategy B)
|
|
287
|
+
|
|
288
|
+
async def get_badge_metadata(self, badge_id: str) -> BadgeMetadata:
|
|
289
|
+
"""GET /v1/badges/{badge_id}/metadata — live, never-frozen badge data."""
|
|
290
|
+
data = await self._request("GET", f"/v1/badges/{badge_id}/metadata")
|
|
291
|
+
return BadgeMetadata.model_validate(data)
|
|
292
|
+
|
|
293
|
+
async def get_badge_features(self, badge_id: str) -> BadgeFeatures:
|
|
294
|
+
"""GET /v1/badges/{badge_id}/features — cacheable feature flags."""
|
|
295
|
+
data = await self._request("GET", f"/v1/badges/{badge_id}/features")
|
|
296
|
+
return BadgeFeatures.model_validate(data)
|
agentbadge/types.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Typed responses mirroring the canonical JSON Schemas."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VerificationResponse(BaseModel):
|
|
9
|
+
badgeId: str
|
|
10
|
+
status: Literal["active", "expired", "revoked", "downgraded", "unknown"]
|
|
11
|
+
tier: Literal["unknown", "monitored", "verified", "certified"]
|
|
12
|
+
claims: list[str]
|
|
13
|
+
issuer: str
|
|
14
|
+
expires: str
|
|
15
|
+
cardHash: str
|
|
16
|
+
revocationReason: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BadgeDescriptor(BaseModel):
|
|
20
|
+
uri: str
|
|
21
|
+
description: str
|
|
22
|
+
required: bool = False
|
|
23
|
+
params: dict
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ChallengeInfo(BaseModel):
|
|
27
|
+
domain: str
|
|
28
|
+
method: Literal["dns_txt", "well_known_http"]
|
|
29
|
+
token: str
|
|
30
|
+
instructions: str
|
|
31
|
+
expires_at: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EnrollResponse(BaseModel):
|
|
35
|
+
status: Literal["enrolled", "pending_domain_verification"]
|
|
36
|
+
badge: BadgeDescriptor | None = None
|
|
37
|
+
badge_id: str | None = None
|
|
38
|
+
challenge: ChallengeInfo | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SendMessageResponse(BaseModel):
|
|
42
|
+
message_id: str
|
|
43
|
+
badge_id: str
|
|
44
|
+
status: Literal["pending", "delivered"]
|
|
45
|
+
webhook_deliveries: int = 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AgentMessage(BaseModel):
|
|
49
|
+
message_id: str
|
|
50
|
+
message_type: str
|
|
51
|
+
direction: str
|
|
52
|
+
payload: dict
|
|
53
|
+
status: str
|
|
54
|
+
created_at: str
|
|
55
|
+
expires_at: str | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MessagesResponse(BaseModel):
|
|
59
|
+
badge_id: str
|
|
60
|
+
messages: list[AgentMessage]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AckResponse(BaseModel):
|
|
64
|
+
badge_id: str
|
|
65
|
+
acked: list[str]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SubscribeResponse(BaseModel):
|
|
69
|
+
subscription_id: str
|
|
70
|
+
badge_id: str
|
|
71
|
+
webhook_url: str
|
|
72
|
+
secret: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BadgeMetadata(BaseModel):
|
|
76
|
+
badgeId: str
|
|
77
|
+
trustScore: float
|
|
78
|
+
verificationCount: int
|
|
79
|
+
features: list[str]
|
|
80
|
+
ambassadorCode: str | None = None
|
|
81
|
+
recruitUrl: str | None = None
|
|
82
|
+
linkedCredentials: list[dict] = []
|
|
83
|
+
openOffers: int
|
|
84
|
+
lastUpdated: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BadgeFeatures(BaseModel):
|
|
88
|
+
badgeId: str
|
|
89
|
+
features: list[str]
|
|
90
|
+
lastUpdated: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AgentBadgeAPIError(Exception):
|
|
94
|
+
"""Raised when the API returns an error response."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, status_code: int, error: str, message: str, request_id: str | None = None):
|
|
97
|
+
self.status_code = status_code
|
|
98
|
+
self.error = error
|
|
99
|
+
self.message = message
|
|
100
|
+
self.request_id = request_id
|
|
101
|
+
super().__init__(f"[{status_code}] {error}: {message}")
|
agentbadge/verify.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Convenience verification functions."""
|
|
2
|
+
|
|
3
|
+
from agentbadge.client import AgentBadgeClient
|
|
4
|
+
from agentbadge.types import VerificationResponse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def verify_badge(
|
|
8
|
+
badge_id: str, api_url: str = "http://localhost:8000"
|
|
9
|
+
) -> VerificationResponse:
|
|
10
|
+
"""Verify a badge by ID against the given API."""
|
|
11
|
+
async with AgentBadgeClient(api_url) as client:
|
|
12
|
+
return await client.verify_badge(badge_id)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def verify_agent_card(
|
|
16
|
+
card_url: str, api_url: str = "http://localhost:8000"
|
|
17
|
+
) -> VerificationResponse:
|
|
18
|
+
"""Fetch an AgentCard, find its badge extension, and verify the badge."""
|
|
19
|
+
async with AgentBadgeClient(api_url) as client:
|
|
20
|
+
return await client.verify_agent_card(card_url)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentbadge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Badge Python SDK — HTTP client for the Agent Badge API
|
|
5
|
+
Project-URL: Homepage, https://badge.swarmintel.xyz
|
|
6
|
+
Project-URL: Documentation, https://badge.swarmintel.xyz/.well-known/agentbadge.json
|
|
7
|
+
Project-URL: Repository, https://github.com/casey1088/agent-badge
|
|
8
|
+
Author: Swarm Intel
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: a2a,agent,agentcard,identity,verification,x402
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: agentbadge-core~=0.1.0
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Requires-Dist: pydantic>=2.5
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# agentbadge
|
|
23
|
+
|
|
24
|
+
Python HTTP client for the Agent Badge API.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from agentbadge import AgentBadgeClient
|
|
28
|
+
|
|
29
|
+
async with AgentBadgeClient("https://badge.swarmintel.xyz") as client:
|
|
30
|
+
status = await client.verify("bdg_...")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Verification is public and needs no credentials. Buying a badge requires an
|
|
34
|
+
x402 payment from your own wallet; this client never holds a key and never
|
|
35
|
+
pays on your behalf.
|
|
36
|
+
|
|
37
|
+
Nothing here is required to integrate. The API is plain HTTP and the full
|
|
38
|
+
flow is published at
|
|
39
|
+
`https://badge.swarmintel.xyz/.well-known/agentbadge.json`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
agentbadge/__init__.py,sha256=RaNtQJmFtzDrD_iok3ao9IsnEhGBobrvdMWmflKfu1A,1232
|
|
2
|
+
agentbadge/auth.py,sha256=d8ekKUkrn6eNrJOb2ZJEB2d6f0aDQPHk47RJDkGSoEM,957
|
|
3
|
+
agentbadge/client.py,sha256=7nIOHBWgZrSoZSAfitJ_fVdYJ3W9qXibXlLLziGssk8,11720
|
|
4
|
+
agentbadge/types.py,sha256=SsEDRF_Sd-b-We079odUWX1CZ0PXCintUcz64gSeNaM,2299
|
|
5
|
+
agentbadge/verify.py,sha256=91T4n9l5GMSWbrE0Ke__Qtehum9nqI210Ve72d44D-c,710
|
|
6
|
+
agentbadge-0.1.0.dist-info/METADATA,sha256=SNyv2OCMFZho4gKl647IjNBfA07jugApX_3ZMie5tWw,1366
|
|
7
|
+
agentbadge-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
agentbadge-0.1.0.dist-info/RECORD,,
|