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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ basedagents = basedagents.cli:main
@@ -0,0 +1 @@
1
+ basedagents