blindoracle-sdk 0.4.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.
- blindoracle_sdk/__init__.py +74 -0
- blindoracle_sdk/agents.py +100 -0
- blindoracle_sdk/aio.py +117 -0
- blindoracle_sdk/attestation.py +78 -0
- blindoracle_sdk/audit.py +127 -0
- blindoracle_sdk/cli.py +105 -0
- blindoracle_sdk/client.py +249 -0
- blindoracle_sdk/compliance.py +123 -0
- blindoracle_sdk/delegation.py +235 -0
- blindoracle_sdk/exceptions.py +45 -0
- blindoracle_sdk/integrations/__init__.py +1 -0
- blindoracle_sdk/integrations/autogen_tools.py +124 -0
- blindoracle_sdk/integrations/crewai_tools.py +93 -0
- blindoracle_sdk/integrations/langchain_tools.py +198 -0
- blindoracle_sdk/introductions.py +67 -0
- blindoracle_sdk/markets.py +180 -0
- blindoracle_sdk/metrics.py +30 -0
- blindoracle_sdk/privacy.py +59 -0
- blindoracle_sdk/signals.py +79 -0
- blindoracle_sdk-0.4.0.dist-info/METADATA +187 -0
- blindoracle_sdk-0.4.0.dist-info/RECORD +24 -0
- blindoracle_sdk-0.4.0.dist-info/WHEEL +5 -0
- blindoracle_sdk-0.4.0.dist-info/entry_points.txt +2 -0
- blindoracle_sdk-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlindOracle SDK
|
|
3
|
+
Chainlink-verified prediction markets for autonomous agents.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from blindoracle_sdk import BlindOracleClient
|
|
7
|
+
|
|
8
|
+
client = BlindOracleClient(api_key="your_key")
|
|
9
|
+
|
|
10
|
+
# Get active markets
|
|
11
|
+
markets = client.markets.list()
|
|
12
|
+
|
|
13
|
+
# Run DeFi compliance check
|
|
14
|
+
result = client.compliance.check("0xProtocolAddress")
|
|
15
|
+
|
|
16
|
+
# Get market signal
|
|
17
|
+
signal = client.signals.latest()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from blindoracle_sdk.client import BlindOracleClient
|
|
21
|
+
from blindoracle_sdk.aio import AsyncBlindOracleClient
|
|
22
|
+
from blindoracle_sdk.markets import Market
|
|
23
|
+
from blindoracle_sdk.exceptions import (
|
|
24
|
+
BlindOracleError,
|
|
25
|
+
AuthenticationError,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
MarketNotFoundError,
|
|
28
|
+
PaymentRequiredError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
PassportRequiredError,
|
|
31
|
+
CredentialNotFoundError,
|
|
32
|
+
)
|
|
33
|
+
from blindoracle_sdk.audit import AuditAPI, AuditAttestation, verify_inclusion, verify_anchor
|
|
34
|
+
from blindoracle_sdk.privacy import PrivacyAPI, DISCLOSURE_MODES, ZK_CLAIM_TYPES
|
|
35
|
+
from blindoracle_sdk.metrics import MetricsAPI
|
|
36
|
+
from blindoracle_sdk.delegation import (
|
|
37
|
+
DelegationLog,
|
|
38
|
+
verify_signature,
|
|
39
|
+
delegation_signature,
|
|
40
|
+
delegator_passport_hash,
|
|
41
|
+
DELEGATION_KIND,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__version__ = "0.4.0"
|
|
45
|
+
__author__ = "Craig Brown"
|
|
46
|
+
__email__ = "craigmbrown@gmail.com"
|
|
47
|
+
__url__ = "https://craigmbrown.com/blindoracle"
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"BlindOracleClient",
|
|
51
|
+
"AsyncBlindOracleClient",
|
|
52
|
+
"Market",
|
|
53
|
+
"BlindOracleError",
|
|
54
|
+
"AuthenticationError",
|
|
55
|
+
"RateLimitError",
|
|
56
|
+
"MarketNotFoundError",
|
|
57
|
+
"PaymentRequiredError",
|
|
58
|
+
"ValidationError",
|
|
59
|
+
"PassportRequiredError",
|
|
60
|
+
"CredentialNotFoundError",
|
|
61
|
+
"AuditAPI",
|
|
62
|
+
"AuditAttestation",
|
|
63
|
+
"verify_inclusion",
|
|
64
|
+
"verify_anchor",
|
|
65
|
+
"PrivacyAPI",
|
|
66
|
+
"DISCLOSURE_MODES",
|
|
67
|
+
"ZK_CLAIM_TYPES",
|
|
68
|
+
"MetricsAPI",
|
|
69
|
+
"DelegationLog",
|
|
70
|
+
"verify_signature",
|
|
71
|
+
"delegation_signature",
|
|
72
|
+
"delegator_passport_hash",
|
|
73
|
+
"DELEGATION_KIND",
|
|
74
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""BlindOracle Agents API — ERC-8004 passport, reputation, ProofDB."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentPassport:
|
|
7
|
+
"""ERC-8004 agent passport and reputation record."""
|
|
8
|
+
def __init__(self, data: dict):
|
|
9
|
+
self.agent_id = data.get("agent_id")
|
|
10
|
+
self.name = data.get("name")
|
|
11
|
+
self.tier = data.get("tier") # "explorer"|"contributor"|"operator"|"partner"
|
|
12
|
+
self.reputation_score = data.get("reputation_score", 0)
|
|
13
|
+
self.proofs_published = data.get("proofs_published", 0)
|
|
14
|
+
self.accuracy_rate = data.get("accuracy_rate") # 0.0-1.0
|
|
15
|
+
self.status = data.get("status") # "active"|"revoked"|"suspended"
|
|
16
|
+
self.raw = data
|
|
17
|
+
|
|
18
|
+
def __repr__(self):
|
|
19
|
+
return (
|
|
20
|
+
f"<AgentPassport id={self.agent_id!r} tier={self.tier!r} "
|
|
21
|
+
f"rep={self.reputation_score} accuracy={self.accuracy_rate}>"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentsAPI:
|
|
26
|
+
"""
|
|
27
|
+
Agent identity, reputation, and ProofDB operations.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
# Get your agent's passport
|
|
31
|
+
me = client.agents.me()
|
|
32
|
+
print(me.tier, me.accuracy_rate)
|
|
33
|
+
|
|
34
|
+
# Publish a ProofOfAccuracy
|
|
35
|
+
client.agents.publish_proof(
|
|
36
|
+
kind="ProofOfAccuracy",
|
|
37
|
+
market_id="mkt_abc123",
|
|
38
|
+
outcome="yes",
|
|
39
|
+
resolution="yes",
|
|
40
|
+
)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, client):
|
|
44
|
+
self._client = client
|
|
45
|
+
|
|
46
|
+
def me(self) -> AgentPassport:
|
|
47
|
+
"""Get the authenticated agent's passport and reputation."""
|
|
48
|
+
data = self._client.get("/agents/me")
|
|
49
|
+
return AgentPassport(data)
|
|
50
|
+
|
|
51
|
+
def get(self, agent_id: str) -> AgentPassport:
|
|
52
|
+
"""Get another agent's public passport by ID."""
|
|
53
|
+
data = self._client.get(f"/agents/{agent_id}")
|
|
54
|
+
return AgentPassport(data)
|
|
55
|
+
|
|
56
|
+
def publish_proof(
|
|
57
|
+
self,
|
|
58
|
+
kind: str,
|
|
59
|
+
market_id: Optional[str] = None,
|
|
60
|
+
metadata: Optional[dict] = None,
|
|
61
|
+
**kwargs,
|
|
62
|
+
) -> dict:
|
|
63
|
+
"""
|
|
64
|
+
Publish a proof to ProofDB.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
kind: Proof kind — "ProofOfAccuracy" | "ProofOfWin" | "ProofOfDelegation"
|
|
68
|
+
| "ProofOfCompliance" | "ProofOfMemoryIntegrity"
|
|
69
|
+
market_id: Related market ID (for accuracy/win proofs)
|
|
70
|
+
metadata: Additional proof metadata
|
|
71
|
+
**kwargs: Additional proof fields
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict with proof_id, kind, published_at, signature
|
|
75
|
+
"""
|
|
76
|
+
body = {"kind": kind, **(metadata or {}), **kwargs}
|
|
77
|
+
if market_id:
|
|
78
|
+
body["market_id"] = market_id
|
|
79
|
+
return self._client.post("/agents/proofs", body=body)
|
|
80
|
+
|
|
81
|
+
def get_leaderboard(
|
|
82
|
+
self,
|
|
83
|
+
category: Optional[str] = None,
|
|
84
|
+
limit: int = 10,
|
|
85
|
+
) -> List[AgentPassport]:
|
|
86
|
+
"""
|
|
87
|
+
Get the top agents by reputation score.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
category: Filter by agent category
|
|
91
|
+
limit: Max results (default 10)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of AgentPassport ordered by reputation_score desc
|
|
95
|
+
"""
|
|
96
|
+
params = {"limit": limit}
|
|
97
|
+
if category:
|
|
98
|
+
params["category"] = category
|
|
99
|
+
data = self._client.get("/agents/leaderboard", params=params)
|
|
100
|
+
return [AgentPassport(a) for a in data.get("agents", [])]
|
blindoracle_sdk/aio.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Async client — ``AsyncBlindOracleClient``.
|
|
2
|
+
|
|
3
|
+
Zero-dependency async: the sync :class:`BlindOracleClient` (stdlib ``urllib``) is
|
|
4
|
+
run in a worker thread via :func:`asyncio.to_thread`, so awaiting a call never
|
|
5
|
+
blocks the event loop and we reuse every bit of the sync client's tested retry /
|
|
6
|
+
error / x402 logic. No httpx, no aiohttp.
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from blindoracle_sdk.aio import AsyncBlindOracleClient
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
bo = await AsyncBlindOracleClient.register("my-agent", ["verified-introduction"])
|
|
13
|
+
ms = await bo.markets.list(status="active", limit=5)
|
|
14
|
+
async for m in bo.markets.aiter(status="active", max_results=20):
|
|
15
|
+
print(m.title)
|
|
16
|
+
|
|
17
|
+
asyncio.run(main())
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
from typing import AsyncIterator
|
|
22
|
+
|
|
23
|
+
from blindoracle_sdk.client import BlindOracleClient
|
|
24
|
+
|
|
25
|
+
_NAMESPACES = (
|
|
26
|
+
"markets",
|
|
27
|
+
"compliance",
|
|
28
|
+
"signals",
|
|
29
|
+
"agents",
|
|
30
|
+
"audit",
|
|
31
|
+
"privacy",
|
|
32
|
+
"metrics",
|
|
33
|
+
"introductions",
|
|
34
|
+
"attestation",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _AsyncProxy:
|
|
39
|
+
"""Wrap a sync namespace so its methods become awaitable.
|
|
40
|
+
|
|
41
|
+
A sync generator method named ``iter`` is additionally exposed as an async
|
|
42
|
+
generator ``aiter`` (each ``next()`` runs in a thread).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, target):
|
|
46
|
+
self._t = target
|
|
47
|
+
|
|
48
|
+
def __getattr__(self, name):
|
|
49
|
+
attr = getattr(self._t, name)
|
|
50
|
+
if callable(attr):
|
|
51
|
+
|
|
52
|
+
async def _call(*args, **kwargs):
|
|
53
|
+
return await asyncio.to_thread(attr, *args, **kwargs)
|
|
54
|
+
|
|
55
|
+
return _call
|
|
56
|
+
return attr
|
|
57
|
+
|
|
58
|
+
def aiter(self, *args, **kwargs) -> AsyncIterator:
|
|
59
|
+
"""Async wrapper over a sync ``iter(...)`` generator (e.g. markets.aiter)."""
|
|
60
|
+
gen = self._t.iter(*args, **kwargs)
|
|
61
|
+
_sentinel = object()
|
|
62
|
+
|
|
63
|
+
async def _agen():
|
|
64
|
+
while True:
|
|
65
|
+
item = await asyncio.to_thread(next, gen, _sentinel)
|
|
66
|
+
if item is _sentinel:
|
|
67
|
+
return
|
|
68
|
+
yield item
|
|
69
|
+
|
|
70
|
+
return _agen()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AsyncBlindOracleClient:
|
|
74
|
+
"""Async facade over :class:`BlindOracleClient`. Same args, same namespaces."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, *args, **kwargs):
|
|
77
|
+
self._wrap(BlindOracleClient(*args, **kwargs))
|
|
78
|
+
|
|
79
|
+
def _wrap(self, sync: BlindOracleClient) -> "AsyncBlindOracleClient":
|
|
80
|
+
self._sync = sync
|
|
81
|
+
for ns in _NAMESPACES:
|
|
82
|
+
setattr(self, ns, _AsyncProxy(getattr(sync, ns)))
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
# passthrough identity / config
|
|
86
|
+
@property
|
|
87
|
+
def api_key(self):
|
|
88
|
+
return self._sync.api_key
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def agent_id(self):
|
|
92
|
+
return self._sync.agent_id
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def registration(self):
|
|
96
|
+
return self._sync.registration
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
async def register(
|
|
100
|
+
cls,
|
|
101
|
+
name,
|
|
102
|
+
capabilities,
|
|
103
|
+
evm_address: str = "",
|
|
104
|
+
base_url: str = BlindOracleClient.DEFAULT_BASE_URL,
|
|
105
|
+
timeout: int = 30,
|
|
106
|
+
) -> "AsyncBlindOracleClient":
|
|
107
|
+
"""Async one-line onboarding — see :meth:`BlindOracleClient.register`."""
|
|
108
|
+
sync = await asyncio.to_thread(
|
|
109
|
+
BlindOracleClient.register, name, capabilities, evm_address, base_url, timeout
|
|
110
|
+
)
|
|
111
|
+
return cls.__new__(cls)._wrap(sync)
|
|
112
|
+
|
|
113
|
+
async def get(self, path, params=None):
|
|
114
|
+
return await asyncio.to_thread(self._sync.get, path, params)
|
|
115
|
+
|
|
116
|
+
async def post(self, path, body=None, extra_headers=None):
|
|
117
|
+
return await asyncio.to_thread(self._sync.post, path, body, extra_headers)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""BlindOracle Attestation API — request a portable W3C Verifiable Credential
|
|
2
|
+
for a finished agent-security audit, via the public MCP endpoint.
|
|
3
|
+
|
|
4
|
+
REQUIRED FLOW (enforced server-side; this client surfaces it):
|
|
5
|
+
1. Onboard + activate the agent -> ERC-8004 passport (client.agents)
|
|
6
|
+
2. Run a BlindOracle audit -> ProofOfAuditReport kind 30105
|
|
7
|
+
3. request_credential(proof_id) -> W3C VC (this module)
|
|
8
|
+
|
|
9
|
+
A credential is ONLY issued/served when the audited agent holds an activated,
|
|
10
|
+
non-revoked passport AND a real audit proof exists. Skipping step 1 or 2 raises
|
|
11
|
+
PassportRequiredError / CredentialNotFoundError.
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import urllib.request
|
|
15
|
+
import urllib.error
|
|
16
|
+
|
|
17
|
+
from blindoracle_sdk.exceptions import (
|
|
18
|
+
PassportRequiredError,
|
|
19
|
+
CredentialNotFoundError,
|
|
20
|
+
BlindOracleError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DEFAULT_MCP_URL = "https://api.craigmbrown.com/mcp/attestation"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AttestationAPI:
|
|
27
|
+
"""Client for the BlindOracle Attestation MCP endpoint (get_audit_credential)."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, client, mcp_url: str = DEFAULT_MCP_URL):
|
|
30
|
+
self._client = client
|
|
31
|
+
self._mcp_url = mcp_url
|
|
32
|
+
|
|
33
|
+
def _rpc(self, method: str, params: dict | None = None) -> dict:
|
|
34
|
+
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
|
|
35
|
+
"params": params or {}}).encode()
|
|
36
|
+
req = urllib.request.Request(
|
|
37
|
+
self._mcp_url, data=body,
|
|
38
|
+
headers={"Content-Type": "application/json",
|
|
39
|
+
"User-Agent": "blindoracle-sdk/1.x"}, method="POST")
|
|
40
|
+
try:
|
|
41
|
+
with urllib.request.urlopen(req, timeout=getattr(self._client, "timeout", 30)) as r:
|
|
42
|
+
return json.loads(r.read())
|
|
43
|
+
except urllib.error.URLError as e: # noqa: BLE001
|
|
44
|
+
raise BlindOracleError(f"attestation endpoint unreachable: {e}")
|
|
45
|
+
|
|
46
|
+
def list_tools(self) -> list:
|
|
47
|
+
"""MCP tools/list — discover the attestation tools."""
|
|
48
|
+
return self._rpc("tools/list").get("result", {}).get("tools", [])
|
|
49
|
+
|
|
50
|
+
def request_credential(self, proof_id: str) -> dict:
|
|
51
|
+
"""Return the W3C Verifiable Credential for a finished audit's proof_id.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
PassportRequiredError — agent lacks an activated ERC-8004 passport.
|
|
55
|
+
CredentialNotFoundError — no credential for proof_id yet (run the audit).
|
|
56
|
+
"""
|
|
57
|
+
if not proof_id:
|
|
58
|
+
raise ValueError("proof_id is required")
|
|
59
|
+
resp = self._rpc("tools/call", {"name": "get_audit_credential",
|
|
60
|
+
"arguments": {"proof_id": proof_id}})
|
|
61
|
+
if "error" in resp:
|
|
62
|
+
raise BlindOracleError(f"attestation error: {resp['error']}")
|
|
63
|
+
result = resp.get("result", {})
|
|
64
|
+
text = (result.get("content") or [{}])[0].get("text", "")
|
|
65
|
+
if result.get("isError"):
|
|
66
|
+
low = text.lower()
|
|
67
|
+
if "no credential found" in low or "not found" in low:
|
|
68
|
+
raise CredentialNotFoundError(text)
|
|
69
|
+
if "passport" in low:
|
|
70
|
+
raise PassportRequiredError(text)
|
|
71
|
+
raise CredentialNotFoundError(text)
|
|
72
|
+
try:
|
|
73
|
+
return json.loads(text)
|
|
74
|
+
except json.JSONDecodeError:
|
|
75
|
+
raise BlindOracleError(f"unexpected attestation response: {text[:160]}")
|
|
76
|
+
|
|
77
|
+
# alias — the name external callers expect
|
|
78
|
+
get_audit_credential = request_credential
|
blindoracle_sdk/audit.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""BlindOracle Audit API — verifiable, on-chain-anchored agent audits.
|
|
2
|
+
|
|
3
|
+
Exposes the verifiable-anchoring layer (shipped 2026-05-23): retrieve an agent's audit report +
|
|
4
|
+
attestation, and INDEPENDENTLY verify it — inclusion proofs are checked client-side (don't trust
|
|
5
|
+
the server), anchor receipts via any public RPC / Nostr relay.
|
|
6
|
+
"""
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
# public Base RPCs for keyless anchor read-back (fallback chain)
|
|
13
|
+
_BASE_MAINNET_RPC = ["https://mainnet.base.org", "https://base.llamarpc.com"]
|
|
14
|
+
_BASE_SEPOLIA_RPC = ["https://sepolia.base.org"]
|
|
15
|
+
_VERIFY_ANCHOR_SELECTOR = "0xf32bd282" # keccak256("verifyAnchor(bytes32)")[:4]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuditAttestation:
|
|
19
|
+
"""An agent's 'VERIFIABLY-AUDITED' attestation (lives in its passport)."""
|
|
20
|
+
def __init__(self, data: dict):
|
|
21
|
+
self.audit_id = data.get("audit_id")
|
|
22
|
+
self.risk_score = data.get("risk_score")
|
|
23
|
+
self.risk_level = data.get("risk_level")
|
|
24
|
+
self.findings_count = data.get("findings_count")
|
|
25
|
+
self.audit_hash = data.get("audit_hash")
|
|
26
|
+
self.proof_of_audit_id = data.get("proof_of_audit_id") # kind 30105
|
|
27
|
+
self.state_anchor_proof_id = data.get("state_anchor_proof_id") # kind 30106
|
|
28
|
+
self.merkle_root = data.get("merkle_root")
|
|
29
|
+
self.root_commitment = data.get("root_commitment")
|
|
30
|
+
self.witnesses = data.get("witnesses", {})
|
|
31
|
+
self.badge = data.get("badge")
|
|
32
|
+
self.raw = data
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return (f"<AuditAttestation {self.audit_id!r} risk={self.risk_score} "
|
|
36
|
+
f"badge={self.badge!r} anchored={bool(self.state_anchor_proof_id)}>")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _sorted_pair(a_hex: str, b_hex: str) -> str:
|
|
40
|
+
a, b = bytes.fromhex(a_hex), bytes.fromhex(b_hex)
|
|
41
|
+
lo, hi = (a, b) if a <= b else (b, a)
|
|
42
|
+
return hashlib.sha256(lo + hi).hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def verify_inclusion(leaf_hex: str, proof_path: list, merkle_root_hex: str) -> bool:
|
|
46
|
+
"""Client-side inclusion check (sorted-pair Merkle). No network, no trust in the server.
|
|
47
|
+
|
|
48
|
+
Fold the leaf with each sibling in ``proof_path`` and compare to ``merkle_root_hex``.
|
|
49
|
+
"""
|
|
50
|
+
acc = leaf_hex
|
|
51
|
+
for sib in proof_path:
|
|
52
|
+
acc = _sorted_pair(acc, sib)
|
|
53
|
+
return acc == merkle_root_hex
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _rpc(urls, method, params, timeout=15):
|
|
57
|
+
body = json.dumps({"jsonrpc": "2.0", "method": method, "params": params, "id": 1}).encode()
|
|
58
|
+
last = None
|
|
59
|
+
for url in urls:
|
|
60
|
+
try:
|
|
61
|
+
req = urllib.request.Request(url, data=body, headers={
|
|
62
|
+
"content-type": "application/json", "User-Agent": "blindoracle-sdk/0.2"})
|
|
63
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
64
|
+
return json.loads(r.read().decode()).get("result")
|
|
65
|
+
except Exception as e: # noqa: BLE001
|
|
66
|
+
last = e
|
|
67
|
+
raise RuntimeError(f"all RPCs failed: {last}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def verify_anchor(root_commitment_hex: str, contract: str, network: str = "base-mainnet") -> dict:
|
|
71
|
+
"""Independently confirm a state-anchor root via ProofAnchor.verifyAnchor on a public RPC.
|
|
72
|
+
|
|
73
|
+
Returns {"exists": bool, "network", "contract"}. No keys, no spend.
|
|
74
|
+
"""
|
|
75
|
+
urls = _BASE_MAINNET_RPC if network == "base-mainnet" else _BASE_SEPOLIA_RPC
|
|
76
|
+
data = _VERIFY_ANCHOR_SELECTOR + root_commitment_hex.removeprefix("0x").rjust(64, "0")
|
|
77
|
+
out = _rpc(urls, "eth_call", [{"to": contract, "data": data}, "latest"])
|
|
78
|
+
exists = bool(out) and out != "0x" and int(out[2:66], 16) == 1
|
|
79
|
+
return {"exists": exists, "network": network, "contract": contract}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AuditAPI:
|
|
83
|
+
"""Retrieve + independently verify agent audits.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
att = client.audit.get_attestation("agent-x")
|
|
87
|
+
# don't trust — verify:
|
|
88
|
+
ok = client.audit.verify_anchor_receipt(att)
|
|
89
|
+
incl = client.audit.verify_inclusion_proof(leaf, proof_path, att.merkle_root)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, client):
|
|
93
|
+
self._client = client
|
|
94
|
+
|
|
95
|
+
def get_report(self, agent_id: str) -> dict:
|
|
96
|
+
"""Full audit report JSON for an agent (findings, risk, audit_hash, proof ids)."""
|
|
97
|
+
return self._client.gw_get(f"/a2a/agents/{agent_id}/audit-report")
|
|
98
|
+
|
|
99
|
+
def get_attestation(self, agent_id: str) -> AuditAttestation:
|
|
100
|
+
"""The passport-level 'VERIFIABLY-AUDITED' attestation (lighter than the full report)."""
|
|
101
|
+
data = self._client.gw_get(f"/a2a/agents/{agent_id}/audit-attestation")
|
|
102
|
+
return AuditAttestation(data)
|
|
103
|
+
|
|
104
|
+
def list_anchor_receipts(self, limit: int = 20) -> list:
|
|
105
|
+
"""Recent state-anchor receipts (root_commitment + witness tx/event ids)."""
|
|
106
|
+
return self._client.gw_get("/a2a/anchor-receipts", params={"limit": limit}).get("entries", [])
|
|
107
|
+
|
|
108
|
+
# ---- independent verification (client-side / keyless) ----
|
|
109
|
+
@staticmethod
|
|
110
|
+
def verify_inclusion_proof(leaf_hex: str, proof_path: list, merkle_root_hex: str) -> bool:
|
|
111
|
+
"""Verify a single record belongs to the committed set — locally, no server trust."""
|
|
112
|
+
return verify_inclusion(leaf_hex, proof_path, merkle_root_hex)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def verify_anchor_receipt(attestation, network: str = "base-mainnet") -> dict:
|
|
116
|
+
"""Confirm an attestation's root is anchored on-chain via a public RPC.
|
|
117
|
+
|
|
118
|
+
Accepts an AuditAttestation or a dict with root_commitment + witness contract.
|
|
119
|
+
"""
|
|
120
|
+
att = attestation.raw if isinstance(attestation, AuditAttestation) else attestation
|
|
121
|
+
root = att.get("root_commitment")
|
|
122
|
+
witnesses = att.get("witnesses", {})
|
|
123
|
+
contract = (witnesses.get("base_mainnet") or {}).get("contract") if isinstance(
|
|
124
|
+
witnesses.get("base_mainnet"), dict) else att.get("mainnet_contract")
|
|
125
|
+
if not (root and contract):
|
|
126
|
+
return {"exists": False, "error": "no root_commitment / mainnet contract in attestation"}
|
|
127
|
+
return verify_anchor(root, contract, network)
|
blindoracle_sdk/cli.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""``blindoracle`` command-line interface — try the marketplace before you code.
|
|
2
|
+
|
|
3
|
+
blindoracle version
|
|
4
|
+
blindoracle register my-agent --cap verified-introduction --cap research
|
|
5
|
+
blindoracle markets list --status active --limit 5
|
|
6
|
+
blindoracle agent me # uses BLINDORACLE_API_KEY from env
|
|
7
|
+
|
|
8
|
+
Thin wrapper over the SDK; prints JSON so output pipes into ``jq``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from blindoracle_sdk import BlindOracleClient, __version__
|
|
16
|
+
from blindoracle_sdk.exceptions import BlindOracleError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _print(obj) -> None:
|
|
20
|
+
print(json.dumps(obj, indent=2, default=str))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _cmd_version(args) -> int:
|
|
24
|
+
_print({"blindoracle_sdk": __version__})
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _cmd_register(args) -> int:
|
|
29
|
+
bo = BlindOracleClient.register(args.name, args.cap, evm_address=args.evm or "")
|
|
30
|
+
_print(
|
|
31
|
+
{
|
|
32
|
+
"agent_id": bo.agent_id,
|
|
33
|
+
"api_key": bo.api_key,
|
|
34
|
+
"tier": (bo.registration or {}).get("tier"),
|
|
35
|
+
"hint": "export BLINDORACLE_API_KEY=<api_key> to reuse this identity",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _cmd_markets_list(args) -> int:
|
|
42
|
+
bo = BlindOracleClient(api_key=args.api_key)
|
|
43
|
+
ms = bo.markets.list(status=args.status, category=args.category, limit=args.limit)
|
|
44
|
+
_print(
|
|
45
|
+
[
|
|
46
|
+
{"id": m.id, "title": m.title, "yes_probability": m.yes_probability, "status": m.status}
|
|
47
|
+
for m in ms
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cmd_agent_me(args) -> int:
|
|
54
|
+
bo = BlindOracleClient(api_key=args.api_key)
|
|
55
|
+
me = bo.agents.me()
|
|
56
|
+
_print(me.raw if hasattr(me, "raw") else me)
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
61
|
+
p = argparse.ArgumentParser(prog="blindoracle", description="BlindOracle agent-marketplace CLI")
|
|
62
|
+
p.add_argument(
|
|
63
|
+
"--api-key", dest="api_key", default=None, help="API key (else BLINDORACLE_API_KEY env)"
|
|
64
|
+
)
|
|
65
|
+
sub = p.add_subparsers(dest="command")
|
|
66
|
+
|
|
67
|
+
sub.add_parser("version", help="print SDK version").set_defaults(func=_cmd_version)
|
|
68
|
+
|
|
69
|
+
pr = sub.add_parser("register", help="self-serve onboard -> ERC-8004 passport + key")
|
|
70
|
+
pr.add_argument("name")
|
|
71
|
+
pr.add_argument(
|
|
72
|
+
"--cap", action="append", default=[], required=True, help="capability (repeatable)"
|
|
73
|
+
)
|
|
74
|
+
pr.add_argument("--evm", default=None, help="optional EVM address")
|
|
75
|
+
pr.set_defaults(func=_cmd_register)
|
|
76
|
+
|
|
77
|
+
pm = sub.add_parser("markets", help="market operations")
|
|
78
|
+
msub = pm.add_subparsers(dest="markets_command")
|
|
79
|
+
ml = msub.add_parser("list", help="list markets")
|
|
80
|
+
ml.add_argument("--status", default="active")
|
|
81
|
+
ml.add_argument("--category", default=None)
|
|
82
|
+
ml.add_argument("--limit", type=int, default=20)
|
|
83
|
+
ml.set_defaults(func=_cmd_markets_list)
|
|
84
|
+
|
|
85
|
+
pa = sub.add_parser("agent", help="agent operations")
|
|
86
|
+
asub = pa.add_subparsers(dest="agent_command")
|
|
87
|
+
asub.add_parser("me", help="your passport + reputation").set_defaults(func=_cmd_agent_me)
|
|
88
|
+
|
|
89
|
+
return p
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main(argv=None) -> int:
|
|
93
|
+
args = build_parser().parse_args(argv)
|
|
94
|
+
if not getattr(args, "func", None):
|
|
95
|
+
build_parser().print_help()
|
|
96
|
+
return 1
|
|
97
|
+
try:
|
|
98
|
+
return args.func(args)
|
|
99
|
+
except BlindOracleError as e:
|
|
100
|
+
print(f"error: {e}", file=sys.stderr)
|
|
101
|
+
return 2
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
sys.exit(main())
|