erabi-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: erabi-sdk
3
+ Version: 0.1.0
4
+ Summary: Erabi Protocol SDK for Python agents: register, fire intents, report outcomes, get paid.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: cryptography>=42
9
+
10
+ # erabi-sdk
11
+
12
+ The Erabi Protocol SDK for Python agents: register, fire intents, report outcomes,
13
+ get paid — three lines to join the open intent exchange.
14
+
15
+ ```python
16
+ from erabi_sdk import Erabi
17
+
18
+ erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
19
+ choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
20
+ # later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
21
+ ```
22
+
23
+ Every paid influence on the network is signed, labeled, and inspectable: sponsored
24
+ candidates arrive in a separate array, each with a publicly verifiable
25
+ `DisclosureRecord`. Organic results are never reordered by payment. Outcomes only
26
+ count when both parties sign them, and reputation is recomputable by anyone from
27
+ public evidence.
28
+
29
+ Signing is byte-for-byte compatible with the TypeScript SDK (Ed25519 over RFC 8785
30
+ canonical JSON), verified against frozen cross-SDK test vectors.
31
+
32
+ Framework bindings (LangChain, CrewAI, AutoGen) live in `erabi_sdk.integrations` and
33
+ need no framework imports of their own.
34
+
35
+ Spec, schemas, and the reference node: https://github.com/HMAKT99/Erabi
36
+ Apache-2.0.
@@ -0,0 +1,27 @@
1
+ # erabi-sdk
2
+
3
+ The Erabi Protocol SDK for Python agents: register, fire intents, report outcomes,
4
+ get paid — three lines to join the open intent exchange.
5
+
6
+ ```python
7
+ from erabi_sdk import Erabi
8
+
9
+ erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
10
+ choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
11
+ # later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
12
+ ```
13
+
14
+ Every paid influence on the network is signed, labeled, and inspectable: sponsored
15
+ candidates arrive in a separate array, each with a publicly verifiable
16
+ `DisclosureRecord`. Organic results are never reordered by payment. Outcomes only
17
+ count when both parties sign them, and reputation is recomputable by anyone from
18
+ public evidence.
19
+
20
+ Signing is byte-for-byte compatible with the TypeScript SDK (Ed25519 over RFC 8785
21
+ canonical JSON), verified against frozen cross-SDK test vectors.
22
+
23
+ Framework bindings (LangChain, CrewAI, AutoGen) live in `erabi_sdk.integrations` and
24
+ need no framework imports of their own.
25
+
26
+ Spec, schemas, and the reference node: https://github.com/HMAKT99/Erabi
27
+ Apache-2.0.
@@ -0,0 +1,35 @@
1
+ from .canonical import canonicalize
2
+ from .client import DEFAULT_ENDPOINTS, Erabi, ErabiError
3
+ from .envelope import create_envelope, sign_payload, signing_input
4
+ from .keys import (
5
+ AGENT_ID_PREFIX,
6
+ agent_id_from_public_key,
7
+ b58decode,
8
+ b58encode,
9
+ generate_seed,
10
+ public_key_from_seed,
11
+ public_key_to_string,
12
+ sign,
13
+ signature_to_string,
14
+ verify,
15
+ )
16
+
17
+ __all__ = [
18
+ "canonicalize",
19
+ "DEFAULT_ENDPOINTS",
20
+ "Erabi",
21
+ "ErabiError",
22
+ "create_envelope",
23
+ "sign_payload",
24
+ "signing_input",
25
+ "AGENT_ID_PREFIX",
26
+ "agent_id_from_public_key",
27
+ "b58decode",
28
+ "b58encode",
29
+ "generate_seed",
30
+ "public_key_from_seed",
31
+ "public_key_to_string",
32
+ "sign",
33
+ "signature_to_string",
34
+ "verify",
35
+ ]
@@ -0,0 +1,80 @@
1
+ """RFC 8785 (JCS) canonical JSON, matching @erabi/crypto byte-for-byte.
2
+
3
+ Numbers follow ECMAScript Number→string semantics: shortest round-trip
4
+ digits, decimal notation for 1e-6 <= |x| < 1e21, exponential outside, no
5
+ zero-padded exponents. Values JSON cannot represent raise loudly.
6
+ """
7
+
8
+ import json
9
+ import math
10
+ from decimal import Decimal
11
+ from typing import Any
12
+
13
+ __all__ = ["canonicalize", "format_number"]
14
+
15
+
16
+ def format_number(value: float) -> str:
17
+ if isinstance(value, bool): # guard: bools are not numbers here
18
+ raise TypeError("bool passed to format_number")
19
+ if math.isnan(value) or math.isinf(value):
20
+ raise TypeError(f"canonicalize: non-finite number {value!r} is not valid JSON")
21
+ if value == 0:
22
+ return "0"
23
+
24
+ negative = value < 0
25
+ magnitude = abs(value)
26
+
27
+ if 1e-6 <= magnitude < 1e21:
28
+ # Decimal (positional) notation from shortest round-trip digits.
29
+ if float(magnitude).is_integer():
30
+ body = str(int(magnitude))
31
+ else:
32
+ body = format(Decimal(repr(magnitude)), "f")
33
+ body = body.rstrip("0").rstrip(".") if "." in body else body
34
+ return ("-" if negative else "") + body
35
+
36
+ # Exponential notation: normalized mantissa, no zero-padded exponent.
37
+ mantissa, _, exponent = repr(magnitude).partition("e")
38
+ if not exponent:
39
+ # repr produced positional form for an exponential-range value.
40
+ dec = Decimal(repr(magnitude)).normalize()
41
+ sign, digits, exp = dec.as_tuple()
42
+ digit_str = "".join(str(d) for d in digits)
43
+ e = exp + len(digit_str) - 1
44
+ mantissa = digit_str[0] + ("." + digit_str[1:] if len(digit_str) > 1 else "")
45
+ exponent = str(e)
46
+ exp_int = int(exponent)
47
+ mantissa = mantissa.rstrip("0").rstrip(".") if "." in mantissa else mantissa
48
+ exp_str = f"e+{exp_int}" if exp_int >= 0 else f"e-{abs(exp_int)}"
49
+ return ("-" if negative else "") + mantissa + exp_str
50
+
51
+
52
+ def _serialize(value: Any) -> str:
53
+ if value is None:
54
+ return "null"
55
+ if value is True:
56
+ return "true"
57
+ if value is False:
58
+ return "false"
59
+ if isinstance(value, str):
60
+ return json.dumps(value, ensure_ascii=False)
61
+ if isinstance(value, int):
62
+ return str(value)
63
+ if isinstance(value, float):
64
+ return format_number(value)
65
+ if isinstance(value, (list, tuple)):
66
+ return "[" + ",".join(_serialize(item) for item in value) + "]"
67
+ if isinstance(value, dict):
68
+ members = []
69
+ # Sort by UTF-16 code units (RFC 8785 §3.2.3), not code points.
70
+ for key in sorted(value.keys(), key=lambda k: k.encode("utf-16-be")):
71
+ if not isinstance(key, str):
72
+ raise TypeError("canonicalize: object keys must be strings")
73
+ member = value[key]
74
+ members.append(json.dumps(key, ensure_ascii=False) + ":" + _serialize(member))
75
+ return "{" + ",".join(members) + "}"
76
+ raise TypeError(f"canonicalize: cannot canonicalize value of type {type(value).__name__}")
77
+
78
+
79
+ def canonicalize(value: Any) -> str:
80
+ return _serialize(value)
@@ -0,0 +1,187 @@
1
+ """Erabi Python SDK: the 3-line integration, mirroring @erabi/sdk.
2
+
3
+ from erabi_sdk import Erabi
4
+ erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
5
+ choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
6
+ # later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
7
+ """
8
+
9
+ import hashlib
10
+ import json
11
+ import urllib.error
12
+ import urllib.request
13
+ import uuid
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from .canonical import canonicalize
18
+ from .envelope import create_envelope
19
+ from .keys import agent_id_from_public_key, generate_seed, public_key_from_seed, public_key_to_string
20
+
21
+ __all__ = ["Erabi", "ErabiError", "DEFAULT_ENDPOINTS"]
22
+
23
+ DEFAULT_ENDPOINTS = {
24
+ "registry": "http://localhost:4001",
25
+ "exchange": "http://localhost:4002",
26
+ "attribution": "http://localhost:4003",
27
+ "reputation": "http://localhost:4004",
28
+ }
29
+
30
+
31
+ class ErabiError(Exception):
32
+ def __init__(self, status: int, code: str, message: str):
33
+ super().__init__(f"[{status} {code}] {message}")
34
+ self.status = status
35
+ self.code = code
36
+
37
+
38
+ def _request(method: str, url: str, body: Optional[Any] = None) -> Dict[str, Any]:
39
+ data = json.dumps(body).encode("utf-8") if body is not None else None
40
+ request = urllib.request.Request(
41
+ url, data=data, method=method, headers={"content-type": "application/json"}
42
+ )
43
+ try:
44
+ with urllib.request.urlopen(request) as response:
45
+ text = response.read().decode("utf-8")
46
+ return json.loads(text) if text else {}
47
+ except urllib.error.HTTPError as err:
48
+ try:
49
+ payload = json.loads(err.read().decode("utf-8"))
50
+ error = payload.get("error", {})
51
+ raise ErabiError(err.code, error.get("code", "http_error"), error.get("message", str(err)))
52
+ except (ValueError, KeyError):
53
+ raise ErabiError(err.code, "http_error", str(err))
54
+
55
+
56
+ class Erabi:
57
+ def __init__(self, seed: bytes, manifest: Dict[str, Any], endpoints: Dict[str, str], node_id: str):
58
+ self.seed = seed
59
+ self.manifest = manifest
60
+ self.id: str = manifest["id"]
61
+ self.endpoints = endpoints
62
+ self.node_id = node_id
63
+
64
+ @classmethod
65
+ def register(
66
+ cls,
67
+ name: str,
68
+ capabilities: List[str],
69
+ endpoints: Optional[Dict[str, str]] = None,
70
+ endpoint: str = "https://example.invalid/agent",
71
+ owner_type: str = "individual",
72
+ verification: Optional[List[str]] = None,
73
+ payout_binding: Optional[str] = None,
74
+ accepts_sponsored: bool = False,
75
+ max_sponsored_ratio: float = 0.3,
76
+ human_in_loop: bool = True,
77
+ referrer: Optional[str] = None,
78
+ node_id: str = "erabi-sdk-py",
79
+ seed: Optional[bytes] = None,
80
+ ) -> "Erabi":
81
+ merged = {**DEFAULT_ENDPOINTS, **(endpoints or {})}
82
+ seed = seed or generate_seed()
83
+ public_key = public_key_from_seed(seed)
84
+ agent_id = agent_id_from_public_key(public_key)
85
+ created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
86
+ manifest = {
87
+ "spec_version": "0.1",
88
+ "id": agent_id,
89
+ "name": name,
90
+ "public_key": public_key_to_string(public_key),
91
+ "owner": {
92
+ "type": owner_type,
93
+ "verification": verification or [],
94
+ "payout_binding": payout_binding,
95
+ },
96
+ "capabilities": capabilities,
97
+ "endpoint": endpoint,
98
+ "roles": ["consumer", "provider"],
99
+ "policy": {
100
+ "accepts_sponsored": accepts_sponsored,
101
+ "max_sponsored_ratio": max_sponsored_ratio,
102
+ "human_in_loop": human_in_loop,
103
+ },
104
+ "referrer": referrer,
105
+ "created_at": created,
106
+ }
107
+ client = cls(seed, manifest, merged, node_id)
108
+ _request("POST", f"{merged['registry']}/v1/agents", client.signed(manifest))
109
+ return client
110
+
111
+ def signed(self, payload: Any) -> Dict[str, Any]:
112
+ return create_envelope(payload, self.seed, self.id, self.node_id)
113
+
114
+ def intent(
115
+ self,
116
+ category: str,
117
+ query: Optional[str] = None,
118
+ constraints: Optional[Dict[str, Any]] = None,
119
+ human_in_loop: Optional[bool] = None,
120
+ context: Any = None,
121
+ ttl_ms: int = 3000,
122
+ ) -> Dict[str, Any]:
123
+ if context is not None:
124
+ context_hash = "sha256:" + hashlib.sha256(canonicalize(context).encode()).hexdigest()
125
+ else:
126
+ context_hash = "sha256:" + hashlib.sha256(b"erabi:no-context").hexdigest()
127
+ intent = {
128
+ "intent_id": str(uuid.uuid4()),
129
+ "agent_id": self.id,
130
+ "category": category,
131
+ "query": query or category,
132
+ "constraints": constraints or {},
133
+ "context_hash": context_hash,
134
+ "human_in_loop": (
135
+ human_in_loop
136
+ if human_in_loop is not None
137
+ else self.manifest["policy"]["human_in_loop"]
138
+ ),
139
+ "ttl_ms": ttl_ms,
140
+ }
141
+ return _request("POST", f"{self.endpoints['exchange']}/v1/intents", self.signed(intent))
142
+
143
+ def report_outcome(
144
+ self,
145
+ auction_id: str,
146
+ provider_id: str,
147
+ kind: str,
148
+ value_usd: float = 0.0,
149
+ rail_receipt: Optional[Dict[str, str]] = None,
150
+ ) -> Dict[str, Any]:
151
+ return _request(
152
+ "POST",
153
+ f"{self.endpoints['attribution']}/v1/events",
154
+ self.signed(
155
+ {
156
+ "event_id": str(uuid.uuid4()),
157
+ "auction_id": auction_id,
158
+ "kind": kind,
159
+ "provider_id": provider_id,
160
+ "value_usd": value_usd,
161
+ "rail_receipt": rail_receipt,
162
+ }
163
+ ),
164
+ )
165
+
166
+ def confirm_outcome(self, event_id: str, event_hash: str) -> Dict[str, Any]:
167
+ return _request(
168
+ "POST",
169
+ f"{self.endpoints['attribution']}/v1/events/{event_id}/confirm",
170
+ self.signed({"event_id": event_id, "hash": event_hash}),
171
+ )
172
+
173
+ def discover(self, capability: str, limit: int = 10) -> Dict[str, Any]:
174
+ return _request(
175
+ "POST",
176
+ f"{self.endpoints['registry']}/v1/discover",
177
+ {"capability": capability, "limit": limit},
178
+ )
179
+
180
+ def my_reputation(self) -> Dict[str, Any]:
181
+ return _request("GET", f"{self.endpoints['reputation']}/v1/reputation/{self.id}")
182
+
183
+ def my_earnings(self) -> Dict[str, Any]:
184
+ return _request("GET", f"{self.endpoints['attribution']}/v1/earnings/{self.id}")
185
+
186
+ def feedback(self) -> Dict[str, Any]:
187
+ return _request("GET", f"{self.endpoints['attribution']}/v1/feedback/{self.id}")
@@ -0,0 +1,44 @@
1
+ """Spec §6 envelopes: sig = ed25519(canonical_json(payload) || ts || nonce)."""
2
+
3
+ import uuid
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, Optional
6
+
7
+ from .canonical import canonicalize
8
+ from .keys import sign, signature_to_string
9
+
10
+ __all__ = ["signing_input", "sign_payload", "create_envelope"]
11
+
12
+
13
+ def now_iso() -> str:
14
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.") + (
15
+ f"{datetime.now(timezone.utc).microsecond // 1000:03d}Z"
16
+ )
17
+
18
+
19
+ def signing_input(payload: Any, ts: str, nonce: str) -> bytes:
20
+ return (canonicalize(payload) + ts + nonce).encode("utf-8")
21
+
22
+
23
+ def sign_payload(payload: Any, ts: str, nonce: str, seed: bytes) -> str:
24
+ return signature_to_string(sign(signing_input(payload, ts, nonce), seed))
25
+
26
+
27
+ def create_envelope(
28
+ payload: Any,
29
+ seed: bytes,
30
+ key_id: str,
31
+ node_id: str = "erabi-sdk-py",
32
+ ts: Optional[str] = None,
33
+ nonce: Optional[str] = None,
34
+ ) -> Dict[str, Any]:
35
+ ts = ts or now_iso()
36
+ nonce = nonce or str(uuid.uuid4())
37
+ return {
38
+ "payload": payload,
39
+ "sig": sign_payload(payload, ts, nonce, seed),
40
+ "key_id": key_id,
41
+ "ts": ts,
42
+ "nonce": nonce,
43
+ "node_id": node_id,
44
+ }
@@ -0,0 +1,10 @@
1
+ """Framework bindings: thin wrappers over the Erabi SDK.
2
+
3
+ Each module exposes the same four operations (discover, intent,
4
+ report_outcome, my_reputation) in the shape its framework expects, without
5
+ importing the framework itself — the host application supplies it.
6
+ """
7
+
8
+ from .tools import TOOL_SPECS, build_callables
9
+
10
+ __all__ = ["TOOL_SPECS", "build_callables"]
@@ -0,0 +1,22 @@
1
+ """AutoGen binding: register the Erabi callables on agents.
2
+
3
+ register_erabi_tools(erabi, caller=assistant, executor=user_proxy)
4
+
5
+ Uses the standard `register_for_llm` / `register_for_execution` pattern; the
6
+ framework objects are supplied by the host application.
7
+ """
8
+
9
+ from typing import Any
10
+
11
+ from ..client import Erabi
12
+ from .tools import TOOL_SPECS, build_callables
13
+
14
+
15
+ def register_erabi_tools(client: Erabi, caller: Any, executor: Any) -> None:
16
+ callables = build_callables(client)
17
+ for spec in TOOL_SPECS:
18
+ func = callables[spec["name"]]
19
+ func.__name__ = spec["name"]
20
+ func.__doc__ = spec["description"]
21
+ caller.register_for_llm(name=spec["name"], description=spec["description"])(func)
22
+ executor.register_for_execution(name=spec["name"])(func)
@@ -0,0 +1,21 @@
1
+ """CrewAI binding: decorate the Erabi callables with crewai's @tool.
2
+
3
+ from crewai.tools import tool
4
+ tools = get_erabi_tools(erabi, tool)
5
+ """
6
+
7
+ from typing import Any, Callable, List
8
+
9
+ from ..client import Erabi
10
+ from .tools import TOOL_SPECS, build_callables
11
+
12
+
13
+ def get_erabi_tools(client: Erabi, tool_decorator: Callable[..., Any]) -> List[Any]:
14
+ callables = build_callables(client)
15
+ tools = []
16
+ for spec in TOOL_SPECS:
17
+ func = callables[spec["name"]]
18
+ func.__name__ = spec["name"]
19
+ func.__doc__ = spec["description"]
20
+ tools.append(tool_decorator(spec["name"])(func))
21
+ return tools
@@ -0,0 +1,22 @@
1
+ """LangChain binding: `get_erabi_tools(client, Tool)` → list of Tool.
2
+
3
+ from langchain_core.tools import Tool
4
+ tools = get_erabi_tools(erabi, Tool)
5
+ """
6
+
7
+ from typing import Any, List
8
+
9
+ from ..client import Erabi
10
+ from .tools import TOOL_SPECS, build_callables
11
+
12
+
13
+ def get_erabi_tools(client: Erabi, tool_class: Any) -> List[Any]:
14
+ callables = build_callables(client)
15
+ return [
16
+ tool_class(
17
+ name=spec["name"],
18
+ description=spec["description"],
19
+ func=callables[spec["name"]],
20
+ )
21
+ for spec in TOOL_SPECS
22
+ ]
@@ -0,0 +1,89 @@
1
+ """Framework-neutral tool specs shared by all bindings."""
2
+
3
+ import json
4
+ from typing import Any, Callable, Dict, List
5
+
6
+ from ..client import Erabi
7
+
8
+ TOOL_SPECS: List[Dict[str, Any]] = [
9
+ {
10
+ "name": "erabi_discover",
11
+ "description": (
12
+ "Find providers on the Erabi network for a capability, ranked by "
13
+ "reputation x freshness with public evidence trails."
14
+ ),
15
+ "parameters": {
16
+ "type": "object",
17
+ "properties": {
18
+ "capability": {"type": "string"},
19
+ "limit": {"type": "integer", "minimum": 1, "maximum": 50},
20
+ },
21
+ "required": ["capability"],
22
+ },
23
+ },
24
+ {
25
+ "name": "erabi_intent",
26
+ "description": (
27
+ "Fire a moment of choice: returns organic candidates plus clearly "
28
+ "labeled sponsored candidates with signed disclosures. Query must be PII-free."
29
+ ),
30
+ "parameters": {
31
+ "type": "object",
32
+ "properties": {
33
+ "category": {"type": "string"},
34
+ "query": {"type": "string"},
35
+ "max_price_usd": {"type": "number"},
36
+ },
37
+ "required": ["category"],
38
+ },
39
+ },
40
+ {
41
+ "name": "erabi_report_outcome",
42
+ "description": (
43
+ "Report a signed outcome event (selection/conversion/task_success) for a "
44
+ "provider chosen from a consideration set."
45
+ ),
46
+ "parameters": {
47
+ "type": "object",
48
+ "properties": {
49
+ "auction_id": {"type": "string"},
50
+ "provider_id": {"type": "string"},
51
+ "kind": {
52
+ "type": "string",
53
+ "enum": ["selection", "click", "conversion", "task_success", "assisted"],
54
+ },
55
+ "value_usd": {"type": "number"},
56
+ },
57
+ "required": ["auction_id", "provider_id", "kind"],
58
+ },
59
+ },
60
+ {
61
+ "name": "erabi_my_reputation",
62
+ "description": "This agent's reputation score and verifiable evidence trail.",
63
+ "parameters": {"type": "object", "properties": {}},
64
+ },
65
+ ]
66
+
67
+
68
+ def build_callables(client: Erabi) -> Dict[str, Callable[..., str]]:
69
+ """Name → callable returning a JSON string, the lingua franca of tools."""
70
+
71
+ def discover(capability: str, limit: int = 10) -> str:
72
+ return json.dumps(client.discover(capability, limit))
73
+
74
+ def intent(category: str, query: str = "", max_price_usd: float = 0.0) -> str:
75
+ constraints = {"max_price_usd": max_price_usd} if max_price_usd else {}
76
+ return json.dumps(client.intent(category=category, query=query or None, constraints=constraints))
77
+
78
+ def report_outcome(auction_id: str, provider_id: str, kind: str, value_usd: float = 0.0) -> str:
79
+ return json.dumps(client.report_outcome(auction_id, provider_id, kind, value_usd))
80
+
81
+ def my_reputation() -> str:
82
+ return json.dumps(client.my_reputation())
83
+
84
+ return {
85
+ "erabi_discover": discover,
86
+ "erabi_intent": intent,
87
+ "erabi_report_outcome": report_outcome,
88
+ "erabi_my_reputation": my_reputation,
89
+ }
@@ -0,0 +1,100 @@
1
+ """Ed25519 identity: keypairs, base58 wire encodings, agent ids."""
2
+
3
+ import os
4
+ from typing import Tuple
5
+
6
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
7
+ Ed25519PrivateKey,
8
+ Ed25519PublicKey,
9
+ )
10
+ from cryptography.hazmat.primitives.serialization import (
11
+ Encoding,
12
+ NoEncryption,
13
+ PrivateFormat,
14
+ PublicFormat,
15
+ )
16
+
17
+ __all__ = [
18
+ "AGENT_ID_PREFIX",
19
+ "b58encode",
20
+ "b58decode",
21
+ "generate_seed",
22
+ "public_key_from_seed",
23
+ "public_key_to_string",
24
+ "agent_id_from_public_key",
25
+ "sign",
26
+ "signature_to_string",
27
+ ]
28
+
29
+ AGENT_ID_PREFIX = "erabi:agent:"
30
+ _ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
31
+
32
+
33
+ def b58encode(data: bytes) -> str:
34
+ n = int.from_bytes(data, "big")
35
+ out = ""
36
+ while n > 0:
37
+ n, rem = divmod(n, 58)
38
+ out = _ALPHABET[rem] + out
39
+ pad = 0
40
+ for byte in data:
41
+ if byte == 0:
42
+ pad += 1
43
+ else:
44
+ break
45
+ return "1" * pad + out
46
+
47
+
48
+ def b58decode(text: str) -> bytes:
49
+ n = 0
50
+ for char in text:
51
+ n = n * 58 + _ALPHABET.index(char)
52
+ body = n.to_bytes((n.bit_length() + 7) // 8, "big") if n > 0 else b""
53
+ pad = 0
54
+ for char in text:
55
+ if char == "1":
56
+ pad += 1
57
+ else:
58
+ break
59
+ return b"\x00" * pad + body
60
+
61
+
62
+ def generate_seed() -> bytes:
63
+ return os.urandom(32)
64
+
65
+
66
+ def public_key_from_seed(seed: bytes) -> bytes:
67
+ private = Ed25519PrivateKey.from_private_bytes(seed)
68
+ return private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
69
+
70
+
71
+ def public_key_to_string(public_key: bytes) -> str:
72
+ return "ed25519:" + b58encode(public_key)
73
+
74
+
75
+ def agent_id_from_public_key(public_key: bytes) -> str:
76
+ return AGENT_ID_PREFIX + b58encode(public_key)
77
+
78
+
79
+ def sign(message: bytes, seed: bytes) -> bytes:
80
+ return Ed25519PrivateKey.from_private_bytes(seed).sign(message)
81
+
82
+
83
+ def signature_to_string(signature: bytes) -> str:
84
+ return "ed25519:" + b58encode(signature)
85
+
86
+
87
+ def keypair_from_seed(seed: bytes) -> Tuple[bytes, bytes]:
88
+ return seed, public_key_from_seed(seed)
89
+
90
+
91
+ def verify(message: bytes, signature: bytes, public_key: bytes) -> bool:
92
+ try:
93
+ Ed25519PublicKey.from_public_bytes(public_key).verify(signature, message)
94
+ return True
95
+ except Exception:
96
+ return False
97
+
98
+
99
+ def _private_bytes(private: Ed25519PrivateKey) -> bytes:
100
+ return private.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: erabi-sdk
3
+ Version: 0.1.0
4
+ Summary: Erabi Protocol SDK for Python agents: register, fire intents, report outcomes, get paid.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: cryptography>=42
9
+
10
+ # erabi-sdk
11
+
12
+ The Erabi Protocol SDK for Python agents: register, fire intents, report outcomes,
13
+ get paid — three lines to join the open intent exchange.
14
+
15
+ ```python
16
+ from erabi_sdk import Erabi
17
+
18
+ erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
19
+ choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
20
+ # later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
21
+ ```
22
+
23
+ Every paid influence on the network is signed, labeled, and inspectable: sponsored
24
+ candidates arrive in a separate array, each with a publicly verifiable
25
+ `DisclosureRecord`. Organic results are never reordered by payment. Outcomes only
26
+ count when both parties sign them, and reputation is recomputable by anyone from
27
+ public evidence.
28
+
29
+ Signing is byte-for-byte compatible with the TypeScript SDK (Ed25519 over RFC 8785
30
+ canonical JSON), verified against frozen cross-SDK test vectors.
31
+
32
+ Framework bindings (LangChain, CrewAI, AutoGen) live in `erabi_sdk.integrations` and
33
+ need no framework imports of their own.
34
+
35
+ Spec, schemas, and the reference node: https://github.com/HMAKT99/Erabi
36
+ Apache-2.0.
@@ -0,0 +1,17 @@
1
+ README.md
2
+ pyproject.toml
3
+ erabi_sdk/__init__.py
4
+ erabi_sdk/canonical.py
5
+ erabi_sdk/client.py
6
+ erabi_sdk/envelope.py
7
+ erabi_sdk/keys.py
8
+ erabi_sdk.egg-info/PKG-INFO
9
+ erabi_sdk.egg-info/SOURCES.txt
10
+ erabi_sdk.egg-info/dependency_links.txt
11
+ erabi_sdk.egg-info/requires.txt
12
+ erabi_sdk.egg-info/top_level.txt
13
+ erabi_sdk/integrations/__init__.py
14
+ erabi_sdk/integrations/autogen.py
15
+ erabi_sdk/integrations/crewai.py
16
+ erabi_sdk/integrations/langchain.py
17
+ erabi_sdk/integrations/tools.py
@@ -0,0 +1 @@
1
+ cryptography>=42
@@ -0,0 +1 @@
1
+ erabi_sdk
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "erabi-sdk"
3
+ version = "0.1.0"
4
+ description = "Erabi Protocol SDK for Python agents: register, fire intents, report outcomes, get paid."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "Apache-2.0" }
8
+ dependencies = ["cryptography>=42"]
9
+
10
+ [build-system]
11
+ requires = ["setuptools>=68"]
12
+ build-backend = "setuptools.build_meta"
13
+
14
+ [tool.setuptools.packages.find]
15
+ include = ["erabi_sdk*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+