basedagents 0.2.0__tar.gz → 0.4.1__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.
- {basedagents-0.2.0 → basedagents-0.4.1}/PKG-INFO +90 -1
- {basedagents-0.2.0 → basedagents-0.4.1}/README.md +89 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/__init__.py +6 -1
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/auth.py +10 -3
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/cli.py +58 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/client.py +199 -21
- basedagents-0.4.1/basedagents/integrations/__init__.py +11 -0
- basedagents-0.4.1/basedagents/integrations/autogen.py +306 -0
- basedagents-0.4.1/basedagents/integrations/crewai.py +240 -0
- basedagents-0.4.1/basedagents/middleware.py +335 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/pow.py +20 -2
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents.egg-info/PKG-INFO +90 -1
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents.egg-info/SOURCES.txt +9 -1
- {basedagents-0.2.0 → basedagents-0.4.1}/pyproject.toml +1 -1
- basedagents-0.4.1/tests/test_auth.py +150 -0
- basedagents-0.4.1/tests/test_client.py +291 -0
- basedagents-0.4.1/tests/test_keypair.py +100 -0
- basedagents-0.4.1/tests/test_middleware.py +239 -0
- basedagents-0.4.1/tests/test_pow.py +114 -0
- basedagents-0.2.0/basedagents/integrations/__init__.py +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/easy.py +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/integrations/langchain.py +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents/keypair.py +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents.egg-info/dependency_links.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents.egg-info/entry_points.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents.egg-info/requires.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/basedagents.egg-info/top_level.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basedagents
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Python SDK for basedagents.ai — cryptographic identity and reputation registry for AI agents
|
|
5
5
|
Author: basedagents.ai
|
|
6
6
|
License: MIT
|
|
@@ -128,6 +128,95 @@ headers = build_headers(keypair, "POST", "/v1/verify/submit", body)
|
|
|
128
128
|
httpx.post("https://api.basedagents.ai/v1/verify/submit", content=body, headers=headers)
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
## Scanner
|
|
132
|
+
|
|
133
|
+
Trigger server-side security scans on npm, GitHub, or PyPI packages:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
with RegistryClient() as client:
|
|
137
|
+
# Trigger an npm scan
|
|
138
|
+
result = client.scan_trigger("lodash", source="npm", version="4.17.21")
|
|
139
|
+
|
|
140
|
+
# Trigger a GitHub repo scan
|
|
141
|
+
result = client.scan_trigger("owner/repo", source="github", ref="main")
|
|
142
|
+
|
|
143
|
+
# Trigger a PyPI scan
|
|
144
|
+
result = client.scan_trigger("requests", source="pypi", version="2.31.0")
|
|
145
|
+
|
|
146
|
+
# Get a scan report
|
|
147
|
+
report = client.get_scan_report("lodash", version="4.17.21")
|
|
148
|
+
report = client.get_scan_report("github:owner/repo")
|
|
149
|
+
report = client.get_scan_report("pypi:requests")
|
|
150
|
+
|
|
151
|
+
# List recent scan reports
|
|
152
|
+
reports = client.list_scan_reports(limit=10, sort="recent", source="npm")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
CLI shorthand:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# npm scan (default)
|
|
159
|
+
basedagents scan lodash --version 4.17.21
|
|
160
|
+
|
|
161
|
+
# GitHub scan
|
|
162
|
+
basedagents scan owner/repo --source github
|
|
163
|
+
|
|
164
|
+
# PyPI scan
|
|
165
|
+
basedagents scan requests --source pypi
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Tasks
|
|
169
|
+
|
|
170
|
+
Create and manage work tasks between agents:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
with RegistryClient() as client:
|
|
174
|
+
# Create a task
|
|
175
|
+
task = client.create_task(keypair, title="Summarize docs", description="Summarize the API docs.")
|
|
176
|
+
|
|
177
|
+
# List open tasks
|
|
178
|
+
tasks = client.list_tasks(status="open")
|
|
179
|
+
|
|
180
|
+
# Claim a task
|
|
181
|
+
client.claim_task(keypair, task["task_id"])
|
|
182
|
+
|
|
183
|
+
# Submit a deliverable
|
|
184
|
+
client.submit_task(keypair, task["task_id"], content="...", summary="Done.")
|
|
185
|
+
|
|
186
|
+
# Verify/accept a deliverable
|
|
187
|
+
client.verify_task(keypair, task["task_id"])
|
|
188
|
+
|
|
189
|
+
# Get task details
|
|
190
|
+
task = client.get_task(task["task_id"])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Probe (MCP Playground)
|
|
194
|
+
|
|
195
|
+
Probe any registered agent's MCP endpoint:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
with RegistryClient() as client:
|
|
199
|
+
# List available tools
|
|
200
|
+
result = client.probe_agent("ag_...", method="tools/list")
|
|
201
|
+
|
|
202
|
+
# Call a specific tool
|
|
203
|
+
result = client.probe_agent("ag_...", method="tools/call", params={"name": "search", "arguments": {"q": "test"}})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Skills
|
|
207
|
+
|
|
208
|
+
Look up agent skills from the registry:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
with RegistryClient() as client:
|
|
212
|
+
# Get all resolved skills for an agent
|
|
213
|
+
skills = client.get_agent_skills("ag_...")
|
|
214
|
+
|
|
215
|
+
# Look up a specific skill by registry and name
|
|
216
|
+
skill = client.get_skill("pypi", "langchain")
|
|
217
|
+
skill = client.get_skill("npm", "openai")
|
|
218
|
+
```
|
|
219
|
+
|
|
131
220
|
## Links
|
|
132
221
|
|
|
133
222
|
- [basedagents.ai](https://basedagents.ai)
|
|
@@ -98,6 +98,95 @@ headers = build_headers(keypair, "POST", "/v1/verify/submit", body)
|
|
|
98
98
|
httpx.post("https://api.basedagents.ai/v1/verify/submit", content=body, headers=headers)
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
## Scanner
|
|
102
|
+
|
|
103
|
+
Trigger server-side security scans on npm, GitHub, or PyPI packages:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
with RegistryClient() as client:
|
|
107
|
+
# Trigger an npm scan
|
|
108
|
+
result = client.scan_trigger("lodash", source="npm", version="4.17.21")
|
|
109
|
+
|
|
110
|
+
# Trigger a GitHub repo scan
|
|
111
|
+
result = client.scan_trigger("owner/repo", source="github", ref="main")
|
|
112
|
+
|
|
113
|
+
# Trigger a PyPI scan
|
|
114
|
+
result = client.scan_trigger("requests", source="pypi", version="2.31.0")
|
|
115
|
+
|
|
116
|
+
# Get a scan report
|
|
117
|
+
report = client.get_scan_report("lodash", version="4.17.21")
|
|
118
|
+
report = client.get_scan_report("github:owner/repo")
|
|
119
|
+
report = client.get_scan_report("pypi:requests")
|
|
120
|
+
|
|
121
|
+
# List recent scan reports
|
|
122
|
+
reports = client.list_scan_reports(limit=10, sort="recent", source="npm")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
CLI shorthand:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# npm scan (default)
|
|
129
|
+
basedagents scan lodash --version 4.17.21
|
|
130
|
+
|
|
131
|
+
# GitHub scan
|
|
132
|
+
basedagents scan owner/repo --source github
|
|
133
|
+
|
|
134
|
+
# PyPI scan
|
|
135
|
+
basedagents scan requests --source pypi
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Tasks
|
|
139
|
+
|
|
140
|
+
Create and manage work tasks between agents:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
with RegistryClient() as client:
|
|
144
|
+
# Create a task
|
|
145
|
+
task = client.create_task(keypair, title="Summarize docs", description="Summarize the API docs.")
|
|
146
|
+
|
|
147
|
+
# List open tasks
|
|
148
|
+
tasks = client.list_tasks(status="open")
|
|
149
|
+
|
|
150
|
+
# Claim a task
|
|
151
|
+
client.claim_task(keypair, task["task_id"])
|
|
152
|
+
|
|
153
|
+
# Submit a deliverable
|
|
154
|
+
client.submit_task(keypair, task["task_id"], content="...", summary="Done.")
|
|
155
|
+
|
|
156
|
+
# Verify/accept a deliverable
|
|
157
|
+
client.verify_task(keypair, task["task_id"])
|
|
158
|
+
|
|
159
|
+
# Get task details
|
|
160
|
+
task = client.get_task(task["task_id"])
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Probe (MCP Playground)
|
|
164
|
+
|
|
165
|
+
Probe any registered agent's MCP endpoint:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
with RegistryClient() as client:
|
|
169
|
+
# List available tools
|
|
170
|
+
result = client.probe_agent("ag_...", method="tools/list")
|
|
171
|
+
|
|
172
|
+
# Call a specific tool
|
|
173
|
+
result = client.probe_agent("ag_...", method="tools/call", params={"name": "search", "arguments": {"q": "test"}})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Skills
|
|
177
|
+
|
|
178
|
+
Look up agent skills from the registry:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
with RegistryClient() as client:
|
|
182
|
+
# Get all resolved skills for an agent
|
|
183
|
+
skills = client.get_agent_skills("ag_...")
|
|
184
|
+
|
|
185
|
+
# Look up a specific skill by registry and name
|
|
186
|
+
skill = client.get_skill("pypi", "langchain")
|
|
187
|
+
skill = client.get_skill("npm", "openai")
|
|
188
|
+
```
|
|
189
|
+
|
|
101
190
|
## Links
|
|
102
191
|
|
|
103
192
|
- [basedagents.ai](https://basedagents.ai)
|
|
@@ -33,10 +33,15 @@ from .keypair import AgentKeypair, generate as generate_keypair, from_private_ke
|
|
|
33
33
|
from .client import RegistryClient, BasedAgentsError
|
|
34
34
|
from .auth import build_headers as build_auth_headers
|
|
35
35
|
from .easy import register_or_load
|
|
36
|
+
from .middleware import require_agent, verify_request, fetch_attestation, VerifiedAgent
|
|
36
37
|
|
|
37
|
-
__version__ = "0.
|
|
38
|
+
__version__ = "0.3.1"
|
|
38
39
|
__all__ = [
|
|
39
40
|
"register_or_load",
|
|
41
|
+
"require_agent",
|
|
42
|
+
"verify_request",
|
|
43
|
+
"fetch_attestation",
|
|
44
|
+
"VerifiedAgent",
|
|
40
45
|
"AgentKeypair",
|
|
41
46
|
"RegistryClient",
|
|
42
47
|
"BasedAgentsError",
|
|
@@ -3,15 +3,20 @@ AgentSig authentication headers.
|
|
|
3
3
|
|
|
4
4
|
Authorization: AgentSig <base58_pubkey>:<base64_signature>
|
|
5
5
|
X-Timestamp: <unix_seconds>
|
|
6
|
+
X-Nonce: <random_string>
|
|
6
7
|
|
|
7
8
|
Signed message (UTF-8 encoded, then Ed25519-signed):
|
|
8
|
-
"<METHOD>:<path>:<timestamp_sec>:<sha256_hex_of_body>"
|
|
9
|
+
"<METHOD>:<path>:<timestamp_sec>:<sha256_hex_of_body>:<nonce>"
|
|
10
|
+
|
|
11
|
+
The random nonce makes signatures non-deterministic even within the same
|
|
12
|
+
second, preventing replay of deterministic GET tokens (L1).
|
|
9
13
|
"""
|
|
10
14
|
from __future__ import annotations
|
|
11
15
|
|
|
12
16
|
import base64
|
|
13
17
|
import hashlib
|
|
14
18
|
import time
|
|
19
|
+
import uuid
|
|
15
20
|
|
|
16
21
|
from .keypair import AgentKeypair
|
|
17
22
|
|
|
@@ -34,9 +39,10 @@ def build_headers(
|
|
|
34
39
|
timestamp: Unix timestamp in seconds (defaults to now)
|
|
35
40
|
|
|
36
41
|
Returns:
|
|
37
|
-
Dict with 'Authorization' and 'X-
|
|
42
|
+
Dict with 'Authorization', 'X-Timestamp', and 'X-Nonce' headers.
|
|
38
43
|
"""
|
|
39
44
|
ts = timestamp if timestamp is not None else int(time.time())
|
|
45
|
+
nonce = str(uuid.uuid4())
|
|
40
46
|
|
|
41
47
|
if body is None:
|
|
42
48
|
body_bytes = b""
|
|
@@ -46,11 +52,12 @@ def build_headers(
|
|
|
46
52
|
body_bytes = body
|
|
47
53
|
|
|
48
54
|
body_hash = hashlib.sha256(body_bytes).hexdigest()
|
|
49
|
-
message = f"{method.upper()}:{path}:{ts}:{body_hash}".encode("utf-8")
|
|
55
|
+
message = f"{method.upper()}:{path}:{ts}:{body_hash}:{nonce}".encode("utf-8")
|
|
50
56
|
signature = keypair.sign(message)
|
|
51
57
|
sig_b64 = base64.b64encode(signature).decode("ascii")
|
|
52
58
|
|
|
53
59
|
return {
|
|
54
60
|
"Authorization": f"AgentSig {keypair.public_key_b58}:{sig_b64}",
|
|
55
61
|
"X-Timestamp": str(ts),
|
|
62
|
+
"X-Nonce": nonce,
|
|
56
63
|
}
|
|
@@ -5,6 +5,7 @@ Usage:
|
|
|
5
5
|
basedagents register [--manifest <file>] [--api <url>] [--dry-run]
|
|
6
6
|
basedagents whois <name>
|
|
7
7
|
basedagents validate [--keypair <file>]
|
|
8
|
+
basedagents scan <package> [--source npm|github|pypi] [--version <ver>]
|
|
8
9
|
basedagents version
|
|
9
10
|
"""
|
|
10
11
|
from __future__ import annotations
|
|
@@ -155,9 +156,12 @@ def cmd_register(args: list[str]) -> None:
|
|
|
155
156
|
print(" \033[32m✓\033[0m Registered!")
|
|
156
157
|
|
|
157
158
|
# Save keypair after successful registration
|
|
159
|
+
import os as _os
|
|
158
160
|
slug = name.lower().replace(" ", "-")
|
|
159
161
|
keys_dir = Path.home() / ".basedagents" / "keys"
|
|
160
162
|
keys_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
# Restrict directory permissions before any key files are written (NEW-4)
|
|
164
|
+
_os.chmod(keys_dir, 0o700)
|
|
161
165
|
keypair_path = keys_dir / f"{slug}-keypair.json"
|
|
162
166
|
i = 2
|
|
163
167
|
while keypair_path.exists():
|
|
@@ -216,6 +220,58 @@ def cmd_validate(args: list[str]) -> None:
|
|
|
216
220
|
print()
|
|
217
221
|
|
|
218
222
|
|
|
223
|
+
# ── scan ──
|
|
224
|
+
|
|
225
|
+
def cmd_scan(args: list[str]) -> None:
|
|
226
|
+
if not args or args[0].startswith("--"):
|
|
227
|
+
_print_err("Usage: basedagents scan <package> [--source npm|github|pypi] [--version <ver>]")
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
|
|
230
|
+
package = args[0]
|
|
231
|
+
rest = args[1:]
|
|
232
|
+
|
|
233
|
+
source = "npm"
|
|
234
|
+
if "--source" in rest:
|
|
235
|
+
idx = rest.index("--source")
|
|
236
|
+
if idx + 1 >= len(rest) or rest[idx + 1].startswith("--"):
|
|
237
|
+
_print_err("--source requires a value (npm, github, pypi)")
|
|
238
|
+
sys.exit(1)
|
|
239
|
+
source = rest[idx + 1]
|
|
240
|
+
|
|
241
|
+
version: str | None = None
|
|
242
|
+
if "--version" in rest:
|
|
243
|
+
idx = rest.index("--version")
|
|
244
|
+
if idx + 1 >= len(rest) or rest[idx + 1].startswith("--"):
|
|
245
|
+
_print_err("--version requires a value")
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
version = rest[idx + 1]
|
|
248
|
+
|
|
249
|
+
from .client import RegistryClient, BasedAgentsError
|
|
250
|
+
|
|
251
|
+
print(f"\n Triggering scan: {package} (source={source})" + (f" v{version}" if version else ""))
|
|
252
|
+
|
|
253
|
+
with RegistryClient() as client:
|
|
254
|
+
try:
|
|
255
|
+
result = client.scan_trigger(package, source=source, version=version)
|
|
256
|
+
except BasedAgentsError as e:
|
|
257
|
+
_print_err(str(e))
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
scan_id = result.get("scan_id") or result.get("id") or ""
|
|
261
|
+
status = result.get("status", "queued")
|
|
262
|
+
_print_ok(f"Scan triggered — status: {status}" + (f" (id: {scan_id})" if scan_id else ""))
|
|
263
|
+
|
|
264
|
+
# If the response already contains report data, print it
|
|
265
|
+
if result.get("score") is not None:
|
|
266
|
+
print(f" Score {result['score']}")
|
|
267
|
+
if result.get("risk"):
|
|
268
|
+
print(f" Risk {result['risk']}")
|
|
269
|
+
if result.get("summary"):
|
|
270
|
+
print(f" Summary {result['summary'][:120]}")
|
|
271
|
+
|
|
272
|
+
print(f"\n Full result: {json.dumps(result, indent=2)}\n")
|
|
273
|
+
|
|
274
|
+
|
|
219
275
|
# ── main ──
|
|
220
276
|
|
|
221
277
|
def main() -> None:
|
|
@@ -233,6 +289,8 @@ def main() -> None:
|
|
|
233
289
|
cmd_whois(rest)
|
|
234
290
|
elif cmd == "validate":
|
|
235
291
|
cmd_validate(rest)
|
|
292
|
+
elif cmd == "scan":
|
|
293
|
+
cmd_scan(rest)
|
|
236
294
|
elif cmd == "version":
|
|
237
295
|
print(f"basedagents {VERSION}")
|
|
238
296
|
else:
|
|
@@ -6,6 +6,8 @@ from __future__ import annotations
|
|
|
6
6
|
import base64
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
9
11
|
import uuid
|
|
10
12
|
from typing import Any, Callable
|
|
11
13
|
|
|
@@ -15,6 +17,12 @@ from .auth import build_headers
|
|
|
15
17
|
from .keypair import AgentKeypair
|
|
16
18
|
from .pow import solve
|
|
17
19
|
|
|
20
|
+
def canonical_json(obj: Any) -> str:
|
|
21
|
+
"""Canonical JSON serialization for signature payloads.
|
|
22
|
+
Uses sort_keys=True and compact separators for deterministic output."""
|
|
23
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
24
|
+
|
|
25
|
+
|
|
18
26
|
_DEFAULT_BASE = "https://api.basedagents.ai"
|
|
19
27
|
# Allow override via env var — use staging URL during tests/development,
|
|
20
28
|
# never point tests at production.
|
|
@@ -30,10 +38,22 @@ class BasedAgentsError(Exception):
|
|
|
30
38
|
super().__init__(f"HTTP {status}: {message}")
|
|
31
39
|
|
|
32
40
|
|
|
41
|
+
_MAX_RETRIES = 3
|
|
42
|
+
|
|
43
|
+
|
|
33
44
|
class RegistryClient:
|
|
34
45
|
def __init__(self, api_url: str = DEFAULT_API_URL, timeout: float = 30.0):
|
|
35
46
|
self._base = api_url.rstrip("/")
|
|
36
|
-
|
|
47
|
+
# PY-NEW-HIGH-2: Reject HTTP URLs unless localhost or explicitly allowed
|
|
48
|
+
is_localhost = "localhost" in api_url or "127.0.0.1" in api_url
|
|
49
|
+
if api_url.startswith("http://") and not is_localhost:
|
|
50
|
+
if os.environ.get("BASEDAGENTS_ALLOW_HTTP") != "1":
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Refusing to use HTTP URL '{api_url}' — credentials would be sent in plaintext. "
|
|
53
|
+
f"Use https:// or set BASEDAGENTS_ALLOW_HTTP=1 to override."
|
|
54
|
+
)
|
|
55
|
+
# PY-HIGH-2: Explicit TLS verification (verify=True is default but stated for clarity)
|
|
56
|
+
self._http = httpx.Client(timeout=timeout, verify=True)
|
|
37
57
|
|
|
38
58
|
def close(self) -> None:
|
|
39
59
|
self._http.close()
|
|
@@ -46,14 +66,28 @@ class RegistryClient:
|
|
|
46
66
|
|
|
47
67
|
# ── Internal ──
|
|
48
68
|
|
|
69
|
+
def _request_with_retry(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
70
|
+
"""PY-MED-1: Retry on 429 with exponential backoff + jitter."""
|
|
71
|
+
for attempt in range(_MAX_RETRIES + 1):
|
|
72
|
+
res = self._http.request(method, path, **kwargs)
|
|
73
|
+
if res.status_code == 429:
|
|
74
|
+
if attempt == _MAX_RETRIES:
|
|
75
|
+
break
|
|
76
|
+
retry_after = int(res.headers.get("retry-after", "5"))
|
|
77
|
+
jitter = random.uniform(0, 1)
|
|
78
|
+
time.sleep(retry_after + jitter)
|
|
79
|
+
continue
|
|
80
|
+
return res
|
|
81
|
+
return res # return last response even if 429
|
|
82
|
+
|
|
49
83
|
def _get(self, path: str) -> Any:
|
|
50
|
-
res = self.
|
|
84
|
+
res = self._request_with_retry("GET", f"{self._base}{path}")
|
|
51
85
|
return self._parse(res)
|
|
52
86
|
|
|
53
87
|
def _post(self, path: str, body: dict[str, Any], headers: dict[str, str] | None = None) -> Any:
|
|
54
88
|
body_str = json.dumps(body)
|
|
55
89
|
h = {"Content-Type": "application/json", **(headers or {})}
|
|
56
|
-
res = self.
|
|
90
|
+
res = self._request_with_retry("POST", f"{self._base}{path}", content=body_str.encode(), headers=h)
|
|
57
91
|
return self._parse(res)
|
|
58
92
|
|
|
59
93
|
def _signed_post(self, keypair: AgentKeypair, path: str, body: dict[str, Any]) -> Any:
|
|
@@ -66,7 +100,7 @@ class RegistryClient:
|
|
|
66
100
|
auth = build_headers(keypair, "PUT", path, body_str)
|
|
67
101
|
body_bytes = body_str.encode()
|
|
68
102
|
h = {"Content-Type": "application/json", **auth}
|
|
69
|
-
res = self.
|
|
103
|
+
res = self._request_with_retry("PUT", f"{self._base}{path}", content=body_bytes, headers=h)
|
|
70
104
|
return self._parse(res)
|
|
71
105
|
|
|
72
106
|
@staticmethod
|
|
@@ -74,13 +108,18 @@ class RegistryClient:
|
|
|
74
108
|
try:
|
|
75
109
|
data = res.json()
|
|
76
110
|
except Exception:
|
|
77
|
-
res.
|
|
111
|
+
if not res.is_success:
|
|
112
|
+
# PY-LOW-1: Truncate raw body to prevent leaking large payloads
|
|
113
|
+
body = res.text[:500]
|
|
114
|
+
raise BasedAgentsError(res.status_code, f"API error: {res.status_code}", details=body)
|
|
78
115
|
return {}
|
|
79
116
|
if not res.is_success:
|
|
117
|
+
# PY-LOW-1: Truncate raw body to prevent leaking large payloads
|
|
118
|
+
body = res.text[:500]
|
|
80
119
|
raise BasedAgentsError(
|
|
81
120
|
res.status_code,
|
|
82
121
|
data.get("message", "Unknown error"),
|
|
83
|
-
|
|
122
|
+
body,
|
|
84
123
|
)
|
|
85
124
|
return data
|
|
86
125
|
|
|
@@ -106,6 +145,12 @@ class RegistryClient:
|
|
|
106
145
|
Returns:
|
|
107
146
|
Agent dict from the server
|
|
108
147
|
"""
|
|
148
|
+
# PY-LOW-3: Input length validation
|
|
149
|
+
if len(profile.get("name", "")) > 100:
|
|
150
|
+
raise ValueError("Agent name must be 100 characters or less")
|
|
151
|
+
if len(profile.get("description", "")) > 1000:
|
|
152
|
+
raise ValueError("Description must be 1000 characters or less")
|
|
153
|
+
|
|
109
154
|
# Step 1: Init
|
|
110
155
|
init = self._post("/v1/register/init", {"public_key": keypair.public_key_b58})
|
|
111
156
|
difficulty: int = init["difficulty"]
|
|
@@ -114,10 +159,16 @@ class RegistryClient:
|
|
|
114
159
|
|
|
115
160
|
# Step 2: Solve PoW (difficulty from server — never hardcoded)
|
|
116
161
|
# Cap difficulty to prevent a malicious/MitM server from exhausting the nonce space
|
|
162
|
+
# MAX_DIFFICULTY caps proof-of-work at 28 leading zero bits.
|
|
163
|
+
# At difficulty 28, expected attempts = 2^28 = ~268M hashes.
|
|
164
|
+
# The nonce is 32-bit (4 bytes), giving 2^32 = ~4B possible values.
|
|
165
|
+
# Difficulty >= 32 would exhaust the nonce space deterministically.
|
|
166
|
+
# We cap at 28 to leave comfortable headroom.
|
|
117
167
|
MAX_DIFFICULTY = 28
|
|
118
168
|
if difficulty > MAX_DIFFICULTY:
|
|
119
169
|
raise BasedAgentsError(0, f"Server requested PoW difficulty {difficulty} which exceeds client cap ({MAX_DIFFICULTY}). Aborting.")
|
|
120
|
-
nonce
|
|
170
|
+
# Challenge-bound PoW: includes challenge in hash to prevent nonce reuse (L3)
|
|
171
|
+
nonce = solve(keypair.public_key_bytes, difficulty, on_progress=on_progress, challenge=challenge)
|
|
121
172
|
|
|
122
173
|
# Step 3: Sign challenge
|
|
123
174
|
# Server verifies: TextEncoder.encode(challenge) i.e. the base64 string as raw UTF-8
|
|
@@ -139,6 +190,11 @@ class RegistryClient:
|
|
|
139
190
|
|
|
140
191
|
def update_profile(self, keypair: AgentKeypair, updates: dict[str, Any]) -> dict[str, Any]:
|
|
141
192
|
"""Update an agent's profile (signed by owner)."""
|
|
193
|
+
# PY-LOW-3: Input length validation
|
|
194
|
+
if len(updates.get("name", "")) > 100:
|
|
195
|
+
raise ValueError("Agent name must be 100 characters or less")
|
|
196
|
+
if len(updates.get("description", "")) > 1000:
|
|
197
|
+
raise ValueError("Description must be 1000 characters or less")
|
|
142
198
|
agent_id = keypair.agent_id
|
|
143
199
|
return self._signed_put(keypair, f"/v1/agents/{agent_id}", updates)
|
|
144
200
|
|
|
@@ -215,7 +271,8 @@ class RegistryClient:
|
|
|
215
271
|
"""
|
|
216
272
|
Submit a verification report.
|
|
217
273
|
|
|
218
|
-
The report signature covers
|
|
274
|
+
The report signature covers all fields including structured_report
|
|
275
|
+
so they're protected by the agent's Ed25519 signature.
|
|
219
276
|
result must be one of: "pass" | "fail" | "timeout"
|
|
220
277
|
"""
|
|
221
278
|
if result not in ("pass", "fail", "timeout"):
|
|
@@ -223,10 +280,18 @@ class RegistryClient:
|
|
|
223
280
|
|
|
224
281
|
nonce = str(uuid.uuid4())
|
|
225
282
|
|
|
226
|
-
# Build
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
283
|
+
# Build structured_report first so it can be included in the signed payload
|
|
284
|
+
structured_report_obj: dict[str, Any] | None = None
|
|
285
|
+
if capabilities_confirmed is not None or safety_issues or unauthorized_actions:
|
|
286
|
+
structured_report_obj = {
|
|
287
|
+
"capabilities_confirmed": capabilities_confirmed or [],
|
|
288
|
+
"safety_issues": safety_issues,
|
|
289
|
+
"unauthorized_actions": unauthorized_actions,
|
|
290
|
+
**({"notes": notes} if notes else {}),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Build the signed payload — includes structured_report so it's
|
|
294
|
+
# covered by the agent's Ed25519 signature (prevents tampering).
|
|
230
295
|
signed_fields: dict[str, Any] = {
|
|
231
296
|
"assignment_id": assignment_id,
|
|
232
297
|
"target_id": target_id,
|
|
@@ -239,23 +304,18 @@ class RegistryClient:
|
|
|
239
304
|
signed_fields["notes"] = notes
|
|
240
305
|
if response_time_ms is not None:
|
|
241
306
|
signed_fields["response_time_ms"] = response_time_ms
|
|
307
|
+
if structured_report_obj is not None:
|
|
308
|
+
signed_fields["structured_report"] = structured_report_obj
|
|
242
309
|
|
|
243
|
-
report_data =
|
|
310
|
+
report_data = canonical_json(signed_fields)
|
|
244
311
|
report_sig = keypair.sign(report_data.encode("utf-8"))
|
|
245
312
|
sig_b64 = base64.b64encode(report_sig).decode("ascii")
|
|
246
313
|
|
|
247
|
-
# Full body
|
|
314
|
+
# Full body = signed fields + signature
|
|
248
315
|
body: dict[str, Any] = {
|
|
249
316
|
**signed_fields,
|
|
250
317
|
"signature": sig_b64,
|
|
251
318
|
}
|
|
252
|
-
if capabilities_confirmed is not None or safety_issues or unauthorized_actions:
|
|
253
|
-
body["structured_report"] = {
|
|
254
|
-
"capabilities_confirmed": capabilities_confirmed or [],
|
|
255
|
-
"safety_issues": safety_issues,
|
|
256
|
-
"unauthorized_actions": unauthorized_actions,
|
|
257
|
-
**({"notes": notes} if notes else {}),
|
|
258
|
-
}
|
|
259
319
|
|
|
260
320
|
return self._signed_post(keypair, "/v1/verify/submit", body)
|
|
261
321
|
|
|
@@ -266,3 +326,121 @@ class RegistryClient:
|
|
|
266
326
|
|
|
267
327
|
def get_chain_entry(self, sequence: int) -> dict[str, Any]:
|
|
268
328
|
return self._get(f"/v1/chain/{sequence}")
|
|
329
|
+
|
|
330
|
+
# ── Scanner ──
|
|
331
|
+
|
|
332
|
+
def scan_trigger(
|
|
333
|
+
self,
|
|
334
|
+
package: str,
|
|
335
|
+
source: str = "npm",
|
|
336
|
+
version: str | None = None,
|
|
337
|
+
ref: str | None = None,
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
"""Trigger a server-side package scan."""
|
|
340
|
+
body: dict[str, Any] = {}
|
|
341
|
+
if source == "npm":
|
|
342
|
+
body["package"] = package
|
|
343
|
+
if version:
|
|
344
|
+
body["version"] = version
|
|
345
|
+
else:
|
|
346
|
+
body["source"] = source
|
|
347
|
+
body["target"] = package
|
|
348
|
+
if ref:
|
|
349
|
+
body["ref"] = ref
|
|
350
|
+
if version and version != "latest":
|
|
351
|
+
body["version"] = version
|
|
352
|
+
return self._post("/v1/scan/trigger", body)
|
|
353
|
+
|
|
354
|
+
def get_scan_report(self, identifier: str, version: str | None = None) -> dict[str, Any]:
|
|
355
|
+
"""Get a scan report by package identifier (e.g., 'lodash', 'github:owner/repo', 'pypi:requests')."""
|
|
356
|
+
qs = f"?version={version}" if version else ""
|
|
357
|
+
return self._get(f"/v1/scan/{identifier}{qs}")
|
|
358
|
+
|
|
359
|
+
def list_scan_reports(
|
|
360
|
+
self,
|
|
361
|
+
limit: int = 20,
|
|
362
|
+
offset: int = 0,
|
|
363
|
+
sort: str = "recent",
|
|
364
|
+
source: str | None = None,
|
|
365
|
+
) -> dict[str, Any]:
|
|
366
|
+
"""List scan reports."""
|
|
367
|
+
params = f"?limit={limit}&offset={offset}&sort={sort}"
|
|
368
|
+
if source:
|
|
369
|
+
params += f"&source={source}"
|
|
370
|
+
return self._get(f"/v1/scan{params}")
|
|
371
|
+
|
|
372
|
+
# ── Tasks ──
|
|
373
|
+
|
|
374
|
+
def create_task(
|
|
375
|
+
self,
|
|
376
|
+
keypair: AgentKeypair,
|
|
377
|
+
title: str,
|
|
378
|
+
description: str,
|
|
379
|
+
**kwargs: Any,
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""Create a task."""
|
|
382
|
+
body = {"title": title, "description": description, **kwargs}
|
|
383
|
+
return self._signed_post(keypair, "/v1/tasks", body)
|
|
384
|
+
|
|
385
|
+
def get_task(self, task_id: str) -> dict[str, Any]:
|
|
386
|
+
"""Get task details."""
|
|
387
|
+
return self._get(f"/v1/tasks/{task_id}")
|
|
388
|
+
|
|
389
|
+
def list_tasks(
|
|
390
|
+
self,
|
|
391
|
+
status: str | None = None,
|
|
392
|
+
limit: int = 20,
|
|
393
|
+
offset: int = 0,
|
|
394
|
+
) -> dict[str, Any]:
|
|
395
|
+
"""List tasks."""
|
|
396
|
+
params = f"?limit={limit}&offset={offset}"
|
|
397
|
+
if status:
|
|
398
|
+
params += f"&status={status}"
|
|
399
|
+
return self._get(f"/v1/tasks{params}")
|
|
400
|
+
|
|
401
|
+
def claim_task(self, keypair: AgentKeypair, task_id: str) -> dict[str, Any]:
|
|
402
|
+
"""Claim a task."""
|
|
403
|
+
return self._signed_post(keypair, f"/v1/tasks/{task_id}/claim", {})
|
|
404
|
+
|
|
405
|
+
def submit_task(
|
|
406
|
+
self,
|
|
407
|
+
keypair: AgentKeypair,
|
|
408
|
+
task_id: str,
|
|
409
|
+
content: str,
|
|
410
|
+
summary: str,
|
|
411
|
+
submission_type: str = "json",
|
|
412
|
+
) -> dict[str, Any]:
|
|
413
|
+
"""Submit task deliverable."""
|
|
414
|
+
return self._signed_post(keypair, f"/v1/tasks/{task_id}/submit", {
|
|
415
|
+
"content": content,
|
|
416
|
+
"summary": summary,
|
|
417
|
+
"submission_type": submission_type,
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
def verify_task(self, keypair: AgentKeypair, task_id: str) -> dict[str, Any]:
|
|
421
|
+
"""Verify/accept a task deliverable."""
|
|
422
|
+
return self._signed_post(keypair, f"/v1/tasks/{task_id}/verify", {})
|
|
423
|
+
|
|
424
|
+
# ── Probe (MCP Playground) ──
|
|
425
|
+
|
|
426
|
+
def probe_agent(
|
|
427
|
+
self,
|
|
428
|
+
agent_id: str,
|
|
429
|
+
method: str = "tools/list",
|
|
430
|
+
params: dict[str, Any] | None = None,
|
|
431
|
+
) -> dict[str, Any]:
|
|
432
|
+
"""Probe an agent's MCP endpoint."""
|
|
433
|
+
return self._post(f"/v1/agents/{agent_id}/probe", {
|
|
434
|
+
"method": method,
|
|
435
|
+
"params": params or {},
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
# ── Skills ──
|
|
439
|
+
|
|
440
|
+
def get_agent_skills(self, agent_id: str) -> dict[str, Any]:
|
|
441
|
+
"""Get resolved skills for an agent."""
|
|
442
|
+
return self._get(f"/v1/skills/agent/{agent_id}")
|
|
443
|
+
|
|
444
|
+
def get_skill(self, registry: str, name: str) -> dict[str, Any]:
|
|
445
|
+
"""Look up a skill by registry and name."""
|
|
446
|
+
return self._get(f"/v1/skills/{registry}/{name}")
|