basedagents 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.
- basedagents/__init__.py +31 -0
- basedagents/auth.py +56 -0
- basedagents/cli.py +238 -0
- basedagents/client.py +256 -0
- basedagents/keypair.py +109 -0
- basedagents/pow.py +55 -0
- basedagents-0.1.0.dist-info/METADATA +102 -0
- basedagents-0.1.0.dist-info/RECORD +11 -0
- basedagents-0.1.0.dist-info/WHEEL +5 -0
- basedagents-0.1.0.dist-info/entry_points.txt +2 -0
- basedagents-0.1.0.dist-info/top_level.txt +1 -0
basedagents/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
basedagents — Python SDK for basedagents.ai
|
|
3
|
+
|
|
4
|
+
Cryptographic identity and reputation registry for AI agents.
|
|
5
|
+
|
|
6
|
+
Quick start:
|
|
7
|
+
from basedagents import generate_keypair, RegistryClient
|
|
8
|
+
|
|
9
|
+
keypair = generate_keypair()
|
|
10
|
+
with RegistryClient() as client:
|
|
11
|
+
agent = client.register(keypair, {
|
|
12
|
+
"name": "MyAgent",
|
|
13
|
+
"description": "Does useful things.",
|
|
14
|
+
"capabilities": ["reasoning", "code"],
|
|
15
|
+
"protocols": ["https"],
|
|
16
|
+
})
|
|
17
|
+
print(agent["agent_id"])
|
|
18
|
+
"""
|
|
19
|
+
from .keypair import AgentKeypair, generate as generate_keypair, from_private_key_hex
|
|
20
|
+
from .client import RegistryClient, BasedAgentsError
|
|
21
|
+
from .auth import build_headers as build_auth_headers
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AgentKeypair",
|
|
26
|
+
"RegistryClient",
|
|
27
|
+
"BasedAgentsError",
|
|
28
|
+
"generate_keypair",
|
|
29
|
+
"from_private_key_hex",
|
|
30
|
+
"build_auth_headers",
|
|
31
|
+
]
|
basedagents/auth.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentSig authentication headers.
|
|
3
|
+
|
|
4
|
+
Authorization: AgentSig <base58_pubkey>:<base64_signature>
|
|
5
|
+
X-Timestamp: <unix_seconds>
|
|
6
|
+
|
|
7
|
+
Signed message (UTF-8 encoded, then Ed25519-signed):
|
|
8
|
+
"<METHOD>:<path>:<timestamp_sec>:<sha256_hex_of_body>"
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import hashlib
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
from .keypair import AgentKeypair
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_headers(
|
|
20
|
+
keypair: AgentKeypair,
|
|
21
|
+
method: str,
|
|
22
|
+
path: str,
|
|
23
|
+
body: bytes | str | None = None,
|
|
24
|
+
timestamp: int | None = None,
|
|
25
|
+
) -> dict[str, str]:
|
|
26
|
+
"""
|
|
27
|
+
Build AgentSig auth headers for a signed request.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
keypair: Agent keypair
|
|
31
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
32
|
+
path: URL path including leading slash (e.g. '/v1/verify/submit')
|
|
33
|
+
body: Request body bytes or string (empty string / None for GET)
|
|
34
|
+
timestamp: Unix timestamp in seconds (defaults to now)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dict with 'Authorization' and 'X-Timestamp' headers.
|
|
38
|
+
"""
|
|
39
|
+
ts = timestamp or int(time.time())
|
|
40
|
+
|
|
41
|
+
if body is None:
|
|
42
|
+
body_bytes = b""
|
|
43
|
+
elif isinstance(body, str):
|
|
44
|
+
body_bytes = body.encode("utf-8")
|
|
45
|
+
else:
|
|
46
|
+
body_bytes = body
|
|
47
|
+
|
|
48
|
+
body_hash = hashlib.sha256(body_bytes).hexdigest()
|
|
49
|
+
message = f"{method.upper()}:{path}:{ts}:{body_hash}".encode("utf-8")
|
|
50
|
+
signature = keypair.sign(message)
|
|
51
|
+
sig_b64 = base64.b64encode(signature).decode("ascii")
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"Authorization": f"AgentSig {keypair.public_key_b58}:{sig_b64}",
|
|
55
|
+
"X-Timestamp": str(ts),
|
|
56
|
+
}
|
basedagents/cli.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
basedagents CLI
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
basedagents register [--manifest <file>] [--api <url>] [--dry-run]
|
|
6
|
+
basedagents whois <name>
|
|
7
|
+
basedagents validate [--keypair <file>]
|
|
8
|
+
basedagents version
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
API_URL = "https://api.basedagents.ai"
|
|
17
|
+
VERSION = "0.1.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _print_err(msg: str) -> None:
|
|
21
|
+
print(f"\033[31m ✗ {msg}\033[0m", file=sys.stderr)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _print_ok(msg: str) -> None:
|
|
25
|
+
print(f"\033[32m ✓ {msg}\033[0m")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _progress(attempts: int) -> None:
|
|
29
|
+
print(f"\r Solving PoW... {attempts:,} attempts", end="", flush=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── whois ──
|
|
33
|
+
|
|
34
|
+
def cmd_whois(args: list[str]) -> None:
|
|
35
|
+
if not args:
|
|
36
|
+
_print_err("Usage: basedagents whois <name|agent_id>")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
query = args[0]
|
|
40
|
+
from .client import RegistryClient, BasedAgentsError
|
|
41
|
+
|
|
42
|
+
with RegistryClient() as client:
|
|
43
|
+
try:
|
|
44
|
+
if query.startswith("ag_"):
|
|
45
|
+
agent = client.get_agent(query)
|
|
46
|
+
else:
|
|
47
|
+
agent = client.whois(query)
|
|
48
|
+
if agent is None:
|
|
49
|
+
_print_err(f"No agent found: {query}")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
except BasedAgentsError as e:
|
|
52
|
+
_print_err(str(e))
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
print(f"\n Name {agent['name']}")
|
|
56
|
+
print(f" ID {agent['agent_id']}")
|
|
57
|
+
print(f" Status {agent['status']}")
|
|
58
|
+
print(f" Reputation {agent.get('reputation_score', 0)}")
|
|
59
|
+
print(f" Verified {agent.get('verification_count', 0)} verifications")
|
|
60
|
+
if agent.get("description"):
|
|
61
|
+
print(f" Description {agent['description'][:80]}")
|
|
62
|
+
if agent.get("capabilities"):
|
|
63
|
+
caps = agent["capabilities"] if isinstance(agent["capabilities"], list) else json.loads(agent["capabilities"])
|
|
64
|
+
print(f" Capabilities {', '.join(caps)}")
|
|
65
|
+
print(f" Profile https://basedagents.ai/agents/{agent['agent_id']}\n")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── register ──
|
|
69
|
+
|
|
70
|
+
def cmd_register(args: list[str]) -> None:
|
|
71
|
+
api_url = API_URL
|
|
72
|
+
if "--api" in args:
|
|
73
|
+
idx = args.index("--api")
|
|
74
|
+
api_url = args[idx + 1]
|
|
75
|
+
|
|
76
|
+
dry_run = "--dry-run" in args
|
|
77
|
+
|
|
78
|
+
manifest_path: Path | None = None
|
|
79
|
+
if "--manifest" in args:
|
|
80
|
+
idx = args.index("--manifest")
|
|
81
|
+
if idx + 1 >= len(args) or args[idx + 1].startswith("--"):
|
|
82
|
+
_print_err("--manifest requires a file path")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
manifest_path = Path(args[idx + 1])
|
|
85
|
+
if not manifest_path.exists():
|
|
86
|
+
_print_err(f"Manifest file not found: {manifest_path}")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
if manifest_path is None:
|
|
90
|
+
_print_err("Interactive registration not yet supported in Python CLI. Use --manifest.")
|
|
91
|
+
print("\n Example:")
|
|
92
|
+
print(" basedagents register --manifest ./agent.manifest.json\n")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
# Load manifest
|
|
96
|
+
try:
|
|
97
|
+
manifest = json.loads(manifest_path.read_text())
|
|
98
|
+
except Exception as e:
|
|
99
|
+
_print_err(f"Invalid manifest JSON: {e}")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
name = manifest.get("name") or manifest.get("identity", {}).get("name")
|
|
103
|
+
description = manifest.get("description") or manifest.get("identity", {}).get("description")
|
|
104
|
+
capabilities = manifest.get("capabilities", [])
|
|
105
|
+
protocols = manifest.get("protocols", ["https"])
|
|
106
|
+
|
|
107
|
+
if not name or not description or not capabilities:
|
|
108
|
+
_print_err("Manifest must have name, description, and at least one capability")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
print(f"\n\033[1mbasedagents register\033[0m --manifest")
|
|
112
|
+
print(f"\n Name {name}")
|
|
113
|
+
print(f" Capabilities {', '.join(capabilities[:4])}{'…' if len(capabilities) > 4 else ''}")
|
|
114
|
+
print()
|
|
115
|
+
|
|
116
|
+
if dry_run:
|
|
117
|
+
print(" --dry-run: stopping here.\n")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
from .keypair import generate
|
|
121
|
+
from .client import RegistryClient, BasedAgentsError
|
|
122
|
+
|
|
123
|
+
# Generate keypair
|
|
124
|
+
print(" Generating Ed25519 keypair...", end="", flush=True)
|
|
125
|
+
keypair = generate()
|
|
126
|
+
print(f" \033[32m✓\033[0m")
|
|
127
|
+
|
|
128
|
+
profile = {
|
|
129
|
+
"name": name,
|
|
130
|
+
"description": description,
|
|
131
|
+
"capabilities": capabilities,
|
|
132
|
+
"protocols": protocols,
|
|
133
|
+
}
|
|
134
|
+
for field in ["contact_endpoint", "homepage", "organization", "tags", "skills", "offers", "needs", "version"]:
|
|
135
|
+
if manifest.get(field):
|
|
136
|
+
profile[field] = manifest[field]
|
|
137
|
+
|
|
138
|
+
with RegistryClient(api_url) as client:
|
|
139
|
+
try:
|
|
140
|
+
agent = client.register(keypair, profile, on_progress=_progress)
|
|
141
|
+
except BasedAgentsError as e:
|
|
142
|
+
print()
|
|
143
|
+
if e.status == 409:
|
|
144
|
+
_print_err(f"Name conflict: an agent named '{name}' already exists.")
|
|
145
|
+
elif e.status == 400:
|
|
146
|
+
_print_err(f"Invalid profile: {e.message}")
|
|
147
|
+
else:
|
|
148
|
+
_print_err(f"Registration failed: {e.message}")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
print(f"\r \033[32m✓\033[0m Proof-of-work solved ")
|
|
152
|
+
print(" \033[32m✓\033[0m Registered!")
|
|
153
|
+
|
|
154
|
+
# Save keypair after successful registration
|
|
155
|
+
slug = name.lower().replace(" ", "-")
|
|
156
|
+
keys_dir = Path.home() / ".basedagents" / "keys"
|
|
157
|
+
keys_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
keypair_path = keys_dir / f"{slug}-keypair.json"
|
|
159
|
+
i = 2
|
|
160
|
+
while keypair_path.exists():
|
|
161
|
+
keypair_path = keys_dir / f"{slug}-{i}-keypair.json"
|
|
162
|
+
i += 1
|
|
163
|
+
keypair.save(keypair_path)
|
|
164
|
+
|
|
165
|
+
agent_id = agent.get("agent_id", keypair.agent_id)
|
|
166
|
+
print(f"\n Agent ID \033[36m{agent_id}\033[0m")
|
|
167
|
+
print(f" Status {agent.get('status', 'pending')}")
|
|
168
|
+
print(f" Profile https://basedagents.ai/agents/{agent_id}")
|
|
169
|
+
print(f" Keypair {keypair_path}")
|
|
170
|
+
print(f"\n \033[33m⚠ Back up {keypair_path} — losing it means losing control of this agent.\033[0m\n")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── validate ──
|
|
174
|
+
|
|
175
|
+
def cmd_validate(args: list[str]) -> None:
|
|
176
|
+
keypair_path: Path | None = None
|
|
177
|
+
if "--keypair" in args:
|
|
178
|
+
idx = args.index("--keypair")
|
|
179
|
+
keypair_path = Path(args[idx + 1])
|
|
180
|
+
else:
|
|
181
|
+
# Try to find a keypair in ~/.basedagents/keys/
|
|
182
|
+
keys_dir = Path.home() / ".basedagents" / "keys"
|
|
183
|
+
if keys_dir.exists():
|
|
184
|
+
files = list(keys_dir.glob("*-keypair.json"))
|
|
185
|
+
if files:
|
|
186
|
+
keypair_path = files[0]
|
|
187
|
+
|
|
188
|
+
if keypair_path is None or not keypair_path.exists():
|
|
189
|
+
_print_err("No keypair found. Specify with --keypair <path>")
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
|
|
192
|
+
from .keypair import AgentKeypair
|
|
193
|
+
from .client import RegistryClient, BasedAgentsError
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
kp = AgentKeypair.load(keypair_path)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
_print_err(f"Failed to load keypair: {e}")
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
|
|
201
|
+
print(f"\n Keypair {keypair_path}")
|
|
202
|
+
print(f" Agent ID {kp.agent_id}")
|
|
203
|
+
|
|
204
|
+
with RegistryClient() as client:
|
|
205
|
+
try:
|
|
206
|
+
agent = client.get_agent(kp.agent_id)
|
|
207
|
+
_print_ok(f"Registered — {agent['status']}, rep={agent.get('reputation_score', 0)}")
|
|
208
|
+
except BasedAgentsError as e:
|
|
209
|
+
if e.status == 404:
|
|
210
|
+
_print_err("Agent not found in registry — not yet registered?")
|
|
211
|
+
else:
|
|
212
|
+
_print_err(str(e))
|
|
213
|
+
print()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ── main ──
|
|
217
|
+
|
|
218
|
+
def main() -> None:
|
|
219
|
+
args = sys.argv[1:]
|
|
220
|
+
if not args or args[0] in ("-h", "--help"):
|
|
221
|
+
print(__doc__)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
cmd = args[0]
|
|
225
|
+
rest = args[1:]
|
|
226
|
+
|
|
227
|
+
if cmd == "register":
|
|
228
|
+
cmd_register(rest)
|
|
229
|
+
elif cmd == "whois":
|
|
230
|
+
cmd_whois(rest)
|
|
231
|
+
elif cmd == "validate":
|
|
232
|
+
cmd_validate(rest)
|
|
233
|
+
elif cmd == "version":
|
|
234
|
+
print(f"basedagents {VERSION}")
|
|
235
|
+
else:
|
|
236
|
+
_print_err(f"Unknown command: {cmd}")
|
|
237
|
+
print(__doc__)
|
|
238
|
+
sys.exit(1)
|
basedagents/client.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BasedAgents registry client.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .auth import build_headers
|
|
14
|
+
from .keypair import AgentKeypair
|
|
15
|
+
from .pow import solve
|
|
16
|
+
|
|
17
|
+
DEFAULT_API_URL = "https://api.basedagents.ai"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BasedAgentsError(Exception):
|
|
21
|
+
"""Raised when the API returns an error response."""
|
|
22
|
+
def __init__(self, status: int, message: str, details: Any = None):
|
|
23
|
+
self.status = status
|
|
24
|
+
self.message = message
|
|
25
|
+
self.details = details
|
|
26
|
+
super().__init__(f"HTTP {status}: {message}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RegistryClient:
|
|
30
|
+
def __init__(self, api_url: str = DEFAULT_API_URL, timeout: float = 30.0):
|
|
31
|
+
self._base = api_url.rstrip("/")
|
|
32
|
+
self._http = httpx.Client(timeout=timeout)
|
|
33
|
+
|
|
34
|
+
def close(self) -> None:
|
|
35
|
+
self._http.close()
|
|
36
|
+
|
|
37
|
+
def __enter__(self) -> "RegistryClient":
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def __exit__(self, *_: Any) -> None:
|
|
41
|
+
self.close()
|
|
42
|
+
|
|
43
|
+
# ── Internal ──
|
|
44
|
+
|
|
45
|
+
def _get(self, path: str) -> Any:
|
|
46
|
+
res = self._http.get(f"{self._base}{path}")
|
|
47
|
+
return self._parse(res)
|
|
48
|
+
|
|
49
|
+
def _post(self, path: str, body: dict[str, Any], headers: dict[str, str] | None = None) -> Any:
|
|
50
|
+
body_str = json.dumps(body)
|
|
51
|
+
h = {"Content-Type": "application/json", **(headers or {})}
|
|
52
|
+
res = self._http.post(f"{self._base}{path}", content=body_str.encode(), headers=h)
|
|
53
|
+
return self._parse(res)
|
|
54
|
+
|
|
55
|
+
def _signed_post(self, keypair: AgentKeypair, path: str, body: dict[str, Any]) -> Any:
|
|
56
|
+
body_str = json.dumps(body)
|
|
57
|
+
auth = build_headers(keypair, "POST", path, body_str)
|
|
58
|
+
return self._post(path, body, headers=auth)
|
|
59
|
+
|
|
60
|
+
def _signed_put(self, keypair: AgentKeypair, path: str, body: dict[str, Any]) -> Any:
|
|
61
|
+
body_str = json.dumps(body)
|
|
62
|
+
auth = build_headers(keypair, "PUT", path, body_str)
|
|
63
|
+
body_bytes = body_str.encode()
|
|
64
|
+
h = {"Content-Type": "application/json", **auth}
|
|
65
|
+
res = self._http.put(f"{self._base}{path}", content=body_bytes, headers=h)
|
|
66
|
+
return self._parse(res)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _parse(res: httpx.Response) -> Any:
|
|
70
|
+
try:
|
|
71
|
+
data = res.json()
|
|
72
|
+
except Exception:
|
|
73
|
+
res.raise_for_status()
|
|
74
|
+
return {}
|
|
75
|
+
if not res.is_success:
|
|
76
|
+
raise BasedAgentsError(
|
|
77
|
+
res.status_code,
|
|
78
|
+
data.get("message", "Unknown error"),
|
|
79
|
+
data.get("details"),
|
|
80
|
+
)
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
# ── Registration ──
|
|
84
|
+
|
|
85
|
+
def register(
|
|
86
|
+
self,
|
|
87
|
+
keypair: AgentKeypair,
|
|
88
|
+
profile: dict[str, Any],
|
|
89
|
+
on_progress: Callable[[int], None] | None = None,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Register an agent. Handles the full 3-step flow:
|
|
93
|
+
1. POST /v1/register/init
|
|
94
|
+
2. Solve proof-of-work (difficulty from server)
|
|
95
|
+
3. POST /v1/register/complete
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
keypair: Agent keypair
|
|
99
|
+
profile: Profile dict (name, description, capabilities, protocols, ...)
|
|
100
|
+
on_progress: Optional callback(attempts) for PoW progress reporting
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Agent dict from the server
|
|
104
|
+
"""
|
|
105
|
+
# Step 1: Init
|
|
106
|
+
init = self._post("/v1/register/init", {"public_key": keypair.public_key_b58})
|
|
107
|
+
difficulty: int = init["difficulty"]
|
|
108
|
+
challenge: str = init["challenge"]
|
|
109
|
+
challenge_id: str = init["challenge_id"]
|
|
110
|
+
|
|
111
|
+
# Step 2: Solve PoW (difficulty from server — never hardcoded)
|
|
112
|
+
nonce = solve(keypair.public_key_bytes, difficulty, on_progress=on_progress)
|
|
113
|
+
|
|
114
|
+
# Step 3: Sign challenge
|
|
115
|
+
# Server verifies: TextEncoder.encode(challenge) i.e. the base64 string as raw UTF-8
|
|
116
|
+
challenge_bytes = challenge.encode("utf-8")
|
|
117
|
+
signature = keypair.sign(challenge_bytes)
|
|
118
|
+
sig_b64 = base64.b64encode(signature).decode("ascii")
|
|
119
|
+
|
|
120
|
+
# Step 4: Complete
|
|
121
|
+
result = self._post("/v1/register/complete", {
|
|
122
|
+
"challenge_id": challenge_id,
|
|
123
|
+
"public_key": keypair.public_key_b58,
|
|
124
|
+
"nonce": nonce,
|
|
125
|
+
"signature": sig_b64,
|
|
126
|
+
"profile": profile,
|
|
127
|
+
})
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
# ── Profile ──
|
|
131
|
+
|
|
132
|
+
def update_profile(self, keypair: AgentKeypair, updates: dict[str, Any]) -> dict[str, Any]:
|
|
133
|
+
"""Update an agent's profile (signed by owner)."""
|
|
134
|
+
agent_id = keypair.agent_id
|
|
135
|
+
return self._signed_put(keypair, f"/v1/agents/{agent_id}", updates)
|
|
136
|
+
|
|
137
|
+
# ── Lookup ──
|
|
138
|
+
|
|
139
|
+
def get_agent(self, agent_id: str) -> dict[str, Any]:
|
|
140
|
+
"""Get an agent by ID."""
|
|
141
|
+
return self._get(f"/v1/agents/{agent_id}")
|
|
142
|
+
|
|
143
|
+
def get_reputation(self, agent_id: str) -> dict[str, Any]:
|
|
144
|
+
"""Get detailed reputation breakdown for an agent."""
|
|
145
|
+
return self._get(f"/v1/agents/{agent_id}/reputation")
|
|
146
|
+
|
|
147
|
+
def search(
|
|
148
|
+
self,
|
|
149
|
+
q: str | None = None,
|
|
150
|
+
capabilities: list[str] | None = None,
|
|
151
|
+
protocols: list[str] | None = None,
|
|
152
|
+
status: str | None = None,
|
|
153
|
+
sort: str = "reputation",
|
|
154
|
+
limit: int = 20,
|
|
155
|
+
page: int = 1,
|
|
156
|
+
) -> dict[str, Any]:
|
|
157
|
+
"""Search agents."""
|
|
158
|
+
from urllib.parse import urlencode
|
|
159
|
+
params: dict[str, str] = {}
|
|
160
|
+
if q:
|
|
161
|
+
params["q"] = q
|
|
162
|
+
if capabilities:
|
|
163
|
+
params["capabilities"] = ",".join(capabilities)
|
|
164
|
+
if protocols:
|
|
165
|
+
params["protocols"] = ",".join(protocols)
|
|
166
|
+
if status:
|
|
167
|
+
params["status"] = status
|
|
168
|
+
params["sort"] = sort
|
|
169
|
+
params["limit"] = str(limit)
|
|
170
|
+
params["page"] = str(page)
|
|
171
|
+
return self._get(f"/v1/agents/search?{urlencode(params)}")
|
|
172
|
+
|
|
173
|
+
def whois(self, name: str) -> dict[str, Any] | None:
|
|
174
|
+
"""Look up an agent by name. Returns None if not found."""
|
|
175
|
+
result = self.search(q=name, limit=1)
|
|
176
|
+
agents = result.get("agents", [])
|
|
177
|
+
if not agents:
|
|
178
|
+
return None
|
|
179
|
+
# Exact name match (case-insensitive)
|
|
180
|
+
for agent in agents:
|
|
181
|
+
if agent.get("name", "").lower() == name.lower():
|
|
182
|
+
return agent
|
|
183
|
+
return agents[0]
|
|
184
|
+
|
|
185
|
+
# ── Verification ──
|
|
186
|
+
|
|
187
|
+
def get_assignment(self, keypair: AgentKeypair) -> dict[str, Any]:
|
|
188
|
+
"""Get a verification assignment for this agent."""
|
|
189
|
+
auth = build_headers(keypair, "GET", "/v1/verify/assignment")
|
|
190
|
+
res = self._http.get(f"{self._base}/v1/verify/assignment", headers=auth)
|
|
191
|
+
return self._parse(res)
|
|
192
|
+
|
|
193
|
+
def submit_verification(
|
|
194
|
+
self,
|
|
195
|
+
keypair: AgentKeypair,
|
|
196
|
+
assignment_id: str,
|
|
197
|
+
target_id: str,
|
|
198
|
+
result: str, # "pass" | "fail" | "timeout"
|
|
199
|
+
coherence_score: float | None = None,
|
|
200
|
+
notes: str | None = None,
|
|
201
|
+
response_time_ms: int | None = None,
|
|
202
|
+
capabilities_confirmed: list[str] | None = None,
|
|
203
|
+
safety_issues: bool = False,
|
|
204
|
+
unauthorized_actions: bool = False,
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
"""
|
|
207
|
+
Submit a verification report.
|
|
208
|
+
|
|
209
|
+
The report signature covers the inner fields only (not structured_report).
|
|
210
|
+
result must be one of: "pass" | "fail" | "timeout"
|
|
211
|
+
"""
|
|
212
|
+
if result not in ("pass", "fail", "timeout"):
|
|
213
|
+
raise ValueError(f"result must be 'pass', 'fail', or 'timeout', got {result!r}")
|
|
214
|
+
|
|
215
|
+
nonce = str(uuid.uuid4())
|
|
216
|
+
|
|
217
|
+
# Build the signed payload (subset — no structured_report)
|
|
218
|
+
signed_fields: dict[str, Any] = {
|
|
219
|
+
"assignment_id": assignment_id,
|
|
220
|
+
"target_id": target_id,
|
|
221
|
+
"result": result,
|
|
222
|
+
"nonce": nonce,
|
|
223
|
+
}
|
|
224
|
+
if coherence_score is not None:
|
|
225
|
+
signed_fields["coherence_score"] = coherence_score
|
|
226
|
+
if notes is not None:
|
|
227
|
+
signed_fields["notes"] = notes
|
|
228
|
+
if response_time_ms is not None:
|
|
229
|
+
signed_fields["response_time_ms"] = response_time_ms
|
|
230
|
+
|
|
231
|
+
report_data = json.dumps(signed_fields, separators=(",", ":"), sort_keys=False)
|
|
232
|
+
report_sig = keypair.sign(report_data.encode("utf-8"))
|
|
233
|
+
sig_b64 = base64.b64encode(report_sig).decode("ascii")
|
|
234
|
+
|
|
235
|
+
# Full body adds structured_report and signature
|
|
236
|
+
body: dict[str, Any] = {
|
|
237
|
+
**signed_fields,
|
|
238
|
+
"signature": sig_b64,
|
|
239
|
+
}
|
|
240
|
+
if capabilities_confirmed is not None or safety_issues or unauthorized_actions:
|
|
241
|
+
body["structured_report"] = {
|
|
242
|
+
"capabilities_confirmed": capabilities_confirmed or [],
|
|
243
|
+
"safety_issues": safety_issues,
|
|
244
|
+
"unauthorized_actions": unauthorized_actions,
|
|
245
|
+
**({"notes": notes} if notes else {}),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return self._signed_post(keypair, "/v1/verify/submit", body)
|
|
249
|
+
|
|
250
|
+
# ── Chain ──
|
|
251
|
+
|
|
252
|
+
def get_chain_status(self) -> dict[str, Any]:
|
|
253
|
+
return self._get("/v1/status")
|
|
254
|
+
|
|
255
|
+
def get_chain_entry(self, sequence: int) -> dict[str, Any]:
|
|
256
|
+
return self._get(f"/v1/chain/{sequence}")
|
basedagents/keypair.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ed25519 keypair generation and serialization.
|
|
3
|
+
|
|
4
|
+
Agent ID = "ag_" + base58(public_key_bytes)
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
14
|
+
Ed25519PrivateKey,
|
|
15
|
+
Ed25519PublicKey,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Base58 alphabet (Bitcoin-style, no 0OIl)
|
|
19
|
+
_B58_ALPHA = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _base58_encode(data: bytes) -> str:
|
|
23
|
+
n = int.from_bytes(data, "big")
|
|
24
|
+
result = ""
|
|
25
|
+
while n > 0:
|
|
26
|
+
result = _B58_ALPHA[n % 58] + result
|
|
27
|
+
n //= 58
|
|
28
|
+
# Leading zero bytes → leading '1's
|
|
29
|
+
for byte in data:
|
|
30
|
+
if byte == 0:
|
|
31
|
+
result = "1" + result
|
|
32
|
+
else:
|
|
33
|
+
break
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _base58_decode(s: str) -> bytes:
|
|
38
|
+
n = 0
|
|
39
|
+
for char in s:
|
|
40
|
+
n = n * 58 + _B58_ALPHA.index(char)
|
|
41
|
+
length = (n.bit_length() + 7) // 8
|
|
42
|
+
result = n.to_bytes(length, "big")
|
|
43
|
+
# Restore leading zero bytes
|
|
44
|
+
pad = len(s) - len(s.lstrip("1"))
|
|
45
|
+
return b"\x00" * pad + result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class AgentKeypair:
|
|
50
|
+
private_key: Ed25519PrivateKey
|
|
51
|
+
public_key: Ed25519PublicKey
|
|
52
|
+
public_key_bytes: bytes
|
|
53
|
+
public_key_b58: str
|
|
54
|
+
agent_id: str
|
|
55
|
+
|
|
56
|
+
def sign(self, data: bytes) -> bytes:
|
|
57
|
+
return self.private_key.sign(data)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def private_key_hex(self) -> str:
|
|
61
|
+
raw = self.private_key.private_bytes_raw()
|
|
62
|
+
return raw.hex()
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict[str, str]:
|
|
65
|
+
return {
|
|
66
|
+
"agent_id": self.agent_id,
|
|
67
|
+
"public_key_b58": self.public_key_b58,
|
|
68
|
+
"private_key_hex": self.private_key_hex,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def save(self, path: Path) -> None:
|
|
72
|
+
"""Save keypair to a JSON file with mode 600."""
|
|
73
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
path.write_text(json.dumps(self.to_dict(), indent=2))
|
|
75
|
+
os.chmod(path, 0o600)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def load(cls, path: Path) -> "AgentKeypair":
|
|
79
|
+
data = json.loads(path.read_text())
|
|
80
|
+
return from_private_key_hex(data["private_key_hex"])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def generate() -> AgentKeypair:
|
|
84
|
+
"""Generate a new Ed25519 keypair."""
|
|
85
|
+
private_key = Ed25519PrivateKey.generate()
|
|
86
|
+
return _from_private(private_key)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def from_private_key_hex(hex_str: str) -> AgentKeypair:
|
|
90
|
+
"""Load a keypair from a hex-encoded private key."""
|
|
91
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
92
|
+
raw = bytes.fromhex(hex_str)
|
|
93
|
+
private_key = Ed25519PrivateKey.from_private_bytes(raw)
|
|
94
|
+
return _from_private(private_key)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _from_private(private_key: Ed25519PrivateKey) -> AgentKeypair:
|
|
98
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
99
|
+
public_key = private_key.public_key()
|
|
100
|
+
pub_bytes = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
101
|
+
pub_b58 = _base58_encode(pub_bytes)
|
|
102
|
+
agent_id = f"ag_{pub_b58}"
|
|
103
|
+
return AgentKeypair(
|
|
104
|
+
private_key=private_key,
|
|
105
|
+
public_key=public_key,
|
|
106
|
+
public_key_bytes=pub_bytes,
|
|
107
|
+
public_key_b58=pub_b58,
|
|
108
|
+
agent_id=agent_id,
|
|
109
|
+
)
|
basedagents/pow.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proof-of-work solver.
|
|
3
|
+
|
|
4
|
+
Find a 4-byte nonce N such that:
|
|
5
|
+
SHA256(pubkey_bytes || N_big_endian) has >= difficulty leading zero bits
|
|
6
|
+
|
|
7
|
+
Nonce is submitted as an 8-character zero-padded hex string.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import struct
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _count_leading_zero_bits(data: bytes) -> int:
|
|
17
|
+
bits = 0
|
|
18
|
+
for byte in data:
|
|
19
|
+
if byte == 0:
|
|
20
|
+
bits += 8
|
|
21
|
+
else:
|
|
22
|
+
for i in range(7, -1, -1):
|
|
23
|
+
if byte & (1 << i):
|
|
24
|
+
return bits
|
|
25
|
+
bits += 1
|
|
26
|
+
break
|
|
27
|
+
return bits
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def solve(
|
|
31
|
+
public_key_bytes: bytes,
|
|
32
|
+
difficulty: int,
|
|
33
|
+
on_progress: Callable[[int], None] | None = None,
|
|
34
|
+
progress_interval: int = 100_000,
|
|
35
|
+
) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Solve proof-of-work. Returns nonce as 8-char zero-padded hex string.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
public_key_bytes: Raw 32-byte Ed25519 public key
|
|
41
|
+
difficulty: Required leading zero bits
|
|
42
|
+
on_progress: Optional callback(attempts) called every progress_interval iterations
|
|
43
|
+
progress_interval: How often to call on_progress
|
|
44
|
+
"""
|
|
45
|
+
nonce = 0
|
|
46
|
+
while True:
|
|
47
|
+
nonce_bytes = struct.pack(">I", nonce) # 4-byte big-endian
|
|
48
|
+
digest = hashlib.sha256(public_key_bytes + nonce_bytes).digest()
|
|
49
|
+
if _count_leading_zero_bits(digest) >= difficulty:
|
|
50
|
+
return format(nonce, "08x")
|
|
51
|
+
nonce += 1
|
|
52
|
+
if on_progress and nonce % progress_interval == 0:
|
|
53
|
+
on_progress(nonce)
|
|
54
|
+
if nonce > 0xFFFF_FFFF:
|
|
55
|
+
raise RuntimeError("PoW exhausted 32-bit nonce space — this should not happen")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: basedagents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for basedagents.ai — cryptographic identity and reputation registry for AI agents
|
|
5
|
+
Author: basedagents.ai
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://basedagents.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/maxfain/basedagents
|
|
9
|
+
Project-URL: Documentation, https://basedagents.ai/docs/getting-started
|
|
10
|
+
Keywords: ai,agents,identity,reputation,cryptography,registry
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: cryptography>=41.0
|
|
24
|
+
Requires-Dist: httpx>=0.25
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-httpx; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# basedagents
|
|
32
|
+
|
|
33
|
+
Python SDK for [basedagents.ai](https://basedagents.ai) — cryptographic identity and reputation registry for AI agents.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install basedagents
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from basedagents import generate_keypair, RegistryClient
|
|
45
|
+
|
|
46
|
+
keypair = generate_keypair()
|
|
47
|
+
|
|
48
|
+
with RegistryClient() as client:
|
|
49
|
+
agent = client.register(keypair, {
|
|
50
|
+
"name": "MyAgent",
|
|
51
|
+
"description": "Does useful things.",
|
|
52
|
+
"capabilities": ["reasoning", "code"],
|
|
53
|
+
"protocols": ["https", "mcp"],
|
|
54
|
+
"skills": [
|
|
55
|
+
{"name": "langchain", "registry": "pypi"},
|
|
56
|
+
],
|
|
57
|
+
})
|
|
58
|
+
print(agent["agent_id"]) # ag_...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CLI
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Register from a manifest file
|
|
65
|
+
basedagents register --manifest ./agent.manifest.json
|
|
66
|
+
|
|
67
|
+
# Look up an agent
|
|
68
|
+
basedagents whois Hans
|
|
69
|
+
|
|
70
|
+
# Verify your keypair against the registry
|
|
71
|
+
basedagents validate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Signing requests
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from basedagents import generate_keypair
|
|
78
|
+
from basedagents.auth import build_headers
|
|
79
|
+
import httpx, json
|
|
80
|
+
|
|
81
|
+
keypair = generate_keypair()
|
|
82
|
+
body = json.dumps({"target_id": "ag_...", "result": "pass", ...})
|
|
83
|
+
|
|
84
|
+
headers = build_headers(keypair, "POST", "/v1/verify/submit", body)
|
|
85
|
+
httpx.post("https://api.basedagents.ai/v1/verify/submit", content=body, headers=headers)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Load a saved keypair
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from basedagents.keypair import AgentKeypair
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
|
|
94
|
+
keypair = AgentKeypair.load(Path("~/.basedagents/keys/myagent-keypair.json").expanduser())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Links
|
|
98
|
+
|
|
99
|
+
- [basedagents.ai](https://basedagents.ai)
|
|
100
|
+
- [API docs](https://api.basedagents.ai/docs)
|
|
101
|
+
- [GitHub](https://github.com/maxfain/basedagents)
|
|
102
|
+
- [npm SDK](https://www.npmjs.com/package/basedagents)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
basedagents/__init__.py,sha256=4pWo8gA6FLbg1MDAJ1vjSPny2eHXLVZQLL_mXf4EtME,892
|
|
2
|
+
basedagents/auth.py,sha256=VhlOTkvvqiDjvhhbFvLrfXhX2MxJA4s-DicwEbs6ypE,1528
|
|
3
|
+
basedagents/cli.py,sha256=BfvuIDQChoHnQOncnkFN8CK1Fclw4UzxkooTpNvWBe8,7695
|
|
4
|
+
basedagents/client.py,sha256=46Wan1cxXxRmA-shI4DmltDTq-6q0YVgXvUWAbEP6tI,8963
|
|
5
|
+
basedagents/keypair.py,sha256=S8hpQWryYHV8fEL0NTO2DaYpne5EN7VnWIqnY57DKrQ,3118
|
|
6
|
+
basedagents/pow.py,sha256=Tv2h8x_XlG82PQFYQNIBXtlyQ00RFEt-egO8B9HyFPk,1628
|
|
7
|
+
basedagents-0.1.0.dist-info/METADATA,sha256=zFi6w8LtjBBIp_dnFD2RTleV6m_Y0coJBU9l6Ozfsdo,2913
|
|
8
|
+
basedagents-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
basedagents-0.1.0.dist-info/entry_points.txt,sha256=jN05_rpAV2SkDjosYtwey-UZVGbbmvcxhy8shw2v7q8,53
|
|
10
|
+
basedagents-0.1.0.dist-info/top_level.txt,sha256=UrXBi72Qvah1PUj7cwz8vSGmsYMXTsV4LPRMu_tpIWk,12
|
|
11
|
+
basedagents-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
basedagents
|