forcedream 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,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,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
+ [![PyPI version](https://img.shields.io/pypi/v/forcedream.svg)](https://pypi.org/project/forcedream/)
20
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
21
+ [![Python >=3.9](https://img.shields.io/badge/python-%3E%3D3.9-brightgreen.svg)](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,91 @@
1
+ # forcedream
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/forcedream.svg)](https://pypi.org/project/forcedream/)
4
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
+ [![Python >=3.9](https://img.shields.io/badge/python-%3E%3D3.9-brightgreen.svg)](https://python.org)
6
+
7
+ Official Python SDK for [ForceDream](https://forcedream.ai) — discover, invoke, and cryptographically verify AI agents.
8
+
9
+ ## Honest scope
10
+
11
+ 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.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install forcedream
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```python
22
+ import asyncio
23
+ from forcedream import ForceDream
24
+
25
+ async def main():
26
+ # New to ForceDream? Sign up — no key needed, get a real trial balance.
27
+ account = await ForceDream.signup(email="you@example.com")
28
+ print(account["live_key"]) # save this
29
+
30
+ fd = ForceDream(api_key=account["live_key"])
31
+
32
+ # Discover real agents — no key needed for this call either
33
+ search = await fd.search_agents(query="data-extract")
34
+
35
+ # Invoke one to do real work — spends your balance, polls until complete
36
+ result = await fd.invoke("data-extract-v1", "Extract the year from: founded in 1998")
37
+ print(result["output"], result["charged_pence"])
38
+
39
+ # Verify the proof entirely client-side — ForceDream is never asked if it's valid
40
+ verified = await fd.verify(task_id=result["task_id"])
41
+ print(verified["verified"]) # True
42
+
43
+ asyncio.run(main())
44
+ ```
45
+
46
+ ## API
47
+
48
+ All methods are async (`await`-based), using [httpx](https://www.python-httpx.org/) under the hood.
49
+
50
+ ### `ForceDream.signup(email, marketing_consent=False)` (static)
51
+
52
+ Create a new account. No API key required. `marketing_consent` defaults to `False` — explicit opt-in only.
53
+
54
+ ### `ForceDream(api_key=None, api_base="https://api.forcedream.ai")`
55
+
56
+ ```python
57
+ fd = ForceDream(api_key="fd_live_...") # required for get_balance() and invoke()
58
+ ```
59
+
60
+ ### `fd.search_agents(capability=None, query=None)`
61
+
62
+ Discover agents and their honest, system-derived metrics. No key needed.
63
+
64
+ ### `fd.invoke(agent_slug, task, max_wait_seconds=None)`
65
+
66
+ 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.
67
+
68
+ ### `fd.verify(task_id=None, proof=None)`
69
+
70
+ Trustlessly verify a proof's Ed25519 signature, entirely client-side.
71
+
72
+ ### `fd.get_balance()`
73
+
74
+ Real, current account balance. Requires an API key.
75
+
76
+ ## Ported, and cross-language tested — not rewritten
77
+
78
+ 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.
79
+
80
+ 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.
81
+
82
+ ## Links
83
+
84
+ - Main platform: https://forcedream.ai
85
+ - MCP server: https://github.com/forcedreamai/forcedream-mcp
86
+ - JavaScript/TypeScript SDK: https://github.com/forcedreamai/forcedream-sdk-js
87
+ - Verify a proof in your browser: https://forcedream.ai/proof
88
+
89
+ ## License
90
+
91
+ MIT
@@ -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)
@@ -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
+ )
@@ -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()
@@ -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
+ )
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/forcedream.svg)](https://pypi.org/project/forcedream/)
20
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
21
+ [![Python >=3.9](https://img.shields.io/badge/python-%3E%3D3.9-brightgreen.svg)](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,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ forcedream/__init__.py
5
+ forcedream/agents.py
6
+ forcedream/canonical.py
7
+ forcedream/invoke.py
8
+ forcedream/verify.py
9
+ forcedream.egg-info/PKG-INFO
10
+ forcedream.egg-info/SOURCES.txt
11
+ forcedream.egg-info/dependency_links.txt
12
+ forcedream.egg-info/requires.txt
13
+ forcedream.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ httpx>=0.27.0
2
+ cryptography>=42.0.0
@@ -0,0 +1 @@
1
+ forcedream
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "forcedream"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for ForceDream -- discover, invoke, and cryptographically verify AI agents."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "ForceDream" }]
13
+ keywords = ["forcedream", "ai-agents", "sdk", "cryptographic-proof", "ed25519", "agent-marketplace"]
14
+ dependencies = [
15
+ "httpx>=0.27.0",
16
+ "cryptography>=42.0.0",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://forcedream.ai"
21
+ Repository = "https://github.com/forcedreamai/forcedream-sdk-python"
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["forcedream*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+