dominusnode-camel 2.0.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.
- dominusnode_camel/__init__.py +15 -0
- dominusnode_camel/_core.py +454 -0
- dominusnode_camel/tools.py +40 -0
- dominusnode_camel-2.0.0.dist-info/METADATA +102 -0
- dominusnode_camel-2.0.0.dist-info/RECORD +8 -0
- dominusnode_camel-2.0.0.dist-info/WHEEL +5 -0
- dominusnode_camel-2.0.0.dist-info/licenses/LICENSE +21 -0
- dominusnode_camel-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""CAMEL-AI tools for the DominusNode AI-agent relay network (v2).
|
|
2
|
+
|
|
3
|
+
Example::
|
|
4
|
+
|
|
5
|
+
from dominusnode_camel import DominusNodeToolkit
|
|
6
|
+
|
|
7
|
+
tools = DominusNodeToolkit().get_tools() # reads DOMINUSNODE_API_KEY
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._core import TOOL_NAMES, DominusNodeTools
|
|
11
|
+
from .tools import DominusNodeToolkit
|
|
12
|
+
|
|
13
|
+
__version__ = "2.0.0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["DominusNodeToolkit", "DominusNodeTools", "TOOL_NAMES", "__version__"]
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""Framework-neutral DominusNode v2 tool set, built on the ``dominusnode`` SDK.
|
|
2
|
+
|
|
3
|
+
Every framework adapter in this package wraps :class:`DominusNodeTools`. The 19
|
|
4
|
+
methods named in :data:`TOOL_NAMES` are the standard v2 integration tools. All
|
|
5
|
+
HTTP, SSRF/DNS-rebinding checks, gateway username building and validation live
|
|
6
|
+
in the SDK; this module only adapts arguments and formats results for an LLM.
|
|
7
|
+
|
|
8
|
+
Security properties kept here:
|
|
9
|
+
|
|
10
|
+
- The API key comes from ``DOMINUSNODE_API_KEY`` or constructor config, never
|
|
11
|
+
from a tool argument, and is never returned in a tool output.
|
|
12
|
+
- Every result and error string is passed through the SDK's
|
|
13
|
+
``scrub_credentials`` (dn_live_/dn_test_/dn_node_/dn_proxy_ keys and JWTs).
|
|
14
|
+
- Signing flows (node register, withdraw, payout wallet) are not exposed.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import inspect
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, get_type_hints
|
|
22
|
+
|
|
23
|
+
from dominusnode import (
|
|
24
|
+
DEFAULT_BASE_URL,
|
|
25
|
+
DEFAULT_HTTP_PROXY_PORT,
|
|
26
|
+
DEFAULT_PROXY_HOST,
|
|
27
|
+
DEFAULT_SOCKS5_PROXY_PORT,
|
|
28
|
+
DominusNodeClient,
|
|
29
|
+
json_default,
|
|
30
|
+
scrub_credentials,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
API_KEY_ENV = "DOMINUSNODE_API_KEY"
|
|
34
|
+
BASE_URL_ENV = "DOMINUSNODE_BASE_URL"
|
|
35
|
+
PROXY_HOST_ENV = "DOMINUSNODE_PROXY_HOST"
|
|
36
|
+
PROXY_PORT_ENV = "DOMINUSNODE_PROXY_PORT"
|
|
37
|
+
|
|
38
|
+
MAX_BODY_CHARS = 100_000
|
|
39
|
+
DEFAULT_BODY_CHARS = 4_000
|
|
40
|
+
KEY_PLACEHOLDER = "<DOMINUSNODE_API_KEY>"
|
|
41
|
+
|
|
42
|
+
TOOL_NAMES: Tuple[str, ...] = (
|
|
43
|
+
"dominusnode_fetch",
|
|
44
|
+
"dominusnode_proxy_url",
|
|
45
|
+
"dominusnode_network_policy",
|
|
46
|
+
"dominusnode_network_pricing",
|
|
47
|
+
"dominusnode_network_stats",
|
|
48
|
+
"dominusnode_balance",
|
|
49
|
+
"dominusnode_transactions",
|
|
50
|
+
"dominusnode_topup_crypto",
|
|
51
|
+
"dominusnode_check_payment",
|
|
52
|
+
"dominusnode_usage",
|
|
53
|
+
"dominusnode_active_sessions",
|
|
54
|
+
"dominusnode_report_session",
|
|
55
|
+
"dominusnode_node_list",
|
|
56
|
+
"dominusnode_node_tier",
|
|
57
|
+
"dominusnode_node_setup_guide",
|
|
58
|
+
"dominusnode_credits_balance",
|
|
59
|
+
"dominusnode_credits_convert",
|
|
60
|
+
"dominusnode_payouts_list",
|
|
61
|
+
"dominusnode_x402_info",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
NODE_SETUP_GUIDE = """DominusNode relay node setup (earn by contributing an IP)
|
|
65
|
+
|
|
66
|
+
How earning works
|
|
67
|
+
- Your node dials OUT to the gateway (no inbound ports) and relays other agents' traffic.
|
|
68
|
+
- You earn your tier's share of each paid GB relayed; live prices and shares are in
|
|
69
|
+
dominusnode_network_pricing. Earnings mature before they become available.
|
|
70
|
+
- Self-dealing earns nothing: your own, your wallet's, your team's and your /24's traffic is
|
|
71
|
+
never routed to your node.
|
|
72
|
+
- Tiers rise with paid tenure, uptime, active IPs and reputation (see dominusnode_node_tier).
|
|
73
|
+
- The node floor is fixed: ports 80/443 only, never private/loopback/metadata targets,
|
|
74
|
+
connection metadata only (no payload logging). See dominusnode_network_policy.
|
|
75
|
+
|
|
76
|
+
Prerequisites
|
|
77
|
+
- A wallet-linked account and a personal dn_live_ API key (not a team key).
|
|
78
|
+
- Node.js >= 18 and git on the machine whose IP you contribute.
|
|
79
|
+
- IPs in sanctioned countries (Cuba, Iran, North Korea, Russia, Syria) cannot join.
|
|
80
|
+
|
|
81
|
+
Step 1: Store the owner wallet key in a private file (never pass it as an argument or to an LLM)
|
|
82
|
+
umask 077 && printf '%s' "$WALLET_PRIVATE_KEY_HEX" > ~/.dominusnode/wallet.key
|
|
83
|
+
|
|
84
|
+
Step 2: Register the node with the node agent CLI (signing happens locally, not through this tool)
|
|
85
|
+
git clone https://github.com/0xcircuitbreaker/DomiNode.git && cd DomiNode
|
|
86
|
+
npm ci && npm run build -w @dominusnode/shared -w @dominusnode/node-agent
|
|
87
|
+
export DN_API_KEY # your dn_live_ key, from the environment only
|
|
88
|
+
node packages/node-agent/dist/src/main.js register --wallet-key-file ~/.dominusnode/wallet.key
|
|
89
|
+
(Alternatively use the DominusNode MCP server or SDK nodes.register with a signer key file.)
|
|
90
|
+
|
|
91
|
+
Step 3: Run the node agent on the machine that owns the IP
|
|
92
|
+
node packages/node-agent/dist/src/main.js run --key-file ~/.dominusnode/node.json
|
|
93
|
+
Optional limits (can only tighten): --ports 443 --max-streams 32 --gb-per-day 5 --req-per-sec 20
|
|
94
|
+
Keep it running (systemd, pm2, or a container restart policy): uptime drives your tier.
|
|
95
|
+
|
|
96
|
+
Step 4: Verify
|
|
97
|
+
dominusnode_node_list shows status and last-seen time once the tunnel is up.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _env_int(name: str, default: int) -> int:
|
|
102
|
+
raw = os.environ.get(name)
|
|
103
|
+
if not raw:
|
|
104
|
+
return default
|
|
105
|
+
try:
|
|
106
|
+
v = int(raw)
|
|
107
|
+
except ValueError:
|
|
108
|
+
return default
|
|
109
|
+
return v if 1 <= v <= 65535 else default
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_JSON_SCALARS = (dict, list, tuple, str, int, float, bool, type(None))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _json_default(obj: object) -> object:
|
|
116
|
+
"""SDK ``json_default`` (Secret -> [REDACTED]) with a ``str`` fallback for anything else."""
|
|
117
|
+
try:
|
|
118
|
+
out = json_default(obj)
|
|
119
|
+
except TypeError:
|
|
120
|
+
return str(obj)
|
|
121
|
+
return out if isinstance(out, _JSON_SCALARS) else str(out)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class DominusNodeTools:
|
|
125
|
+
"""The 19 standard DominusNode v2 tools as plain Python methods returning strings.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
api_key: Optional config value; defaults to the ``DOMINUSNODE_API_KEY`` env var.
|
|
129
|
+
Never accepted as a tool argument.
|
|
130
|
+
base_url: API base URL (default ``DOMINUSNODE_BASE_URL`` or the public API).
|
|
131
|
+
proxy_host: Gateway host for ``dominusnode_proxy_url``.
|
|
132
|
+
http_proxy_port: Gateway HTTP port.
|
|
133
|
+
client: A pre-built ``dominusnode.DominusNodeClient`` (already connected or not).
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
api_key: Optional[str] = None,
|
|
139
|
+
base_url: Optional[str] = None,
|
|
140
|
+
proxy_host: Optional[str] = None,
|
|
141
|
+
http_proxy_port: Optional[int] = None,
|
|
142
|
+
client: Any = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
self.__key = api_key or os.environ.get(API_KEY_ENV) or None
|
|
145
|
+
self._base_url = base_url or os.environ.get(BASE_URL_ENV) or DEFAULT_BASE_URL
|
|
146
|
+
self._proxy_host = proxy_host or os.environ.get(PROXY_HOST_ENV) or DEFAULT_PROXY_HOST
|
|
147
|
+
self._http_port = http_proxy_port or _env_int(PROXY_PORT_ENV, DEFAULT_HTTP_PROXY_PORT)
|
|
148
|
+
self._client = client
|
|
149
|
+
self._connected = client is not None and getattr(client, "is_connected", False) is True
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
return f"DominusNodeTools(configured={'true' if self.__key or self._client else 'false'})"
|
|
153
|
+
|
|
154
|
+
__str__ = __repr__
|
|
155
|
+
|
|
156
|
+
def __reduce__(self) -> Any:
|
|
157
|
+
raise TypeError("DominusNodeTools cannot be pickled")
|
|
158
|
+
|
|
159
|
+
# -- plumbing ---------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _sdk(self) -> Any:
|
|
162
|
+
if self._client is None:
|
|
163
|
+
self._client = DominusNodeClient(
|
|
164
|
+
base_url=self._base_url,
|
|
165
|
+
proxy_host=self._proxy_host,
|
|
166
|
+
http_proxy_port=self._http_port,
|
|
167
|
+
socks5_proxy_port=DEFAULT_SOCKS5_PROXY_PORT,
|
|
168
|
+
)
|
|
169
|
+
if not self._connected:
|
|
170
|
+
if not self.__key:
|
|
171
|
+
raise RuntimeError(f"DominusNode API key not configured: set {API_KEY_ENV}")
|
|
172
|
+
self._client.connect_with_key(self.__key)
|
|
173
|
+
self._connected = True
|
|
174
|
+
return self._client
|
|
175
|
+
|
|
176
|
+
def _clean(self, text: str) -> str:
|
|
177
|
+
out = scrub_credentials(text)
|
|
178
|
+
if self.__key and len(self.__key) >= 8:
|
|
179
|
+
out = out.replace(self.__key, "[REDACTED]")
|
|
180
|
+
return out
|
|
181
|
+
|
|
182
|
+
def _ok(self, value: Any) -> str:
|
|
183
|
+
if isinstance(value, str):
|
|
184
|
+
return self._clean(value)
|
|
185
|
+
return self._clean(json.dumps(value, default=_json_default, indent=2, sort_keys=True))
|
|
186
|
+
|
|
187
|
+
def _err(self, exc: BaseException) -> str:
|
|
188
|
+
return "Error: " + self._clean(f"{type(exc).__name__}: {exc}")
|
|
189
|
+
|
|
190
|
+
def _call(self, fn: Callable[[Any], Any]) -> str:
|
|
191
|
+
try:
|
|
192
|
+
return self._ok(fn(self._sdk()))
|
|
193
|
+
except Exception as exc: # noqa: BLE001 - every error is returned scrubbed to the model
|
|
194
|
+
return self._err(exc)
|
|
195
|
+
|
|
196
|
+
def close(self) -> None:
|
|
197
|
+
if self._client is not None:
|
|
198
|
+
try:
|
|
199
|
+
self._client.disconnect()
|
|
200
|
+
except Exception: # noqa: BLE001
|
|
201
|
+
pass
|
|
202
|
+
self._connected = False
|
|
203
|
+
|
|
204
|
+
# -- 1-2 proxy ---------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def dominusnode_fetch(
|
|
207
|
+
self,
|
|
208
|
+
url: str,
|
|
209
|
+
method: str = "GET",
|
|
210
|
+
country: Optional[str] = None,
|
|
211
|
+
ip_class: Optional[str] = None,
|
|
212
|
+
session_id: Optional[str] = None,
|
|
213
|
+
allow_http: bool = False,
|
|
214
|
+
max_chars: int = DEFAULT_BODY_CHARS,
|
|
215
|
+
) -> str:
|
|
216
|
+
"""Fetch a public URL through the DominusNode relay network of contributed exit IPs.
|
|
217
|
+
|
|
218
|
+
Billed per GB at the live price from dominusnode_network_pricing. GET or HEAD only, ports
|
|
219
|
+
80/443, private/local targets refused, redirects returned (not followed), 10 MB cap.
|
|
220
|
+
HTTPS is end-to-end encrypted; plaintext http:// needs allow_http=true and is visible to
|
|
221
|
+
the exit node, so never send secrets over it.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
url: Absolute https:// (or http:// with allow_http) URL to fetch.
|
|
225
|
+
method: GET or HEAD.
|
|
226
|
+
country: Optional ISO-2 exit country, e.g. US or DE (sanctioned countries refused).
|
|
227
|
+
ip_class: Optional exit class: residential, dc or mobile.
|
|
228
|
+
session_id: Optional sticky-session id (1-64 letters/digits) to keep the same exit.
|
|
229
|
+
allow_http: Opt in to plaintext http:// (exit-visible).
|
|
230
|
+
max_chars: Maximum body characters returned to you (1-100000, default 4000).
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def run(dn: Any) -> Any:
|
|
234
|
+
limit = max(1, min(int(max_chars), MAX_BODY_CHARS))
|
|
235
|
+
r = dn.proxy.fetch(
|
|
236
|
+
url,
|
|
237
|
+
method=str(method).upper(),
|
|
238
|
+
country=country,
|
|
239
|
+
ip_class=ip_class,
|
|
240
|
+
session_id=session_id,
|
|
241
|
+
allow_http=bool(allow_http),
|
|
242
|
+
)
|
|
243
|
+
body = r.text()
|
|
244
|
+
keep = ("content-type", "content-length", "location", "retry-after", "last-modified")
|
|
245
|
+
return {
|
|
246
|
+
"status": r.status,
|
|
247
|
+
"statusText": r.status_text,
|
|
248
|
+
"headers": {k: v for k, v in r.headers.items() if k.lower() in keep},
|
|
249
|
+
"body": body[:limit],
|
|
250
|
+
"bodyTruncated": bool(r.truncated) or len(body) > limit,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return self._call(run)
|
|
254
|
+
|
|
255
|
+
def dominusnode_proxy_url(
|
|
256
|
+
self,
|
|
257
|
+
country: Optional[str] = None,
|
|
258
|
+
ip_class: Optional[str] = None,
|
|
259
|
+
session_id: Optional[str] = None,
|
|
260
|
+
allow_http: bool = False,
|
|
261
|
+
) -> str:
|
|
262
|
+
"""Build the gateway proxy URL for routing your own HTTP client through the relay network.
|
|
263
|
+
|
|
264
|
+
Routing flags go in the proxy username (hyphenated, e.g. country-US-class-dc); the API key
|
|
265
|
+
goes in the password. The key is NOT returned: the URL contains a placeholder that your
|
|
266
|
+
runtime substitutes from the DOMINUSNODE_API_KEY environment variable.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
country: Optional ISO-2 exit country (sanctioned countries refused).
|
|
270
|
+
ip_class: Optional exit class: residential, dc or mobile.
|
|
271
|
+
session_id: Optional sticky-session id (1-64 letters/digits).
|
|
272
|
+
allow_http: Allow plaintext http:// targets (visible to the exit node).
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def run(dn: Any) -> Any:
|
|
276
|
+
user = dn.proxy.build_username(
|
|
277
|
+
country=country, ip_class=ip_class, session_id=session_id, allow_http=bool(allow_http)
|
|
278
|
+
)
|
|
279
|
+
return {
|
|
280
|
+
"username": user,
|
|
281
|
+
"proxyUrl": f"http://{user}:{KEY_PLACEHOLDER}@{self._proxy_host}:{self._http_port}",
|
|
282
|
+
"note": f"Replace {KEY_PLACEHOLDER} with the value of ${API_KEY_ENV}; never log the real URL.",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return self._call(run)
|
|
286
|
+
|
|
287
|
+
# -- 3-5 network -------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
def dominusnode_network_policy(self) -> str:
|
|
290
|
+
"""Show the relay network policy: allowed ports, blocked targets, sanctioned countries, logging rules."""
|
|
291
|
+
return self._call(lambda dn: dn.network.policy())
|
|
292
|
+
|
|
293
|
+
def dominusnode_network_pricing(self) -> str:
|
|
294
|
+
"""Show live per-GB prices by IP class (in µUSD; 1 USD = 1,000,000 µUSD) and node earning shares.
|
|
295
|
+
|
|
296
|
+
Always use this instead of assuming prices.
|
|
297
|
+
"""
|
|
298
|
+
return self._call(lambda dn: dn.network.pricing())
|
|
299
|
+
|
|
300
|
+
def dominusnode_network_stats(self) -> str:
|
|
301
|
+
"""Show relay network statistics: online nodes, countries and IP classes available."""
|
|
302
|
+
return self._call(lambda dn: dn.network.stats())
|
|
303
|
+
|
|
304
|
+
# -- 6-9 wallet --------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def dominusnode_balance(self) -> str:
|
|
307
|
+
"""Show your prepaid wallet balance used to pay for relay traffic."""
|
|
308
|
+
return self._call(lambda dn: dn.wallet.get_balance())
|
|
309
|
+
|
|
310
|
+
def dominusnode_transactions(self, limit: int = 20, offset: int = 0) -> str:
|
|
311
|
+
"""List recent wallet transactions (top-ups and usage charges).
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
limit: Number of transactions to return (1-100).
|
|
315
|
+
offset: Number of transactions to skip.
|
|
316
|
+
"""
|
|
317
|
+
return self._call(lambda dn: dn.wallet.get_transactions(limit=limit, offset=offset))
|
|
318
|
+
|
|
319
|
+
def dominusnode_topup_crypto(self, amount_cents: int, currency: str) -> str:
|
|
320
|
+
"""Create a crypto invoice to top up your wallet ($10-$1,000).
|
|
321
|
+
|
|
322
|
+
Returns a payment address/invoice; check it with dominusnode_check_payment.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
amount_cents: Amount in US cents, 1000 to 100000.
|
|
326
|
+
currency: One of btc, eth, ltc, xmr, zec, usdc, sol, usdt, dai, bnb, link.
|
|
327
|
+
"""
|
|
328
|
+
return self._call(lambda dn: dn.wallet.top_up_crypto(amount_cents, currency))
|
|
329
|
+
|
|
330
|
+
def dominusnode_check_payment(self, invoice_id: str) -> str:
|
|
331
|
+
"""Check the status of a crypto top-up invoice.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
invoice_id: Invoice UUID returned by dominusnode_topup_crypto.
|
|
335
|
+
"""
|
|
336
|
+
return self._call(lambda dn: dn.wallet.get_crypto_status(invoice_id))
|
|
337
|
+
|
|
338
|
+
# -- 10-12 usage and sessions -----------------------------------------
|
|
339
|
+
|
|
340
|
+
def dominusnode_usage(self, since: Optional[str] = None, until: Optional[str] = None) -> str:
|
|
341
|
+
"""Summarise your relay bandwidth usage and cost.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
since: Optional ISO-8601 start date/time.
|
|
345
|
+
until: Optional ISO-8601 end date/time.
|
|
346
|
+
"""
|
|
347
|
+
return self._call(lambda dn: dn.usage.get(from_=since, to=until))
|
|
348
|
+
|
|
349
|
+
def dominusnode_active_sessions(self) -> str:
|
|
350
|
+
"""List your active proxy sessions (id, start time, status; exit IPs are never shown)."""
|
|
351
|
+
return self._call(lambda dn: dn.sessions.get_active())
|
|
352
|
+
|
|
353
|
+
def dominusnode_report_session(self, session_id: str, reason: str) -> str:
|
|
354
|
+
"""Report a bad relay session so the exit node's reputation is updated.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
session_id: Session UUID from dominusnode_active_sessions.
|
|
358
|
+
reason: One of blocked, captcha, rate_limited, slow, bad_content, other.
|
|
359
|
+
"""
|
|
360
|
+
return self._call(lambda dn: dn.sessions.report(session_id, reason))
|
|
361
|
+
|
|
362
|
+
# -- 13-15 nodes -------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
def dominusnode_node_list(self, limit: int = 50, offset: int = 0) -> str:
|
|
365
|
+
"""List the relay nodes you contribute (status, class, last seen).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
limit: Number of nodes to return (1-500).
|
|
369
|
+
offset: Number of nodes to skip.
|
|
370
|
+
"""
|
|
371
|
+
return self._call(lambda dn: dn.nodes.list(limit=limit, offset=offset))
|
|
372
|
+
|
|
373
|
+
def dominusnode_node_tier(self) -> str:
|
|
374
|
+
"""Show your node-operator tier, earning share and progress to the next tier."""
|
|
375
|
+
return self._call(lambda dn: dn.nodes.tier())
|
|
376
|
+
|
|
377
|
+
def dominusnode_node_setup_guide(self) -> str:
|
|
378
|
+
"""Explain how to earn by contributing an IP: run the DominusNode node agent, step by step."""
|
|
379
|
+
return self._ok(NODE_SETUP_GUIDE)
|
|
380
|
+
|
|
381
|
+
# -- 16-18 earnings ----------------------------------------------------
|
|
382
|
+
|
|
383
|
+
def dominusnode_credits_balance(self) -> str:
|
|
384
|
+
"""Show node-operator credits earned by relaying traffic (µUSD; pending and available)."""
|
|
385
|
+
return self._call(lambda dn: dn.credits.balance())
|
|
386
|
+
|
|
387
|
+
def dominusnode_credits_convert(self, amount_musd: str, idempotency_key: Optional[str] = None) -> str:
|
|
388
|
+
"""Convert available earned credits into wallet balance to spend on relay traffic.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
amount_musd: Amount in µUSD as a digit string (10000 = $0.01; min 10000).
|
|
392
|
+
idempotency_key: Optional key from a previous attempt, to retry safely.
|
|
393
|
+
"""
|
|
394
|
+
return self._call(lambda dn: dn.credits.convert(amount_musd, idempotency_key=idempotency_key))
|
|
395
|
+
|
|
396
|
+
def dominusnode_payouts_list(self, limit: int = 20, offset: int = 0) -> str:
|
|
397
|
+
"""List your on-chain payouts (withdrawals of earned credits). Withdrawing needs a signer, not this tool.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
limit: Number of payouts to return (1-200).
|
|
401
|
+
offset: Number of payouts to skip.
|
|
402
|
+
"""
|
|
403
|
+
return self._call(lambda dn: dn.payouts.list(limit=limit, offset=offset))
|
|
404
|
+
|
|
405
|
+
# -- 19 x402 -----------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def dominusnode_x402_info(self) -> str:
|
|
408
|
+
"""Show x402 (HTTP 402 micropayment) support details for paying per request."""
|
|
409
|
+
return self._call(lambda dn: dn.x402.get_info())
|
|
410
|
+
|
|
411
|
+
# -- helpers for adapters ---------------------------------------------
|
|
412
|
+
|
|
413
|
+
def tool_functions(self) -> List[Callable[..., str]]:
|
|
414
|
+
"""The 19 bound tool methods, in standard order."""
|
|
415
|
+
return [getattr(self, n) for n in TOOL_NAMES]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
# Docstring helpers shared by adapters
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
_ARGS_RE = re.compile(r"^\s{4,}(\w+):\s*(.+)$")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def describe(fn: Callable[..., Any]) -> str:
|
|
426
|
+
"""Tool description: the docstring without its Args section."""
|
|
427
|
+
doc = inspect.getdoc(fn) or ""
|
|
428
|
+
return doc.split("\nArgs:")[0].strip()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def arg_docs(fn: Callable[..., Any]) -> Dict[str, str]:
|
|
432
|
+
doc = fn.__doc__ or ""
|
|
433
|
+
if "Args:" not in doc:
|
|
434
|
+
return {}
|
|
435
|
+
out: Dict[str, str] = {}
|
|
436
|
+
for line in doc.split("Args:", 1)[1].splitlines():
|
|
437
|
+
m = _ARGS_RE.match(line)
|
|
438
|
+
if m:
|
|
439
|
+
out[m.group(1)] = m.group(2).strip()
|
|
440
|
+
return out
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def args_model(fn: Callable[..., Any], model_name: str) -> Any:
|
|
444
|
+
"""Build a pydantic model for a tool method's arguments (for schema-driven frameworks)."""
|
|
445
|
+
from pydantic import Field, create_model
|
|
446
|
+
|
|
447
|
+
docs = arg_docs(fn)
|
|
448
|
+
hints = get_type_hints(fn)
|
|
449
|
+
fields: Dict[str, Any] = {}
|
|
450
|
+
for name, p in inspect.signature(fn).parameters.items():
|
|
451
|
+
ann = hints.get(name, str)
|
|
452
|
+
default = ... if p.default is inspect.Parameter.empty else p.default
|
|
453
|
+
fields[name] = (ann, Field(default, description=docs.get(name, name)))
|
|
454
|
+
return create_model(model_name, **fields)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""CAMEL-AI toolkit exposing the 19 standard DominusNode v2 tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, Optional
|
|
4
|
+
|
|
5
|
+
from camel.toolkits import BaseToolkit, FunctionTool
|
|
6
|
+
|
|
7
|
+
from ._core import DominusNodeTools
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DominusNodeToolkit(BaseToolkit):
|
|
11
|
+
"""DominusNode relay-network tools for CAMEL agents.
|
|
12
|
+
|
|
13
|
+
The API key is read from ``DOMINUSNODE_API_KEY`` (or passed as config); it is never a
|
|
14
|
+
tool argument and never appears in tool output.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
|
|
18
|
+
from camel.agents import ChatAgent
|
|
19
|
+
agent = ChatAgent(system_message="...", tools=DominusNodeToolkit().get_tools())
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
api_key: Optional[str] = None,
|
|
25
|
+
base_url: Optional[str] = None,
|
|
26
|
+
proxy_host: Optional[str] = None,
|
|
27
|
+
http_proxy_port: Optional[int] = None,
|
|
28
|
+
client: Any = None,
|
|
29
|
+
timeout: Optional[float] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(timeout=timeout)
|
|
32
|
+
self._core = DominusNodeTools(
|
|
33
|
+
api_key=api_key, base_url=base_url, proxy_host=proxy_host, http_proxy_port=http_proxy_port, client=client
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
37
|
+
return [FunctionTool(fn) for fn in self._core.tool_functions()]
|
|
38
|
+
|
|
39
|
+
def __repr__(self) -> str:
|
|
40
|
+
return "DominusNodeToolkit()"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dominusnode-camel
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: CAMEL-AI toolkit for the DominusNode AI-agent relay network (19 v2 tools on the dominusnode SDK)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/integrations/camel-ai
|
|
7
|
+
Project-URL: Repository, https://github.com/0xcircuitbreaker/DomiNode
|
|
8
|
+
Project-URL: Issues, https://github.com/0xcircuitbreaker/DomiNode/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/0xcircuitbreaker/DomiNode/blob/master/integrations/camel-ai/CHANGELOG.md
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: camel-ai>=0.2.0
|
|
14
|
+
Requires-Dist: mcp<2
|
|
15
|
+
Requires-Dist: dominusnode<3,>=2.0.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# dominusnode-camel
|
|
21
|
+
|
|
22
|
+
CAMEL-AI tools for the **DominusNode AI-agent relay network** (v2). Agents fetch the web through
|
|
23
|
+
exit IPs contributed by other operators, pay per GB from a prepaid wallet (live prices from
|
|
24
|
+
`dominusnode_network_pricing`, nothing hardcoded), and can earn by contributing their own IP
|
|
25
|
+
with the DominusNode node agent.
|
|
26
|
+
|
|
27
|
+
Built on the official [`dominusnode`](../../sdks/python) Python SDK v2: all HTTP, SSRF and
|
|
28
|
+
DNS-rebinding checks, gateway username flags and validation come from the SDK.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install dominusnode-camel
|
|
32
|
+
export DOMINUSNODE_API_KEY=dn_live_... # read from the environment, never a tool argument
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Defaults: API `https://api.dominusnode.daedalusdevelopmentgroup.com`, relay gateway `proxy.dominusnode.daedalusdevelopmentgroup.com`. Override with `DOMINUSNODE_BASE_URL` / `DOMINUSNODE_PROXY_HOST`.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from camel.agents import ChatAgent
|
|
41
|
+
from dominusnode_camel import DominusNodeToolkit
|
|
42
|
+
|
|
43
|
+
agent = ChatAgent(
|
|
44
|
+
system_message="You browse through the DominusNode relay network. Check pricing first.",
|
|
45
|
+
tools=DominusNodeToolkit().get_tools(), # 19 camel FunctionTools
|
|
46
|
+
)
|
|
47
|
+
print(agent.step("Fetch https://example.com from a residential exit in the US").msgs[0].content)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`DominusNodeToolkit` is a `camel.toolkits.BaseToolkit`; each tool is a `FunctionTool` whose
|
|
51
|
+
OpenAI schema comes from the method signature and Google-style docstring. Errors come back
|
|
52
|
+
as scrubbed `Error: ...` strings.
|
|
53
|
+
|
|
54
|
+
## The 19 tools
|
|
55
|
+
|
|
56
|
+
| # | Tool | What it does | SDK call |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| 1 | `dominusnode_fetch` | Fetch a public URL through the relay (GET/HEAD; country, ip_class, session_id, allow_http) | `proxy.fetch` |
|
|
59
|
+
| 2 | `dominusnode_proxy_url` | Gateway proxy URL/username for your own client (key shown as a placeholder) | `proxy.build_username` |
|
|
60
|
+
| 3 | `dominusnode_network_policy` | Relay policy: ports, blocked targets, sanctioned countries | `network.policy` |
|
|
61
|
+
| 4 | `dominusnode_network_pricing` | Live per-GB prices by IP class and node earning shares | `network.pricing` |
|
|
62
|
+
| 5 | `dominusnode_network_stats` | Online nodes, countries, IP classes | `network.stats` |
|
|
63
|
+
| 6 | `dominusnode_balance` | Prepaid wallet balance | `wallet.get_balance` |
|
|
64
|
+
| 7 | `dominusnode_transactions` | Wallet transactions | `wallet.get_transactions` |
|
|
65
|
+
| 8 | `dominusnode_topup_crypto` | Crypto top-up invoice ($10-$1,000) | `wallet.top_up_crypto` |
|
|
66
|
+
| 9 | `dominusnode_check_payment` | Crypto invoice status | `wallet.get_crypto_status` |
|
|
67
|
+
| 10 | `dominusnode_usage` | Bandwidth usage and cost | `usage.get` |
|
|
68
|
+
| 11 | `dominusnode_active_sessions` | Active sessions (no IPs) | `sessions.get_active` |
|
|
69
|
+
| 12 | `dominusnode_report_session` | Report a bad session | `sessions.report` |
|
|
70
|
+
| 13 | `dominusnode_node_list` | Your relay nodes | `nodes.list` |
|
|
71
|
+
| 14 | `dominusnode_node_tier` | Node-operator tier and share | `nodes.tier` |
|
|
72
|
+
| 15 | `dominusnode_node_setup_guide` | Steps to earn by running the node agent | `static text` |
|
|
73
|
+
| 16 | `dominusnode_credits_balance` | Earned node credits | `credits.balance` |
|
|
74
|
+
| 17 | `dominusnode_credits_convert` | Convert credits into wallet balance | `credits.convert` |
|
|
75
|
+
| 18 | `dominusnode_payouts_list` | On-chain payouts | `payouts.list` |
|
|
76
|
+
| 19 | `dominusnode_x402_info` | x402 micropayment info | `x402.get_info` |
|
|
77
|
+
|
|
78
|
+
## Security
|
|
79
|
+
|
|
80
|
+
- The API key comes from `DOMINUSNODE_API_KEY` (or constructor config). It is never a tool
|
|
81
|
+
argument, never in a tool schema and never in tool output; `dominusnode_proxy_url` returns a
|
|
82
|
+
`<DOMINUSNODE_API_KEY>` placeholder.
|
|
83
|
+
- Every result and error is scrubbed of `dn_live_`/`dn_test_`/`dn_node_`/`dn_proxy_` keys and JWTs.
|
|
84
|
+
- `dominusnode_fetch` (via the SDK): GET/HEAD only, ports 80/443, private/local hosts and
|
|
85
|
+
DNS-rebinding refused, redirects not followed, 10 MB cap, sanctioned countries refused.
|
|
86
|
+
Plaintext `http://` needs `allow_http=true` and is visible to the exit node.
|
|
87
|
+
- Signed flows (node registration, withdrawals, payout wallet) are not LLM tools: use the
|
|
88
|
+
DominusNode MCP server, the node agent, or the SDK with a signer key file.
|
|
89
|
+
|
|
90
|
+
Configuration env vars: `DOMINUSNODE_API_KEY`, `DOMINUSNODE_BASE_URL`, `DOMINUSNODE_PROXY_HOST`,
|
|
91
|
+
`DOMINUSNODE_PROXY_PORT`.
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install -e ../../sdks/python -e '.[dev]'
|
|
97
|
+
pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
dominusnode_camel/__init__.py,sha256=3jyyD3BadWHsRMLYWTbEUsXKDJmvDSrIES6b5ZIhLaY,405
|
|
2
|
+
dominusnode_camel/_core.py,sha256=cNkqLQWg65e2M5qLj3rxizCm5vYyoOcr44b0TA04T8k,18743
|
|
3
|
+
dominusnode_camel/tools.py,sha256=AMsjKna4k9UoGfoIPZORORLj_2_g0BX57TbvOZQR-xw,1281
|
|
4
|
+
dominusnode_camel-2.0.0.dist-info/licenses/LICENSE,sha256=NBkjfgN0tpbJjWf7uqsPCOMgq7uuSEW4uaGDCpB8HbI,1069
|
|
5
|
+
dominusnode_camel-2.0.0.dist-info/METADATA,sha256=sLZaLehiRF5vJe8TWsXu6XReeMGYPSgenh5HhYXFu04,5083
|
|
6
|
+
dominusnode_camel-2.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
dominusnode_camel-2.0.0.dist-info/top_level.txt,sha256=R6E1oZSpv_alMwXWrUDcdX1cOv4x_njR4vSdRmF4GU4,18
|
|
8
|
+
dominusnode_camel-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominus Node
|
|
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
|
+
dominusnode_camel
|