agentnode-sdk 0.1.0__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.
@@ -0,0 +1,46 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ *.egg
9
+
10
+ # Node
11
+ node_modules/
12
+ cli/dist/
13
+
14
+ # Next.js
15
+ web/.next/
16
+ web/out/
17
+
18
+ # Environment
19
+ .env
20
+ .env.local
21
+ .env.production
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+
29
+ # OS
30
+ .DS_Store
31
+ Thumbs.db
32
+
33
+ # Test
34
+ .pytest_cache/
35
+ .coverage
36
+ htmlcov/
37
+
38
+ # Build artifacts
39
+ *.tar.gz
40
+ *.whl
41
+
42
+ # Spec documents
43
+ *.docx
44
+
45
+ # Claude Code
46
+ .claude/
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentnode-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents.
5
+ Project-URL: Homepage, https://agentnode.net
6
+ Project-URL: Repository, https://github.com/agentnode-ai/agentnode
7
+ Project-URL: Documentation, https://agentnode.net/docs
8
+ License-Expression: MIT
9
+ Keywords: agent,agentnode,ai,capabilities,langchain,mcp
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.25
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == 'dev'
20
+ Requires-Dist: pytest-asyncio; extra == 'dev'
21
+ Requires-Dist: respx; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # agentnode-sdk
25
+
26
+ Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents.
@@ -0,0 +1,3 @@
1
+ # agentnode-sdk
2
+
3
+ Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents.
@@ -0,0 +1,33 @@
1
+ """AgentNode Python SDK — discover, resolve, and install AI agent capabilities."""
2
+
3
+ from agentnode_sdk.client import AgentNode, AgentNodeClient
4
+ from agentnode_sdk.exceptions import (
5
+ AgentNodeError,
6
+ AuthError,
7
+ NotFoundError,
8
+ ValidationError,
9
+ )
10
+ from agentnode_sdk.models import (
11
+ InstallMetadata,
12
+ PackageDetail,
13
+ ResolvedPackage,
14
+ ResolveResult,
15
+ SearchHit,
16
+ SearchResult,
17
+ )
18
+
19
+ __version__ = "0.1.0"
20
+ __all__ = [
21
+ "AgentNode",
22
+ "AgentNodeClient",
23
+ "AgentNodeError",
24
+ "NotFoundError",
25
+ "AuthError",
26
+ "ValidationError",
27
+ "PackageDetail",
28
+ "SearchResult",
29
+ "SearchHit",
30
+ "ResolveResult",
31
+ "ResolvedPackage",
32
+ "InstallMetadata",
33
+ ]
@@ -0,0 +1,371 @@
1
+ """AgentNode API client. Spec §14."""
2
+ from __future__ import annotations
3
+
4
+ import httpx
5
+
6
+ from agentnode_sdk.exceptions import (
7
+ AgentNodeError,
8
+ AuthError,
9
+ NotFoundError,
10
+ ValidationError,
11
+ )
12
+ from agentnode_sdk.models import (
13
+ ArtifactInfo,
14
+ CapabilityInfo,
15
+ DependencyInfo,
16
+ InstallMetadata,
17
+ PackageDetail,
18
+ PermissionsInfo,
19
+ ResolvedPackage,
20
+ ResolveResult,
21
+ ScoreBreakdown,
22
+ SearchHit,
23
+ SearchResult,
24
+ )
25
+
26
+ DEFAULT_BASE_URL = "https://api.agentnode.net/v1"
27
+
28
+ ERROR_CLASS_MAP = {
29
+ 401: AuthError,
30
+ 403: AuthError,
31
+ 404: NotFoundError,
32
+ 422: ValidationError,
33
+ }
34
+
35
+
36
+ class AgentNode:
37
+ """Spec-compliant SDK client (§14.3). Returns plain dicts."""
38
+
39
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL):
40
+ self._client = httpx.Client(
41
+ base_url=base_url.rstrip("/"),
42
+ headers={"X-API-Key": api_key},
43
+ timeout=30,
44
+ )
45
+
46
+ def close(self):
47
+ self._client.close()
48
+
49
+ def __enter__(self):
50
+ return self
51
+
52
+ def __exit__(self, *args):
53
+ self.close()
54
+
55
+ # --- Public API ---
56
+
57
+ def search(
58
+ self,
59
+ query: str = "",
60
+ capability_id: str = "",
61
+ framework: str = "",
62
+ sort_by: str = "relevance",
63
+ page: int = 1,
64
+ ) -> dict:
65
+ body = {k: v for k, v in {
66
+ "q": query,
67
+ "capability_id": capability_id,
68
+ "framework": framework,
69
+ "sort_by": sort_by,
70
+ "page": page,
71
+ }.items() if v}
72
+ return self._handle(self._client.post("/search", json=body))
73
+
74
+ def resolve_upgrade(
75
+ self,
76
+ missing_capability: str,
77
+ framework: str = "",
78
+ runtime: str = "",
79
+ current_capabilities: list[str] | None = None,
80
+ policy: dict | None = None,
81
+ ) -> dict:
82
+ return self._post(
83
+ "/resolve-upgrade",
84
+ missing_capability=missing_capability,
85
+ current_capabilities=current_capabilities or [],
86
+ framework=framework,
87
+ runtime=runtime,
88
+ policy=policy or {},
89
+ )
90
+
91
+ def check_policy(
92
+ self,
93
+ package_slug: str,
94
+ framework: str = "",
95
+ policy: dict | None = None,
96
+ ) -> dict:
97
+ return self._post(
98
+ "/check-policy",
99
+ package_slug=package_slug,
100
+ framework=framework,
101
+ policy=policy or {},
102
+ )
103
+
104
+ def get_install_metadata(self, package_slug: str, version: str = "") -> dict:
105
+ """Read-only install metadata. Does NOT create installation records."""
106
+ params = {"version": version} if version else {}
107
+ return self._handle(
108
+ self._client.get(f"/packages/{package_slug}/install-info", params=params)
109
+ )
110
+
111
+ def get_package(self, slug: str) -> dict:
112
+ return self._handle(self._client.get(f"/packages/{slug}"))
113
+
114
+ def validate(self, manifest: dict) -> dict:
115
+ return self._handle(
116
+ self._client.post("/packages/validate", json={"manifest": manifest})
117
+ )
118
+
119
+ def install(
120
+ self,
121
+ package_slug: str,
122
+ version: str = "",
123
+ source: str = "sdk",
124
+ event_type: str = "install",
125
+ ) -> dict:
126
+ """Create installation record and get artifact URL."""
127
+ body: dict = {"source": source, "event_type": event_type}
128
+ if version:
129
+ body["version"] = version
130
+ return self._handle(
131
+ self._client.post(f"/packages/{package_slug}/install", json=body)
132
+ )
133
+
134
+ def recommend(
135
+ self,
136
+ missing_capabilities: list[str],
137
+ framework: str = "",
138
+ runtime: str = "",
139
+ ) -> dict:
140
+ return self._post(
141
+ "/recommend",
142
+ missing_capabilities=missing_capabilities,
143
+ framework=framework,
144
+ runtime=runtime,
145
+ )
146
+
147
+ # --- Internal ---
148
+
149
+ def _post(self, path: str, **kwargs) -> dict:
150
+ # Keep lists/dicts even if empty; only filter out None and empty strings
151
+ body = {k: v for k, v in kwargs.items() if v is not None and v != ""}
152
+ return self._handle(self._client.post(path, json=body))
153
+
154
+ def _handle(self, response: httpx.Response) -> dict:
155
+ """Parse response. Raise typed AgentNodeError on API errors."""
156
+ if response.status_code >= 400:
157
+ try:
158
+ body = response.json()
159
+ err = body.get("error", {})
160
+ code = err.get("code", "UNKNOWN")
161
+ message = err.get("message", response.text)
162
+ except (ValueError, KeyError):
163
+ code, message = "UNKNOWN", response.text
164
+ exc_class = ERROR_CLASS_MAP.get(response.status_code, AgentNodeError)
165
+ raise exc_class(code, message)
166
+ return response.json()
167
+
168
+
169
+ class AgentNodeClient:
170
+ """Extended client returning typed dataclass models. Backward-compatible."""
171
+
172
+ def __init__(
173
+ self,
174
+ base_url: str = DEFAULT_BASE_URL,
175
+ api_key: str | None = None,
176
+ token: str | None = None,
177
+ timeout: float = 30.0,
178
+ ):
179
+ self.base_url = base_url.rstrip("/")
180
+ headers = {}
181
+ if api_key:
182
+ headers["X-API-Key"] = api_key
183
+ elif token:
184
+ headers["Authorization"] = f"Bearer {token}"
185
+ self._client = httpx.Client(
186
+ base_url=self.base_url, headers=headers, timeout=timeout
187
+ )
188
+
189
+ def close(self):
190
+ self._client.close()
191
+
192
+ def __enter__(self):
193
+ return self
194
+
195
+ def __exit__(self, *args):
196
+ self.close()
197
+
198
+ def _request(self, method: str, path: str, **kwargs) -> dict:
199
+ resp = self._client.request(method, path, **kwargs)
200
+ if resp.status_code >= 400:
201
+ data = resp.json()
202
+ err = data.get("error", {})
203
+ exc_class = ERROR_CLASS_MAP.get(resp.status_code, AgentNodeError)
204
+ raise exc_class(
205
+ code=err.get("code", "UNKNOWN"),
206
+ message=err.get("message", resp.text),
207
+ )
208
+ return resp.json()
209
+
210
+ # --- Search ---
211
+
212
+ def search(
213
+ self,
214
+ query: str = "",
215
+ package_type: str | None = None,
216
+ capability_id: str | None = None,
217
+ framework: str | None = None,
218
+ sort_by: str | None = None,
219
+ page: int = 1,
220
+ per_page: int = 20,
221
+ ) -> SearchResult:
222
+ body: dict = {"q": query, "page": page, "per_page": per_page}
223
+ if package_type:
224
+ body["package_type"] = package_type
225
+ if capability_id:
226
+ body["capability_id"] = capability_id
227
+ if framework:
228
+ body["framework"] = framework
229
+ if sort_by:
230
+ body["sort_by"] = sort_by
231
+
232
+ data = self._request("POST", "/search", json=body)
233
+ hits = [
234
+ SearchHit(
235
+ slug=h["slug"],
236
+ name=h["name"],
237
+ package_type=h["package_type"],
238
+ summary=h["summary"],
239
+ publisher_slug=h.get("publisher_slug", ""),
240
+ trust_level=h.get("trust_level", "unverified"),
241
+ latest_version=h.get("latest_version"),
242
+ runtime=h.get("runtime"),
243
+ capability_ids=h.get("capability_ids", []),
244
+ download_count=h.get("download_count", 0),
245
+ )
246
+ for h in data.get("hits", [])
247
+ ]
248
+ return SearchResult(query=data["query"], hits=hits, total=data["total"])
249
+
250
+ # --- Resolve ---
251
+
252
+ def resolve(
253
+ self,
254
+ capabilities: list[str],
255
+ framework: str | None = None,
256
+ runtime: str | None = None,
257
+ package_type: str | None = None,
258
+ limit: int = 10,
259
+ ) -> ResolveResult:
260
+ body: dict = {"capabilities": capabilities, "limit": limit}
261
+ if framework:
262
+ body["framework"] = framework
263
+ if runtime:
264
+ body["runtime"] = runtime
265
+ if package_type:
266
+ body["package_type"] = package_type
267
+
268
+ data = self._request("POST", "/resolve", json=body)
269
+ results = [
270
+ ResolvedPackage(
271
+ slug=r["slug"],
272
+ name=r["name"],
273
+ package_type=r["package_type"],
274
+ summary=r["summary"],
275
+ version=r["version"],
276
+ publisher_slug=r["publisher_slug"],
277
+ trust_level=r["trust_level"],
278
+ score=r["score"],
279
+ breakdown=ScoreBreakdown(**r["breakdown"]),
280
+ matched_capabilities=r.get("matched_capabilities", []),
281
+ )
282
+ for r in data.get("results", [])
283
+ ]
284
+ return ResolveResult(results=results, total=data["total"])
285
+
286
+ # --- Package detail ---
287
+
288
+ def get_package(self, slug: str) -> PackageDetail:
289
+ data = self._request("GET", f"/packages/{slug}")
290
+ lv = data.get("latest_version")
291
+ return PackageDetail(
292
+ slug=data["slug"],
293
+ name=data["name"],
294
+ package_type=data["package_type"],
295
+ summary=data["summary"],
296
+ description=data.get("description"),
297
+ download_count=data["download_count"],
298
+ is_deprecated=data["is_deprecated"],
299
+ latest_version=lv["version_number"] if lv else None,
300
+ )
301
+
302
+ # --- Install metadata ---
303
+
304
+ def get_install_metadata(
305
+ self, slug: str, version: str | None = None
306
+ ) -> InstallMetadata:
307
+ params = {}
308
+ if version:
309
+ params["version"] = version
310
+ data = self._request("GET", f"/packages/{slug}/install-info", params=params)
311
+
312
+ artifact = None
313
+ if data.get("artifact"):
314
+ a = data["artifact"]
315
+ artifact = ArtifactInfo(
316
+ url=a.get("url"),
317
+ hash_sha256=a.get("hash_sha256"),
318
+ size_bytes=a.get("size_bytes"),
319
+ )
320
+
321
+ caps = [
322
+ CapabilityInfo(
323
+ name=c["name"],
324
+ capability_id=c["capability_id"],
325
+ capability_type=c["capability_type"],
326
+ )
327
+ for c in data.get("capabilities", [])
328
+ ]
329
+ deps = [
330
+ DependencyInfo(
331
+ package_slug=d["package_slug"],
332
+ role=d.get("role"),
333
+ is_required=d["is_required"],
334
+ min_version=d.get("min_version"),
335
+ )
336
+ for d in data.get("dependencies", [])
337
+ ]
338
+ perms = None
339
+ if data.get("permissions"):
340
+ p = data["permissions"]
341
+ perms = PermissionsInfo(
342
+ network_level=p["network_level"],
343
+ filesystem_level=p["filesystem_level"],
344
+ code_execution_level=p["code_execution_level"],
345
+ data_access_level=p["data_access_level"],
346
+ user_approval_level=p["user_approval_level"],
347
+ )
348
+
349
+ return InstallMetadata(
350
+ slug=data["slug"],
351
+ version=data["version"],
352
+ package_type=data["package_type"],
353
+ install_mode=data["install_mode"],
354
+ hosting_type=data["hosting_type"],
355
+ runtime=data["runtime"],
356
+ entrypoint=data.get("entrypoint"),
357
+ artifact=artifact,
358
+ capabilities=caps,
359
+ dependencies=deps,
360
+ permissions=perms,
361
+ )
362
+
363
+ # --- Download ---
364
+
365
+ def download(self, slug: str, version: str | None = None) -> str | None:
366
+ """Track download and return artifact URL."""
367
+ params = {}
368
+ if version:
369
+ params["version"] = version
370
+ data = self._request("POST", f"/packages/{slug}/download", params=params)
371
+ return data.get("download_url")
@@ -0,0 +1,25 @@
1
+ """Exception hierarchy for AgentNode SDK. Spec §14.1."""
2
+
3
+
4
+ class AgentNodeError(Exception):
5
+ """Base error for all AgentNode API errors."""
6
+
7
+ def __init__(self, code: str, message: str):
8
+ self.code = code
9
+ self.message = message
10
+ super().__init__(f"[{code}] {message}")
11
+
12
+
13
+ class NotFoundError(AgentNodeError):
14
+ """Package or resource not found (404)."""
15
+ pass
16
+
17
+
18
+ class AuthError(AgentNodeError):
19
+ """Authentication or authorization failure (401/403)."""
20
+ pass
21
+
22
+
23
+ class ValidationError(AgentNodeError):
24
+ """Manifest or input validation failure (422)."""
25
+ pass
@@ -0,0 +1,112 @@
1
+ """Data models for the AgentNode SDK."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass
8
+ class SearchHit:
9
+ slug: str
10
+ name: str
11
+ package_type: str
12
+ summary: str
13
+ publisher_slug: str
14
+ trust_level: str
15
+ latest_version: str | None = None
16
+ runtime: str | None = None
17
+ capability_ids: list[str] = field(default_factory=list)
18
+ download_count: int = 0
19
+
20
+
21
+ @dataclass
22
+ class SearchResult:
23
+ query: str
24
+ hits: list[SearchHit]
25
+ total: int
26
+
27
+
28
+ @dataclass
29
+ class ScoreBreakdown:
30
+ capability: float
31
+ framework: float
32
+ runtime: float
33
+ trust: float
34
+ permissions: float
35
+
36
+
37
+ @dataclass
38
+ class ResolvedPackage:
39
+ slug: str
40
+ name: str
41
+ package_type: str
42
+ summary: str
43
+ version: str
44
+ publisher_slug: str
45
+ trust_level: str
46
+ score: float
47
+ breakdown: ScoreBreakdown
48
+ matched_capabilities: list[str] = field(default_factory=list)
49
+
50
+
51
+ @dataclass
52
+ class ResolveResult:
53
+ results: list[ResolvedPackage]
54
+ total: int
55
+
56
+
57
+ @dataclass
58
+ class ArtifactInfo:
59
+ url: str | None
60
+ hash_sha256: str | None
61
+ size_bytes: int | None
62
+
63
+
64
+ @dataclass
65
+ class CapabilityInfo:
66
+ name: str
67
+ capability_id: str
68
+ capability_type: str
69
+
70
+
71
+ @dataclass
72
+ class DependencyInfo:
73
+ package_slug: str
74
+ role: str | None
75
+ is_required: bool
76
+ min_version: str | None = None
77
+
78
+
79
+ @dataclass
80
+ class PermissionsInfo:
81
+ network_level: str
82
+ filesystem_level: str
83
+ code_execution_level: str
84
+ data_access_level: str
85
+ user_approval_level: str
86
+
87
+
88
+ @dataclass
89
+ class InstallMetadata:
90
+ slug: str
91
+ version: str
92
+ package_type: str
93
+ install_mode: str
94
+ hosting_type: str
95
+ runtime: str
96
+ entrypoint: str | None
97
+ artifact: ArtifactInfo | None
98
+ capabilities: list[CapabilityInfo] = field(default_factory=list)
99
+ dependencies: list[DependencyInfo] = field(default_factory=list)
100
+ permissions: PermissionsInfo | None = None
101
+
102
+
103
+ @dataclass
104
+ class PackageDetail:
105
+ slug: str
106
+ name: str
107
+ package_type: str
108
+ summary: str
109
+ description: str | None
110
+ download_count: int
111
+ is_deprecated: bool
112
+ latest_version: str | None = None
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentnode-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ keywords = ["ai", "agent", "capabilities", "agentnode", "mcp", "langchain"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Software Development :: Libraries",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+ dependencies = [
22
+ "httpx>=0.25",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://agentnode.net"
27
+ Repository = "https://github.com/agentnode-ai/agentnode"
28
+ Documentation = "https://agentnode.net/docs"
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest", "pytest-asyncio", "respx"]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["agentnode_sdk"]
File without changes
@@ -0,0 +1,152 @@
1
+ """Unit tests for AgentNode SDK client."""
2
+ import json
3
+
4
+ import httpx
5
+ import pytest
6
+ import respx
7
+
8
+ from agentnode_sdk import AgentNodeClient
9
+
10
+ BASE = "https://api.agentnode.net"
11
+
12
+
13
+ @respx.mock
14
+ def test_search():
15
+ respx.get(f"{BASE}/v1/search").mock(return_value=httpx.Response(200, json={
16
+ "query": "pdf",
17
+ "hits": [{
18
+ "slug": "pdf-reader",
19
+ "name": "PDF Reader",
20
+ "package_type": "toolpack",
21
+ "summary": "Read PDFs",
22
+ "publisher_slug": "test",
23
+ "trust_level": "verified",
24
+ "latest_version": "1.0.0",
25
+ "runtime": "python",
26
+ "capability_ids": ["pdf_extraction"],
27
+ "download_count": 100,
28
+ }],
29
+ "total": 1,
30
+ "limit": 20,
31
+ "offset": 0,
32
+ }))
33
+
34
+ with AgentNodeClient() as client:
35
+ result = client.search("pdf")
36
+ assert result.total == 1
37
+ assert result.hits[0].slug == "pdf-reader"
38
+ assert result.hits[0].capability_ids == ["pdf_extraction"]
39
+
40
+
41
+ @respx.mock
42
+ def test_resolve():
43
+ respx.post(f"{BASE}/v1/resolve").mock(return_value=httpx.Response(200, json={
44
+ "results": [{
45
+ "slug": "pdf-reader",
46
+ "name": "PDF Reader",
47
+ "package_type": "toolpack",
48
+ "summary": "Read PDFs",
49
+ "version": "1.0.0",
50
+ "publisher_slug": "test",
51
+ "trust_level": "verified",
52
+ "score": 0.85,
53
+ "breakdown": {
54
+ "capability": 1.0,
55
+ "framework": 1.0,
56
+ "runtime": 1.0,
57
+ "trust": 0.5,
58
+ "permissions": 0.9,
59
+ },
60
+ "matched_capabilities": ["pdf_extraction"],
61
+ }],
62
+ "total": 1,
63
+ }))
64
+
65
+ with AgentNodeClient() as client:
66
+ result = client.resolve(["pdf_extraction"])
67
+ assert result.total == 1
68
+ assert result.results[0].score == 0.85
69
+ assert result.results[0].breakdown.capability == 1.0
70
+
71
+
72
+ @respx.mock
73
+ def test_get_package():
74
+ respx.get(f"{BASE}/v1/packages/pdf-reader").mock(return_value=httpx.Response(200, json={
75
+ "slug": "pdf-reader",
76
+ "name": "PDF Reader",
77
+ "package_type": "toolpack",
78
+ "summary": "Read PDFs",
79
+ "description": "A great PDF reader",
80
+ "download_count": 42,
81
+ "is_deprecated": False,
82
+ "latest_version": {"version_number": "1.0.0", "channel": "stable", "published_at": "2026-01-01T00:00:00Z"},
83
+ "publisher": {"slug": "test", "display_name": "Test", "trust_level": "verified"},
84
+ "blocks": {},
85
+ }))
86
+
87
+ with AgentNodeClient() as client:
88
+ pkg = client.get_package("pdf-reader")
89
+ assert pkg.slug == "pdf-reader"
90
+ assert pkg.latest_version == "1.0.0"
91
+ assert pkg.download_count == 42
92
+
93
+
94
+ @respx.mock
95
+ def test_get_install_metadata():
96
+ respx.get(f"{BASE}/v1/packages/pdf-reader/install").mock(return_value=httpx.Response(200, json={
97
+ "slug": "pdf-reader",
98
+ "version": "1.0.0",
99
+ "package_type": "toolpack",
100
+ "install_mode": "package",
101
+ "hosting_type": "agentnode_hosted",
102
+ "runtime": "python",
103
+ "entrypoint": "pdf_reader.tool",
104
+ "artifact": {"url": "https://s3.example.com/artifact.tar.gz", "hash_sha256": "abc123", "size_bytes": 1000},
105
+ "capabilities": [{"name": "extract", "capability_id": "pdf_extraction", "capability_type": "tool"}],
106
+ "dependencies": [],
107
+ "permissions": {
108
+ "network_level": "none",
109
+ "filesystem_level": "temp",
110
+ "code_execution_level": "none",
111
+ "data_access_level": "input_only",
112
+ "user_approval_level": "never",
113
+ },
114
+ "published_at": "2026-01-01T00:00:00Z",
115
+ }))
116
+
117
+ with AgentNodeClient() as client:
118
+ meta = client.get_install_metadata("pdf-reader")
119
+ assert meta.slug == "pdf-reader"
120
+ assert meta.entrypoint == "pdf_reader.tool"
121
+ assert meta.artifact.url == "https://s3.example.com/artifact.tar.gz"
122
+ assert len(meta.capabilities) == 1
123
+ assert meta.permissions.network_level == "none"
124
+
125
+
126
+ @respx.mock
127
+ def test_error_handling():
128
+ respx.get(f"{BASE}/v1/packages/nonexistent").mock(return_value=httpx.Response(404, json={
129
+ "error": {"code": "PACKAGE_NOT_FOUND", "message": "Not found", "details": {}}
130
+ }))
131
+
132
+ with AgentNodeClient() as client:
133
+ with pytest.raises(Exception) as exc_info:
134
+ client.get_package("nonexistent")
135
+ assert "PACKAGE_NOT_FOUND" in str(exc_info.value)
136
+
137
+
138
+ @respx.mock
139
+ def test_api_key_auth():
140
+ route = respx.get(f"{BASE}/v1/packages/test").mock(return_value=httpx.Response(200, json={
141
+ "slug": "test", "name": "Test", "package_type": "toolpack",
142
+ "summary": "Test", "description": None, "download_count": 0,
143
+ "is_deprecated": False, "latest_version": None,
144
+ "publisher": {"slug": "t", "display_name": "T", "trust_level": "unverified"},
145
+ "blocks": {},
146
+ }))
147
+
148
+ with AgentNodeClient(api_key="ank_test123") as client:
149
+ client.get_package("test")
150
+
151
+ assert route.called
152
+ assert route.calls[0].request.headers["x-api-key"] == "ank_test123"