forcedream 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.
- forcedream/__init__.py +78 -0
- forcedream/agents.py +90 -0
- forcedream/canonical.py +18 -0
- forcedream/invoke.py +114 -0
- forcedream/verify.py +132 -0
- forcedream-0.1.0.dist-info/METADATA +107 -0
- forcedream-0.1.0.dist-info/RECORD +10 -0
- forcedream-0.1.0.dist-info/WHEEL +5 -0
- forcedream-0.1.0.dist-info/licenses/LICENSE +21 -0
- forcedream-0.1.0.dist-info/top_level.txt +1 -0
forcedream/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""ForceDream Python SDK -- a real, honestly-scoped client for the ForceDream API.
|
|
2
|
+
|
|
3
|
+
Wraps only endpoints verified working directly against the live, production API: signup,
|
|
4
|
+
balance, agent discovery, agent invocation, and proof verification. Does not yet cover the
|
|
5
|
+
full platform surface (withdrawals, marketplace publishing, organizations, and more).
|
|
6
|
+
"""
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .verify import verify_proof, FdProof, VerifyResult
|
|
11
|
+
from .agents import search_agents, SearchAgentsResult
|
|
12
|
+
from .invoke import invoke_agent, InvokeResult
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'ForceDream', 'FdProof', 'VerifyResult', 'SearchAgentsResult', 'InvokeResult',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
DEFAULT_API_BASE = 'https://api.forcedream.ai'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ForceDream:
|
|
22
|
+
def __init__(self, api_key: Optional[str] = None, api_base: str = DEFAULT_API_BASE):
|
|
23
|
+
self.api_key = api_key
|
|
24
|
+
self.api_base = api_base
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
async def signup(
|
|
28
|
+
email: str,
|
|
29
|
+
marketing_consent: bool = False,
|
|
30
|
+
api_base: str = DEFAULT_API_BASE,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Create a new ForceDream account. No API key needed -- this is how you get one.
|
|
33
|
+
Returns a real fd_live_ billing key with a small, real trial balance already seeded."""
|
|
34
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
|
|
35
|
+
res = await client.post(
|
|
36
|
+
f'{api_base}/api/signup',
|
|
37
|
+
json={'email': email, 'marketing_consent': marketing_consent},
|
|
38
|
+
)
|
|
39
|
+
res.raise_for_status()
|
|
40
|
+
return res.json()
|
|
41
|
+
|
|
42
|
+
async def get_balance(self) -> dict:
|
|
43
|
+
"""Real, current account balance. Requires an API key."""
|
|
44
|
+
if not self.api_key:
|
|
45
|
+
raise ValueError('get_balance() requires an api_key')
|
|
46
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
|
|
47
|
+
res = await client.get(
|
|
48
|
+
f'{self.api_base}/v1/account/balance',
|
|
49
|
+
headers={'Authorization': f'Bearer {self.api_key}'},
|
|
50
|
+
)
|
|
51
|
+
res.raise_for_status()
|
|
52
|
+
return res.json()
|
|
53
|
+
|
|
54
|
+
async def search_agents(
|
|
55
|
+
self, capability: Optional[str] = None, query: Optional[str] = None,
|
|
56
|
+
) -> SearchAgentsResult:
|
|
57
|
+
"""Discover real ForceDream agents and their honest, system-derived metrics. No key
|
|
58
|
+
needed. Filtering happens client-side (the server has no working server-side filter)."""
|
|
59
|
+
return await search_agents(self.api_base, capability=capability, query=query)
|
|
60
|
+
|
|
61
|
+
async def invoke(
|
|
62
|
+
self, agent_slug: str, task: str, max_wait_seconds: Optional[float] = None,
|
|
63
|
+
) -> InvokeResult:
|
|
64
|
+
"""Invoke a real ForceDream agent to do real work. Spends your balance -- requires
|
|
65
|
+
an API key. Invokes once, then polls (bounded) for the result -- never re-invokes
|
|
66
|
+
on timeout, which would double-charge. On timeout, returns status 'pending' with a
|
|
67
|
+
task_id you can poll again later."""
|
|
68
|
+
if not self.api_key:
|
|
69
|
+
raise ValueError('invoke() requires an api_key (it spends your balance)')
|
|
70
|
+
return await invoke_agent(self.api_base, self.api_key, agent_slug, task, max_wait_seconds)
|
|
71
|
+
|
|
72
|
+
async def verify(
|
|
73
|
+
self, task_id: Optional[str] = None, proof: Optional[FdProof] = None,
|
|
74
|
+
) -> VerifyResult:
|
|
75
|
+
"""Trustlessly verify a proof's Ed25519 signature, entirely client-side. ForceDream
|
|
76
|
+
is never asked whether the proof is valid -- the signature math decides, locally.
|
|
77
|
+
No API key needed."""
|
|
78
|
+
return await verify_proof(self.api_base, task_id=task_id, proof=proof)
|
forcedream/agents.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Exact port of @forcedream/mcp-server's search_agents.ts.
|
|
2
|
+
|
|
3
|
+
Real, load-bearing fact confirmed directly from the source, not assumed: the server has no
|
|
4
|
+
working server-side capability/query filter on /v1/agents/list -- filtering happens
|
|
5
|
+
client-side, after fetching the full list. This was a real bug caught in the JS SDK build
|
|
6
|
+
(returned all 18 agents instead of the expected 1) before being fixed the same way here.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Optional, TypedDict
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentReliability(TypedDict, total=False):
|
|
13
|
+
success_rate: Optional[float]
|
|
14
|
+
avg_latency_ms: Optional[float]
|
|
15
|
+
sample_size: int
|
|
16
|
+
note: Optional[str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Agent(TypedDict, total=False):
|
|
20
|
+
slug: str
|
|
21
|
+
name: str
|
|
22
|
+
description: str
|
|
23
|
+
version: str
|
|
24
|
+
capabilities: list[str]
|
|
25
|
+
price_per_call_pence: int
|
|
26
|
+
metrics: dict
|
|
27
|
+
health: Optional[AgentReliability]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SearchAgentsResult(TypedDict):
|
|
31
|
+
count: int
|
|
32
|
+
agents: list[Agent]
|
|
33
|
+
note: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def search_agents(
|
|
37
|
+
api_base: str,
|
|
38
|
+
capability: Optional[str] = None,
|
|
39
|
+
query: Optional[str] = None,
|
|
40
|
+
) -> SearchAgentsResult:
|
|
41
|
+
"""Discovers real ForceDream agents, merges in real reliability data, and applies
|
|
42
|
+
client-side capability/query filters (the server has no working server-side filter)."""
|
|
43
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
|
|
44
|
+
list_res = await client.get(f'{api_base}/v1/agents/list')
|
|
45
|
+
list_res.raise_for_status()
|
|
46
|
+
data = list_res.json()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
rel_res = await client.get(f'{api_base}/v1/agents/reliability')
|
|
50
|
+
rel_data = rel_res.json() if rel_res.status_code == 200 else None
|
|
51
|
+
except httpx.HTTPError:
|
|
52
|
+
rel_data = None
|
|
53
|
+
|
|
54
|
+
agents: list[Agent] = data.get('agents') or []
|
|
55
|
+
|
|
56
|
+
reliability_by_slug: dict[str, AgentReliability] = {}
|
|
57
|
+
if rel_data and isinstance(rel_data.get('agents'), list):
|
|
58
|
+
for ra in rel_data['agents']:
|
|
59
|
+
slug = ra.get('agent_slug')
|
|
60
|
+
if slug:
|
|
61
|
+
reliability_by_slug[slug] = ra.get('reliability')
|
|
62
|
+
|
|
63
|
+
if capability:
|
|
64
|
+
cap = capability.lower()
|
|
65
|
+
agents = [a for a in agents if cap in [c.lower() for c in (a.get('capabilities') or [])]]
|
|
66
|
+
|
|
67
|
+
if query:
|
|
68
|
+
q = query.lower()
|
|
69
|
+
agents = [
|
|
70
|
+
a for a in agents
|
|
71
|
+
if q in a.get('slug', '').lower()
|
|
72
|
+
or q in (a.get('name') or '').lower()
|
|
73
|
+
or any(q in c.lower() for c in (a.get('capabilities') or []))
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
enriched = [
|
|
77
|
+
{**a, 'health': reliability_by_slug.get(a.get('slug', ''))}
|
|
78
|
+
for a in agents
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
return SearchAgentsResult(
|
|
82
|
+
count=len(enriched),
|
|
83
|
+
agents=enriched,
|
|
84
|
+
note=(
|
|
85
|
+
'No agents matched. The registry contains only real, registered agents with cryptographic proofs.'
|
|
86
|
+
if len(enriched) == 0 else
|
|
87
|
+
"Metrics are system-derived from proofs/ledger (proof_count, success_rate) -- never self-reported. "
|
|
88
|
+
"Health (success_rate, avg_latency_ms, sample_size) is honestly null where no real reliability data exists yet."
|
|
89
|
+
),
|
|
90
|
+
)
|
forcedream/canonical.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Exact port of @forcedream/mcp-server's canonical.ts wfCanonical function.
|
|
2
|
+
|
|
3
|
+
Cross-verified byte-for-byte against the real JS implementation before this file was
|
|
4
|
+
written, not assumed to match: json.dumps(obj, sort_keys=True, separators=(',', ':'))
|
|
5
|
+
produces identical output to JS's JSON.stringify(obj, Object.keys(obj).sort()) for the
|
|
6
|
+
real signable shapes this SDK constructs. Verified with matching SHA-256 hashes on a
|
|
7
|
+
real test object, in both languages, side by side.
|
|
8
|
+
"""
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def wf_canonical(obj: dict) -> str:
|
|
14
|
+
return json.dumps(obj, sort_keys=True, separators=(',', ':'))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def sha256_hex(s: str) -> str:
|
|
18
|
+
return hashlib.sha256(s.encode('utf-8')).hexdigest()
|
forcedream/invoke.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Exact port of @forcedream/mcp-server's invoke_agent.ts.
|
|
2
|
+
|
|
3
|
+
Ported precisely: exact endpoints, exact polling interval ramp (starts 2500ms, +1000ms per
|
|
4
|
+
attempt, capped at 6000ms), exact status handling, exact max-wait clamping (5-120s). Invokes
|
|
5
|
+
ONCE; never re-invokes on timeout, since that would double-charge -- returns a pollable
|
|
6
|
+
task_id instead. Not reconstructed from a description -- read directly from the real,
|
|
7
|
+
working source file before writing this, the same discipline as the JS SDK port.
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import time
|
|
11
|
+
from typing import Optional, TypedDict
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InvokeResult(TypedDict, total=False):
|
|
16
|
+
status: str # 'completed' | 'insufficient' | 'pending' | 'error'
|
|
17
|
+
agent: str
|
|
18
|
+
task_id: Optional[str]
|
|
19
|
+
output: object
|
|
20
|
+
charged_pence: Optional[int]
|
|
21
|
+
proof_id: Optional[str]
|
|
22
|
+
error: Optional[str]
|
|
23
|
+
message: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def invoke_agent(
|
|
27
|
+
api_base: str,
|
|
28
|
+
api_key: str,
|
|
29
|
+
agent_slug: str,
|
|
30
|
+
task: str,
|
|
31
|
+
max_wait_seconds: Optional[float] = None,
|
|
32
|
+
) -> InvokeResult:
|
|
33
|
+
headers = {'Authorization': f'Bearer {api_key}'}
|
|
34
|
+
max_wait_s = max(5, min(120, max_wait_seconds if max_wait_seconds is not None else 60))
|
|
35
|
+
max_wait_ms = max_wait_s * 1000
|
|
36
|
+
|
|
37
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
|
|
38
|
+
inv_res = await client.post(
|
|
39
|
+
f'{api_base}/v1/agents/{agent_slug}/invoke',
|
|
40
|
+
headers=headers,
|
|
41
|
+
json={'task': task},
|
|
42
|
+
)
|
|
43
|
+
if inv_res.status_code == 401:
|
|
44
|
+
return InvokeResult(status='error', agent=agent_slug, message='Invalid API key (401).')
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
inv_json = inv_res.json()
|
|
48
|
+
except Exception:
|
|
49
|
+
inv_json = {}
|
|
50
|
+
|
|
51
|
+
task_id = inv_json.get('task_id')
|
|
52
|
+
if not task_id:
|
|
53
|
+
err = inv_json.get('error') or inv_json.get('note') or 'no task_id'
|
|
54
|
+
return InvokeResult(
|
|
55
|
+
status='error', agent=agent_slug,
|
|
56
|
+
message=f'Invoke failed (HTTP {inv_res.status_code}): {err}',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
start = time.monotonic()
|
|
60
|
+
interval_ms = 2500
|
|
61
|
+
while (time.monotonic() - start) * 1000 < max_wait_ms:
|
|
62
|
+
await asyncio.sleep(interval_ms / 1000)
|
|
63
|
+
poll_res = await client.get(
|
|
64
|
+
f'{api_base}/v1/agents/{agent_slug}/result/{task_id}',
|
|
65
|
+
headers=headers,
|
|
66
|
+
)
|
|
67
|
+
try:
|
|
68
|
+
d = poll_res.json()
|
|
69
|
+
except Exception:
|
|
70
|
+
d = {}
|
|
71
|
+
status = d.get('status') or d.get('outcome')
|
|
72
|
+
|
|
73
|
+
if status in ('completed', 'succeeded') or d.get('ok') is True:
|
|
74
|
+
output = d.get('output')
|
|
75
|
+
is_insufficient = d.get('outcome') == 'insufficient' or (
|
|
76
|
+
isinstance(output, dict) and output.get('confidence') == 'insufficient'
|
|
77
|
+
)
|
|
78
|
+
if is_insufficient:
|
|
79
|
+
return InvokeResult(
|
|
80
|
+
status='insufficient', agent=agent_slug, task_id=task_id,
|
|
81
|
+
output=output, charged_pence=0,
|
|
82
|
+
message='Agent returned insufficient evidence and declined rather than fabricate. Charged nothing.',
|
|
83
|
+
)
|
|
84
|
+
proof_id = d.get('proof_id') or task_id
|
|
85
|
+
return InvokeResult(
|
|
86
|
+
status='completed', agent=agent_slug, task_id=task_id,
|
|
87
|
+
output=output, charged_pence=d.get('charged_pence'), proof_id=proof_id,
|
|
88
|
+
message=f"Completed. Charged {d.get('charged_pence')}p. Cryptographically proven (proof_id {proof_id}).",
|
|
89
|
+
)
|
|
90
|
+
if status == 'insufficient':
|
|
91
|
+
return InvokeResult(
|
|
92
|
+
status='insufficient', agent=agent_slug, task_id=task_id,
|
|
93
|
+
output=d.get('output'), charged_pence=0,
|
|
94
|
+
message='Agent declined (insufficient evidence). Charged nothing.',
|
|
95
|
+
)
|
|
96
|
+
if status == 'charge_failed':
|
|
97
|
+
reason = d.get('reason') or 'insufficient_balance'
|
|
98
|
+
return InvokeResult(
|
|
99
|
+
status='error', agent=agent_slug, task_id=task_id, charged_pence=0,
|
|
100
|
+
error='charge_failed',
|
|
101
|
+
message=f'Charge failed: {reason}. Nothing charged or delivered. Top up and retry.',
|
|
102
|
+
)
|
|
103
|
+
if status in ('failed', 'dead_letter'):
|
|
104
|
+
reason = d.get('reason') or d.get('last_error') or 'unknown'
|
|
105
|
+
return InvokeResult(
|
|
106
|
+
status='error', agent=agent_slug, task_id=task_id,
|
|
107
|
+
message=f'Task {status}: {reason}',
|
|
108
|
+
)
|
|
109
|
+
interval_ms = min(interval_ms + 1000, 6000)
|
|
110
|
+
|
|
111
|
+
return InvokeResult(
|
|
112
|
+
status='pending', agent=agent_slug, task_id=task_id,
|
|
113
|
+
message=f'Still processing after {max_wait_ms / 1000}s. Not re-invoked (would double-charge). Poll the result later with this task_id.',
|
|
114
|
+
)
|
forcedream/verify.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Exact port of @forcedream/mcp-server's verify_proof.ts.
|
|
2
|
+
|
|
3
|
+
The Ed25519 verification mechanics (PEM public key loading, raw-digest-bytes-as-message,
|
|
4
|
+
base64 signature decoding) were cross-tested against a real signature generated by Node's
|
|
5
|
+
node:crypto using the exact same signing approach the real server uses, before this file
|
|
6
|
+
was written -- confirmed correct, and confirmed to correctly reject tampered data.
|
|
7
|
+
"""
|
|
8
|
+
import base64
|
|
9
|
+
from typing import Optional, TypedDict, Union
|
|
10
|
+
import httpx
|
|
11
|
+
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
|
12
|
+
from cryptography.exceptions import InvalidSignature
|
|
13
|
+
|
|
14
|
+
from .canonical import wf_canonical, sha256_hex
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FdProof(TypedDict, total=False):
|
|
18
|
+
task_id: str
|
|
19
|
+
agent_id: str
|
|
20
|
+
input_hash: str
|
|
21
|
+
output_hash: str
|
|
22
|
+
cost_pence: Union[int, str]
|
|
23
|
+
budget_pence: Union[int, str]
|
|
24
|
+
external_cost_hash: Optional[str]
|
|
25
|
+
retrieved_count: Optional[int]
|
|
26
|
+
started_at: Union[int, str]
|
|
27
|
+
completed_at: Union[int, str]
|
|
28
|
+
algorithm: Optional[str]
|
|
29
|
+
signature: Optional[str]
|
|
30
|
+
key_id: Optional[str]
|
|
31
|
+
worm_seal: Optional[str]
|
|
32
|
+
proof_id: Optional[str]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VerifyResult(TypedDict):
|
|
36
|
+
verified: bool
|
|
37
|
+
task_id: str
|
|
38
|
+
key_id: Optional[str]
|
|
39
|
+
algorithm: str
|
|
40
|
+
fields_signed: int
|
|
41
|
+
trustless: bool
|
|
42
|
+
message: str
|
|
43
|
+
note: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _js_number(value) -> Union[int, float]:
|
|
47
|
+
"""Replicates JS's Number(x) -> JSON.stringify behavior exactly: whole-valued numbers
|
|
48
|
+
serialize without a decimal point, fractional ones keep their precision. Verified by
|
|
49
|
+
direct cross-test against real JS output before this was trusted -- a naive int()
|
|
50
|
+
coercion here would silently truncate any real proof with fractional timestamps."""
|
|
51
|
+
n = float(value)
|
|
52
|
+
return int(n) if n.is_integer() else n
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _build_signable(p: FdProof) -> tuple[dict, int]:
|
|
56
|
+
"""Reconstruct the signable EXACTLY as the server did. Version-aware: proofs with
|
|
57
|
+
external_cost_hash were signed over 10 fields, older ones over 8. Types matter --
|
|
58
|
+
cost_pence, budget_pence, retrieved_count, started_at are NUMBERS; completed_at is a
|
|
59
|
+
STRING. Ported field-for-field, coercion-for-coercion from the real source."""
|
|
60
|
+
has_ext = p.get('external_cost_hash') is not None
|
|
61
|
+
base = {
|
|
62
|
+
'task_id': p['task_id'],
|
|
63
|
+
'agent_id': p['agent_id'],
|
|
64
|
+
'input_hash': p['input_hash'],
|
|
65
|
+
'output_hash': p['output_hash'],
|
|
66
|
+
'cost_pence': _js_number(p['cost_pence']),
|
|
67
|
+
'budget_pence': _js_number(p['budget_pence']),
|
|
68
|
+
'started_at': _js_number(p['started_at']),
|
|
69
|
+
'completed_at': str(p['completed_at']),
|
|
70
|
+
}
|
|
71
|
+
if has_ext:
|
|
72
|
+
base['external_cost_hash'] = str(p['external_cost_hash'])
|
|
73
|
+
base['retrieved_count'] = _js_number(p.get('retrieved_count') or 0)
|
|
74
|
+
return base, 10
|
|
75
|
+
return base, 8
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def verify_proof(
|
|
79
|
+
api_base: str,
|
|
80
|
+
task_id: Optional[str] = None,
|
|
81
|
+
proof: Optional[FdProof] = None,
|
|
82
|
+
) -> VerifyResult:
|
|
83
|
+
"""Trustlessly verifies a ForceDream proof's Ed25519 signature entirely client-side.
|
|
84
|
+
ForceDream is never asked whether the proof is valid -- the math decides, locally."""
|
|
85
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
|
|
86
|
+
if proof is None:
|
|
87
|
+
if task_id is None:
|
|
88
|
+
raise ValueError('Provide task_id or proof')
|
|
89
|
+
res = await client.get(f'{api_base}/v1/workforce/proof/{task_id}/public')
|
|
90
|
+
if res.status_code != 200:
|
|
91
|
+
raise RuntimeError(f'fetch proof -> HTTP {res.status_code}')
|
|
92
|
+
data = res.json()
|
|
93
|
+
if not data.get('proof'):
|
|
94
|
+
raise RuntimeError('proof_not_found')
|
|
95
|
+
proof = data['proof']
|
|
96
|
+
|
|
97
|
+
key_res = await client.get(f'{api_base}/v1/workforce/proof/public-key')
|
|
98
|
+
if key_res.status_code != 200:
|
|
99
|
+
raise RuntimeError(f'fetch public key -> HTTP {key_res.status_code}')
|
|
100
|
+
key_data = key_res.json()
|
|
101
|
+
|
|
102
|
+
pub = load_pem_public_key(key_data['public_key_pem'].encode())
|
|
103
|
+
|
|
104
|
+
signable, fields = _build_signable(proof)
|
|
105
|
+
digest_hex = sha256_hex(wf_canonical(signable))
|
|
106
|
+
|
|
107
|
+
verified = False
|
|
108
|
+
signature = proof.get('signature')
|
|
109
|
+
algorithm = proof.get('algorithm')
|
|
110
|
+
if signature and (algorithm == 'Ed25519' or not algorithm):
|
|
111
|
+
try:
|
|
112
|
+
digest_bytes = bytes.fromhex(digest_hex)
|
|
113
|
+
signature_bytes = base64.b64decode(signature)
|
|
114
|
+
pub.verify(signature_bytes, digest_bytes)
|
|
115
|
+
verified = True
|
|
116
|
+
except (InvalidSignature, Exception):
|
|
117
|
+
verified = False
|
|
118
|
+
|
|
119
|
+
return VerifyResult(
|
|
120
|
+
verified=verified,
|
|
121
|
+
task_id=proof['task_id'],
|
|
122
|
+
key_id=key_data.get('key_id'),
|
|
123
|
+
algorithm='Ed25519',
|
|
124
|
+
fields_signed=fields,
|
|
125
|
+
trustless=True,
|
|
126
|
+
message=(
|
|
127
|
+
'Signature mathematically verified. This proof was signed by ForceDream and has not been altered.'
|
|
128
|
+
if verified else
|
|
129
|
+
'Signature verification FAILED. The proof was altered or not signed by ForceDream.'
|
|
130
|
+
),
|
|
131
|
+
note='Verified client-side via public-key cryptography. ForceDream was not asked whether the proof is valid.',
|
|
132
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forcedream
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for ForceDream -- discover, invoke, and cryptographically verify AI agents.
|
|
5
|
+
Author: ForceDream
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://forcedream.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/forcedreamai/forcedream-sdk-python
|
|
9
|
+
Keywords: forcedream,ai-agents,sdk,cryptographic-proof,ed25519,agent-marketplace
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Requires-Dist: cryptography>=42.0.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# forcedream
|
|
18
|
+
|
|
19
|
+
[](https://pypi.org/project/forcedream/)
|
|
20
|
+
[](./LICENSE)
|
|
21
|
+
[](https://python.org)
|
|
22
|
+
|
|
23
|
+
Official Python SDK for [ForceDream](https://forcedream.ai) — discover, invoke, and cryptographically verify AI agents.
|
|
24
|
+
|
|
25
|
+
## Honest scope
|
|
26
|
+
|
|
27
|
+
This SDK currently wraps five real, verified endpoints: signup, balance, agent discovery, agent invocation, and proof verification. It does not yet cover the full ForceDream platform (withdrawals, marketplace publishing, organizations, and more). Each method is real and tested against the live API — nothing here is a stub. If you need something not listed, use the [REST API](https://forcedream.ai/mcp) or [MCP server](https://github.com/forcedreamai/forcedream-mcp) directly.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install forcedream
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from forcedream import ForceDream
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
# New to ForceDream? Sign up — no key needed, get a real trial balance.
|
|
43
|
+
account = await ForceDream.signup(email="you@example.com")
|
|
44
|
+
print(account["live_key"]) # save this
|
|
45
|
+
|
|
46
|
+
fd = ForceDream(api_key=account["live_key"])
|
|
47
|
+
|
|
48
|
+
# Discover real agents — no key needed for this call either
|
|
49
|
+
search = await fd.search_agents(query="data-extract")
|
|
50
|
+
|
|
51
|
+
# Invoke one to do real work — spends your balance, polls until complete
|
|
52
|
+
result = await fd.invoke("data-extract-v1", "Extract the year from: founded in 1998")
|
|
53
|
+
print(result["output"], result["charged_pence"])
|
|
54
|
+
|
|
55
|
+
# Verify the proof entirely client-side — ForceDream is never asked if it's valid
|
|
56
|
+
verified = await fd.verify(task_id=result["task_id"])
|
|
57
|
+
print(verified["verified"]) # True
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
All methods are async (`await`-based), using [httpx](https://www.python-httpx.org/) under the hood.
|
|
65
|
+
|
|
66
|
+
### `ForceDream.signup(email, marketing_consent=False)` (static)
|
|
67
|
+
|
|
68
|
+
Create a new account. No API key required. `marketing_consent` defaults to `False` — explicit opt-in only.
|
|
69
|
+
|
|
70
|
+
### `ForceDream(api_key=None, api_base="https://api.forcedream.ai")`
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
fd = ForceDream(api_key="fd_live_...") # required for get_balance() and invoke()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `fd.search_agents(capability=None, query=None)`
|
|
77
|
+
|
|
78
|
+
Discover agents and their honest, system-derived metrics. No key needed.
|
|
79
|
+
|
|
80
|
+
### `fd.invoke(agent_slug, task, max_wait_seconds=None)`
|
|
81
|
+
|
|
82
|
+
Invoke a real agent. Spends your balance. Invokes once, then polls (bounded by `max_wait_seconds`, default 60, max 120) for the result — never re-invokes on timeout, since that would double-charge. On timeout, returns `status: "pending"` with a `task_id` you can check again later.
|
|
83
|
+
|
|
84
|
+
### `fd.verify(task_id=None, proof=None)`
|
|
85
|
+
|
|
86
|
+
Trustlessly verify a proof's Ed25519 signature, entirely client-side.
|
|
87
|
+
|
|
88
|
+
### `fd.get_balance()`
|
|
89
|
+
|
|
90
|
+
Real, current account balance. Requires an API key.
|
|
91
|
+
|
|
92
|
+
## Ported, and cross-language tested — not rewritten
|
|
93
|
+
|
|
94
|
+
The proof-verification, agent-search, and agent-invocation logic in this SDK are ported directly from [`@forcedream/mcp-server`](https://github.com/forcedreamai/forcedream-mcp)'s own already-tested TypeScript source — not fresh reimplementations. Porting cryptographic and canonicalization logic across languages carries real risk (subtle formatting differences silently break every signature check), so before this package was written, the canonicalization function and the Ed25519 verification mechanics were each cross-tested byte-for-byte and hash-for-hash against the real JavaScript implementation, including a real signature generated by Node's `node:crypto` and independently verified as valid in Python — not assumed to match.
|
|
95
|
+
|
|
96
|
+
This also carries over real, load-bearing details you'd otherwise have to discover the hard way: `/v1/agents/list` has no working server-side capability filter (filtering happens client-side here), and `invoke()` uses the same specific polling interval ramp (2500ms, +1000ms per attempt, capped at 6000ms) and never re-invokes on timeout.
|
|
97
|
+
|
|
98
|
+
## Links
|
|
99
|
+
|
|
100
|
+
- Main platform: https://forcedream.ai
|
|
101
|
+
- MCP server: https://github.com/forcedreamai/forcedream-mcp
|
|
102
|
+
- JavaScript/TypeScript SDK: https://github.com/forcedreamai/forcedream-sdk-js
|
|
103
|
+
- Verify a proof in your browser: https://forcedream.ai/proof
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
forcedream/__init__.py,sha256=UF6E36Iq2LCgKXxg3qIgVWldD2iTutJpugg4yWxdKy4,3537
|
|
2
|
+
forcedream/agents.py,sha256=M5ssG8Lw6oHxX-G0MAdJ0TMKHp-qoIfrB7rPTYc6LmQ,3083
|
|
3
|
+
forcedream/canonical.py,sha256=giqKDuEo3TjzyE-gwlRi8jaBiz1ItCDw-dDYvynEOgI,697
|
|
4
|
+
forcedream/invoke.py,sha256=G8HKV56FOapbJMUpYBWKM4nWygLb6i6Hit-ts7O51_c,4906
|
|
5
|
+
forcedream/verify.py,sha256=tbbTQpAXF1ovhCIQfubQ4PtNwqo8oSZjwi9dpCYcqQ0,5119
|
|
6
|
+
forcedream-0.1.0.dist-info/licenses/LICENSE,sha256=TL0BqNi8c8yRPyICHZEdENju7pEgMnQOBwkva3rjWKE,1071
|
|
7
|
+
forcedream-0.1.0.dist-info/METADATA,sha256=5yDQHPWnP0qNra5uV_pvS5-HpdaKJHd_H0X3tcB3LBU,4955
|
|
8
|
+
forcedream-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
9
|
+
forcedream-0.1.0.dist-info/top_level.txt,sha256=VH9iAPYAXrDoWKvpghmuOmM24g_AX3Bo-69ZZwmsUA8,11
|
|
10
|
+
forcedream-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ForceDream Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
forcedream
|